From 3bf6d97f3f5e8af2ba681f981b23003366519d36 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Mon, 23 Jun 2025 18:00:04 +0800 Subject: [PATCH 1/3] Migrate to http-handler and http-rewriter --- CLAUDE.md | 124 ++ Cargo.toml | 51 +- __test__/headers.spec.mjs | 8 +- __test__/request.spec.mjs | 4 +- __test__/rewriter.spec.mjs | 15 +- build.rs | 22 + crates/lang_handler/Cargo.toml | 22 - crates/lang_handler/build.rs | 7 - crates/lang_handler/src/handler.rs | 61 - crates/lang_handler/src/headers.rs | 457 ------- crates/lang_handler/src/lib.rs | 198 --- crates/lang_handler/src/request.rs | 611 --------- crates/lang_handler/src/response.rs | 454 ------- .../src/rewrite/condition/closure.rs | 31 - .../src/rewrite/condition/existence.rs | 96 -- .../src/rewrite/condition/group.rs | 121 -- .../src/rewrite/condition/header.rs | 74 - .../src/rewrite/condition/method.rs | 65 - .../lang_handler/src/rewrite/condition/mod.rs | 126 -- .../src/rewrite/condition/path.rs | 64 - .../src/rewrite/conditional_rewriter.rs | 107 -- crates/lang_handler/src/rewrite/mod.rs | 121 -- .../src/rewrite/rewriter/closure.rs | 36 - .../src/rewrite/rewriter/header.rs | 85 -- .../lang_handler/src/rewrite/rewriter/href.rs | 86 -- .../src/rewrite/rewriter/method.rs | 68 - .../lang_handler/src/rewrite/rewriter/mod.rs | 102 -- .../lang_handler/src/rewrite/rewriter/path.rs | 80 -- .../src/rewrite/rewriter/sequence.rs | 76 -- crates/lang_handler/src/test.rs | 162 --- crates/php/Cargo.toml | 27 - crates/php/src/main.rs | 56 - crates/php/src/request_context.rs | 184 --- crates/php_node/Cargo.toml | 17 - crates/php_node/build.rs | 80 -- crates/php_node/src/headers.rs | 345 ----- crates/php_node/src/lib.rs | 14 - crates/php_node/src/request.rs | 200 --- crates/php_node/src/response.rs | 165 --- crates/php_node/src/rewriter.rs | 355 ----- index.d.ts | 1206 ++++++++++++++--- index.js | 7 +- package.json | 4 +- {crates/php/src => src}/embed.rs | 144 +- {crates/php/src => src}/exception.rs | 4 +- {crates/php/src => src}/lib.rs | 36 +- crates/php_node/src/runtime.rs => src/napi.rs | 53 +- src/request_context.rs | 165 +++ src/rewriter_impl.rs | 5 + {crates/php/src => src}/sapi.rs | 77 +- {crates/php/src => src}/scopes.rs | 0 {crates/php/src => src}/strings.rs | 0 {crates/php/src => src}/test.rs | 0 53 files changed, 1588 insertions(+), 5090 deletions(-) create mode 100644 CLAUDE.md create mode 100644 build.rs delete mode 100644 crates/lang_handler/Cargo.toml delete mode 100644 crates/lang_handler/build.rs delete mode 100644 crates/lang_handler/src/handler.rs delete mode 100644 crates/lang_handler/src/headers.rs delete mode 100644 crates/lang_handler/src/lib.rs delete mode 100644 crates/lang_handler/src/request.rs delete mode 100644 crates/lang_handler/src/response.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/closure.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/existence.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/group.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/header.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/method.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/mod.rs delete mode 100644 crates/lang_handler/src/rewrite/condition/path.rs delete mode 100644 crates/lang_handler/src/rewrite/conditional_rewriter.rs delete mode 100644 crates/lang_handler/src/rewrite/mod.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/closure.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/header.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/href.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/method.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/mod.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/path.rs delete mode 100644 crates/lang_handler/src/rewrite/rewriter/sequence.rs delete mode 100644 crates/lang_handler/src/test.rs delete mode 100644 crates/php/Cargo.toml delete mode 100644 crates/php/src/main.rs delete mode 100644 crates/php/src/request_context.rs delete mode 100644 crates/php_node/Cargo.toml delete mode 100644 crates/php_node/build.rs delete mode 100644 crates/php_node/src/headers.rs delete mode 100644 crates/php_node/src/lib.rs delete mode 100644 crates/php_node/src/request.rs delete mode 100644 crates/php_node/src/response.rs delete mode 100644 crates/php_node/src/rewriter.rs rename {crates/php/src => src}/embed.rs (63%) rename {crates/php/src => src}/exception.rs (97%) rename {crates/php/src => src}/lib.rs (63%) rename crates/php_node/src/runtime.rs => src/napi.rs (70%) create mode 100644 src/request_context.rs create mode 100644 src/rewriter_impl.rs rename {crates/php/src => src}/sapi.rs (85%) rename {crates/php/src => src}/scopes.rs (100%) rename {crates/php/src => src}/strings.rs (100%) rename {crates/php/src => src}/test.rs (100%) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8859b58 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,124 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +@platformatic/php-node is a Node.js native addon that embeds PHP within the same process as Node.js, enabling seamless communication without network overhead. It's built with Rust using NAPI-RS for safe and performant bindings. + +## Essential Commands + +### Development +```bash +# Build release version +npm run build + +# Build debug version (faster compilation, includes debug symbols) +npm run build:debug + +# Run all tests +npm test + +# Run specific test file +npx ava __test__/headers.spec.mjs + +# Lint JavaScript code +npm run lint + +# Create universal binary (macOS) +npm run universal + +# Version bump +npm run version +``` + +### Rust-specific builds +```bash +# Build with proper rpath for linking libphp +RUSTFLAGS="-C link-args=-Wl,-rpath,\$ORIGIN" npm run build + +# Build specific crate +cargo build --manifest-path crates/php_node/Cargo.toml --release +``` + +## Architecture + +### Multi-language Structure +- **Rust** (`/crates`): Core implementation using Cargo workspace + - `lang_handler`: Generic language handler abstractions + - `php`: PHP embedding and SAPI implementation + - `php_node`: NAPI bindings exposing Rust to Node.js +- **JavaScript**: Node.js API layer (`index.js`, `index.d.ts`) +- **PHP**: Embedded runtime via libphp.{so,dylib} + +### Key Components + +1. **PHP Class** (`index.js`): Main entry point for creating PHP environments + - Manages rewriter rules for URL routing + - Handles request/response lifecycle + - Supports both sync and async request handling + +2. **Request/Response Model**: Web standards-compatible implementation + - `Request` class with headers, body, method + - `Response` class with status, headers, body + - `Headers` class with case-insensitive header handling + +3. **Rewriter System**: Apache mod_rewrite-like functionality + - Conditional rules with regex patterns + - Environment variable support + - Rule chaining with [L], [R], [C] flags + +4. **SAPI Implementation**: Custom PHP SAPI in Rust + - Direct Zend API usage for performance + - Thread-safe with TSRM support + - Reusable PHP environments across requests + +## Critical Development Notes + +1. **System Dependencies Required**: + - Linux: `libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libonig-dev re2c libpq5` + - macOS: `openssl@3 curl sqlite libxml2 oniguruma postgresql@16` + +2. **PHP Runtime**: Must have `libphp.so` (Linux) or `libphp.dylib` (macOS) in project root + +3. **Testing**: AVA framework with 3-minute timeout due to PHP startup overhead + +4. **Type Definitions**: `index.d.ts` is auto-generated by NAPI-RS - do not edit manually + +5. **Platform Support**: x64 Linux, x64/arm64 macOS (pre-built binaries in `/npm`) + +6. **Recent Architecture Changes**: + - `lang_handler` crate no longer uses `napi` features directly (removed from dependencies) + - `php` crate uses custom fork of ext-php-rs from platformatic GitHub org + +## Common Tasks + +### Adding New NAPI Functions +1. Implement in Rust under `crates/php_node/src/` +2. Use `#[napi]` attributes for exposed functions/classes +3. Run `npm run build` to regenerate TypeScript definitions + +### Modifying Request/Response Handling +- Core logic in `crates/php/src/sapi.rs` +- JavaScript wrapper in `index.js` +- Headers handling in `crates/php_node/src/headers.rs` + +### Debugging PHP Issues +- Check INTERNALS.md for PHP embedding details +- Use `npm run build:debug` for debug symbols +- PHP superglobals set via `SG(...)` macro in Rust code + +### Working with Rewriter Rules +The rewriter system supports Apache mod_rewrite-like functionality: +- Create rules with conditions (header, host, method, path, query) +- Apply rewriters (header, href, method, path, query, status) +- Use flags like [L] (last), [R] (redirect), [C] (chain) +- Example: `new Rewriter([{ conditions: [{type: 'path', args: ['^/old/(.*)$']}], rewriters: [{type: 'path', args: ['^/old/(.*)$', '/new/$1']}] }])` + +## Project Files Reference + +- `index.js`: Main JavaScript API, exports PHP, Request, Response, Headers, Rewriter classes +- `crates/php_node/src/lib.rs`: NAPI bindings entry point +- `crates/php/src/sapi.rs`: PHP SAPI implementation (core request handling) +- `crates/lang_handler/src/`: Generic language handler abstractions (request/response/rewriter) +- `__test__/*.spec.mjs`: Test files for each component \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9dc13c5..a8a15d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,47 @@ -[workspace] -resolver = "2" -members = [ - "crates/lang_handler", - "crates/php", - "crates/php_node" -] +[package] +edition = "2021" +name = "php-node" +version = "1.4.0" +authors = ["Platformatic Inc. (https://platformatic.dev)"] +license = "MIT" +repository = "https://github.com/platformatic/php-node" + +[features] +default = [] +napi-support = ["dep:napi", "dep:napi-derive", "dep:napi-build", "http-handler/napi-support", "http-rewriter/napi-support"] + +[lib] +name = "php_node" +crate-type = ["cdylib"] +path = "src/lib.rs" + +[dependencies] +async-trait = "0.1.88" +bytes = "1.10.1" +hostname = "0.4.1" +ext-php-rs = { git = "https://github.com/platformatic/ext-php-rs.git" } +http-handler = { git = "https://github.com/platformatic/http-handler.git" } +# http-handler = { path = "../http-handler" } +http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git" } +# http-rewriter = { path = "../http-rewriter" } +libc = "0.2.171" +# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +napi = { version = "3", default-features = false, features = ["napi4"], optional = true } +napi-derive = { version = "3", optional = true } +once_cell = "1.21.0" +tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] } +regex = "1.0" + +[dev-dependencies] +tokio-test = "0.4" + +[build-dependencies] +autotools = "0.2" +bindgen = "0.69.4" +cc = "1.1.7" +downloader = "0.2.8" +file-mode = "0.1.2" +napi-build = { version = "2.2.1", optional = true } # [profile.release] # lto = true diff --git a/__test__/headers.spec.mjs b/__test__/headers.spec.mjs index a267f8e..872d2c3 100644 --- a/__test__/headers.spec.mjs +++ b/__test__/headers.spec.mjs @@ -19,20 +19,20 @@ test('only last set is used for get', (t) => { const headers = new Headers() headers.set('Content-Type', 'application/json') headers.add('Content-Type', 'text/html') - t.is(headers.size, 1) + t.is(headers.size, 2) t.assert(headers.has('Content-Type')) - t.is(headers.get('Content-Type'), 'text/html') + t.is(headers.get('Content-Type'), 'application/json') }) test('adding a header with multiple values works and stores to a single entry', (t) => { const headers = new Headers() headers.add('Accept', 'application/json') headers.add('Accept', 'text/html') - t.is(headers.size, 1) + t.is(headers.size, 2) t.assert(headers.has('Accept')) t.deepEqual(headers.getAll('Accept'), ['application/json', 'text/html']) t.deepEqual(headers.getLine('Accept'), 'application/json,text/html') - t.deepEqual(headers.get('Accept'), 'text/html') + t.deepEqual(headers.get('Accept'), 'application/json') }) test('deleting a header adjusts size and removes value', (t) => { diff --git a/__test__/request.spec.mjs b/__test__/request.spec.mjs index 34ed448..4b135b3 100644 --- a/__test__/request.spec.mjs +++ b/__test__/request.spec.mjs @@ -33,7 +33,7 @@ test('full construction', (t) => { t.assert(req.body instanceof Buffer) t.is(req.body.toString('utf8'), 'Hello, from Node.js!') t.assert(req.headers instanceof Headers) - t.is(req.headers.size, 3) + t.is(req.headers.size, 4) t.is(req.headers.get('Content-Type'), 'application/json') t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html']) t.is(req.headers.get('X-Custom-Header'), 'CustomValue') @@ -58,7 +58,7 @@ test('construction with headers instance', (t) => { t.assert(req.body instanceof Buffer) t.is(req.body.toString('utf8'), 'Hello, from Node.js!') t.assert(req.headers instanceof Headers) - t.is(req.headers.size, 3) + t.is(req.headers.size, 4) t.is(req.headers.get('Content-Type'), 'application/json') t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html']) t.is(req.headers.get('X-Custom-Header'), 'CustomValue') diff --git a/__test__/rewriter.spec.mjs b/__test__/rewriter.spec.mjs index 964d28e..5610fbd 100644 --- a/__test__/rewriter.spec.mjs +++ b/__test__/rewriter.spec.mjs @@ -3,11 +3,13 @@ import test from 'ava' import { Request, Rewriter } from '../index.js' const docroot = import.meta.dirname +const filename = import.meta.filename.replace(docroot, '') test('existence condition', (t) => { const req = new Request({ + docroot, method: 'GET', - url: 'http://example.com/util.mjs', + url: `http://example.com${filename}`, headers: { TEST: ['foo'] } @@ -16,7 +18,7 @@ test('existence condition', (t) => { const rewriter = new Rewriter([ { conditions: [ - { type: 'exists' } + { type: 'exists', args: [] } ], rewriters: [ { @@ -27,11 +29,12 @@ test('existence condition', (t) => { } ]) - t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404') + t.is(rewriter.rewrite(req).url, 'http://example.com/404') }) test('non-existence condition', (t) => { const req = new Request({ + docroot, method: 'GET', url: 'http://example.com/index.php', headers: { @@ -53,7 +56,7 @@ test('non-existence condition', (t) => { } ]) - t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404') + t.is(rewriter.rewrite(req).url, 'http://example.com/404') }) test('condition groups - AND', (t) => { @@ -196,7 +199,7 @@ test('header rewriting', (t) => { test('href rewriting', (t) => { const rewriter = new Rewriter([{ rewriters: [ - { type: 'href', args: [ '^(.*)$', '/index.php?route=${1}' ] } + { type: 'href', args: [ '^http://example.com(.*)$', '/index.php?route=${1}' ] } ] }]) @@ -213,7 +216,7 @@ test('href rewriting', (t) => { test('method rewriting', (t) => { const rewriter = new Rewriter([{ rewriters: [ - { type: 'method', args: ['GET', 'POST'] } + { type: 'method', args: ['POST'] } ] }]) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8608e64 --- /dev/null +++ b/build.rs @@ -0,0 +1,22 @@ +use std::env; + +#[cfg(feature = "napi-support")] +extern crate napi_build; + +fn main() { + #[cfg(feature = "napi-support")] + napi_build::setup(); + + // Check for manual PHP_RPATH override, otherwise try LD_PRELOAD_PATH, + // and finally fallback to hard-coded /usr/local/lib path. + // + // PHP_RPATH may also be $ORIGIN to instruct the build to search for libphp + // in the same directory as the *.node bindings file. + let php_rpath = env::var("PHP_RPATH") + .or_else(|_| env::var("LD_PRELOAD_PATH")) + .unwrap_or("/usr/local/lib".to_string()); + + println!("cargo:rustc-link-search={php_rpath}"); + println!("cargo:rustc-link-lib=dylib=php"); + println!("cargo:rustc-link-arg=-Wl,-rpath,{php_rpath}"); +} diff --git a/crates/lang_handler/Cargo.toml b/crates/lang_handler/Cargo.toml deleted file mode 100644 index d13c053..0000000 --- a/crates/lang_handler/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -edition = "2021" -name = "lang_handler" -version = "0.0.0" - -[lib] -name = "lang_handler" -crate-type = ["cdylib", "rlib"] -path = "src/lib.rs" - -[features] -default = [] -napi = ["dep:napi", "dep:napi-build"] - -[build-dependencies] -napi-build = { version = "2.2.1", optional = true } - -[dependencies] -bytes = "1.10.1" -napi = { version = "3.0.0-beta.8", default-features = false, features = ["napi4"], optional = true } -regex = "1.11.1" -url = "2.5.4" diff --git a/crates/lang_handler/build.rs b/crates/lang_handler/build.rs deleted file mode 100644 index 4ecbc6c..0000000 --- a/crates/lang_handler/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(feature = "napi")] -extern crate napi_build; - -fn main() { - #[cfg(feature = "napi")] - napi_build::setup(); -} diff --git a/crates/lang_handler/src/handler.rs b/crates/lang_handler/src/handler.rs deleted file mode 100644 index b334c40..0000000 --- a/crates/lang_handler/src/handler.rs +++ /dev/null @@ -1,61 +0,0 @@ -use super::{Request, Response}; - -/// Enables a type to support handling HTTP requests. -/// -/// # Example -/// -/// ``` -/// use lang_handler::{Handler, Request, Response, ResponseBuilder}; -/// -/// struct MyHandler; -/// -/// impl Handler for MyHandler { -/// type Error = String; -/// -/// fn handle(&self, request: Request) -> Result { -/// let response = Response::builder() -/// .status(200) -/// .header("Content-Type", "text/plain") -/// .body(request.body()) -/// .build(); -/// -/// Ok(response) -/// } -/// } -pub trait Handler { - /// The type of error that can occur while handling a request. - type Error; - - /// Handles an HTTP request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Handler, Request, Response}; - /// - /// # struct MyHandler; - /// # impl Handler for MyHandler { - /// # type Error = String; - /// # - /// # fn handle(&self, request: Request) -> Result { - /// # let response = Response::builder() - /// # .status(200) - /// # .header("Content-Type", "text/plain") - /// # .body(request.body()) - /// # .build(); - /// # - /// # Ok(response) - /// # } - /// # } - /// # let handler = MyHandler; - /// # - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// let response = handler.handle(request).unwrap(); - /// ``` - fn handle(&self, request: Request) -> Result; -} diff --git a/crates/lang_handler/src/headers.rs b/crates/lang_handler/src/headers.rs deleted file mode 100644 index 3687193..0000000 --- a/crates/lang_handler/src/headers.rs +++ /dev/null @@ -1,457 +0,0 @@ -use std::collections::{hash_map::Entry, HashMap}; - -/// Represents a single HTTP header value or multiple values for the same header. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Header { - /// A single value for a header. - Single(String), - - /// Multiple values for a header, stored as a vector. - Multiple(Vec), -} - -impl From<&Header> for String { - fn from(header: &Header) -> String { - match header { - Header::Single(value) => value.to_owned(), - Header::Multiple(values) => values.join(","), - } - } -} - -// TODO: Figure out why From> conflicts with From> -impl From for Header { - fn from(value: String) -> Header { - Header::Single(value) - } -} - -impl From<&str> for Header { - fn from(value: &str) -> Header { - Header::Single(value.to_string()) - } -} - -impl From> for Header { - fn from(values: Vec) -> Header { - Header::Multiple(values) - } -} - -#[cfg(feature = "napi")] -mod napi_header { - use super::*; - - use napi::bindgen_prelude::*; - use napi::sys; - - impl FromNapiValue for Header { - unsafe fn from_napi_value(env: sys::napi_env, value: sys::napi_value) -> Result { - let mut header_type = sys::ValueType::napi_undefined; - unsafe { check_status!(sys::napi_typeof(env, value, &mut header_type)) }?; - - let header_type: ValueType = header_type.into(); - - match header_type { - ValueType::String => { - let s = String::from_napi_value(env, value)?; - Ok(Header::Single(s)) - } - ValueType::Object => { - let obj = Vec::::from_napi_value(env, value)?; - Ok(Header::Multiple(obj)) - } - _ => Err(napi::Error::new( - Status::InvalidArg, - "Expected a string or an object for Header", - )), - } - } - } - - impl ToNapiValue for Header { - unsafe fn to_napi_value(env: sys::napi_env, value: Self) -> Result { - match value { - Header::Single(value) => String::to_napi_value(env, value), - Header::Multiple(values) => Vec::::to_napi_value(env, values), - } - } - } -} - -/// A multi-map of HTTP headers. -/// -/// # Examples -/// -/// ``` -/// # use lang_handler::Headers; -/// let mut headers = Headers::new(); -/// headers.set("Content-Type", "text/plain"); -/// -/// assert_eq!(headers.get("Content-Type"), Some("text/plain".to_string())); -/// ``` -#[derive(Debug, Clone)] -pub struct Headers(HashMap); - -impl Headers { - /// Creates a new `Headers` instance. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let headers = Headers::new(); - /// ``` - pub fn new() -> Self { - Headers(HashMap::new()) - } - - /// Checks if a header field exists. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// - /// assert!(headers.has("Content-Type")); - /// assert!(!headers.has("Accept")); - /// ``` - pub fn has(&self, key: K) -> bool - where - K: AsRef, - { - self.0.contains_key(key.as_ref().to_lowercase().as_str()) - } - - /// Returns the last single value associated with a header field. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.add("Accept", "text/plain"); - /// headers.add("Accept", "application/json"); - /// - /// assert_eq!(headers.get("Accept"), Some("application/json".to_string())); - /// ``` - pub fn get(&self, key: K) -> Option - where - K: AsRef, - { - match self.0.get(key.as_ref().to_lowercase().as_str()) { - Some(Header::Single(value)) => Some(value.clone()), - Some(Header::Multiple(values)) => values.last().cloned(), - None => None, - } - } - - /// Returns all values associated with a header field as a `Vec`. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.add("Accept", "text/plain"); - /// headers.add("Accept", "application/json"); - /// - /// assert_eq!(headers.get_all("Accept"), vec![ - /// "text/plain".to_string(), - /// "application/json".to_string() - /// ]); - /// - /// headers.set("Content-Type", "text/plain"); - /// assert_eq!(headers.get_all("Content-Type"), vec!["text/plain".to_string()]); - /// ``` - pub fn get_all(&self, key: K) -> Vec - where - K: AsRef, - { - match self.0.get(key.as_ref().to_lowercase().as_str()) { - Some(Header::Single(value)) => vec![value.clone()], - Some(Header::Multiple(values)) => values.clone(), - None => Vec::new(), - } - } - - /// Returns all values associated with a header field as a single - /// comma-separated string. - /// - /// # Note - /// - /// Some headers support delivery as a comma-separated list of values, - /// but most require multiple header lines to send multiple values. - /// Typically you should use `get_all` rather than `get_line` and send - /// multiple header lines. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.add("Accept", "text/plain"); - /// headers.add("Accept", "application/json"); - /// - /// assert_eq!(headers.get_line("Accept"), Some("text/plain,application/json".to_string())); - /// ``` - pub fn get_line(&self, key: K) -> Option - where - K: AsRef, - { - self - .0 - .get(key.as_ref().to_lowercase().as_str()) - .map(|v| v.into()) - } - - /// Sets a header field, replacing any existing values. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// headers.set("Content-Type", "text/html"); - /// - /// assert_eq!(headers.get("Content-Type"), Some("text/html".to_string())); - /// ``` - pub fn set(&mut self, key: K, value: V) - where - K: Into, - V: Into
, - { - self.0.insert(key.into().to_lowercase(), value.into()); - } - - /// Add a header with the given value without replacing existing ones. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.add("Accept", "text/plain"); - /// headers.add("Accept", "application/json"); - /// - /// assert_eq!(headers.get_all("Accept"), vec![ - /// "text/plain".to_string(), - /// "application/json".to_string() - /// ]); - /// ``` - pub fn add(&mut self, key: K, value: V) - where - K: Into, - V: Into, - { - let key = key.into().to_lowercase(); - let value = value.into(); - - match self.0.entry(key) { - Entry::Vacant(e) => { - e.insert(Header::Single(value)); - } - Entry::Occupied(mut e) => { - let header = e.get_mut(); - *header = match header { - Header::Single(existing_value) => { - let mut values = vec![existing_value.clone()]; - values.push(value); - Header::Multiple(values) - } - Header::Multiple(values) => { - values.push(value); - Header::Multiple(values.clone()) - } - }; - } - } - } - - /// Removes a header field. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// headers.remove("Content-Type"); - /// - /// assert_eq!(headers.get("Content-Type"), None); - /// ``` - pub fn remove(&mut self, key: K) - where - K: AsRef, - { - self.0.remove(key.as_ref().to_lowercase().as_str()); - } - - /// Clears all headers. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::Headers; - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// headers.set("Accept", "application/json"); - /// headers.clear(); - /// - /// assert_eq!(headers.get("Content-Type"), None); - /// assert_eq!(headers.get("Accept"), None); - /// ``` - pub fn clear(&mut self) { - self.0.clear(); - } - - /// Returns the number of headers. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::Headers; - /// - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// headers.set("Accept", "application/json"); - /// - /// assert_eq!(headers.len(), 2); - /// ``` - pub fn len(&self) -> usize { - self.0.len() - } - - /// Checks if the headers are empty. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::Headers; - /// - /// let headers = Headers::new(); - /// - /// assert_eq!(headers.is_empty(), true); - /// ``` - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns an iterator over the headers. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{Headers, Header}; - /// let mut headers = Headers::new(); - /// headers.add("Accept", "text/plain"); - /// headers.add("Accept", "application/json"); - /// - /// for (key, values) in headers.iter() { - /// println!("{}: {:?}", key, values); - /// } - /// - /// # assert_eq!(headers.iter().collect::>(), vec![ - /// # (&"accept".to_string(), &Header::Multiple(vec![ - /// # "text/plain".to_string(), - /// # "application/json".to_string() - /// # ])) - /// # ]); - /// ``` - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } -} - -impl Default for Headers { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "napi")] -mod napi_headers { - use super::*; - - use std::ptr; - - use napi::bindgen_prelude::*; - use napi::sys; - - impl FromNapiValue for Headers { - unsafe fn from_napi_value(env: sys::napi_env, value: sys::napi_value) -> Result { - let mut header_type = sys::ValueType::napi_undefined; - unsafe { check_status!(sys::napi_typeof(env, value, &mut header_type)) }?; - - let header_type: ValueType = header_type.into(); - - if header_type != ValueType::Object { - return Err(napi::Error::new( - napi::Status::InvalidArg, - "Expected an object for Headers", - )); - } - - let mut headers = Headers::new(); - unsafe { - let mut keys: sys::napi_value = ptr::null_mut(); - check_status!( - sys::napi_get_property_names(env, value, &mut keys), - "Failed to get Headers property names" - )?; - - let mut key_count = 0; - check_status!(sys::napi_get_array_length(env, keys, &mut key_count))?; - - for i in 0..key_count { - let mut key: sys::napi_value = ptr::null_mut(); - check_status!( - sys::napi_get_element(env, keys, i, &mut key), - "Failed to get header name" - )?; - let key_str = String::from_napi_value(env, key)?; - let key_cstr = std::ffi::CString::new(key_str.clone())?; - - let mut header_value: sys::napi_value = ptr::null_mut(); - check_status!( - sys::napi_get_named_property(env, value, key_cstr.as_ptr(), &mut header_value), - "Failed to get header value" - )?; - - if let Ok(header) = Header::from_napi_value(env, header_value) { - headers.set(key_str, header); - } - } - } - - Ok(headers) - } - } - - impl ToNapiValue for Headers { - unsafe fn to_napi_value(env: sys::napi_env, value: Self) -> Result { - let mut result: sys::napi_value = ptr::null_mut(); - unsafe { - check_status!( - sys::napi_create_object(env, &mut result), - "Failed to create Headers object" - )?; - - for (key, header) in value.iter() { - let key_cstr = std::ffi::CString::new(key.to_string())?; - let value_napi_value = Header::to_napi_value(env, header.to_owned())?; - - check_status!( - sys::napi_set_named_property(env, result, key_cstr.as_ptr(), value_napi_value), - "Failed to set header property" - )?; - } - } - - Ok(result) - } - } -} diff --git a/crates/lang_handler/src/lib.rs b/crates/lang_handler/src/lib.rs deleted file mode 100644 index bca6a97..0000000 --- a/crates/lang_handler/src/lib.rs +++ /dev/null @@ -1,198 +0,0 @@ -#![warn(missing_docs)] - -//! # HTTP Request Management -//! -//! Lang Handler is a library intended for managing HTTP requests between -//! multiple languages. It provides types for representing Headers, Request, -//! and Response, as well as providing a Handler trait for dispatching -//! Request objects into some other system which produces a Response. -//! This may be another language runtime, or it could be a server application -//! directly in Rust. -//! -//! # Building a Request -//! -//! The `Request` type provides a `builder` method which allows you to -//! construct a `Request` object using a fluent API. This allows you to -//! set the URL, HTTP method, headers, body, and other properties of the -//! request in a clear and concise manner. -//! -//! ```rust -//! use lang_handler::{Request, RequestBuilder}; -//! -//! let request = Request::builder() -//! .method("GET") -//! .url("http://example.com") -//! .header("Accept", "application/json") -//! .build() -//! .expect("should build request"); -//! ``` -//! -//! # Reading a Request -//! -//! The `Request` type also provides methods to read the [`Url`], HTTP method, -//! [`Headers`], and body of the request. This allows you to access the -//! properties of the request in a straightforward manner. -//! -//! ```rust -//! # use lang_handler::Request; -//! # -//! # let request = Request::builder() -//! # .method("GET") -//! # .url("http://example.com") -//! # .header("Accept", "application/json") -//! # .build() -//! # .expect("should build request"); -//! # -//! assert_eq!(request.method(), "GET"); -//! assert_eq!(request.url().to_string(), "http://example.com/"); -//! assert_eq!(request.headers().get("Accept"), Some("application/json".to_string())); -//! assert_eq!(request.body(), ""); -//! ``` -//! -//! # Building a Response -//! -//! The `Response` type also provides a `builder` method which allows you to -//! construct a `Response` object using a fluent API. This allows you to -//! set the status code, [`Headers`], body, and other properties of the -//! response in a clear and concise manner. -//! -//! ```rust -//! use lang_handler::{Response, ResponseBuilder}; -//! -//! let response = Response::builder() -//! .status(200) -//! .header("Content-Type", "application/json") -//! .body("{\"message\": \"Hello, world!\"}") -//! .log("This is a log message") -//! .exception("This is an exception message") -//! .build(); -//! ``` -//! -//! # Reading a Response -//! -//! The `Response` type provides methods to read the status code, [`Headers`], -//! body, log, and exception of the response. -//! -//! ```rust -//! # use lang_handler::{Response, ResponseBuilder}; -//! # -//! # let response = Response::builder() -//! # .status(200) -//! # .header("Content-Type", "text/plain") -//! # .body("Hello, World!") -//! # .log("This is a log message") -//! # .exception("This is an exception message") -//! # .build(); -//! # -//! assert_eq!(response.status(), 200); -//! assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); -//! assert_eq!(response.body(), "Hello, World!"); -//! assert_eq!(response.log(), "This is a log message"); -//! assert_eq!(response.exception(), Some(&"This is an exception message".to_string())); -//! ``` -//! # Managing Headers -//! -//! The `Headers` type provides methods to read and manipulate HTTP headers. -//! -//! ```rust -//! use lang_handler::Headers; -//! -//! // Setting and getting headers -//! let mut headers = Headers::new(); -//! headers.set("Content-Type", "application/json"); -//! assert_eq!(headers.get("Content-Type"), Some("application/json".to_string())); -//! -//! // Checking if a header exists -//! assert!(headers.has("Content-Type")); -//! -//! // Removing headers -//! headers.remove("Content-Type"); -//! assert_eq!(headers.get("Content-Type"), None); -//! -//! // Adding multiple values to a header -//! headers.add("Set-Cookie", "sessionid=abc123"); -//! headers.add("Set-Cookie", "userid=42"); -//! -//! // Iterating over headers -//! for (name, value) in headers.iter() { -//! println!("{}: {:?}", name, value); -//! } -//! -//! // Getting all values for a header -//! let cookies = headers.get_all("Set-Cookie"); -//! assert_eq!(cookies, vec!["sessionid=abc123", "userid=42"]); -//! -//! // Getting a set of headers as a string line -//! headers.add("Accept", "text/plain"); -//! headers.add("Accept", "application/json"); -//! let accept_header = headers.get_line("Accept"); -//! -//! // Counting header lines -//! assert!(headers.len() > 0); -//! -//! // Clearing all headers -//! headers.clear(); -//! -//! // Checking if headers are empty -//! assert!(headers.is_empty()); -//! ``` -//! # Handling Requests -//! -//! The `Handler` trait is used to define how a [`Request`] is handled. It -//! provides a method `handle` which takes a [`Request`] and returns a -//! [`Response`]. This allows you to implement custom logic for handling -//! requests, such as routing them to different services or processing them -//! in some way. -//! -//! ```rust -//! use lang_handler::{ -//! Handler, -//! Request, -//! RequestBuilder, -//! Response, -//! ResponseBuilder -//! }; -//! -//! pub struct EchoServer; -//! impl Handler for EchoServer { -//! type Error = String; -//! fn handle(&self, request: Request) -> Result { -//! let response = Response::builder() -//! .status(200) -//! .body(request.body()) -//! .build(); -//! -//! Ok(response) -//! } -//! } -//! -//! let handler = EchoServer; -//! -//! let request = Request::builder() -//! .method("POST") -//! .url("http://example.com") -//! .header("Accept", "application/json") -//! .body("Hello, world!") -//! .build() -//! .expect("should build request"); -//! -//! let response = handler.handle(request) -//! .expect("should handle request"); -//! -//! assert_eq!(response.status(), 200); -//! assert_eq!(response.body(), "Hello, world!"); -//! ``` - -mod handler; -mod headers; -mod request; -mod response; -pub mod rewrite; -mod test; - -pub use handler::Handler; -pub use headers::{Header, Headers}; -pub use request::{Request, RequestBuilder, RequestBuilderException}; -pub use response::{Response, ResponseBuilder}; -pub use test::{MockRoot, MockRootBuilder}; -pub use url::Url; diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs deleted file mode 100644 index b6ffd28..0000000 --- a/crates/lang_handler/src/request.rs +++ /dev/null @@ -1,611 +0,0 @@ -use std::{fmt::Debug, net::SocketAddr}; - -use bytes::{Bytes, BytesMut}; -use url::Url; - -use super::Headers; - -/// Represents an HTTP request. Includes the method, URL, headers, and body. -/// -/// # Examples -/// -/// ``` -/// use lang_handler::{Request, Headers}; -/// -/// let request = Request::builder() -/// .method("POST") -/// .url("http://example.com/test.php") -/// .header("Accept", "text/html") -/// .header("Accept", "application/json") -/// .header("Host", "example.com") -/// .body("Hello, World!") -/// .build() -/// .expect("should build request"); -/// -/// assert_eq!(request.method(), "POST"); -/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); -/// assert_eq!(request.headers().get_all("Accept"), vec![ -/// "text/html".to_string(), -/// "application/json".to_string() -/// ]); -/// assert_eq!(request.headers().get("Host"), Some("example.com".to_string())); -/// assert_eq!(request.body(), "Hello, World!"); -/// ``` -#[derive(Clone, Debug)] -pub struct Request { - method: String, - url: Url, - headers: Headers, - // TODO: Support Stream bodies when napi.rs supports it - body: Bytes, - local_socket: Option, - remote_socket: Option, -} - -unsafe impl Sync for Request {} -unsafe impl Send for Request {} - -impl Request { - /// Creates a new `Request` with the given method, URL, headers, and body. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let mut headers = Headers::new(); - /// headers.set("Accept", "text/html"); - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// headers, - /// "Hello, World!", - /// None, - /// None, - /// ); - pub fn new>( - method: String, - url: Url, - headers: Headers, - body: T, - local_socket: Option, - remote_socket: Option, - ) -> Self { - Self { - method, - url, - headers, - body: body.into(), - local_socket, - remote_socket, - } - } - - /// Creates a new `RequestBuilder` to build a `Request`. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, RequestBuilder}; - /// - /// let request = Request::builder() - /// .method("POST") - /// .url("http://example.com/test.php") - /// .header("Content-Type", "text/html") - /// .header("Content-Length", 13.to_string()) - /// .body("Hello, World!") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.method(), "POST"); - /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); - /// assert_eq!(request.headers().get("Content-Type"), Some("text/html".to_string())); - /// assert_eq!(request.headers().get("Content-Length"), Some("13".to_string())); - /// assert_eq!(request.body(), "Hello, World!"); - /// ``` - pub fn builder() -> RequestBuilder { - RequestBuilder::new() - } - - /// Creates a new `RequestBuilder` to extend a `Request`. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, RequestBuilder}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com/test.php") - /// .header("Content-Type", "text/plain") - /// .build() - /// .expect("should build request"); - /// - /// let extended = request.extend() - /// .method("POST") - /// .header("Content-Length", 12.to_string()) - /// .body("Hello, World") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(extended.method(), "POST"); - /// assert_eq!(extended.url().as_str(), "http://example.com/test.php"); - /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); - /// assert_eq!(extended.headers().get("Content-Length"), Some("12".to_string())); - /// assert_eq!(extended.body(), "Hello, World"); - /// ``` - pub fn extend(&self) -> RequestBuilder { - RequestBuilder::extend(self) - } - - /// Returns the method of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// Headers::new(), - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.method(), "POST"); - /// ``` - pub fn method(&self) -> &str { - &self.method - } - - /// Returns the URL of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// Headers::new(), - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); - /// ``` - pub fn url(&self) -> &Url { - &self.url - } - - /// Returns the headers of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let mut headers = Headers::new(); - /// headers.set("Accept", "text/html"); - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// headers, - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); - /// ``` - pub fn headers(&self) -> &Headers { - &self.headers - } - - /// Returns the body of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// Headers::new(), - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.body(), "Hello, World!"); - /// ``` - pub fn body(&self) -> Bytes { - self.body.clone() - } - - /// Returns the local socket address of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// Headers::new(), - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.local_socket(), None); - /// ``` - pub fn local_socket(&self) -> Option { - self.local_socket - } - - /// Returns the remote socket address of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Request, Headers}; - /// - /// let request = Request::new( - /// "POST".to_string(), - /// "http://example.com/test.php".parse().unwrap(), - /// Headers::new(), - /// "Hello, World!", - /// None, - /// None, - /// ); - /// - /// assert_eq!(request.remote_socket(), None); - /// ``` - pub fn remote_socket(&self) -> Option { - self.remote_socket - } -} - -/// Errors which may be produced when building a Request from a RequestBuilder. -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum RequestBuilderException { - /// Url is required - UrlMissing, - /// Url could not be parsed - UrlParseFailed(String), - /// SocketAddr could not be parsed - SocketParseFailed(String), -} - -impl std::fmt::Display for RequestBuilderException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RequestBuilderException::UrlMissing => write!(f, "Expected url to be set"), - RequestBuilderException::UrlParseFailed(u) => write!(f, "Failed to parse url: \"{}\"", u), - RequestBuilderException::SocketParseFailed(s) => { - write!(f, "Failed to parse socket info: \"{}\"", s) - } - } - } -} - -/// Builds an HTTP request. -/// -/// # Examples -/// -/// ``` -/// use lang_handler::{Request, RequestBuilder}; -/// -/// let request = Request::builder() -/// .method("POST") -/// .url("http://example.com/test.php") -/// .header("Content-Type", "text/html") -/// .header("Content-Length", 13.to_string()) -/// .body("Hello, World!") -/// .build() -/// .expect("should build request"); -/// -/// assert_eq!(request.method(), "POST"); -/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); -/// assert_eq!(request.headers().get("Content-Type"), Some("text/html".to_string())); -/// assert_eq!(request.headers().get("Content-Length"), Some("13".to_string())); -/// assert_eq!(request.body(), "Hello, World!"); -/// ``` -#[derive(Clone)] -pub struct RequestBuilder { - method: Option, - url: Option, - headers: Headers, - body: BytesMut, - local_socket: Option, - remote_socket: Option, -} - -impl RequestBuilder { - /// Creates a new `RequestBuilder`. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let builder = RequestBuilder::new(); - /// ``` - pub fn new() -> Self { - Self { - method: None, - url: None, - headers: Headers::new(), - body: BytesMut::with_capacity(1024), - local_socket: None, - remote_socket: None, - } - } - - /// Creates a new `RequestBuilder` to extend an existing `Request`. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{Headers, Request, RequestBuilder}; - /// - /// let mut headers = Headers::new(); - /// headers.set("Accept", "text/html"); - /// - /// let request = Request::new( - /// "GET".to_string(), - /// "http://example.com".parse().unwrap(), - /// headers, - /// "Hello, World!", - /// None, - /// None - /// ); - /// - /// let extended = RequestBuilder::extend(&request) - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(extended.method(), "GET"); - /// assert_eq!(extended.url().as_str(), "http://example.com/"); - /// assert_eq!(extended.headers().get("Accept"), Some("text/html".to_string())); - /// assert_eq!(extended.body(), "Hello, World!"); - /// ``` - pub fn extend(request: &Request) -> Self { - Self { - method: Some(request.method().into()), - url: Some(request.url().to_string()), - headers: request.headers().clone(), - body: BytesMut::from(request.body()), - local_socket: request.local_socket.map(|s| s.to_string()), - remote_socket: request.remote_socket.map(|s| s.to_string()), - } - } - - /// Sets the method of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .method("POST") - /// .url("http://example.com/test.php") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.method(), "POST"); - /// ``` - pub fn method>(mut self, method: T) -> Self { - self.method = Some(method.into()); - self - } - - /// Sets the URL of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); - /// ``` - pub fn url(mut self, url: T) -> Self - where - T: Into, - { - self.url = Some(url.into()); - self - } - - /// Sets a header of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .header("Accept", "text/html") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); - /// ``` - pub fn header(mut self, key: K, value: V) -> Self - where - K: Into, - V: Into, - { - self.headers.add(key.into(), value.into()); - self - } - - /// Replaces the entire header set of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{RequestBuilder, Headers}; - /// - /// let mut headers = Headers::new(); - /// headers.set("Accept", "text/html"); - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .headers(headers) - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); - /// ``` - pub fn headers(mut self, headers: T) -> Self - where - T: Into, - { - self.headers = headers.into(); - self - } - - /// Sets the body of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .body("Hello, World!") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.body(), "Hello, World!"); - /// ``` - pub fn body>(mut self, body: T) -> Self { - self.body = body.into(); - self - } - - /// Sets the local socket of the request. - /// - /// # Examples - /// - /// ``` - /// use std::net::SocketAddr; - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .local_socket("127.0.0.1:8080") - /// .build() - /// .expect("should build request"); - /// - /// let expected = "127.0.0.1:8080" - /// .parse::() - /// .expect("should parse"); - /// assert_eq!(request.local_socket(), Some(expected)); - /// ``` - pub fn local_socket(mut self, local_socket: T) -> Self - where - T: Into, - { - self.local_socket = Some(local_socket.into()); - self - } - - /// Sets the remote socket of the request. - /// - /// # Examples - /// - /// ``` - /// use std::net::SocketAddr; - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .remote_socket("127.0.0.1:8080") - /// .build() - /// .expect("should build request"); - /// - /// let expected = "127.0.0.1:8080" - /// .parse::() - /// .expect("should parse"); - /// assert_eq!(request.remote_socket(), Some(expected)); - /// ``` - pub fn remote_socket(mut self, remote_socket: T) -> Self - where - T: Into, - { - self.remote_socket = Some(remote_socket.into()); - self - } - - /// Builds the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::RequestBuilder; - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.method(), "GET"); - /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); - /// assert_eq!(request.body(), ""); - /// ``` - pub fn build(self) -> Result { - Ok(Request { - method: self.method.unwrap_or_else(|| "GET".to_string()), - url: parse_url(self.url)?, - headers: self.headers, - body: self.body.freeze(), - local_socket: parse_socket(self.local_socket)?, - remote_socket: parse_socket(self.remote_socket)?, - }) - } -} - -impl Default for RequestBuilder { - fn default() -> Self { - Self::new() - } -} - -fn parse_url(url: Option) -> Result { - url - .ok_or(RequestBuilderException::UrlMissing) - .and_then(|u| { - u.parse() - .map_err(|_| RequestBuilderException::UrlParseFailed(u)) - }) -} - -fn parse_socket(socket: Option) -> Result, RequestBuilderException> { - socket.map_or_else( - || Ok(None), - |s| { - Ok(Some(s.parse::().map_err(|_| { - RequestBuilderException::SocketParseFailed(s) - })?)) - }, - ) -} diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs deleted file mode 100644 index 06f24cf..0000000 --- a/crates/lang_handler/src/response.rs +++ /dev/null @@ -1,454 +0,0 @@ -use bytes::{Bytes, BytesMut}; - -use super::Headers; - -/// Represents an HTTP response. This includes the status code, headers, body, log, and exception. -/// -/// # Example -/// -/// ``` -/// # use lang_handler::{Response, ResponseBuilder}; -/// let response = Response::builder() -/// .status(200) -/// .header("Content-Type", "text/plain") -/// .body("Hello, World!") -/// .build(); -/// -/// assert_eq!(response.status(), 200); -/// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); -/// assert_eq!(response.body(), "Hello, World!"); -/// ``` -#[derive(Clone, Debug)] -pub struct Response { - status: i32, - headers: Headers, - // TODO: Support Stream bodies when napi.rs supports it - body: Bytes, - log: Bytes, - exception: Option, -} - -impl Response { - /// Creates a new response with the given status code, headers, body, log, and exception. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::{Response, Headers}; - /// let mut headers = Headers::new(); - /// headers.set("Content-Type", "text/plain"); - /// - /// let response = Response::new(200, headers, "Hello, World!", "log", Some("exception".to_string())); - /// - /// assert_eq!(response.status(), 200); - /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); - /// assert_eq!(response.body(), "Hello, World!"); - /// assert_eq!(response.log(), "log"); - /// assert_eq!(response.exception(), Some(&"exception".to_string())); - /// ``` - pub fn new( - status: i32, - headers: Headers, - body: B, - log: L, - exception: Option, - ) -> Self - where - B: Into, - L: Into, - { - Self { - status, - headers, - body: body.into(), - log: log.into(), - exception, - } - } - - /// Creates a new response builder. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::Response; - /// let response = Response::builder() - /// .status(200) - /// .header("Content-Type", "text/plain") - /// .body("Hello, World!") - /// .build(); - /// - /// assert_eq!(response.status(), 200); - /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); - /// assert_eq!(response.body(), "Hello, World!"); - /// ``` - pub fn builder() -> ResponseBuilder { - ResponseBuilder::new() - } - - /// Create a new response builder that extends the given response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::{Response, ResponseBuilder}; - /// let response = Response::builder() - /// .status(200) - /// .header("Content-Type", "text/plain") - /// .body("Hello, World!") - /// .build(); - /// - /// let extended = response.extend() - /// .status(201) - /// .build(); - /// - /// assert_eq!(extended.status(), 201); - /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); - /// assert_eq!(extended.body(), "Hello, World!"); - /// ``` - pub fn extend(&self) -> ResponseBuilder { - ResponseBuilder::extend(self) - } - - /// Returns the status code of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::Response; - /// let response = Response::builder() - /// .status(200) - /// .build(); - /// - /// assert_eq!(response.status(), 200); - /// ``` - pub fn status(&self) -> i32 { - self.status - } - - /// Returns the [`Headers`] of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::{Response, Headers}; - /// let response = Response::builder() - /// .status(200) - /// .header("Content-Type", "text/plain") - /// .build(); - /// - /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); - /// ``` - pub fn headers(&self) -> &Headers { - &self.headers - } - - /// Returns the body of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::Response; - /// let response = Response::builder() - /// .status(200) - /// .body("Hello, World!") - /// .build(); - /// - /// assert_eq!(response.body(), "Hello, World!"); - /// ``` - pub fn body(&self) -> Bytes { - self.body.clone() - } - - /// Returns the log of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::Response; - /// let response = Response::builder() - /// .status(200) - /// .log("log") - /// .build(); - /// - /// assert_eq!(response.log(), "log"); - /// ``` - pub fn log(&self) -> Bytes { - self.log.clone() - } - - /// Returns the exception of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::Response; - /// let response = Response::builder() - /// .status(200) - /// .exception("exception") - /// .build(); - /// - /// assert_eq!(response.exception(), Some(&"exception".to_string())); - /// ``` - pub fn exception(&self) -> Option<&String> { - self.exception.as_ref() - } -} - -/// A builder for creating an HTTP response. -/// -/// # Example -/// -/// ``` -/// # use lang_handler::{Response, ResponseBuilder}; -/// let response = Response::builder() -/// .status(200) -/// .header("Content-Type", "text/plain") -/// .body("Hello, World!") -/// .build(); -/// -/// assert_eq!(response.status(), 200); -/// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); -/// assert_eq!(response.body(), "Hello, World!"); -/// ``` -#[derive(Clone, Debug)] -pub struct ResponseBuilder { - status: Option, - headers: Headers, - pub(crate) body: BytesMut, - pub(crate) log: BytesMut, - exception: Option, -} - -impl ResponseBuilder { - /// Creates a new response builder. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let builder = ResponseBuilder::new(); - /// ``` - pub fn new() -> Self { - ResponseBuilder { - status: None, - headers: Headers::new(), - body: BytesMut::with_capacity(1024), - log: BytesMut::with_capacity(1024), - exception: None, - } - } - - /// Builds the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let response = ResponseBuilder::new() - /// .build(); - /// - /// assert_eq!(response.status(), 200); - /// assert_eq!(response.body(), ""); - /// assert_eq!(response.log(), ""); - /// assert_eq!(response.exception(), None); - /// ``` - pub fn build(&self) -> Response { - Response { - status: self.status.unwrap_or(200), - headers: self.headers.clone(), - body: self.body.clone().freeze(), - log: self.log.clone().freeze(), - exception: self.exception.clone(), - } - } - - /// Creates a new response builder that extends the given response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::{Response, ResponseBuilder}; - /// let response = Response::builder() - /// .status(200) - /// .header("Content-Type", "text/plain") - /// .body("Hello, World!") - /// .build(); - /// - /// let extended = response.extend() - /// .status(201) - /// .build(); - /// - /// assert_eq!(extended.status(), 201); - /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); - /// assert_eq!(extended.body(), "Hello, World!"); - /// ``` - pub fn extend(response: &Response) -> Self { - ResponseBuilder { - status: Some(response.status), - headers: response.headers.clone(), - body: BytesMut::from(response.body()), - log: BytesMut::from(response.log()), - exception: response.exception.clone(), - } - } - - /// Sets the status code of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let response = ResponseBuilder::new() - /// .status(300) - /// .build(); - /// - /// assert_eq!(response.status(), 300); - /// ``` - pub fn status(&mut self, status: i32) -> &mut Self { - self.status = Some(status); - self - } - - /// Sets the headers of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let response = ResponseBuilder::new() - /// .header("Content-Type", "text/plain") - /// .build(); - /// - /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - K: Into, - V: Into, - { - self.headers.add(key, value); - self - } - - /// Replaces the entire header set of the request. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::{RequestBuilder, Headers}; - /// - /// let mut headers = Headers::new(); - /// headers.set("Accept", "text/html"); - /// - /// let request = RequestBuilder::new() - /// .url("http://example.com/test.php") - /// .headers(headers) - /// .build() - /// .expect("should build request"); - /// - /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); - /// ``` - pub fn headers(mut self, headers: T) -> Self - where - T: Into, - { - self.headers = headers.into(); - self - } - - /// Sets the body of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let builder = ResponseBuilder::new() - /// .body("Hello, World!") - /// .build(); - /// - /// assert_eq!(builder.body(), "Hello, World!"); - /// ``` - pub fn body>(&mut self, body: B) -> &mut Self { - self.body = body.into(); - self - } - - /// Appends to the body of the response. - /// - /// # Example - /// - /// ``` - /// use lang_handler::ResponseBuilder; - /// - /// let response = ResponseBuilder::new() - /// .body("Hello, ") - /// .body_write("World!") - /// .build(); - /// - /// assert_eq!(response.body(), "Hello, World!"); - /// ``` - pub fn body_write>(&mut self, body: B) -> &mut Self { - self.body.extend_from_slice(&body.into()); - self - } - - /// Sets the log of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let builder = ResponseBuilder::new() - /// .log("log") - /// .build(); - /// - /// assert_eq!(builder.log(), "log"); - /// ``` - pub fn log>(&mut self, log: L) -> &mut Self { - self.log = log.into(); - self - } - - /// Appends to the log of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let builder = ResponseBuilder::new() - /// .log_write("logs") - /// .log_write("more logs") - /// .build(); - /// - /// assert_eq!(builder.log(), "logs\nmore logs\n"); - /// ``` - pub fn log_write>(&mut self, log: L) -> &mut Self { - self.log.extend_from_slice(&log.into()); - self.log.extend_from_slice(b"\n"); - self - } - - /// Sets the exception of the response. - /// - /// # Example - /// - /// ``` - /// # use lang_handler::ResponseBuilder; - /// let builder = ResponseBuilder::new() - /// .exception("exception") - /// .build(); - /// - /// assert_eq!(builder.exception(), Some(&"exception".to_string())); - /// ``` - pub fn exception>(&mut self, exception: E) -> &mut Self { - self.exception = Some(exception.into()); - self - } -} - -impl Default for ResponseBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/lang_handler/src/rewrite/condition/closure.rs b/crates/lang_handler/src/rewrite/condition/closure.rs deleted file mode 100644 index 9ebf5c8..0000000 --- a/crates/lang_handler/src/rewrite/condition/closure.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::Path; - -use super::{Condition, Request}; - -impl Condition for F -where - F: Fn(&Request, &Path) -> bool + Sync + Send, -{ - /// Matches if calling the Fn(&Request) with the given request returns true - /// - /// # Examples - /// - /// ``` - /// # use std::path::Path; - /// # use lang_handler::{Request, rewrite::Condition}; - /// # let docroot = std::env::temp_dir(); - /// let condition = |request: &Request, _docroot: &Path| -> bool { - /// request.url().path().contains("/foo") - /// }; - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("request should build"); - /// - /// assert!(!condition.matches(&request, &docroot)); - /// ``` - fn matches(&self, request: &Request, docroot: &Path) -> bool { - self(request, docroot) - } -} diff --git a/crates/lang_handler/src/rewrite/condition/existence.rs b/crates/lang_handler/src/rewrite/condition/existence.rs deleted file mode 100644 index 8513e42..0000000 --- a/crates/lang_handler/src/rewrite/condition/existence.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::path::Path; - -use super::Condition; -use super::Request; - -/// Match if request path exists -#[derive(Clone, Debug, Default)] -pub struct ExistenceCondition; - -impl Condition for ExistenceCondition { - /// An ExistenceCondition matches a request if the path segment of the - /// request url exists in the provided base directory. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # rewrite::{Condition, ExistenceCondition}, - /// # Request, - /// # MockRoot - /// # }; - /// # - /// # let docroot = MockRoot::builder() - /// # .file("exists.php", "") - /// # .build() - /// # .expect("should prepare docroot"); - /// let condition = ExistenceCondition; - /// - /// let request = Request::builder() - /// .url("http://example.com/exists.php") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!condition.matches( - /// # &request.extend() - /// # .url("http://example.com/does_not_exist.php") - /// # .build() - /// # .expect("should build request"), - /// # &docroot - /// # )); - /// ``` - fn matches(&self, request: &Request, docroot: &Path) -> bool { - let path = request.url().path(); - docroot - .join(path.strip_prefix("/").unwrap_or(path)) - .canonicalize() - .is_ok() - } -} - -/// Match if request path does not exist -#[derive(Clone, Debug, Default)] -pub struct NonExistenceCondition; - -impl Condition for NonExistenceCondition { - /// A NonExistenceCondition matches a request if the path segment of the - /// request url does not exist in the provided base directory. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # rewrite::{Condition, NonExistenceCondition}, - /// # Request, - /// # MockRoot - /// # }; - /// # - /// # let docroot = MockRoot::builder() - /// # .file("exists.php", "") - /// # .build() - /// # .expect("should prepare docroot"); - /// let condition = NonExistenceCondition; - /// - /// let request = Request::builder() - /// .url("http://example.com/does_not_exist.php") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!condition.matches( - /// # &request.extend() - /// # .url("http://example.com/exists.php") - /// # .build() - /// # .expect("should build request"), - /// # &docroot - /// # )); - /// ``` - fn matches(&self, request: &Request, docroot: &Path) -> bool { - let path = request.url().path(); - docroot - .join(path.strip_prefix("/").unwrap_or(path)) - .canonicalize() - .is_err() - } -} diff --git a/crates/lang_handler/src/rewrite/condition/group.rs b/crates/lang_handler/src/rewrite/condition/group.rs deleted file mode 100644 index ea2804b..0000000 --- a/crates/lang_handler/src/rewrite/condition/group.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::path::Path; - -use super::{Condition, Request}; - -// Tested via Condition::and(...) and Condition::or(...) doctests - -/// This provides logical grouping of conditions using either AND or OR -/// combination behaviours. -pub enum ConditionGroup -where - A: Condition + ?Sized, - B: Condition + ?Sized, -{ - /// Combines two conditions using logical OR. - Or(Box, Box), - - /// Combines two conditions using logical AND. - And(Box, Box), -} - -impl ConditionGroup -where - A: Condition + ?Sized, - B: Condition + ?Sized, -{ - /// Constructs a new ConditionGroup with the given conditions combined using - /// logical AND. - /// - /// # Examples - /// - /// ``` - /// # use std::path::Path; - /// # use lang_handler::{Request, rewrite::{Condition, ConditionGroup}}; - /// # let docroot = std::env::temp_dir(); - /// let condition = ConditionGroup::and( - /// Box::new(|_req: &Request, _docroot: &Path| true), - /// Box::new(|_req: &Request, _docroot: &Path| false), - /// ); - /// - /// let request = Request::builder() - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// assert!(!condition.matches(&request, &docroot)); - /// # - /// # assert!(ConditionGroup::and( - /// # Box::new(|_req: &Request, _docroot: &Path| true), - /// # Box::new(|_req: &Request, _docroot: &Path| true), - /// # ).matches(&request, &docroot)); - /// ``` - pub fn and(a: Box, b: Box) -> Box { - Box::new(ConditionGroup::And(a, b)) - } - - /// Constructs a new ConditionGroup with the given conditions combined using - /// logical OR. - /// - /// # Examples - /// - /// ``` - /// # use std::path::Path; - /// # use lang_handler::{Request, rewrite::{Condition, ConditionGroup}}; - /// # let docroot = std::env::temp_dir(); - /// let condition = ConditionGroup::or( - /// Box::new(|_req: &Request, _docroot: &Path| true), - /// Box::new(|_req: &Request, _docroot: &Path| false), - /// ); - /// - /// let request = Request::builder() - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # - /// # assert!(!ConditionGroup::or( - /// # Box::new(|_req: &Request, _docroot: &Path| false), - /// # Box::new(|_req: &Request, _docroot: &Path| false), - /// # ).matches(&request, &docroot)); - pub fn or(a: Box, b: Box) -> Box { - Box::new(ConditionGroup::Or(a, b)) - } -} - -impl Condition for ConditionGroup -where - A: Condition + ?Sized, - B: Condition + ?Sized, -{ - /// Evaluates the condition group against the provided request. - /// - /// # Examples - /// - /// ``` - /// # use std::path::Path; - /// # let docroot = std::env::temp_dir(); - /// # use lang_handler::{Request, rewrite::{Condition, ConditionGroup}}; - /// let condition = ConditionGroup::or( - /// Box::new(|_req: &Request, _docroot: &Path| true), - /// Box::new(|_req: &Request, _docroot: &Path| false), - /// ); - /// - /// let request = Request::builder() - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!ConditionGroup::or( - /// # Box::new(|_req: &Request, _docroot: &Path| false), - /// # Box::new(|_req: &Request, _docroot: &Path| false), - /// # ).matches(&request, &docroot)); - /// ``` - fn matches(&self, request: &Request, docroot: &Path) -> bool { - match self { - ConditionGroup::Or(a, b) => a.matches(request, docroot) || b.matches(request, docroot), - ConditionGroup::And(a, b) => a.matches(request, docroot) && b.matches(request, docroot), - } - } -} diff --git a/crates/lang_handler/src/rewrite/condition/header.rs b/crates/lang_handler/src/rewrite/condition/header.rs deleted file mode 100644 index 41c5794..0000000 --- a/crates/lang_handler/src/rewrite/condition/header.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::{fmt::Debug, path::Path}; - -use regex::{Error, Regex}; - -use super::Condition; -use crate::Request; - -/// Matches a request header to a regex pattern -#[derive(Clone, Debug)] -pub struct HeaderCondition { - name: String, - pattern: Regex, -} - -impl HeaderCondition { - /// Construct a new HeaderCondition matching the given header name and Regex - /// pattern. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Condition, HeaderCondition}; - /// # use lang_handler::Request; - /// let condition = HeaderCondition::new("TEST", "^foo$") - /// .expect("should be valid regex"); - /// ``` - pub fn new(name: S, pattern: R) -> Result, Error> - where - S: Into, - R: TryInto, - Error: From<>::Error>, - { - let name = name.into(); - let pattern = pattern.try_into()?; - Ok(Box::new(Self { name, pattern })) - } -} - -impl Condition for HeaderCondition { - /// A HeaderCondition matches a given request if the header specified in the - /// constructor is both present and matches the given Regex pattern. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Condition, HeaderCondition}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let condition = HeaderCondition::new("TEST", "^foo$") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .header("TEST", "foo") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!condition.matches( - /// # &request.extend() - /// # .header("TEST", "bar") - /// # .build() - /// # .expect("should build request"), - /// # &docroot - /// # )); - /// ``` - fn matches(&self, request: &Request, _docroot: &Path) -> bool { - request - .headers() - .get_line(&self.name) - .map(|line| self.pattern.is_match(&line)) - .unwrap_or(false) - } -} diff --git a/crates/lang_handler/src/rewrite/condition/method.rs b/crates/lang_handler/src/rewrite/condition/method.rs deleted file mode 100644 index 7783a3f..0000000 --- a/crates/lang_handler/src/rewrite/condition/method.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{fmt::Debug, path::Path}; - -use regex::{Error, Regex}; - -use super::Condition; -use crate::Request; - -/// Matches a request method to a regex pattern -#[derive(Clone, Debug)] -pub struct MethodCondition(Regex); - -impl MethodCondition { - /// Construct a new MethodCondition matching the Request method to the given - /// Regex pattern. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Condition, MethodCondition}; - /// # use lang_handler::Request; - /// let condition = MethodCondition::new("GET") - /// .expect("should be valid regex"); - /// ``` - pub fn new(pattern: R) -> Result, Error> - where - R: TryInto, - Error: From<>::Error>, - { - let pattern = pattern.try_into()?; - Ok(Box::new(Self(pattern))) - } -} - -impl Condition for MethodCondition { - /// A MethodCondition matches a given request if the Request method matches - /// the given Regex pattern. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Condition, MethodCondition}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let condition = MethodCondition::new("GET") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!condition.matches( - /// # &request.extend() - /// # .method("POST") - /// # .build() - /// # .expect("should build request"), - /// # &docroot - /// # )); - /// ``` - fn matches(&self, request: &Request, _docroot: &Path) -> bool { - self.0.is_match(request.method()) - } -} diff --git a/crates/lang_handler/src/rewrite/condition/mod.rs b/crates/lang_handler/src/rewrite/condition/mod.rs deleted file mode 100644 index e9561c8..0000000 --- a/crates/lang_handler/src/rewrite/condition/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -mod closure; -mod existence; -mod group; -mod header; -mod method; -mod path; - -use std::path::Path; - -use crate::Request; - -pub use existence::{ExistenceCondition, NonExistenceCondition}; -pub use group::ConditionGroup; -pub use header::HeaderCondition; -pub use method::MethodCondition; -pub use path::PathCondition; - -/// A Condition is used to match against request state before deciding to apply -/// a given Rewrite or set of Rewrites. -pub trait Condition: Sync + Send { - /// A Condition must implement a `matches(request) -> bool` method which - /// receives a request object to determine if the condition is met. - fn matches(&self, request: &Request, docroot: &Path) -> bool; -} - -impl ConditionExt for T where T: Condition {} - -/// Extends Condition with combinators like `and` and `or`. -pub trait ConditionExt: Condition { - /// Make a new condition which must pass both conditions - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{Condition, ConditionExt, PathCondition, HeaderCondition} - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let path = PathCondition::new("^/index.php$") - /// .expect("should be valid regex"); - /// - /// let header = HeaderCondition::new("TEST", "^foo$") - /// .expect("should be valid regex"); - /// - /// let condition = path.and(header); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .header("TEST", "foo") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # - /// # // SHould _not_ match if either condition does not match - /// # let only_header = Request::builder() - /// # .url("http://example.com/nope.php") - /// # .header("TEST", "foo") - /// # .build() - /// # .expect("request should build"); - /// # - /// # assert!(!condition.matches(&only_header, &docroot)); - /// # - /// # let only_url = Request::builder() - /// # .url("http://example.com/index.php") - /// # .build() - /// # .expect("request should build"); - /// # - /// # assert!(!condition.matches(&only_url, &docroot)); - /// ``` - fn and(self: Box, other: Box) -> Box> - where - C: Condition + ?Sized, - { - ConditionGroup::and(self, other) - } - - /// Make a new condition which must pass either condition - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{Condition, ConditionExt, PathCondition, HeaderCondition} - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let path = PathCondition::new("^/index.php$") - /// .expect("should be valid regex"); - /// - /// let header = HeaderCondition::new("TEST", "^foo$") - /// .expect("should be valid regex"); - /// - /// let condition = path.or(header); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # - /// # // Should match if one condition does not - /// # let only_header = Request::builder() - /// # .url("http://example.com/nope.php") - /// # .header("TEST", "foo") - /// # .build() - /// # .expect("request should build"); - /// # - /// # assert!(condition.matches(&only_header, &docroot)); - /// # - /// # let only_url = Request::builder() - /// # .url("http://example.com/index.php") - /// # .build() - /// # .expect("request should build"); - /// # - /// # assert!(condition.matches(&only_url, &docroot)); - /// ``` - fn or(self: Box, other: Box) -> Box> - where - C: Condition + ?Sized, - { - ConditionGroup::or(self, other) - } -} diff --git a/crates/lang_handler/src/rewrite/condition/path.rs b/crates/lang_handler/src/rewrite/condition/path.rs deleted file mode 100644 index cfec01c..0000000 --- a/crates/lang_handler/src/rewrite/condition/path.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{fmt::Debug, path::Path}; - -use regex::{Error, Regex}; - -use super::Condition; -use super::Request; - -/// Match request path to a regex pattern -#[derive(Clone, Debug)] -pub struct PathCondition { - pattern: Regex, -} - -impl PathCondition { - /// Construct a new PathCondition matching the given Regex pattern. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::PathCondition; - /// let condition = PathCondition::new("^/index.php$") - /// .expect("should be valid regex"); - /// ``` - pub fn new(pattern: R) -> Result, Error> - where - R: TryInto, - Error: From<>::Error>, - { - let pattern = pattern.try_into()?; - Ok(Box::new(Self { pattern })) - } -} - -impl Condition for PathCondition { - /// A PathCondition matches a request if the path segment of the request url - /// matches the pattern given when constructing the PathCondition. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Condition, PathCondition}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let condition = PathCondition::new("^/index.php$") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// assert!(condition.matches(&request, &docroot)); - /// # assert!(!condition.matches( - /// # &request.extend() - /// # .url("http://example.com/other.php") - /// # .build() - /// # .expect("should build request"), - /// # &docroot - /// # )); - /// ``` - fn matches(&self, request: &Request, _docroot: &Path) -> bool { - self.pattern.is_match(request.url().path()) - } -} diff --git a/crates/lang_handler/src/rewrite/conditional_rewriter.rs b/crates/lang_handler/src/rewrite/conditional_rewriter.rs deleted file mode 100644 index 24a369e..0000000 --- a/crates/lang_handler/src/rewrite/conditional_rewriter.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::path::Path; - -use crate::{ - rewrite::{Condition, Rewriter}, - Request, RequestBuilderException, -}; - -// Tested via Rewriter::when(...) doc-test - -/// This provides a rewriter that applies another rewriter conditionally based -/// on a condition. -pub struct ConditionalRewriter(Box, Box) -where - R: Rewriter + ?Sized, - C: Condition + ?Sized; - -impl ConditionalRewriter -where - R: Rewriter + ?Sized, - C: Condition + ?Sized, -{ - /// Constructs a new ConditionalRewriter with the given rewriter and - /// condition. The rewriter will only be applied if the condition matches - /// the request. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::rewrite::{ - /// # Rewriter, - /// # ConditionalRewriter, - /// # PathCondition, - /// # PathRewriter - /// # }; - /// let condition = PathCondition::new("^/index\\.php$") - /// .expect("should be valid regex"); - /// - /// let rewriter = PathRewriter::new("^(.*)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let conditional_rewriter = - /// ConditionalRewriter::new(rewriter, condition); - /// ``` - pub fn new(rewriter: Box, condition: Box) -> Box { - Box::new(Self(rewriter, condition)) - } -} - -impl Rewriter for ConditionalRewriter -where - R: Rewriter + ?Sized, - C: Condition + ?Sized, -{ - /// A ConditionalRewriter matches a request if its condition matches the - /// request. If it does, the rewriter is applied to the request. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{ - /// # Condition, - /// # ConditionalRewriter, - /// # PathCondition, - /// # PathRewriter, - /// # Rewriter - /// # } - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let condition = PathCondition::new("^/index\\.php$") - /// .expect("should be valid regex"); - /// - /// let rewriter = PathRewriter::new("^(.*)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let conditional_rewriter = - /// ConditionalRewriter::new(rewriter, condition); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = conditional_rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/foo/index.php".to_string()); - /// # - /// # let request = Request::builder() - /// # .url("http://example.com/other.php") - /// # .build() - /// # .expect("should build request"); - /// # - /// # let new_request = conditional_rewriter.rewrite(request, &docroot) - /// # .expect("should rewrite request"); - /// # - /// # assert_eq!(new_request.url().path(), "/other.php".to_string()); - /// ``` - fn rewrite(&self, request: Request, docroot: &Path) -> Result { - if !self.1.matches(&request, docroot) { - return Ok(request); - } - - self.0.rewrite(request, docroot) - } -} diff --git a/crates/lang_handler/src/rewrite/mod.rs b/crates/lang_handler/src/rewrite/mod.rs deleted file mode 100644 index 9d95aa7..0000000 --- a/crates/lang_handler/src/rewrite/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! # Request Rewriting -//! -//! There are two sets of tools to manage request rewriting: -//! -//! - A [`Condition`] matches existing Request state by some given criteria. -//! - A [`Rewriter`] applies replacement logic to produce new Request state. -//! -//! # Conditions -//! -//! There are several types of [`Condition`] for matching Request state: -//! -//! - [`HeaderCondition`] matches if named header matches the given pattern. -//! - [`PathCondition`] matches if Request path matches the given pattern. -//! - [`ExistenceCondition`] matches if Request path resolves to a real file. -//! - [`NonExistenceCondition`] matches if Request path does not resolve. -//! -//! In addition to these core types, any function with a `Fn(&Request) -> bool` -//! signature may also be used anywhere a [`Condition`] is expected. This -//! allows any arbitrary logic to be applied to decide a match. Because a -//! Request may be dispatched to any thread, these functions must be -//! `Send + Sync`. -//! -//! ``` -//! # use lang_handler::{Request, rewrite::Condition}; -//! let condition = |request: &Request| -> bool { -//! request.url().path().starts_with("/foo") -//! }; -//! ``` -//! -//! Multiple [Condition] types may be grouped together to form logical -//! conditions using `condition.and(other)` or `condition.or(other)` to apply -//! conditions with AND or OR logic respectively. -//! -//! # Rewriters -//! -//! There are several types of [`Rewriter`] for rewriting Request state: -//! -//! - [`HeaderRewriter`] rewrites named header using pattern and replacement. -//! - [`PathRewriter`] rewrites Request path using pattern and replacement. -//! -//! As with [`Condition`], any function with a `Fn(Request) -> Request` -//! signature may also be used anywhere a [`Rewriter`] is accepted. This allows -//! any custom logic to be used to produce a rewritten Request. Because a -//! Request may be dispatched to any thread, these functions must be -//! `Send + Sync`. -//! -//! ``` -//! # use lang_handler::{Request, RequestBuilderException, rewrite::Rewriter}; -//! let rewriter = |request: Request| -> Result { -//! request.extend() -//! .url("http://example.com/rewritten") -//! .build() -//! }; -//! ``` -//! -//! Multiple Rewriters may be sequenced using `rewriter.then(other)` to apply -//! in order. -//! -//! # Combining Conditions and Rewriters -//! -//! Rewriters on their own _always_ apply, but this is generally not desirable -//! so Conditions exist to switch their application on or off. This is done -//! using `rewriter.when(condition)` to apply a [`Rewriter`] only when the given -//! [`Condition`] matches. -//! -//! # Complex sequencing -//! -//! Using the condition grouping and rewriter sequencing combinators, one can -//! achieve some quite complex rewriting logic. -//! -//! ```rust -//! # use lang_handler::rewrite::{ -//! # Condition, -//! # ConditionExt, -//! # HeaderCondition, -//! # PathCondition, -//! # Rewriter, -//! # RewriterExt, -//! # PathRewriter -//! # }; -//! # -//! let admin = { -//! let is_admin_path = PathCondition::new("^/admin") -//! .expect("regex is valid"); -//! -//! let is_admin_header = HeaderCondition::new("ADMIN_PASSWORD", "not-very-secure") -//! .expect("regex is valid"); -//! -//! let is_bypass = HeaderCondition::new("DEV_BYPASS", "do-not-use-this") -//! .expect("regex is valid"); -//! -//! let admin_conditions = is_admin_path -//! .and(is_admin_header) -//! .or(is_bypass); -//! -//! let admin_rewrite = PathRewriter::new("^(/admin)", "/secret") -//! .expect("regex is valid"); -//! -//! admin_rewrite.when(admin_conditions) -//! }; -//! -//! let login = { -//! let condition = PathCondition::new("^/login$") -//! .expect("regex is valid"); -//! -//! let rewriter = PathRewriter::new(".*", "/auth") -//! .expect("regex is valid"); -//! -//! rewriter.when(condition) -//! }; -//! -//! let rewrite_rules = admin.then(login); -//! ``` - -mod condition; -mod conditional_rewriter; -mod rewriter; - -pub use condition::*; -pub use conditional_rewriter::ConditionalRewriter; -pub use rewriter::*; diff --git a/crates/lang_handler/src/rewrite/rewriter/closure.rs b/crates/lang_handler/src/rewrite/rewriter/closure.rs deleted file mode 100644 index f0e6f3f..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/closure.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::path::Path; - -use super::{Request, RequestBuilderException, Rewriter}; - -impl Rewriter for F -where - F: Fn(Request, &Path) -> Result + Sync + Send, -{ - /// Rewrites the request by calling the Fn(&Request) with the given request - /// - /// # Examples - /// - /// ``` - /// # use std::path::Path; - /// # use lang_handler::{Request, rewrite::Rewriter}; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = |request: Request, docroot: &Path| { - /// request.extend() - /// .url("http://example.com/foo/bar") - /// .build() - /// }; - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("request should build"); - /// - /// let new_request = rewriter.rewrite(request, &docroot) - /// .expect("rewriting should succeed"); - /// - /// assert_eq!(new_request.url().path(), "/foo/bar".to_string()); - /// ``` - fn rewrite(&self, request: Request, docroot: &Path) -> Result { - self(request, docroot) - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/header.rs b/crates/lang_handler/src/rewrite/rewriter/header.rs deleted file mode 100644 index 83d42e3..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/header.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::path::Path; - -use regex::{Error, Regex}; - -use super::{Request, RequestBuilderException, Rewriter}; - -/// Rewrite a request header using a given pattern and replacement. -pub struct HeaderRewriter { - name: String, - pattern: Regex, - replacement: String, -} - -impl HeaderRewriter { - /// Construct a new HeaderRewriter to replace the named header using the - /// provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, HeaderRewriter}; - /// # use lang_handler::Request; - /// let rewriter = HeaderRewriter::new("TEST", "(foo)", "$1bar") - /// .expect("should be valid regex"); - /// ``` - pub fn new(name: N, pattern: R, replacement: S) -> Result, Error> - where - N: Into, - R: TryInto, - Error: From<>::Error>, - S: Into, - { - let name = name.into(); - let pattern = pattern.try_into()?; - let replacement = replacement.into(); - Ok(Box::new(Self { - name, - pattern, - replacement, - })) - } -} - -impl Rewriter for HeaderRewriter { - /// Rewrite named header using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, HeaderRewriter}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = HeaderRewriter::new("TEST", "(foo)", "${1}bar") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .header("TEST", "foo") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!( - /// new_request.headers().get("TEST"), - /// Some("foobar".to_string()) - /// ); - /// ``` - fn rewrite(&self, request: Request, _docroot: &Path) -> Result { - let HeaderRewriter { - name, - pattern, - replacement, - } = self; - - match request.headers().get(name) { - None => Ok(request), - Some(value) => request - .extend() - .header(name, pattern.replace(&value, replacement.clone())) - .build(), - } - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/href.rs b/crates/lang_handler/src/rewrite/rewriter/href.rs deleted file mode 100644 index f7f92da..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/href.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::path::Path; - -use regex::{Error, Regex}; -use url::Url; - -use super::{Request, RequestBuilderException, Rewriter}; - -/// Rewrite a request href using a given pattern and replacement. -pub struct HrefRewriter(Regex, String); - -impl HrefRewriter { - /// Construct HrefRewriter using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, HrefRewriter}; - /// # use lang_handler::Request; - /// let rewriter = HrefRewriter::new("^(/foo)$", "/index.php") - /// .expect("should be valid regex"); - /// ``` - pub fn new(pattern: R, replacement: S) -> Result, Error> - where - R: TryInto, - Error: From<>::Error>, - S: Into, - { - let pattern = pattern.try_into()?; - let replacement = replacement.into(); - Ok(Box::new(Self(pattern, replacement))) - } -} - -impl Rewriter for HrefRewriter { - /// Rewrite request path using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, HrefRewriter}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = HrefRewriter::new("^(.*)$", "/index.php?route=$1") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .url("http://example.com/foo/bar") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/index.php".to_string()); - /// assert_eq!(new_request.url().query(), Some("route=/foo/bar")); - /// ``` - fn rewrite(&self, request: Request, _docroot: &Path) -> Result { - let HrefRewriter(pattern, replacement) = self; - let url = request.url(); - - let input = { - let path = url.path(); - let query = url.query().map_or(String::new(), |q| format!("?{}", q)); - let fragment = url.fragment().map_or(String::new(), |f| format!("#{}", f)); - format!("{}{}{}", path, query, fragment) - }; - let output = pattern.replace(&input, replacement); - - // No change, return original request - if input == output { - return Ok(request); - } - - let base_url_string = format!("{}://{}", url.scheme(), url.authority()); - let base_url = Url::parse(&base_url_string) - .map_err(|_| RequestBuilderException::UrlParseFailed(base_url_string.clone()))?; - - let options = Url::options().base_url(Some(&base_url)); - - let copy = options.parse(output.as_ref()).map_err(|_| { - RequestBuilderException::UrlParseFailed(format!("{}{}", base_url_string, output)) - })?; - - request.extend().url(copy).build() - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/method.rs b/crates/lang_handler/src/rewrite/rewriter/method.rs deleted file mode 100644 index d5010d9..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/method.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::path::Path; - -use regex::{Error, Regex}; - -use super::{Request, RequestBuilderException, Rewriter}; - -/// Rewrite a request header using a given pattern and replacement. -pub struct MethodRewriter(Regex, String); - -impl MethodRewriter { - /// Construct a new MethodRewriter to replace the Request method using the - /// provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, MethodRewriter}; - /// # use lang_handler::Request; - /// let rewriter = MethodRewriter::new("PUT", "POST") - /// .expect("should be valid regex"); - /// ``` - pub fn new(pattern: R, replacement: S) -> Result, Error> - where - R: TryInto, - Error: From<>::Error>, - S: Into, - { - let pattern = pattern.try_into()?; - let replacement = replacement.into(); - Ok(Box::new(Self(pattern, replacement))) - } -} - -impl Rewriter for MethodRewriter { - /// Rewrite Request method using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, MethodRewriter}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = MethodRewriter::new("PUT", "POST") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .method("PUT") - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.method(), "POST".to_string()); - /// ``` - fn rewrite(&self, request: Request, _docroot: &Path) -> Result { - let MethodRewriter(pattern, replacement) = self; - - let input = request.method(); - let output = pattern.replace(input, replacement.clone()); - if output == input { - return Ok(request); - } - - request.extend().method(output).build() - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/mod.rs b/crates/lang_handler/src/rewrite/rewriter/mod.rs deleted file mode 100644 index ccacb69..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::path::Path; - -use crate::{ - rewrite::{Condition, ConditionalRewriter}, - Request, RequestBuilderException, -}; - -mod closure; -mod header; -mod href; -mod method; -mod path; -mod sequence; - -pub use header::HeaderRewriter; -pub use href::HrefRewriter; -pub use method::MethodRewriter; -pub use path::PathRewriter; -pub use sequence::RewriterSequence; - -/// A Rewriter simply applies its rewrite function to produce a possibly new -/// request object. -pub trait Rewriter: Sync + Send { - /// Rewrite a request using the rewriter's logic. - fn rewrite(&self, request: Request, docroot: &Path) -> Result; -} - -impl RewriterExt for T where T: Rewriter {} - -/// Extends Rewriter with combinators like `when` and `then`. -pub trait RewriterExt: Rewriter { - /// Add a condition to a rewriter to make it apply conditionally - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{Rewriter, RewriterExt, PathCondition, PathRewriter} - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = PathRewriter::new("^(/index\\.php)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let condition = PathCondition::new("^/index\\.php$") - /// .expect("should be valid regex"); - /// - /// let conditional_rewriter = rewriter.when(condition); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = conditional_rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/foo/index.php".to_string()); - /// ``` - fn when(self: Box, condition: Box) -> Box> - where - C: Condition + ?Sized, - { - ConditionalRewriter::new(self, condition) - } - - /// Add a rewriter to be applied in sequence. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{Rewriter, RewriterExt, PathRewriter, HeaderRewriter} - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let first = PathRewriter::new("^(/index.php)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let second = PathRewriter::new("foo/index", "foo/bar") - /// .expect("should be valid regex"); - /// - /// let sequence = first.then(second); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .header("TEST", "foo") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = sequence.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/foo/bar.php".to_string()); - /// ``` - fn then(self: Box, rewriter: Box) -> Box> - where - R: Rewriter + ?Sized, - { - RewriterSequence::new(self, rewriter) - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/path.rs b/crates/lang_handler/src/rewrite/rewriter/path.rs deleted file mode 100644 index 5bab304..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/path.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::path::Path; - -use regex::{Error, Regex}; - -use super::{Request, RequestBuilderException, Rewriter}; - -/// Rewrite a request path using a given pattern and replacement. -pub struct PathRewriter { - pattern: Regex, - replacement: String, -} - -impl PathRewriter { - /// Construct PathRewriter using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, PathRewriter}; - /// # use lang_handler::Request; - /// let rewriter = PathRewriter::new("^(/foo)$", "/index.php") - /// .expect("should be valid regex"); - /// ``` - pub fn new(pattern: R, replacement: S) -> Result, Error> - where - R: TryInto, - Error: From<>::Error>, - S: Into, - { - let pattern = pattern.try_into()?; - let replacement = replacement.into(); - Ok(Box::new(Self { - pattern, - replacement, - })) - } -} - -impl Rewriter for PathRewriter { - /// Rewrite request path using the provided regex pattern and replacement. - /// - /// # Examples - /// - /// ``` - /// # use lang_handler::rewrite::{Rewriter, PathRewriter}; - /// # use lang_handler::Request; - /// # let docroot = std::env::temp_dir(); - /// let rewriter = PathRewriter::new("^(/foo)$", "/index.php") - /// .expect("should be valid regex"); - /// - /// let request = Request::builder() - /// .url("http://example.com/foo") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = rewriter.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/index.php".to_string()); - /// ``` - fn rewrite(&self, request: Request, _docroot: &Path) -> Result { - let PathRewriter { - pattern, - replacement, - } = self; - - let input = request.url().path(); - let output = pattern.replace(input, replacement.clone()); - - // No change, return original request - if input == output { - return Ok(request); - } - - let mut copy = request.url().clone(); - copy.set_path(output.as_ref()); - - request.extend().url(copy).build() - } -} diff --git a/crates/lang_handler/src/rewrite/rewriter/sequence.rs b/crates/lang_handler/src/rewrite/rewriter/sequence.rs deleted file mode 100644 index 0382530..0000000 --- a/crates/lang_handler/src/rewrite/rewriter/sequence.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::path::Path; - -use super::{Request, RequestBuilderException, Rewriter}; - -// Tested via Rewriter::then(...) doc-test - -/// This provides sequencing of rewriters. -pub struct RewriterSequence(Box, Box) -where - A: Rewriter + ?Sized, - B: Rewriter + ?Sized; - -impl RewriterSequence -where - A: Rewriter + ?Sized, - B: Rewriter + ?Sized, -{ - /// Constructs a new RewriterSequence with the given rewriters applied in - /// sequence. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::rewrite::{Rewriter, RewriterSequence, PathRewriter}; - /// let first = PathRewriter::new("^(.*)$", "/bar$1") - /// .expect("should be valid regex"); - /// - /// let second = PathRewriter::new("^(.*)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let sequence = RewriterSequence::new(first, second); - /// ``` - pub fn new(a: Box, b: Box) -> Box { - Box::new(Self(a, b)) - } -} - -impl Rewriter for RewriterSequence -where - A: Rewriter + ?Sized, - B: Rewriter + ?Sized, -{ - /// Rewrite a request using the first rewriter, then the second. - /// - /// # Examples - /// - /// ```rust - /// # use std::path::Path; - /// # use lang_handler::{ - /// # Request, - /// # rewrite::{Rewriter, RewriterSequence, PathRewriter} - /// # }; - /// # let docroot = std::env::temp_dir(); - /// let first = PathRewriter::new("^(.*)$", "/bar$1") - /// .expect("should be valid regex"); - /// - /// let second = PathRewriter::new("^(.*)$", "/foo$1") - /// .expect("should be valid regex"); - /// - /// let sequence = RewriterSequence::new(first, second); - /// - /// let request = Request::builder() - /// .url("http://example.com/index.php") - /// .build() - /// .expect("should build request"); - /// - /// let new_request = sequence.rewrite(request, &docroot) - /// .expect("should rewrite request"); - /// - /// assert_eq!(new_request.url().path(), "/foo/bar/index.php".to_string()); - /// ``` - fn rewrite(&self, request: Request, docroot: &Path) -> Result { - let request = self.0.rewrite(request, docroot)?; - self.1.rewrite(request, docroot) - } -} diff --git a/crates/lang_handler/src/test.rs b/crates/lang_handler/src/test.rs deleted file mode 100644 index f9da5cd..0000000 --- a/crates/lang_handler/src/test.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::{ - collections::HashMap, - env::temp_dir, - fs::{create_dir_all, File}, - io::{Error, ErrorKind, Write}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, -}; - -/// A mock document root for testing purposes. -pub struct MockRoot(PathBuf); - -impl MockRoot { - /// Create a new MockRoot with the given document root and files. - /// - /// # Examples - /// - /// ``` - /// # use std::{collections::HashMap, env::temp_dir, path::PathBuf}; - /// # use lang_handler::MockRoot; - /// # let docroot = std::env::temp_dir().join("test"); - /// let files = HashMap::from([ - /// (PathBuf::new().join("file1.txt"), "Hello, world!".to_string()), - /// (PathBuf::new().join("file2.txt"), "Goodbye, world!".to_string()) - /// ]); - /// - /// let mock_root = MockRoot::new(&docroot, files) - /// .expect("should create mock root"); - /// ``` - pub fn new(docroot: D, files: H) -> Result - where - D: AsRef, - H: Into>, - { - let docroot = docroot.as_ref(); - create_dir_all(docroot)?; - - let map: HashMap = files.into(); - for (path, contents) in map.iter() { - let stripped = path.strip_prefix("/").unwrap_or(path); - - let file_path = docroot.join(stripped); - if let Some(parent) = file_path.parent() { - create_dir_all(parent)?; - } - - let mut file = File::create(file_path)?; - file.write_all(contents.as_bytes())?; - } - - // This unwrap should be safe due to creating the docroot base dir above. - Ok(Self( - docroot - .canonicalize() - .map_err(|err| Error::new(ErrorKind::Other, err))?, - )) - } - - /// Create a new MockRoot with the given document root and files. - /// - /// # Examples - /// - /// ``` - /// use lang_handler::MockRoot; - /// - /// let mock_root = MockRoot::builder() - /// .file("file1.txt", "Hello, world!") - /// .file("file2.txt", "Goodbye, world!") - /// .build() - /// .unwrap(); - /// ``` - pub fn builder() -> MockRootBuilder { - MockRootBuilder::default() - } -} - -// TODO: Somehow this happens too early? -// impl Drop for MockRoot { -// fn drop(&mut self) { -// remove_dir_all(&self.0).ok(); -// } -// } - -impl Deref for MockRoot { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MockRoot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// A builder for creating a MockRoot with specified files. -#[derive(Debug)] -pub struct MockRootBuilder(PathBuf, HashMap); - -impl MockRootBuilder { - /// Create a new MockRootBuilder with the specified document root. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::MockRootBuilder; - /// # let docroot = std::env::temp_dir().join("test"); - /// let builder = MockRootBuilder::new(&docroot); - /// ``` - pub fn new(docroot: D) -> Self - where - D: AsRef, - { - Self(docroot.as_ref().to_owned(), HashMap::new()) - } - - /// Add a file to the MockRootBuilder. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::MockRootBuilder; - /// # let docroot = std::env::temp_dir().join("test"); - /// let builder = MockRootBuilder::new(&docroot) - /// .file("bar.txt", "Hello, world!"); - /// ``` - pub fn file(mut self, path: P, contents: C) -> MockRootBuilder - where - P: AsRef, - C: Into, - { - let path = path.as_ref().to_owned(); - let contents = contents.into(); - - self.1.insert(path, contents); - self - } - - /// Build the MockRoot. - /// - /// # Examples - /// - /// ```rust - /// # use lang_handler::MockRootBuilder; - /// # let docroot = std::env::temp_dir().join("test"); - /// let root = MockRootBuilder::new(&docroot) - /// .file("bar.txt", "Hello, world!") - /// .build() - /// .expect("should create mock root"); - /// ``` - pub fn build(self) -> Result { - MockRoot::new(self.0, self.1) - } -} - -impl Default for MockRootBuilder { - fn default() -> Self { - Self::new(temp_dir().join("php-temp-dir-base")) - } -} diff --git a/crates/php/Cargo.toml b/crates/php/Cargo.toml deleted file mode 100644 index fd1cf67..0000000 --- a/crates/php/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -edition = "2021" -name = "php" -version = "0.0.0" - -[lib] -name = "php" -path = "src/lib.rs" - -[[bin]] -name = "php-main" -path = "src/main.rs" - -[dependencies] -bytes = "1.10.1" -hostname = "0.4.1" -ext-php-rs = { version = "0.14.0", features = ["embed"] } -lang_handler = { path = "../lang_handler", features = ["napi"] } -libc = "0.2.171" -once_cell = "1.21.0" - -[build-dependencies] -autotools = "0.2" -bindgen = "0.69.4" -cc = "1.1.7" -downloader = "0.2.8" -file-mode = "0.1.2" diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs deleted file mode 100644 index 5cf31b5..0000000 --- a/crates/php/src/main.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{env::current_dir, fs::File, io::Write, path::PathBuf}; - -use php::{ - rewrite::{PathRewriter, Rewriter}, - Embed, Handler, Request, -}; - -pub fn main() { - let _temp_file = TempFile::new("index.php", ""); - - let docroot = current_dir().expect("should have current_dir"); - - let rewriter = PathRewriter::new("test", "index").expect("should be valid regex"); - - let maybe_rewriter: Option> = Some(rewriter); - let embed = Embed::new_with_args(docroot, maybe_rewriter, std::env::args()) - .expect("should construct embed"); - - let request = Request::builder() - .method("POST") - .url("http://example.com/test.php") - .header("Content-Type", "text/html") - .header("Content-Length", 13.to_string()) - .body("Hello, World!") - .build() - .expect("should build request"); - - println!("request: {:#?}", request); - - let response = embed - .handle(request.clone()) - .expect("should handle request"); - - println!("response: {:#?}", response); -} - -struct TempFile(PathBuf); - -impl TempFile { - pub fn new(path: P, contents: S) -> Self - where - P: Into, - S: Into, - { - let path = path.into(); - let mut file = File::create(path.clone()).unwrap(); - file.write_all(contents.into().as_bytes()).unwrap(); - Self(path) - } -} - -impl Drop for TempFile { - fn drop(&mut self) { - std::fs::remove_file(&self.0).unwrap(); - } -} diff --git a/crates/php/src/request_context.rs b/crates/php/src/request_context.rs deleted file mode 100644 index 146bd04..0000000 --- a/crates/php/src/request_context.rs +++ /dev/null @@ -1,184 +0,0 @@ -use ext_php_rs::zend::SapiGlobals; -use lang_handler::{Request, ResponseBuilder}; -use std::{ffi::c_void, path::PathBuf}; - -/// The request context for the PHP SAPI. -#[derive(Debug)] -pub struct RequestContext { - request: Request, - response_builder: ResponseBuilder, - docroot: PathBuf, -} - -impl RequestContext { - /// Sets the current request context for the PHP SAPI. - /// - /// # Examples - /// - /// ``` - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// let context = RequestContext::current() - /// .expect("should acquire current context"); - /// - /// assert_eq!(context.request().method(), "GET"); - /// ``` - pub fn for_request(request: Request, docroot: S) - where - S: Into, - { - let context = Box::new(RequestContext { - request, - response_builder: ResponseBuilder::new(), - docroot: docroot.into(), - }); - let mut globals = SapiGlobals::get_mut(); - globals.server_context = Box::into_raw(context) as *mut c_void; - } - - /// Retrieve a mutable reference to the request context - /// - /// # Examples - /// - /// ``` - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// let current_context = RequestContext::current() - /// .expect("should acquire current context"); - /// - /// assert_eq!(current_context.request().method(), "GET"); - /// ``` - pub fn current<'a>() -> Option<&'a mut RequestContext> { - let ptr = { - let globals = SapiGlobals::get(); - globals.server_context as *mut RequestContext - }; - if ptr.is_null() { - return None; - } - - Some(unsafe { &mut *ptr }) - } - - /// Reclaim ownership of the RequestContext. Useful for dropping. - /// - /// # Example - /// - /// ``` - /// use ext_php_rs::zend::SapiGlobals; - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// RequestContext::reclaim() - /// .expect("should acquire current context"); - /// - /// assert_eq!(SapiGlobals::get().server_context, std::ptr::null_mut()); - /// ``` - #[allow(dead_code)] - pub fn reclaim() -> Option> { - let ptr = { - let mut globals = SapiGlobals::get_mut(); - std::mem::replace(&mut globals.server_context, std::ptr::null_mut()) - }; - if ptr.is_null() { - return None; - } - Some(unsafe { Box::from_raw(ptr as *mut RequestContext) }) - } - - /// Returns a reference to the request. - /// - /// # Examples - /// - /// ``` - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// let context = RequestContext::current() - /// .expect("should acquire current context"); - /// - /// assert_eq!(context.request().method(), "GET"); - /// ``` - pub fn request(&self) -> &Request { - &self.request - } - - /// Returns a mutable reference to the response builder. - /// - /// # Examples - /// - /// ``` - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// let mut context = RequestContext::current() - /// .expect("should acquire current context"); - /// - /// context.response_builder().status(200); - /// ``` - pub fn response_builder(&mut self) -> &mut ResponseBuilder { - &mut self.response_builder - } - - /// Returns the docroot associated with this request context - /// - /// # Examples - /// - /// ``` - /// # use std::path::PathBuf; - /// use php::{Request, RequestContext}; - /// - /// let request = Request::builder() - /// .method("GET") - /// .url("http://example.com") - /// .build() - /// .expect("should build request"); - /// - /// RequestContext::for_request(request, "/foo"); - /// - /// let mut context = RequestContext::current() - /// .expect("should acquire current context"); - /// - /// assert_eq!(context.docroot(), PathBuf::new().join("/foo")); - /// ``` - pub fn docroot(&self) -> PathBuf { - self.docroot.to_owned() - } -} diff --git a/crates/php_node/Cargo.toml b/crates/php_node/Cargo.toml deleted file mode 100644 index baa088c..0000000 --- a/crates/php_node/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -edition = "2021" -name = "php_node" -version = "0.0.0" - -[lib] -crate-type = ["cdylib"] -path = "src/lib.rs" - -[dependencies] -# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "3", default-features = false, features = ["napi4"] } -napi-derive = "3" -php = { path = "../php" } - -[build-dependencies] -napi-build = "2.2.1" diff --git a/crates/php_node/build.rs b/crates/php_node/build.rs deleted file mode 100644 index a0bc680..0000000 --- a/crates/php_node/build.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::env; - -extern crate napi_build; - -// use std::{env, fs}; -// use std::path::{PathBuf, Path}; - -fn main() { - napi_build::setup(); - - // Check for manual PHP_RPATH override, otherwise try LD_PRELOAD_PATH, - // and finally fallback to hard-coded /usr/local/lib path. - // - // PHP_RPATH may also be $ORIGIN to instruct the build to search for libphp - // in the same directory as the *.node bindings file. - let php_rpath = env::var("PHP_RPATH") - .or_else(|_| env::var("LD_PRELOAD_PATH")) - .unwrap_or("/usr/local/lib".to_string()); - - println!("cargo:rustc-link-search={}", php_rpath); - println!("cargo:rustc-link-lib=dylib=php"); - println!("cargo:rustc-link-arg=-Wl,-rpath,{}", php_rpath); - - // let out_dir = env::var("OUT_DIR").unwrap(); - - // // Tell cargo to look for shared libraries in the specified directory - // let php_dir = PathBuf::from("../../php-src/lib") - // .canonicalize() - // .expect("cannot canonicalize php-src path"); - // println!("cargo:rustc-link-search={}", php_dir.to_str().unwrap()); - // println!("cargo:rustc-link-search={}", out_dir); - - // // If on Linux or MacOS, tell the linker where the shared libraries are - // // on runtime (i.e. LD_LIBRARY_PATH) - // println!("cargo:rustc-link-arg=-Wl,-rpath,{}", out_dir); - - // // Tell cargo to link against the shared library for the specific platform. - // // IMPORTANT: On macOS and Linux the shared library must be linked without - // // the "lib" prefix and the ".so" suffix. On Windows the ".dll" suffix must - // // be omitted. - // let lib_file = match target_and_arch() { - // (Target::Linux, Arch::X86_64) => "libphp_linx64.so", - // (Target::Linux, Arch::AARCH64) => "libphp_linarm64.so", - // (Target::MacOS, Arch::X86_64) => "libphp_macx64.dylib", - // (Target::MacOS, Arch::AARCH64) => "libphp_macarm64.dylib" - // }; - - // copy_dylib_to_target_dir(lib_file); - // } - - // fn copy_dylib_to_target_dir(dylib: &str) { - // let out_dir = env::var("OUT_DIR").unwrap(); - // let src = Path::new("../php-src/lib"); - // let dst = Path::new(&out_dir); - // let _ = fs::copy(src.join(dylib), dst.join(dylib)); - // } - - // enum Target { - // Linux, - // MacOS - // } - - // enum Arch { - // X86_64, - // AARCH64, - // } - - // fn target_and_arch() -> (Target, Arch) { - // let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - // let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - // match (os.as_str(), arch.as_str()) { - // // Linux targets - // ("linux", "x86_64") => (Target::Linux, Arch::X86_64), - // ("linux", "aarch64") => (Target::Linux, Arch::AARCH64), - // // MacOS targets - // ("macos", "x86_64") => (Target::MacOS, Arch::X86_64), - // ("macos", "aarch64") => (Target::MacOS, Arch::AARCH64), - // _ => panic!("Unsupported operating system {} and architecture {}", os, arch), - // } -} diff --git a/crates/php_node/src/headers.rs b/crates/php_node/src/headers.rs deleted file mode 100644 index fbefe6c..0000000 --- a/crates/php_node/src/headers.rs +++ /dev/null @@ -1,345 +0,0 @@ -use std::ptr; - -use napi::bindgen_prelude::*; -use napi::Result; -use napi::{ - sys, - sys::{napi_env, napi_value}, -}; - -use php::{Header, Headers}; - -pub struct Entry(K, V); - -// This represents a map entries key/value pair. -impl ToNapiValue for Entry -where - T1: ToNapiValue, - T2: ToNapiValue, -{ - unsafe fn to_napi_value(env: napi_env, val: Self) -> Result { - let Entry(key, value) = val; - let key_napi_value = T1::to_napi_value(env, key)?; - let value_napi_value = T2::to_napi_value(env, value)?; - - let mut result: napi_value = ptr::null_mut(); - unsafe { - check_status!( - sys::napi_create_array_with_length(env, 2, &mut result), - "Failed to create entry key/value pair" - )?; - - check_status!( - sys::napi_set_element(env, result, 0, key_napi_value), - "Failed to set entry key" - )?; - - check_status!( - sys::napi_set_element(env, result, 1, value_napi_value), - "Failed to set entry value" - )?; - }; - - Ok(result) - } -} - -/// A multi-map of HTTP headers. -/// -/// # Examples -/// -/// ```js -/// const headers = new Headers(); -/// headers.set('Content-Type', 'application/json'); -/// const contentType = headers.get('Content-Type'); -/// ``` -#[napi(js_name = "Headers")] -#[derive(Debug, Clone, Default)] -pub struct PhpHeaders { - headers: Headers, -} - -impl PhpHeaders { - // Create a new PHP headers instance. - pub fn new(headers: Headers) -> Self { - PhpHeaders { headers } - } -} - -#[allow(clippy::from_over_into)] -impl Into for PhpHeaders { - fn into(self) -> Headers { - self.headers - } -} - -impl From for PhpHeaders { - fn from(headers: Headers) -> Self { - PhpHeaders { headers } - } -} - -// This replaces the FromNapiValue impl inherited from ClassInstance to allow -// unwrapping a PhpHeaders instance directly to Headers. This allows both -// object and instance form of Headers to be used interchangeably. -impl FromNapiValue for PhpHeaders { - unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { - let headers = ClassInstance::::from_napi_value(env, napi_val) - .map(|php_headers| php_headers.headers.clone()) - .or_else(|_| Headers::from_napi_value(env, napi_val))?; - - Ok(PhpHeaders { headers }) - } -} - -#[napi] -impl PhpHeaders { - /// Create a new PHP headers instance. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// ``` - #[napi(constructor)] - pub fn constructor(headers: Option) -> Self { - PhpHeaders { - headers: headers.unwrap_or_default(), - } - } - - /// Get the last set value for a given header key. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Accept', 'application/json'); - /// headers.set('Accept', 'text/html'); - /// - /// console.log(headers.get('Accept')); // text/html - /// ``` - #[napi] - pub fn get(&self, key: String) -> Option { - self.headers.get(&key) - } - - /// Get all values for a given header key. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Accept', 'application/json'); - /// headers.set('Accept', 'text/html'); - /// - /// for (const mime of headers.getAll('Accept')) { - /// console.log(mime); - /// } - /// ``` - #[napi] - pub fn get_all(&self, key: String) -> Vec { - self.headers.get_all(&key) - } - - /// Get all values for a given header key as a comma-separated string. - /// - /// This is useful for headers that can have multiple values, such as `Accept`. - /// But note that some headers like `Set-Cookie`, expect separate lines. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Accept', 'application/json'); - /// headers.set('Accept', 'text/html'); - /// - /// console.log(headers.getLine('Accept')); // application/json, text/html - /// ``` - #[napi] - pub fn get_line(&self, key: String) -> Option { - self.headers.get_line(&key) - } - - /// Check if a header key exists. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// - /// console.log(headers.has('Content-Type')); // true - /// console.log(headers.has('Accept')); // false - /// ``` - #[napi] - pub fn has(&self, key: String) -> bool { - self.headers.has(&key) - } - - /// Set a header key/value pair. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// ``` - #[napi] - pub fn set(&mut self, key: String, value: String) { - self.headers.set(key, value) - } - - /// Add a value to a header key. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Accept', 'application/json'); - /// headers.add('Accept', 'text/html'); - /// - /// console.log(headers.get('Accept')); // application/json, text/html - /// ``` - #[napi] - pub fn add(&mut self, key: String, value: String) { - self.headers.add(key, value) - } - - /// Delete a header key/value pair. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.delete('Content-Type'); - /// ``` - #[napi] - pub fn delete(&mut self, key: String) { - self.headers.remove(&key) - } - - /// Clear all header entries. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// headers.clear(); - /// - /// console.log(headers.has('Content-Type')); // false - /// console.log(headers.has('Accept')); // false - /// ``` - #[napi] - pub fn clear(&mut self) { - self.headers.clear() - } - - /// Get the number of header entries. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// - /// console.log(headers.size); // 2 - /// ``` - #[napi(getter)] - pub fn size(&self) -> u32 { - self.headers.len() as u32 - } - - /// Get an iterator over the header entries. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// - /// for (const [name, value] of headers.entries()) { - /// console.log(`${name}: ${value}`); - /// } - /// ``` - #[napi] - pub fn entries(&self) -> Vec> { - self - .headers - .iter() - .flat_map(|(k, v)| match v { - Header::Single(value) => vec![Entry(k.to_owned(), value.clone())], - Header::Multiple(vec) => vec - .iter() - .map(|value| Entry(k.to_owned(), value.clone())) - .collect::>>(), - }) - .collect() - } - - /// Get an iterator over the header keys. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// - /// for (const name of headers.keys()) { - /// console.log(name); - /// } - /// ``` - #[napi] - pub fn keys(&self) -> Vec { - self.headers.iter().map(|(k, _)| k.to_owned()).collect() - } - - /// Get an iterator over the header values. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// - /// for (const value of headers.values()) { - /// console.log(value); - /// } - /// ``` - #[napi] - pub fn values(&self) -> Vec { - self.entries().into_iter().map(|entry| entry.1).collect() - } - - /// Execute a callback for each header entry. - /// - /// # Examples - /// - /// ```js - /// const headers = new Headers(); - /// headers.set('Content-Type', 'application/json'); - /// headers.set('Accept', 'application/json'); - /// - /// headers.forEach((value, name, headers) => { - /// console.log(`${name}: ${value}`); - /// }); - /// ``` - #[napi] - pub fn for_each Result<()>>( - &self, - this: This, - callback: F, - ) -> Result<()> { - for entry in self.entries() { - callback(entry.1, entry.0, this)?; - } - Ok(()) - } -} diff --git a/crates/php_node/src/lib.rs b/crates/php_node/src/lib.rs deleted file mode 100644 index 4d583a5..0000000 --- a/crates/php_node/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] -extern crate napi_derive; - -mod headers; -mod request; -mod response; -mod rewriter; -mod runtime; - -pub use headers::PhpHeaders; -pub use request::PhpRequest; -pub use response::PhpResponse; -pub use rewriter::PhpRewriter; -pub use runtime::PhpRuntime; diff --git a/crates/php_node/src/request.rs b/crates/php_node/src/request.rs deleted file mode 100644 index 62bc25d..0000000 --- a/crates/php_node/src/request.rs +++ /dev/null @@ -1,200 +0,0 @@ -use napi::bindgen_prelude::*; -use napi::Result; - -use php::{Request, RequestBuilder}; - -use crate::PhpHeaders; - -#[napi(object)] -#[derive(Default)] -pub struct PhpRequestSocketOptions { - /// The string representation of the local IP address the remote client is connecting on. - pub local_address: String, - /// The numeric representation of the local port. For example, 80 or 21. - pub local_port: u16, - /// The string representation of the local IP family, e.g., "IPv4" or "IPv6". - pub local_family: String, - /// The string representation of the remote IP address. - pub remote_address: String, - /// The numeric representation of the remote port. For example, 80 or 21. - pub remote_port: u16, - /// The string representation of the remote IP family, e.g., "IPv4" or "IPv6". - pub remote_family: String, -} - -/// Options for creating a new PHP request. -#[napi(object)] -#[derive(Default)] -pub struct PhpRequestOptions { - /// The HTTP method for the request. - pub method: Option, - /// The URL for the request. - pub url: String, - /// The headers for the request. - pub headers: Option, - /// The body for the request. - pub body: Option, - /// The socket information for the request. - pub socket: Option, -} - -/// A PHP request. -/// -/// # Examples -/// -/// ```js -/// const request = new Request({ -/// method: 'GET', -/// url: 'http://example.com', -/// headers: { -/// 'Content-Type': ['application/json'] -/// }, -/// body: new Uint8Array([1, 2, 3, 4]) -/// }); -/// ``` -#[napi(js_name = "Request")] -pub struct PhpRequest { - pub(crate) request: Request, -} - -// Future ideas: -// - Support passing in a Node.js IncomingMessage object directly? -// - Support web standard Request objects? -#[napi] -impl PhpRequest { - /// Create a new PHP request. - /// - /// # Examples - /// - /// ```js - /// const request = new Request({ - /// method: 'GET', - /// url: 'http://example.com', - /// headers: { - /// 'Content-Type': ['application/json'] - /// }, - /// body: new Uint8Array([1, 2, 3, 4]) - /// }); - /// ``` - #[napi(constructor)] - pub fn constructor(options: PhpRequestOptions) -> Result { - let mut builder: RequestBuilder = Request::builder().url(&options.url); - - if let Some(method) = options.method { - builder = builder.method(method) - } - - fn sock_addr(family: &str, address: &str, port: u16) -> String { - if family == "IPv6" { - format!("[{}]:{}", address, port) - } else { - format!("{}:{}", address, port) - } - } - - if let Some(socket) = options.socket { - let local_socket = sock_addr( - &socket.local_family, - &socket.local_address, - socket.local_port, - ); - let remote_socket = sock_addr( - &socket.local_family, - &socket.remote_address, - socket.remote_port, - ); - - builder = builder - .local_socket(&local_socket) - .remote_socket(&remote_socket); - } - - if let Some(headers) = options.headers { - builder = builder.headers(headers); - } - - if let Some(body) = options.body { - builder = builder.body(body.as_ref()) - } - - Ok(PhpRequest { - request: builder - .build() - .map_err(|err| Error::from_reason(err.to_string()))?, - }) - } - - /// Get the HTTP method for the request. - /// - /// # Examples - /// - /// ```js - /// const request = new Request({ - /// method: 'GET' - /// }); - /// - /// console.log(request.method); - /// ``` - #[napi(getter, enumerable = true)] - pub fn method(&self) -> String { - self.request.method().to_owned() - } - - /// Get the URL for the request. - /// - /// # Examples - /// - /// ```js - /// const request = new Request({ - /// url: 'http://example.com' - /// }); - /// - /// console.log(request.url); - /// ``` - #[napi(getter, enumerable = true)] - pub fn url(&self) -> String { - self.request.url().as_str().to_owned() - } - - /// Get the headers for the request. - /// - /// # Examples - /// - /// ```js - /// const request = new Request({ - /// headers: { - /// 'Accept': ['application/json', 'text/html'] - /// } - /// }); - /// - /// for (const mime of request.headers.get('Accept')) { - /// console.log(mime); - /// } - /// ``` - #[napi(getter, enumerable = true)] - pub fn headers(&self) -> PhpHeaders { - PhpHeaders::new(self.request.headers().clone()) - } - - /// Get the body for the request. - /// - /// # Examples - /// - /// ```js - /// const request = new Request({ - /// body: new Uint8Array([1, 2, 3, 4]) - /// }); - /// - /// console.log(request.body); - /// ``` - #[napi(getter, enumerable = true)] - pub fn body(&self) -> Buffer { - self.request.body().to_vec().into() - } -} - -impl From<&PhpRequest> for Request { - fn from(request: &PhpRequest) -> Self { - request.request.clone() - } -} diff --git a/crates/php_node/src/response.rs b/crates/php_node/src/response.rs deleted file mode 100644 index fecbb72..0000000 --- a/crates/php_node/src/response.rs +++ /dev/null @@ -1,165 +0,0 @@ -use napi::bindgen_prelude::*; -use napi::Result; - -use php::Response; - -use crate::PhpHeaders; - -/// Options for creating a new PHP response. -#[napi(object)] -#[derive(Default)] -pub struct PhpResponseOptions { - /// The HTTP status code for the response. - pub status: Option, - /// The headers for the response. - pub headers: Option, - /// The body for the response. - pub body: Option, - /// The log for the response. - pub log: Option, - /// The exception for the response. - pub exception: Option, -} - -/// A PHP response. -#[napi(js_name = "Response")] -pub struct PhpResponse { - response: Response, -} - -impl PhpResponse { - // Create a new PHP response instance. - pub fn new(response: Response) -> Self { - PhpResponse { response } - } -} - -#[napi] -impl PhpResponse { - /// Create a new PHP response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// status: 200, - /// headers: { - /// 'Content-Type': ['application/json'] - /// }, - /// body: new Uint8Array([1, 2, 3, 4]) - /// }); - /// ``` - #[napi(constructor)] - pub fn constructor(options: Option) -> Result { - let options = options.unwrap_or_default(); - let mut builder = Response::builder(); - - if let Some(status) = options.status { - builder.status(status); - } - - if let Some(headers) = options.headers { - builder = builder.headers(headers); - } - - if let Some(body) = options.body { - builder.body(body.as_ref()); - } - - if let Some(log) = options.log { - builder.log(log.as_ref()); - } - - if let Some(exception) = options.exception { - builder.exception(exception); - } - - Ok(PhpResponse { - response: builder.build(), - }) - } - - /// Get the HTTP status code for the response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// status: 200 - /// }); - /// - /// console.log(response.status); - /// ``` - #[napi(getter, enumerable = true)] - pub fn status(&self) -> u32 { - self.response.status() as u32 - } - - /// Get the headers for the response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// headers: { - /// 'Content-Type': ['application/json'] - /// } - /// }); - /// - /// for (const mime of response.headers.get('Content-Type')) { - /// console.log(mime); - /// } - /// ``` - #[napi(getter, enumerable = true)] - pub fn headers(&self) -> PhpHeaders { - PhpHeaders::new(self.response.headers().clone()) - } - - /// Get the body for the response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// body: new Uint8Array([1, 2, 3, 4]) - /// }); - /// - /// console.log(response.body); - /// ``` - #[napi(getter, enumerable = true)] - pub fn body(&self) -> Buffer { - self.response.body().to_vec().into() - } - - /// Get the log for the response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// log: new Uint8Array([1, 2, 3, 4]) - /// }); - /// - /// console.log(response.log); - /// ``` - #[napi(getter, enumerable = true)] - pub fn log(&self) -> Buffer { - self.response.log().to_vec().into() - } - - /// Get the exception for the response. - /// - /// # Examples - /// - /// ```js - /// const response = new Response({ - /// exception: 'An error occurred' - /// }); - /// - /// console.log(response.exception); - /// ``` - #[napi(getter, enumerable = true)] - pub fn exception(&self) -> Option { - self.response.exception().map(|v| v.to_owned()) - } -} diff --git a/crates/php_node/src/rewriter.rs b/crates/php_node/src/rewriter.rs deleted file mode 100644 index bf3dbb9..0000000 --- a/crates/php_node/src/rewriter.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::{path::Path, str::FromStr}; - -// use napi::bindgen_prelude::*; -use napi::{Error, Result}; - -use php::{ - rewrite::{ - Condition, ConditionExt, ExistenceCondition, HeaderCondition, HeaderRewriter, HrefRewriter, - MethodCondition, MethodRewriter, NonExistenceCondition, PathCondition, PathRewriter, Rewriter, - RewriterExt, - }, - Request, RequestBuilderException, -}; - -use crate::PhpRequest; - -// -// Conditions -// - -#[napi(object)] -#[derive(Clone, Debug, Default)] -pub struct PhpRewriteCondOptions { - #[napi(js_name = "type")] - pub cond_type: String, - pub args: Option>, -} - -pub enum PhpRewriteCond { - Exists, - Header(String, String), - Method(String), - NotExists, - Path(String), -} - -impl Condition for PhpRewriteCond { - fn matches(&self, request: &php::Request, docroot: &Path) -> bool { - match self { - PhpRewriteCond::Exists => ExistenceCondition.matches(request, docroot), - PhpRewriteCond::Header(name, pattern) => { - HeaderCondition::new(name.as_str(), pattern.as_str()) - .map(|v| v.matches(request, docroot)) - .unwrap_or_default() - } - PhpRewriteCond::Method(pattern) => MethodCondition::new(pattern.as_str()) - .map(|v| v.matches(request, docroot)) - .unwrap_or_default(), - PhpRewriteCond::NotExists => NonExistenceCondition.matches(request, docroot), - PhpRewriteCond::Path(pattern) => PathCondition::new(pattern.as_str()) - .map(|v| v.matches(request, docroot)) - .unwrap_or_default(), - } - } -} - -impl TryFrom<&PhpRewriteCondOptions> for Box { - type Error = Error; - - fn try_from(value: &PhpRewriteCondOptions) -> std::result::Result { - let PhpRewriteCondOptions { cond_type, args } = value; - let cond_type = cond_type.to_lowercase(); - let args = args.to_owned().unwrap_or(vec![]); - match cond_type.as_str() { - "exists" => { - if args.is_empty() { - Ok(Box::new(PhpRewriteCond::Exists)) - } else { - Err(Error::from_reason("Wrong number of parameters")) - } - } - "header" => match args.len() { - 2 => { - let name = args[0].to_owned(); - let pattern = args[1].to_owned(); - Ok(Box::new(PhpRewriteCond::Header(name, pattern))) - } - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - "method" => match args.len() { - 1 => Ok(Box::new(PhpRewriteCond::Method(args[0].to_owned()))), - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - "not_exists" | "not-exists" => { - if args.is_empty() { - Ok(Box::new(PhpRewriteCond::NotExists)) - } else { - Err(Error::from_reason("Wrong number of parameters")) - } - } - "path" => match args.len() { - 1 => Ok(Box::new(PhpRewriteCond::Path(args[0].to_owned()))), - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - _ => Err(Error::from_reason(format!( - "Unknown condition type: {}", - cond_type - ))), - } - } -} - -// -// Rewriters -// - -#[napi(object)] -#[derive(Clone, Debug, Default)] -pub struct PhpRewriterOptions { - #[napi(js_name = "type")] - pub rewriter_type: String, - pub args: Vec, -} - -pub enum PhpRewriterType { - Header(String, String, String), - Href(String, String), - Method(String, String), - Path(String, String), -} - -impl Rewriter for PhpRewriterType { - fn rewrite( - &self, - request: Request, - docroot: &Path, - ) -> std::result::Result { - match self { - PhpRewriterType::Path(pattern, replacement) => { - PathRewriter::new(pattern.as_str(), replacement.as_str()) - .map(|v| v.rewrite(request.clone(), docroot)) - .unwrap_or(Ok(request)) - } - PhpRewriterType::Href(pattern, replacement) => { - HrefRewriter::new(pattern.as_str(), replacement.as_str()) - .map(|v| v.rewrite(request.clone(), docroot)) - .unwrap_or(Ok(request)) - } - PhpRewriterType::Method(pattern, replacement) => { - MethodRewriter::new(pattern.as_str(), replacement.as_str()) - .map(|v| v.rewrite(request.clone(), docroot)) - .unwrap_or(Ok(request)) - } - PhpRewriterType::Header(name, pattern, replacement) => { - HeaderRewriter::new(name.as_str(), pattern.as_str(), replacement.as_str()) - .map(|v| v.rewrite(request.clone(), docroot)) - .unwrap_or(Ok(request)) - } - } - } -} - -impl TryFrom<&PhpRewriterOptions> for Box { - type Error = Error; - - fn try_from(value: &PhpRewriterOptions) -> std::result::Result { - let PhpRewriterOptions { - rewriter_type, - args, - } = value; - let rewriter_type = rewriter_type.to_lowercase(); - match rewriter_type.as_str() { - "header" => match args.len() { - 3 => { - let name = args[0].to_owned(); - let pattern = args[1].to_owned(); - let replacement = args[2].to_owned(); - Ok(Box::new(PhpRewriterType::Header( - name, - pattern, - replacement, - ))) - } - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - "href" => match args.len() { - 2 => { - let pattern = args[0].to_owned(); - let replacement = args[1].to_owned(); - Ok(Box::new(PhpRewriterType::Href(pattern, replacement))) - } - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - "method" => match args.len() { - 2 => { - let pattern = args[0].to_owned(); - let replacement = args[1].to_owned(); - Ok(Box::new(PhpRewriterType::Method(pattern, replacement))) - } - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - "path" => match args.len() { - 2 => { - let pattern = args[0].to_owned(); - let replacement = args[1].to_owned(); - Ok(Box::new(PhpRewriterType::Path(pattern, replacement))) - } - _ => Err(Error::from_reason("Wrong number of parameters")), - }, - _ => Err(Error::from_reason(format!( - "Unknown rewriter type: {}", - rewriter_type - ))), - } - } -} - -// -// Conditional Rewriter -// - -pub enum OperationType { - And, - Or, -} -impl FromStr for OperationType { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s { - "and" | "&&" => Ok(OperationType::And), - "or" | "||" => Ok(OperationType::Or), - op => Err(Error::from_reason(format!( - "Unrecognized operation type: {}", - op - ))), - } - } -} - -#[napi(object)] -#[derive(Clone, Debug, Default)] -pub struct PhpConditionalRewriterOptions { - pub operation: Option, - pub conditions: Option>, - pub rewriters: Vec, -} - -pub struct PhpConditionalRewriter(Box); - -impl Rewriter for PhpConditionalRewriter { - fn rewrite( - &self, - request: Request, - docroot: &Path, - ) -> std::result::Result { - self.0.rewrite(request, docroot) - } -} - -impl TryFrom<&PhpConditionalRewriterOptions> for Box { - type Error = Error; - - fn try_from(value: &PhpConditionalRewriterOptions) -> std::result::Result { - let value = value.clone(); - - let operation = value - .operation - .clone() - .unwrap_or("and".into()) - .parse::()?; - - let rewriter = value - .rewriters - .iter() - .try_fold(None::>, |state, next| { - let converted: std::result::Result, Error> = next.try_into(); - converted.map(|converted| { - let res: Option> = match state { - None => Some(converted), - Some(last) => Some(last.then(converted)), - }; - res - }) - })?; - - let condition = value.conditions.unwrap_or_default().iter().try_fold( - None::>, - |state, next| { - let converted: std::result::Result, Error> = next.try_into(); - converted.map(|converted| { - let res: Option> = match state { - None => Some(converted), - Some(last) => Some(match operation { - OperationType::Or => last.or(converted), - OperationType::And => last.and(converted), - }), - }; - res - }) - }, - )?; - - match rewriter { - None => Err(Error::from_reason("No rewriters provided")), - Some(rewriter) => Ok(Box::new(PhpConditionalRewriter(match condition { - None => rewriter, - Some(condition) => rewriter.when(condition), - }))), - } - } -} - -// -// Rewriter JS type -// - -#[napi(js_name = "Rewriter")] -pub struct PhpRewriter(Vec); - -#[napi] -impl PhpRewriter { - #[napi(constructor)] - pub fn constructor(options: Vec) -> Result { - Ok(PhpRewriter(options)) - } - - #[napi] - pub fn rewrite(&self, request: &PhpRequest, docroot: String) -> Result { - let rewriter = self.into_rewriter()?; - let docroot = Path::new(&docroot); - Ok(PhpRequest { - request: rewriter - .rewrite(request.request.to_owned(), docroot) - .map_err(|err| { - Error::from_reason(format!("Failed to rewrite request: {}", err.to_string())) - })?, - }) - } - - pub fn into_rewriter(&self) -> Result> { - if self.0.is_empty() { - return Err(Error::from_reason("No rewrite rules provided")); - } - - let rewriter = self - .0 - .iter() - .try_fold(None::>, |state, next| { - let converted: std::result::Result, Error> = next.try_into(); - converted.map(|converted| { - let res: Option> = match state { - None => Some(converted), - Some(last) => Some(last.then(converted)), - }; - res - }) - })?; - - match rewriter { - None => Err(Error::from_reason("No rewriters provided")), - Some(rewriter) => Ok(rewriter), - } - } -} diff --git a/index.d.ts b/index.d.ts index 3860669..3da03ae 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,27 +1,30 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ /** - * A multi-map of HTTP headers. + * Wraps an http::HeaderMap instance to expose it to JavaScript. * - * # Examples - * - * ```js - * const headers = new Headers(); - * headers.set('Content-Type', 'application/json'); - * const contentType = headers.get('Content-Type'); - * ``` + * It provides methods to access and modify HTTP headers, iterate over them, + * and convert them to a JSON object representation. */ export declare class Headers { /** - * Create a new PHP headers instance. + * Create a new Headers instance. * * # Examples * * ```js - * const headers = new Headers(); + * const headers = new Headers({ + * 'Content-Type': 'application/json', + * 'Accept': ['text/html', 'application/json'] + * }); + * + * console.log(headers.get('Content-Type')); // application/json + * for (const mime of headers.getAll('Accept')) { + * console.log(mime); // text/html, application/json + * } * ``` */ - constructor(headers?: Headers | undefined | null) + constructor(options?: HeaderMap | undefined | null) /** * Get the last set value for a given header key. * @@ -65,10 +68,26 @@ export declare class Headers { * headers.set('Accept', 'application/json'); * headers.set('Accept', 'text/html'); * - * console.log(headers.getLine('Accept')); // application/json, text/html + * console.log(headers.getLine('Accept')); // application/json,text/html * ``` */ getLine(key: string): string | null + /** + * Clear all header entries. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.set('Accept', 'application/json'); + * headers.clear(); + * + * console.log(headers.has('Content-Type')); // false + * console.log(headers.has('Accept')); // false + * ``` + */ + clear(): void /** * Check if a header key exists. * @@ -93,7 +112,7 @@ export declare class Headers { * headers.set('Content-Type', 'application/json'); * ``` */ - set(key: string, value: string): void + set(key: string, value: HeaderMapValue): boolean /** * Add a value to a header key. * @@ -107,7 +126,7 @@ export declare class Headers { * console.log(headers.get('Accept')); // application/json, text/html * ``` */ - add(key: string, value: string): void + add(key: string, value: string): boolean /** * Delete a header key/value pair. * @@ -119,23 +138,7 @@ export declare class Headers { * headers.delete('Content-Type'); * ``` */ - delete(key: string): void - /** - * Clear all header entries. - * - * # Examples - * - * ```js - * const headers = new Headers(); - * headers.set('Content-Type', 'application/json'); - * headers.set('Accept', 'application/json'); - * headers.clear(); - * - * console.log(headers.has('Content-Type')); // false - * console.log(headers.has('Accept')); // false - * ``` - */ - clear(): void + delete(key: string): boolean /** * Get the number of header entries. * @@ -165,7 +168,7 @@ export declare class Headers { * } * ``` */ - entries(): Array> + entries(): Array<[string, string]> /** * Get an iterator over the header keys. * @@ -214,149 +217,122 @@ export declare class Headers { * ``` */ forEach(this: this, callback: (arg0: string, arg1: string, arg2: this) => void): void + /** + * Convert the headers to a JSON object representation. + * + * # Examples + * + * ```js + * const headers = new Headers({ + * 'Content-Type': 'application/json', + * 'Accept': ['text/html', 'application/json'] + * }); + * + * console.log(headers.toJSON()); + * ``` + */ + toJSON(): object } -export type PhpHeaders = Headers /** - * A PHP instance. - * - * # Examples - * - * ```js - * const php = new Php({ - * code: 'echo "Hello, world!";' - * }); - * - * const response = php.handleRequest(new Request({ - * method: 'GET', - * url: 'http://example.com' - * })); + * Wraps an http::Request instance to expose it to JavaScript. * - * console.log(response.status); - * console.log(response.body); - * ``` + * It provides methods to access the HTTP method, URI, headers, and body of + * the request along with a toJSON method to convert it to a JSON object. */ -export declare class Php { +export declare class Request { /** - * Create a new PHP instance. + * Create a new Request from a Request instance. * * # Examples * * ```js - * const php = new Php({ - * docroot: process.cwd(), - * argv: process.argv + * const request = new Request({ + * method: 'GET', + * url: 'http://example.com', + * headers: { + * 'Accept': ['application/json', 'text/html'] + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })), * }); * ``` */ - constructor(options?: PhpOptions | undefined | null) + constructor(options?: RequestOptions | undefined | null) /** - * Handle a PHP request. + * Get the HTTP method for the request. * * # Examples * * ```js - * const php = new Php({ - * docroot: process.cwd(), - * argv: process.argv + * const request = new Request({ + * method: "GET", + * url: "/index.php" * }); * - * const response = php.handleRequest(new Request({ - * method: 'GET', - * url: 'http://example.com' - * })); - * - * console.log(response.status); - * console.log(response.body); + * console.log(request.method); // GET * ``` */ - handleRequest(request: Request, signal?: AbortSignal | undefined | null): Promise + get method(): string /** - * Handle a PHP request synchronously. + * Set the HTTP method for the request. * * # Examples * * ```js - * const php = new Php({ - * docroot: process.cwd(), - * argv: process.argv + * const request = new Request({ + * url: "/index.php" * }); * - * const response = php.handleRequestSync(new Request({ - * method: 'GET', - * url: 'http://example.com' - * })); - * - * console.log(response.status); - * console.log(response.body); + * request.method = "POST"; + * console.log(request.method); // POST * ``` */ - handleRequestSync(request: Request): Response -} -export type PhpRuntime = Php - -/** - * A PHP request. - * - * # Examples - * - * ```js - * const request = new Request({ - * method: 'GET', - * url: 'http://example.com', - * headers: { - * 'Content-Type': ['application/json'] - * }, - * body: new Uint8Array([1, 2, 3, 4]) - * }); - * ``` - */ -export declare class Request { + set method(method: string) /** - * Create a new PHP request. + * Get the full URL for the request, including scheme and authority. * * # Examples * * ```js * const request = new Request({ - * method: 'GET', - * url: 'http://example.com', - * headers: { - * 'Content-Type': ['application/json'] - * }, - * body: new Uint8Array([1, 2, 3, 4]) + * url: "https://example.com/index.php" * }); + * + * console.log(request.url); // https://example.com/index.php * ``` */ - constructor(options: PhpRequestOptions) + get url(): string /** - * Get the HTTP method for the request. + * Set the URL for the request. * * # Examples * * ```js * const request = new Request({ - * method: 'GET' + * url: "https://example.com/index.php" * }); * - * console.log(request.method); + * request.url = "https://example.com/new-url"; + * console.log(request.url); // https://example.com/new-url * ``` */ - get method(): string + set url(url: string) /** - * Get the URL for the request. + * Get the path portion of the URL for the request. * * # Examples * * ```js * const request = new Request({ - * url: 'http://example.com' + * url: "https://example.com/api/users?id=123" * }); * - * console.log(request.url); + * console.log(request.path); // /api/users * ``` */ - get url(): string + get path(): string /** * Get the headers for the request. * @@ -364,38 +340,155 @@ export declare class Request { * * ```js * const request = new Request({ + * url: "/index.php", * headers: { - * 'Accept': ['application/json', 'text/html'] + * 'Content-Type': ['application/json'] * } * }); * - * for (const mime of request.headers.get('Accept')) { - * console.log(mime); + * for (const mime of request.headers.getAll('Content-Type')) { + * console.log(mime); // application/json * } * ``` */ get headers(): Headers /** - * Get the body for the request. + * Set the headers for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: "/index.php" + * }); + * + * request.headers = new Headers({ + * 'Content-Type': ['application/json'] + * }); + * + * for (const mime of request.headers.getAll('Content-Type')) { + * console.log(mime); // application/json + * } + * ``` + */ + set headers(headers: Headers) + /** + * Get the document root for the request, if applicable. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: "/index.php", + * docroot: "/var/www/html" + * }); + * + * console.log(request.docroot); // /var/www/html + * ``` + */ + get docroot(): string | null + /** + * Set the document root for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: "/index.php" + * }); + * + * request.docroot = "/var/www/html"; + * console.log(request.docroot); // /var/www/html + * ``` + */ + set docroot(docroot: string) + /** + * Get the body of the request as a Buffer. * * # Examples * * ```js * const request = new Request({ - * body: new Uint8Array([1, 2, 3, 4]) + * url: "/v2/api/thing", + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) * }); * - * console.log(request.body); + * console.log(request.body.toString()); // {"message":"Hello, world!"} * ``` */ get body(): Buffer + /** + * Set the body of the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: "/v2/api/thing" + * }); + * + * request.body = Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })); + * + * console.log(request.body.toString()); // {"message":"Hello, world!"} + * ``` + */ + set body(body: Buffer) + /** + * Convert the response to a JSON object representation. + * + * # Examples + * + * ```js + * const request = new Request({ + * method: "GET", + * url: "https://example.com/index.php", + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) + * }); + * + * console.log(request.toJSON()); + * ``` + */ + toJSON(): object } -export type PhpRequest = Request -/** A PHP response. */ +/** + * Wraps an http::Response instance to expose it to JavaScript. + * + * It provides methods to access the status code, headers, and body of the + * response along with a toJSON method to convert it to a JSON object. + * + * # Examples + * + * ```js + * const response = new Response({ + * status: 200, + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) + * }); + * + * console.log(response.status); // 200 + * for (const mime of response.headers.getAll('Content-Type')) { + * console.log(mime); // application/json + * } + * console.log(response.body.toString()); // {"message":"Hello, world!"} + * ``` + */ export declare class Response { /** - * Create a new PHP response. + * Create a new Response from a Response instance. * * # Examples * @@ -405,11 +498,11 @@ export declare class Response { * headers: { * 'Content-Type': ['application/json'] * }, - * body: new Uint8Array([1, 2, 3, 4]) + * body: Buffer.from(JSON.stringify({ message: 'Hello, world!' })) * }); * ``` */ - constructor(options?: PhpResponseOptions | undefined | null) + constructor(options?: ResponseOptions | undefined | null) /** * Get the HTTP status code for the response. * @@ -417,13 +510,32 @@ export declare class Response { * * ```js * const response = new Response({ - * status: 200 + * status: 200, + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) * }); * - * console.log(response.status); + * console.log(response.status); // 200 * ``` */ get status(): number + /** + * Set the HTTP status code for the response. + * + * # Examples + * + * ```js + * const response = new Response(); + * + * response.status = 404; + * console.log(response.status); // 404 + * ``` + */ + set status(status: number) /** * Get the headers for the response. * @@ -433,99 +545,158 @@ export declare class Response { * const response = new Response({ * headers: { * 'Content-Type': ['application/json'] - * } + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) * }); * * for (const mime of response.headers.get('Content-Type')) { - * console.log(mime); + * console.log(mime); // application/json * } * ``` */ get headers(): Headers /** - * Get the body for the response. + * Set the headers for the response. + * + * # Examples + * + * ```js + * const response = new Response(); + * + * response.headers = new Headers({ + * 'Content-Type': ['application/json'] + * }); + * + * for (const mime of response.headers.getAll('Content-Type')) { + * console.log(mime); // application/json + * } + * ``` + */ + set headers(headers: Headers) + /** + * Get the body of the response as a Buffer. * * # Examples * * ```js * const response = new Response({ - * body: new Uint8Array([1, 2, 3, 4]) + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) * }); * - * console.log(response.body); + * console.log(response.body.toString()); // {"message":"Hello, world!"} * ``` */ get body(): Buffer /** - * Get the log for the response. + * Set the body of the response. + * + * # Examples + * + * ```js + * const response = new Response(); + * + * response.body = Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })); + * + * console.log(response.body.toString()); // {"message":"Hello, world!"} + * ``` + */ + set body(body: Buffer) + /** + * Get the log of the response as a Buffer. * * # Examples * * ```js * const response = new Response({ - * log: new Uint8Array([1, 2, 3, 4]) + * log: Buffer.from('Log message') * }); * - * console.log(response.log); + * console.log(response.log.toString()); // Log message * ``` */ get log(): Buffer /** - * Get the exception for the response. + * Get the exception of the response. * * # Examples * * ```js * const response = new Response({ - * exception: 'An error occurred' + * exception: 'Error message' * }); * - * console.log(response.exception); + * console.log(response.exception); // Error message * ``` */ get exception(): string | null + /** + * Convert the response to a JSON object representation. + * + * # Examples + * + * ```js + * const response = new Response({ + * status: 200, + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: Buffer.from(JSON.stringify({ + * message: 'Hello, world!' + * })) + * }); + * + * console.log(response.toJSON()); + * ``` + */ + toJSON(): object } -export type PhpResponse = Response -export declare class Rewriter { - constructor(options: Array) - rewrite(request: Request, docroot: string): Request -} -export type PhpRewriter = Rewriter +/** A multi-map of HTTP headers. Any given header key can have multiple values. */ +export type HeaderMap = + Record -export interface PhpConditionalRewriterOptions { - operation?: string - conditions?: Array - rewriters: Array -} +/** A header entry value, which can be either a string or array of strings. */ +export type HeaderMapValue = + string | Array -/** Options for creating a new PHP instance. */ -export interface PhpOptions { - /** The command-line arguments for the PHP instance. */ - argv?: Array - /** The document root for the PHP instance. */ - docroot?: string - /** Throw request errors */ - throwRequestErrors?: boolean - /** Request rewriter */ - rewriter?: Rewriter -} - -/** Options for creating a new PHP request. */ -export interface PhpRequestOptions { +/** Input options for creating a Request. */ +export interface RequestOptions { /** The HTTP method for the request. */ method?: string /** The URL for the request. */ url: string /** The headers for the request. */ - headers?: Headers + headers?: Headers | HeaderMap /** The body for the request. */ - body?: Uint8Array + body?: Buffer /** The socket information for the request. */ - socket?: PhpRequestSocketOptions + socket?: SocketInfo + /** Document root for the request, if applicable. */ + docroot?: string } -export interface PhpRequestSocketOptions { +/** Input options for creating a Response. */ +export interface ResponseOptions { + /** The HTTP method for the request. */ + status?: number + /** The headers for the request. */ + headers?: Headers | HeaderMap + /** The body for the request. */ + body?: Buffer + /** The log output for the request. */ + log?: Buffer + /** The exception output for the request. */ + exception?: string +} + +/** Input options for creating a SocketInfo. */ +export interface SocketInfo { /** The string representation of the local IP address the remote client is connecting on. */ localAddress: string /** The numeric representation of the local port. For example, 80 or 21. */ @@ -539,27 +710,692 @@ export interface PhpRequestSocketOptions { /** The string representation of the remote IP family, e.g., "IPv4" or "IPv6". */ remoteFamily: string } +/** A N-API wrapper for the `ConditionalRewriter` type. */ +export declare class ConditionalRewriter { + /** + * Rewrite the given request if the condition matches. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} -/** Options for creating a new PHP response. */ -export interface PhpResponseOptions { - /** The HTTP status code for the response. */ - status?: number - /** The headers for the response. */ - headers?: Headers - /** The body for the response. */ - body?: Uint8Array - /** The log for the response. */ - log?: Uint8Array - /** The exception for the response. */ - exception?: string +/** A N-API wrapper for the `ExistenceCondition` type. */ +export declare class ExistenceCondition { + /** + * Create a new existence condition. + * + * # Examples + * + * ```js + * const condition = new ExistenceCondition(); + * ``` + */ + constructor() + /** + * Check if the given request matches the condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `GroupCondition` type. */ +export declare class GroupCondition { + /** + * Check if the given request matches the group condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `HeaderCondition` type. */ +export declare class HeaderCondition { + /** + * Create a new header condition. + * + * # Examples + * + * ```js + * const condition = new HeaderCondition('Content-Type', 'application/json'); + * ``` + */ + constructor(header: string, value: string) + /** + * Check if the given request matches the condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `HeaderRewriter` type. */ +export declare class HeaderRewriter { + /** + * Create a new header rewriter. + * + * # Examples + * + * ```js + * const rewriter = new HeaderRewriter(); + * ``` + */ + constructor(header: string, pattern: string, replacement: string) + /** + * Rewrite the given request headers. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} + +/** A N-API wrapper for the `HrefRewriter` type. */ +export declare class HrefRewriter { + /** + * Create a new href rewriter. + * + * # Examples + * + * ```js + * const rewriter = new HrefRewriter('^http://', 'https://'); + * ``` + */ + constructor(pattern: string, replacement: string) + /** + * Rewrite the given request href. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} + +/** A N-API wrapper for the `MethodCondition` type. */ +export declare class MethodCondition { + /** + * Create a new method condition. + * + * # Examples + * + * ```js + * const condition = new MethodCondition('GET'); + * ``` + */ + constructor(method: string) + /** + * Check if the given request matches the condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `MethodRewriter` type. */ +export declare class MethodRewriter { + /** + * Create a new method rewriter. + * + * # Examples + * + * ```js + * const rewriter = new MethodRewriter(); + * ``` + */ + constructor(method: string) + /** + * Rewrite the given request method. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} + +/** A N-API wrapper for the `NonExistenceCondition` type. */ +export declare class NonExistenceCondition { + /** + * Create a new non-existence condition. + * + * # Examples + * + * ```js + * const condition = new NonExistenceCondition(); + * ``` + */ + constructor() + /** + * Check if the given request matches the condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `PathCondition` type. */ +export declare class PathCondition { + /** + * Create a new path condition. + * + * # Examples + * + * ```js + * const condition = new PathCondition('/path/to/resource'); + * ``` + */ + constructor(pattern: string) + /** + * Check if the given request matches the condition. + * + * # Examples + * + * ```js + * const matches = condition.matches(request); + * ``` + */ + matches(request: Request): boolean + /** + * Create a new condition that matches when both conditions match + * + * # Examples + * + * ```js + * const combined = condition1.and(condition2); + * ``` + */ + and(other: AnyCondition): GroupCondition + /** + * Create a new condition that matches when either condition matches + * + * # Examples + * + * ```js + * const combined = condition1.or(condition2); + * ``` + */ + or(other: AnyCondition): GroupCondition +} + +/** A N-API wrapper for the `PathRewriter` type. */ +export declare class PathRewriter { + /** + * Create a new path rewriter. + * + * # Examples + * + * ```js + * const rewriter = new PathRewriter(); + * ``` + */ + constructor(pattern: string, replacement: string) + /** + * Rewrite the given path. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite('/path/to/resource'); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} + +/** Allows constructing rewriter and condition configurations from JSON. */ +export declare class Rewriter { + /** + * Create a new rewriter from a list of configurations. + * + * # Examples + * + * ```js + * const rewriter = new Rewriter([ + * { + * operation: 'And', + * conditions: [ + * { type: 'Path', args: ['/old-path'] }, + * { type: 'Method', args: ['GET'] } + * ], + * rewriters: [ + * { type: 'Path', args: ['/new-path'] } + * ] + * }, + * { + * conditions: [ + * { type: 'Path', args: ['/api/*'] } + * ], + * rewriters: [ + * { type: 'Header', args: ['X-API-Version', '2'] } + * ] + * } + * ]); + * ``` + */ + constructor(configs: Array) + /** + * Rewrite the given request using the configured rewriter. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request } -export interface PhpRewriteCondOptions { - type: string +/** A N-API wrapper for the `SequenceRewriter` type. */ +export declare class SequenceRewriter { + /** + * Rewrite the given request using the sequence of rewriters. + * + * # Examples + * + * ```js + * const rewritten = rewriter.rewrite(request); + * ``` + */ + rewrite(request: Request): Request + /** + * Chain this rewriter with another, creating a sequence that applies both in order + * + * # Examples + * + * ```js + * const sequence = rewriter1.then(rewriter2); + * ``` + */ + then(other: AnyRewriter): SequenceRewriter + /** + * Apply this rewriter conditionally based on a condition + * + * # Examples + * + * ```js + * const conditional = rewriter.when(condition); + * ``` + */ + when(condition: AnyCondition): ConditionalRewriter +} + +/** Configuration for a conditional rewriter that can be used in a `Rewriter`. */ +export interface ConditionalRewriterConfig { + /** The logical operation to use when applying the condition set */ + operation?: ConditionOperation + /** The conditions that must be met for the rewriters to be applied */ + conditions?: Array + /** The rewriters to apply if the conditions are met */ + rewriters: Array +} + +/** Configuration for a condition that can be used in a `ConditionalRewriterConfig`. */ +export interface ConditionConfig { + /** The type of condition to apply */ + type: ConditionType + /** The arguments for the condition, such as the path or header name */ args?: Array } -export interface PhpRewriterOptions { - type: string - args: Array +/** Describe if a conmdition set is combined with AND or OR logic */ +export declare const enum ConditionOperation { + /** All conditions must match for the rewriters to be applied */ + And = 'and', + /** At least one condition must match for the rewriters to be applied */ + Or = 'or' +} + +/** The types of conditions which may be used in a `ConditionConfig`. */ +export declare const enum ConditionType { + /** Matches based on the request path */ + Path = 'path', + /** Matches based on the request header */ + Header = 'header', + /** Matches based on the request method */ + Method = 'method', + /** Matches if a file exists at the given path */ + Exists = 'exists', + /** Matches if a file does not exist at the given path */ + NotExists = 'not_exists' +} + +/** Configuration for a rewriter that can be used in a `ConditionalRewriterConfig`. */ +export interface RewriterConfig { + /** The type of rewriter to apply */ + type: RewriterType + /** The arguments for the rewriter, such as the pattern and replacement */ + args?: Array +} + +/** The types of rewriters which may be used in a `RewriterConfig`. */ +export declare const enum RewriterType { + /** Rewrites the request path */ + Path = 'path', + /** Rewrites a request header */ + Header = 'header', + /** Rewrites the request method */ + Method = 'method', + /** Rewrites the request href */ + Href = 'href' +} +/** + * A PHP instance. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * + * const response = php.handleRequest(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ +export declare class Php { + /** + * Create a new PHP instance. + * + * # Examples + * + * ```js + * const php = new Php({ + * docroot: process.cwd(), + * argv: process.argv + * }); + * ``` + */ + constructor(options?: PhpOptions | undefined | null) + /** + * Handle a PHP request. + * + * # Examples + * + * ```js + * const php = new Php({ + * docroot: process.cwd(), + * argv: process.argv + * }); + * + * const response = php.handleRequest(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ + handleRequest(request: PhpRequest, signal?: AbortSignal | undefined | null): Promise + /** + * Handle a PHP request synchronously. + * + * # Examples + * + * ```js + * const php = new Php({ + * docroot: process.cwd(), + * argv: process.argv + * }); + * + * const response = php.handleRequestSync(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ + handleRequestSync(request: PhpRequest): PhpResponse +} +export type PhpRuntime = Php + +/** Options for creating a new PHP instance. */ +export interface PhpOptions { + /** The command-line arguments for the PHP instance. */ + argv?: Array + /** The document root for the PHP instance. */ + docroot?: string + /** Throw request errors */ + throwRequestErrors?: boolean + /** Request rewriter */ + rewriter?: NapiRewriter } diff --git a/index.js b/index.js index 83778b7..304ca79 100644 --- a/index.js +++ b/index.js @@ -28,9 +28,10 @@ function getNativeBinding({ platform, arch }) { name += '-msvc' } - const path = process.env.PHP_NODE_TEST - ? `./php.${name}.node` - : `./npm/${name}/binding.node` + const path = `./php.${name}.node` + // const path = process.env.PHP_NODE_TEST + // ? `./php.${name}.node` + // : `./npm/${name}/binding.node` return require(path) } diff --git a/package.json b/package.json index 273de3e..850ce91 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "node": ">= 10" }, "scripts": { - "build": "napi build --manifest-path ./crates/php_node/Cargo.toml --platform --no-js --output-dir . --release", - "build:debug": "napi build --manifest-path ./crates/php_node/Cargo.toml --platform --no-js --output-dir .", + "build": "napi build --platform --no-js --output-dir . --release --features napi-support", + "build:debug": "napi build --platform --no-js --output-dir . --features napi-support", "lint": "oxlint", "test": "ava __test__/**.spec.mjs", "universal": "napi universal", diff --git a/crates/php/src/embed.rs b/src/embed.rs similarity index 63% rename from crates/php/src/embed.rs rename to src/embed.rs index c960fc8..bb00577 100644 --- a/crates/php/src/embed.rs +++ b/src/embed.rs @@ -12,7 +12,7 @@ use ext_php_rs::{ zend::{try_catch, try_catch_first, ExecutorGlobals, SapiGlobals}, }; -use lang_handler::{rewrite::Rewriter, Handler, Request, Response}; +use http_handler::{Handler, Request, Response}; use super::{ sapi::{ensure_sapi, Sapi}, @@ -21,6 +21,12 @@ use super::{ EmbedRequestError, EmbedStartError, RequestContext, }; +/// A simple trait for rewriting requests that works with our specific request type +pub trait RequestRewriter: Send + Sync { + /// Rewrite the given request and return the modified request + fn rewrite_request(&self, request: Request) -> Result; +} + /// Embed a PHP script into a Rust application to handle HTTP requests. pub struct Embed { docroot: PathBuf, @@ -30,7 +36,7 @@ pub struct Embed { #[allow(dead_code)] sapi: Arc, - rewriter: Option>, + rewriter: Option>, } impl std::fmt::Debug for Embed { @@ -39,7 +45,7 @@ impl std::fmt::Debug for Embed { .field("docroot", &self.docroot) .field("args", &self.args) .field("sapi", &self.sapi) - .field("rewriter", &"Box") + .field("rewriter", &"Box") .finish() } } @@ -63,7 +69,10 @@ impl Embed { /// /// let embed = Embed::new(docroot, None); /// ``` - pub fn new(docroot: C, rewriter: Option>) -> Result + pub fn new( + docroot: C, + rewriter: Option>, + ) -> Result where C: AsRef, { @@ -85,7 +94,7 @@ impl Embed { /// ``` pub fn new_with_args( docroot: C, - rewriter: Option>, + rewriter: Option>, args: Args, ) -> Result where @@ -111,7 +120,7 @@ impl Embed { /// ``` pub fn new_with_argv( docroot: C, - rewriter: Option>, + rewriter: Option>, argv: Vec, ) -> Result where @@ -152,6 +161,7 @@ impl Embed { } } +#[async_trait::async_trait] impl Handler for Embed { type Error = EmbedRequestError; @@ -171,37 +181,40 @@ impl Handler for Embed { /// let handler = Embed::new(docroot.clone(), None) /// .expect("should construct Embed"); /// - /// let request = Request::builder() + /// let request = http_handler::request::Request::builder() /// .method("GET") - /// .url("http://example.com") - /// .build() + /// .uri("http://example.com") + /// .body(bytes::BytesMut::new()) /// .expect("should build request"); /// + /// # tokio_test::block_on(async { /// let response = handler.handle(request) + /// .await /// .expect("should handle request"); + /// # }); /// /// //assert_eq!(response.status(), 200); /// //assert_eq!(response.body(), "Hello, world!"); /// ``` - fn handle(&self, request: Request) -> Result { + async fn handle(&self, request: Request) -> Result { let docroot = self.docroot.clone(); // Initialize the SAPI module self.sapi.startup()?; // Get REQUEST_URI _first_ as it needs the pre-rewrite state. - let url = request.url(); + let url = request.uri(); let request_uri = url.path(); // Apply request rewriting rules let mut request = request.clone(); if let Some(rewriter) = &self.rewriter { request = rewriter - .rewrite(request, &docroot) - .map_err(EmbedRequestError::RequestRewriteError)?; + .rewrite_request(request) + .map_err(|e| EmbedRequestError::RequestRewriteError(e.to_string()))?; } - let translated_path = translate_path(&docroot, request.url().path())? + let translated_path = translate_path(&docroot, request.uri().path())? .display() .to_string(); @@ -210,17 +223,19 @@ impl Handler for Embed { let path_translated = estrdup(translated_path.clone()); // Extract request method, query string, and headers - let request_method = estrdup(request.method()); + let request_method = estrdup(request.method().as_str()); let query_string = estrdup(url.query().unwrap_or("")); let headers = request.headers(); let content_type = headers .get("Content-Type") + .and_then(|v| v.to_str().ok()) .map(estrdup) .unwrap_or(std::ptr::null_mut()); let content_length = headers .get("Content-Length") - .map(|v| v.parse::().unwrap_or(0)) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) .unwrap_or(0); // Prepare argv and argc @@ -232,9 +247,11 @@ impl Handler for Embed { let script_name = translated_path.clone(); - let response = try_catch_first(|| { - RequestContext::for_request(request.clone(), docroot.clone()); + // Fixed RefUnwindSafe issue (FIXME.md #1) by setting up RequestContext before try_catch_first + // This avoids the need to rebuild the request inside the closure + RequestContext::for_request(request, docroot.clone()); + let response = try_catch_first(move || { // Set server context { let mut globals = SapiGlobals::get_mut(); @@ -259,68 +276,67 @@ impl Handler for Embed { globals.request_info.content_length = content_length; } - let response_builder = { - let _request_scope = RequestScope::new()?; + let _request_scope = RequestScope::new()?; - // Run script in its own try/catch so bailout doesn't skip request shutdown. + // Run script in its own try/catch so bailout doesn't skip request shutdown. + { + let mut file_handle = FileHandleScope::new(script_name.clone()); + try_catch(|| unsafe { php_execute_script(file_handle.deref_mut()) }) + .map_err(|_| EmbedRequestError::Bailout)?; + } + + if let Some(err) = ExecutorGlobals::take_exception() { { - let mut file_handle = FileHandleScope::new(script_name.clone()); - try_catch(|| unsafe { php_execute_script(file_handle.deref_mut()) }) - .map_err(|_| EmbedRequestError::Bailout)?; + let mut globals = SapiGlobals::get_mut(); + globals.sapi_headers.http_response_code = 500; } - if let Some(err) = ExecutorGlobals::take_exception() { - { - let mut globals = SapiGlobals::get_mut(); - globals.sapi_headers.http_response_code = 500; - } + let ex = Error::Exception(err); - let ex = Error::Exception(err); - - if let Some(ctx) = RequestContext::current() { - ctx.response_builder().exception(ex.to_string()); - } + // Fixed exception handling (FIXME.md #3) by using ResponseExt::set_exception + if let Some(ctx) = RequestContext::current() { + ctx.set_response_exception(ex.to_string()); + ctx.set_response_status(500); + } - return Err(EmbedRequestError::Exception(ex.to_string())); - }; + return Err(EmbedRequestError::Exception(ex.to_string())); + }; - let (mut mimetype, http_response_code) = { - let h = SapiGlobals::get().sapi_headers; - (h.mimetype, h.http_response_code) - }; + let (mut mimetype, http_response_code) = { + let h = SapiGlobals::get().sapi_headers; + (h.mimetype, h.http_response_code) + }; - if mimetype.is_null() { - mimetype = unsafe { sapi_get_default_content_type() }; - } + if mimetype.is_null() { + mimetype = unsafe { sapi_get_default_content_type() }; + } - let mime_ptr = - unsafe { mimetype.as_ref() }.ok_or(EmbedRequestError::FailedToDetermineContentType)?; + let mime_ptr = + unsafe { mimetype.as_ref() }.ok_or(EmbedRequestError::FailedToDetermineContentType)?; - let mime = unsafe { std::ffi::CStr::from_ptr(mime_ptr) } - .to_str() - .map_err(|_| EmbedRequestError::FailedToDetermineContentType)? - .to_owned(); + let mime = unsafe { std::ffi::CStr::from_ptr(mime_ptr) } + .to_str() + .map_err(|_| EmbedRequestError::FailedToDetermineContentType)? + .to_owned(); - unsafe { - efree(mimetype.cast::()); - } + unsafe { + efree(mimetype.cast::()); + } - RequestContext::current() - .map(|ctx| { - ctx - .response_builder() - .status(http_response_code) - .header("Content-Type", mime) - }) - .ok_or(EmbedRequestError::ResponseBuildError)? - }; + // Set the final status and content-type header using the new clean API (FIXME.md #4) + if let Some(ctx) = RequestContext::current() { + ctx.set_response_status(http_response_code as u16); + ctx.add_response_header("Content-Type", mime); + } - Ok(response_builder.build()) + // Build the final response with accumulated data using the extension system + RequestContext::reclaim() + .ok_or(EmbedRequestError::ResponseBuildError)? + .build_response() + .map_err(|_| EmbedRequestError::ResponseBuildError) }) .unwrap_or(Err(EmbedRequestError::Bailout))?; - RequestContext::reclaim(); - Ok(response) } } diff --git a/crates/php/src/exception.rs b/src/exception.rs similarity index 97% rename from crates/php/src/exception.rs rename to src/exception.rs index b3ff80a..c56d3d9 100644 --- a/crates/php/src/exception.rs +++ b/src/exception.rs @@ -1,5 +1,3 @@ -use lang_handler::RequestBuilderException; - /// Set of exceptions which may be produced by php::Embed #[derive(Debug, PartialEq, Eq, Hash)] pub enum EmbedStartError { @@ -74,7 +72,7 @@ pub enum EmbedRequestError { FailedToSetRequestInfo(String), /// Error during request rewriting - RequestRewriteError(RequestBuilderException), + RequestRewriteError(String), } impl std::fmt::Display for EmbedRequestError { diff --git a/crates/php/src/lib.rs b/src/lib.rs similarity index 63% rename from crates/php/src/lib.rs rename to src/lib.rs index 16b6ef0..23d1a35 100644 --- a/crates/php/src/lib.rs +++ b/src/lib.rs @@ -24,42 +24,60 @@ //! let embed = Embed::new_with_args(docroot, None, args()) //! .expect("should construct embed"); //! -//! let request = Request::builder() +//! let request = http_handler::request::Request::builder() //! .method("POST") -//! .url("http://example.com/index.php") +//! .uri("http://example.com/index.php") //! .header("Content-Type", "text/html") -//! .header("Content-Length", 13.to_string()) -//! .body("Hello, World!") -//! .build() +//! .header("Content-Length", "13") +//! .body(bytes::BytesMut::from("Hello, World!")) //! .expect("should build request"); //! +//! # tokio_test::block_on(async { //! let response = embed //! .handle(request.clone()) +//! .await //! .expect("should handle request"); //! //! assert_eq!(response.status(), 200); //! assert_eq!(response.body(), "Hello, World!"); //! println!("Response: {:#?}", response); +//! # }); //! ``` #![warn(rust_2018_idioms)] #![warn(clippy::dbg_macro, clippy::print_stdout)] #![warn(missing_docs)] +#[cfg(feature = "napi-support")] +#[macro_use] +extern crate napi_derive; + mod embed; mod exception; mod request_context; +mod rewriter_impl; mod sapi; mod scopes; mod strings; mod test; -pub use lang_handler::{ - rewrite, Handler, Header, Headers, Request, RequestBuilder, RequestBuilderException, Response, - ResponseBuilder, Url, +#[cfg(feature = "napi-support")] +/// NAPI bindings for exposing PHP to Node.js +pub mod napi; + +pub use http_handler::{ + Handler, Request, RequestBuilderExt, Response, ResponseException, ResponseExt, +}; +pub use http_rewriter as rewrite; + +// Re-export commonly used types from http crate +pub use http_handler::{ + header::HeaderName as Header, HeaderMap as Headers, HeaderName, HeaderValue, Method, StatusCode, + Uri as Url, }; -pub use embed::Embed; +pub use embed::{Embed, RequestRewriter}; pub use exception::{EmbedRequestError, EmbedStartError}; pub use request_context::RequestContext; +pub use rewriter_impl::*; pub use test::{MockRoot, MockRootBuilder}; diff --git a/crates/php_node/src/runtime.rs b/src/napi.rs similarity index 70% rename from crates/php_node/src/runtime.rs rename to src/napi.rs index 0461f78..956c6ed 100644 --- a/crates/php_node/src/runtime.rs +++ b/src/napi.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use napi::bindgen_prelude::*; use napi::{Env, Error, Result, Task}; -use php::{Embed, EmbedRequestError, Handler, Request, Response}; - -use crate::{PhpRequest, PhpResponse, PhpRewriter}; +use crate::{Embed, EmbedRequestError, Handler, Request, RequestRewriter, Response}; +use http_handler::napi::{Request as PhpRequest, Response as PhpResponse}; +use http_rewriter::napi::Rewriter as NapiRewriter; /// Options for creating a new PHP instance. #[napi(object)] @@ -18,7 +18,7 @@ pub struct PhpOptions { /// Throw request errors pub throw_request_errors: Option, /// Request rewriter - pub rewriter: Option>, + pub rewriter: Option>, } /// A PHP instance. @@ -73,8 +73,10 @@ impl PhpRuntime { }) .map_err(|_| Error::from_reason("Could not determine docroot"))?; - let rewriter = if let Some(found) = rewriter { - Some(found.into_rewriter()?) + let rewriter = if let Some(rewriter_ref) = rewriter { + // Dereference to get the actual NapiRewriter and clone it + let owned_rewriter = (*rewriter_ref).clone(); + Some(Box::new(NapiRewriterWrapper(owned_rewriter)) as Box) } else { None }; @@ -119,7 +121,7 @@ impl PhpRuntime { PhpRequestTask { throw_request_errors: self.throw_request_errors, embed: self.embed.clone(), - request: request.into(), + request: request.clone().into_inner(), }, signal, ) @@ -148,14 +150,14 @@ impl PhpRuntime { let mut task = PhpRequestTask { throw_request_errors: self.throw_request_errors, embed: self.embed.clone(), - request: request.into(), + request: request.clone().into_inner(), }; - task.compute().map(PhpResponse::new) + task.compute().map(Into::::into) } } -// Task container to run a PHP request in a worker thread. +/// Task container to run a PHP request in a worker thread. pub struct PhpRequestTask { embed: Arc, request: Request, @@ -169,19 +171,21 @@ impl Task for PhpRequestTask { // Handle the PHP request in the worker thread. fn compute(&mut self) -> Result { - let mut result = self.embed.handle(self.request.clone()); + let runtime = tokio::runtime::Runtime::new().map_err(|e| Error::from_reason(e.to_string()))?; + let mut result = runtime.block_on(self.embed.handle(self.request.clone())); // Translate the various error types into HTTP error responses if !self.throw_request_errors { result = result.or_else(|err| { Ok(match err { - EmbedRequestError::ScriptNotFound(_script_name) => { - Response::builder().status(404).body("Not Found").build() - } - _ => Response::builder() + EmbedRequestError::ScriptNotFound(_script_name) => http_handler::response::Builder::new() + .status(404) + .body(bytes::BytesMut::from("Not Found")) + .unwrap(), + _ => http_handler::response::Builder::new() .status(500) - .body("Internal Server Error") - .build(), + .body(bytes::BytesMut::from("Internal Server Error")) + .unwrap(), }) }) } @@ -191,6 +195,19 @@ impl Task for PhpRequestTask { // Handle converting the PHP response to a JavaScript response in the main thread. fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { - Ok(PhpResponse::new(output)) + Ok(Into::::into(output)) + } +} + +// Wrapper to adapt NapiRewriter to RequestRewriter +struct NapiRewriterWrapper(NapiRewriter); + +impl RequestRewriter for NapiRewriterWrapper { + fn rewrite_request( + &self, + request: Request, + ) -> std::result::Result { + // Call the Rewriter trait method explicitly + ::rewrite(&self.0, request) } } diff --git a/src/request_context.rs b/src/request_context.rs new file mode 100644 index 0000000..2c09bc7 --- /dev/null +++ b/src/request_context.rs @@ -0,0 +1,165 @@ +use bytes::BytesMut; +use ext_php_rs::zend::SapiGlobals; +use http_handler::request::Parts; +use http_handler::{ + BodyBuffer, HeaderMap, HeaderName, HeaderValue, Request, ResponseBuilderExt, ResponseLog, + StatusCode, +}; +use std::{ffi::c_void, path::PathBuf}; + +/// The request context for the PHP SAPI. +/// +/// This has been redesigned to address all issues in FIXME.md: +/// - Uses request Parts to avoid RefUnwindSafe issues (#1) +/// - Stores mutable body for proper consumption (#2) +/// - Uses extension types to accumulate response data (#3, #4) +#[derive(Debug)] +pub struct RequestContext { + request_parts: Parts, + request_body: BytesMut, + response_status: StatusCode, + response_headers: HeaderMap, + response_body: BodyBuffer, + response_log: ResponseLog, + response_exception: Option, + docroot: PathBuf, +} + +impl RequestContext { + /// Sets the current request context for the PHP SAPI. + /// + /// Uses into_parts() to avoid RefUnwindSafe issues (FIXME.md #1). + pub fn for_request(request: Request, docroot: S) + where + S: Into, + { + // Use into_parts() to avoid RefUnwindSafe issues (FIXME.md #1) + let (parts, body) = request.into_parts(); + + let context = Box::new(RequestContext { + request_parts: parts, + request_body: body, + response_status: StatusCode::OK, + response_headers: HeaderMap::new(), + response_body: BodyBuffer::new(), + response_log: ResponseLog::new(), + response_exception: None, + docroot: docroot.into(), + }); + let mut globals = SapiGlobals::get_mut(); + globals.server_context = Box::into_raw(context) as *mut c_void; + } + + /// Retrieve a mutable reference to the request context + pub fn current<'a>() -> Option<&'a mut RequestContext> { + let ptr = { + let globals = SapiGlobals::get(); + globals.server_context as *mut RequestContext + }; + if ptr.is_null() { + return None; + } + + Some(unsafe { &mut *ptr }) + } + + /// Reclaim ownership of the RequestContext. Useful for dropping. + #[allow(dead_code)] + pub fn reclaim() -> Option> { + let ptr = { + let mut globals = SapiGlobals::get_mut(); + std::mem::replace(&mut globals.server_context, std::ptr::null_mut()) + }; + if ptr.is_null() { + return None; + } + Some(unsafe { Box::from_raw(ptr as *mut RequestContext) }) + } + + /// Returns a reference to the request parts. + /// This replaces the old request() method since we now use parts. + pub fn request_parts(&self) -> &Parts { + &self.request_parts + } + + /// Returns a mutable reference to the request body. + /// This allows proper consumption of the body (FIXME.md #2). + pub fn request_body_mut(&mut self) -> &mut BytesMut { + &mut self.request_body + } + + /// Returns a reference to the request body. + pub fn request_body(&self) -> &BytesMut { + &self.request_body + } + + /// Add a header to the response. + pub fn add_response_header(&mut self, key: K, value: V) + where + K: TryInto, + V: TryInto, + { + if let (Ok(header_name), Ok(header_value)) = (key.try_into(), value.try_into()) { + self.response_headers.insert(header_name, header_value); + } + } + + /// Set the response status code. + pub fn set_response_status(&mut self, status: u16) { + if let Ok(status_code) = StatusCode::from_u16(status) { + self.response_status = status_code; + } + } + + /// Write data to the response body. + pub fn write_response_body(&mut self, data: &[u8]) { + self.response_body.append(data); + } + + /// Write to the response log. + /// This uses extension types to accumulate log data (FIXME.md #4). + pub fn write_response_log(&mut self, data: &[u8]) { + self.response_log.append(data); + } + + /// Set an exception on the response. + /// This stores the exception to be added via ResponseBuilderExt (FIXME.md #3). + pub fn set_response_exception(&mut self, exception: impl Into) { + self.response_exception = Some(exception.into()); + } + + /// Build the final response using the accumulated data. + /// This properly uses ResponseBuilderExt for logs and exceptions (FIXME.md #3, #4). + pub fn build_response(self) -> Result { + // Start building the response + let mut builder = http_handler::response::Response::builder().status(self.response_status); + + // Add all headers + for (key, value) in &self.response_headers { + builder = builder.header(key, value); + } + + // Add extensions using ResponseBuilderExt + builder = builder + .body_buffer(self.response_body) + .log(self.response_log.into_bytes()); + + if let Some(exception) = self.response_exception { + builder = builder.exception(exception); + } + + // Get the body buffer from extensions and build final response + let body = builder + .extensions_mut() + .and_then(|ext| ext.remove::()) + .unwrap_or_default() + .into_bytes_mut(); + + builder.body(body) + } + + /// Returns the docroot associated with this request context + pub fn docroot(&self) -> PathBuf { + self.docroot.to_owned() + } +} diff --git a/src/rewriter_impl.rs b/src/rewriter_impl.rs new file mode 100644 index 0000000..1ceb12c --- /dev/null +++ b/src/rewriter_impl.rs @@ -0,0 +1,5 @@ +// Re-export common rewriter types for convenience +pub use http_rewriter::{ + ExistenceCondition, HeaderCondition, HeaderRewriter, HrefRewriter, MethodCondition, + MethodRewriter, NonExistenceCondition, PathCondition, PathRewriter, RewriteError, Rewriter, +}; diff --git a/crates/php/src/sapi.rs b/src/sapi.rs similarity index 85% rename from crates/php/src/sapi.rs rename to src/sapi.rs index 129e4f3..631fda9 100644 --- a/crates/php/src/sapi.rs +++ b/src/sapi.rs @@ -5,8 +5,6 @@ use std::{ sync::{Arc, RwLock, Weak}, }; -use bytes::Buf; - use ext_php_rs::{ alloc::{efree, estrdup}, builders::SapiBuilder, @@ -269,7 +267,8 @@ pub extern "C" fn sapi_module_ub_write(str: *const c_char, str_length: usize) -> let len = bytes.len(); if let Some(ctx) = RequestContext::current() { - ctx.response_builder().body_write(bytes); + // Use new method name for clarity (FIXME.md #4) + ctx.write_response_body(bytes); } len } @@ -292,7 +291,8 @@ pub extern "C" fn sapi_module_send_header(header: *mut SapiHeader, _server_conte // Header value is None for http version + status line if let Some(value) = header.value() { if let Some(ctx) = RequestContext::current() { - ctx.response_builder().header(name, value); + // Use new method that doesn't require mem::replace hacks (FIXME.md #4) + ctx.add_response_header(name, value); } } } @@ -303,20 +303,23 @@ pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> u return 0; } + // Fixed body reading bug from FIXME.md #2 + // Now we properly consume from the mutable body instead of cloning RequestContext::current() - .map(|ctx| ctx.request().body()) - .map(|body| { - let length = length.min(body.len()); - if length == 0 { + .map(|ctx| { + let body = ctx.request_body_mut(); + let actual_length = length.min(body.len()); + if actual_length == 0 { return 0; } - let chunk = body.take(length); + // Properly consume from the original body buffer + let chunk = body.split_to(actual_length); unsafe { - std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut c_char, buffer, length); + std::ptr::copy_nonoverlapping(chunk.as_ptr() as *mut c_char, buffer, actual_length); } - length + actual_length }) .unwrap_or(0) } @@ -324,8 +327,8 @@ pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> u #[no_mangle] pub extern "C" fn sapi_module_read_cookies() -> *mut c_char { RequestContext::current() - .map(|ctx| match ctx.request().headers().get("Cookie") { - Some(cookie) => estrdup(cookie), + .map(|ctx| match ctx.request_parts().headers.get("Cookie") { + Some(cookie) => estrdup(cookie.to_str().unwrap_or("")), None => std::ptr::null_mut(), }) .unwrap_or(std::ptr::null_mut()) @@ -372,17 +375,17 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t // } if let Some(ctx) = RequestContext::current() { - let request = ctx.request(); - let headers = request.headers(); + let request_parts = ctx.request_parts(); + let headers = &request_parts.headers; // Hack to allow ? syntax for the following code. // At the moment any errors are just swallowed, but these could be // collected and reported somehow in the future. Ok::<(), EmbedRequestError>(()) .and_then(|_| { - for (key, values) in headers.iter() { - let value_string: String = values.into(); - let upper = key.to_ascii_uppercase(); + for (key, value) in headers.iter() { + let value_string = value.to_str().unwrap_or("").to_string(); + let upper = key.as_str().to_ascii_uppercase(); let cgi_key = format!("HTTP_{}", upper.replace("-", "_")); env_var(vars, cgi_key, value_string)?; } @@ -400,17 +403,21 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t std::ptr::null_mut() }; - env_var(vars, "REQUEST_SCHEME", request.url().scheme())?; + env_var( + vars, + "REQUEST_SCHEME", + request_parts.uri.scheme_str().unwrap_or("http"), + )?; env_var(vars, "CONTEXT_PREFIX", "")?; env_var(vars, "SERVER_ADMIN", "webmaster@localhost")?; env_var(vars, "GATEWAY_INTERFACE", "CGI/1.1")?; // Laravel seems to think "/register" should be "/index.php/register"? // env_var_c(vars, "PHP_SELF", script_name as *mut c_char)?; - env_var(vars, "PHP_SELF", request.url().path())?; + env_var(vars, "PHP_SELF", request_parts.uri.path())?; // TODO: is "/register", should be "/index.php" - env_var(vars, "SCRIPT_NAME", request.url().path())?; + env_var(vars, "SCRIPT_NAME", request_parts.uri.path())?; // env_var_c(vars, "SCRIPT_NAME", script_name as *mut c_char)?; env_var_c(vars, "PATH_INFO", script_name as *mut c_char)?; env_var_c(vars, "SCRIPT_FILENAME", script_filename)?; @@ -435,14 +442,15 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t env_var_c(vars, "SERVER_SOFTWARE", inner_sapi.name)?; } - if let Some(info) = request.local_socket() { - env_var(vars, "SERVER_ADDR", info.ip().to_string())?; - env_var(vars, "SERVER_PORT", info.port().to_string())?; - } - - if let Some(info) = request.remote_socket() { - env_var(vars, "REMOTE_ADDR", info.ip().to_string())?; - env_var(vars, "REMOTE_PORT", info.port().to_string())?; + if let Some(socket_info) = request_parts.extensions.get::() { + if let Some(local) = socket_info.local { + env_var(vars, "SERVER_ADDR", local.ip().to_string())?; + env_var(vars, "SERVER_PORT", local.port().to_string())?; + } + if let Some(remote) = socket_info.remote { + env_var(vars, "REMOTE_ADDR", remote.ip().to_string())?; + env_var(vars, "REMOTE_PORT", remote.port().to_string())?; + } } if !req_info.request_method.is_null() { @@ -467,7 +475,8 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_int: c_int) { let message = unsafe { CStr::from_ptr(message) }; if let Some(ctx) = RequestContext::current() { - ctx.response_builder().log_write(message.to_bytes()); + // Use new method that uses extension system (FIXME.md #4) + ctx.write_response_log(message.to_bytes()); } } @@ -479,12 +488,12 @@ pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_i pub fn apache_request_headers() -> Result, String> { let mut headers = HashMap::new(); - let request = RequestContext::current() - .map(|ctx| ctx.request()) + let request_parts = RequestContext::current() + .map(|ctx| ctx.request_parts()) .ok_or("Request context unavailable")?; - for (key, value) in request.headers().iter() { - headers.insert(key.to_string(), value.into()); + for (key, value) in request_parts.headers.iter() { + headers.insert(key.to_string(), value.to_str().unwrap_or("").to_string()); } Ok(headers) diff --git a/crates/php/src/scopes.rs b/src/scopes.rs similarity index 100% rename from crates/php/src/scopes.rs rename to src/scopes.rs diff --git a/crates/php/src/strings.rs b/src/strings.rs similarity index 100% rename from crates/php/src/strings.rs rename to src/strings.rs diff --git a/crates/php/src/test.rs b/src/test.rs similarity index 100% rename from crates/php/src/test.rs rename to src/test.rs From 3168f2ed84b4d4d7f26dbb2ceb80984d15fab18b Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 22 Oct 2025 09:41:29 -0700 Subject: [PATCH 2/3] Fix native module loading to support CI artifact structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI downloads artifacts to npm//binding.node but the code was only looking for php..node in the root directory. This change tries the npm directory first (for CI/published builds) and falls back to the root path (for local development builds). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.toml | 10 +++++-- __test__/headers.spec.mjs | 8 +++--- __test__/request.spec.mjs | 4 +-- __test__/rewriter.spec.mjs | 8 ++---- index.d.ts | 16 ++++++----- index.js | 7 ++--- package.json | 4 +-- src/embed.rs | 42 ++++++++++++++++++++++----- src/lib.rs | 6 ++-- src/main.rs | 58 ++++++++++++++++++++++++++++++++++++++ src/napi.rs | 16 ++--------- src/rewriter_impl.rs | 5 ---- src/sapi.rs | 2 -- 13 files changed, 127 insertions(+), 59 deletions(-) create mode 100644 src/main.rs delete mode 100644 src/rewriter_impl.rs diff --git a/Cargo.toml b/Cargo.toml index a8a15d4..551abdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,21 @@ napi-support = ["dep:napi", "dep:napi-derive", "dep:napi-build", "http-handler/n [lib] name = "php_node" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] path = "src/lib.rs" +[[bin]] +name = "php-main" +path = "src/main.rs" + [dependencies] async-trait = "0.1.88" bytes = "1.10.1" hostname = "0.4.1" -ext-php-rs = { git = "https://github.com/platformatic/ext-php-rs.git" } +ext-php-rs = { version = "0.14.0", features = ["embed"] } http-handler = { git = "https://github.com/platformatic/http-handler.git" } # http-handler = { path = "../http-handler" } -http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git" } +http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git", branch = "add-docroot-parameter-support" } # http-rewriter = { path = "../http-rewriter" } libc = "0.2.171" # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix diff --git a/__test__/headers.spec.mjs b/__test__/headers.spec.mjs index 872d2c3..a267f8e 100644 --- a/__test__/headers.spec.mjs +++ b/__test__/headers.spec.mjs @@ -19,20 +19,20 @@ test('only last set is used for get', (t) => { const headers = new Headers() headers.set('Content-Type', 'application/json') headers.add('Content-Type', 'text/html') - t.is(headers.size, 2) + t.is(headers.size, 1) t.assert(headers.has('Content-Type')) - t.is(headers.get('Content-Type'), 'application/json') + t.is(headers.get('Content-Type'), 'text/html') }) test('adding a header with multiple values works and stores to a single entry', (t) => { const headers = new Headers() headers.add('Accept', 'application/json') headers.add('Accept', 'text/html') - t.is(headers.size, 2) + t.is(headers.size, 1) t.assert(headers.has('Accept')) t.deepEqual(headers.getAll('Accept'), ['application/json', 'text/html']) t.deepEqual(headers.getLine('Accept'), 'application/json,text/html') - t.deepEqual(headers.get('Accept'), 'application/json') + t.deepEqual(headers.get('Accept'), 'text/html') }) test('deleting a header adjusts size and removes value', (t) => { diff --git a/__test__/request.spec.mjs b/__test__/request.spec.mjs index 4b135b3..34ed448 100644 --- a/__test__/request.spec.mjs +++ b/__test__/request.spec.mjs @@ -33,7 +33,7 @@ test('full construction', (t) => { t.assert(req.body instanceof Buffer) t.is(req.body.toString('utf8'), 'Hello, from Node.js!') t.assert(req.headers instanceof Headers) - t.is(req.headers.size, 4) + t.is(req.headers.size, 3) t.is(req.headers.get('Content-Type'), 'application/json') t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html']) t.is(req.headers.get('X-Custom-Header'), 'CustomValue') @@ -58,7 +58,7 @@ test('construction with headers instance', (t) => { t.assert(req.body instanceof Buffer) t.is(req.body.toString('utf8'), 'Hello, from Node.js!') t.assert(req.headers instanceof Headers) - t.is(req.headers.size, 4) + t.is(req.headers.size, 3) t.is(req.headers.get('Content-Type'), 'application/json') t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html']) t.is(req.headers.get('X-Custom-Header'), 'CustomValue') diff --git a/__test__/rewriter.spec.mjs b/__test__/rewriter.spec.mjs index 5610fbd..fbd9b94 100644 --- a/__test__/rewriter.spec.mjs +++ b/__test__/rewriter.spec.mjs @@ -7,7 +7,6 @@ const filename = import.meta.filename.replace(docroot, '') test('existence condition', (t) => { const req = new Request({ - docroot, method: 'GET', url: `http://example.com${filename}`, headers: { @@ -29,12 +28,11 @@ test('existence condition', (t) => { } ]) - t.is(rewriter.rewrite(req).url, 'http://example.com/404') + t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404') }) test('non-existence condition', (t) => { const req = new Request({ - docroot, method: 'GET', url: 'http://example.com/index.php', headers: { @@ -56,7 +54,7 @@ test('non-existence condition', (t) => { } ]) - t.is(rewriter.rewrite(req).url, 'http://example.com/404') + t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404') }) test('condition groups - AND', (t) => { @@ -199,7 +197,7 @@ test('header rewriting', (t) => { test('href rewriting', (t) => { const rewriter = new Rewriter([{ rewriters: [ - { type: 'href', args: [ '^http://example.com(.*)$', '/index.php?route=${1}' ] } + { type: 'href', args: [ '^(.*)$', '/index.php?route=$1' ] } ] }]) diff --git a/index.d.ts b/index.d.ts index 3da03ae..eab843e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -721,7 +721,7 @@ export declare class ConditionalRewriter { * const rewritten = rewriter.rewrite(request); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * @@ -887,7 +887,7 @@ export declare class HeaderRewriter { * const rewritten = rewriter.rewrite(request); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * @@ -931,7 +931,7 @@ export declare class HrefRewriter { * const rewritten = rewriter.rewrite(request); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * @@ -1019,7 +1019,7 @@ export declare class MethodRewriter { * const rewritten = rewriter.rewrite(request); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * @@ -1151,7 +1151,7 @@ export declare class PathRewriter { * const rewritten = rewriter.rewrite('/path/to/resource'); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * @@ -1212,9 +1212,11 @@ export declare class Rewriter { * * ```js * const rewritten = rewriter.rewrite(request); + * // Or with explicit docroot: + * const rewritten = rewriter.rewrite(request, '/var/www/html'); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request } /** A N-API wrapper for the `SequenceRewriter` type. */ @@ -1228,7 +1230,7 @@ export declare class SequenceRewriter { * const rewritten = rewriter.rewrite(request); * ``` */ - rewrite(request: Request): Request + rewrite(request: Request, docroot?: string | undefined | null): Request /** * Chain this rewriter with another, creating a sequence that applies both in order * diff --git a/index.js b/index.js index 304ca79..83778b7 100644 --- a/index.js +++ b/index.js @@ -28,10 +28,9 @@ function getNativeBinding({ platform, arch }) { name += '-msvc' } - const path = `./php.${name}.node` - // const path = process.env.PHP_NODE_TEST - // ? `./php.${name}.node` - // : `./npm/${name}/binding.node` + const path = process.env.PHP_NODE_TEST + ? `./php.${name}.node` + : `./npm/${name}/binding.node` return require(path) } diff --git a/package.json b/package.json index 850ce91..da922ce 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "node": ">= 10" }, "scripts": { - "build": "napi build --platform --no-js --output-dir . --release --features napi-support", - "build:debug": "napi build --platform --no-js --output-dir . --features napi-support", + "build": "napi build --platform --no-js --output-dir . --release --features napi-support -- --lib", + "build:debug": "napi build --platform --no-js --output-dir . --features napi-support -- --lib", "lint": "oxlint", "test": "ava __test__/**.spec.mjs", "universal": "napi universal", diff --git a/src/embed.rs b/src/embed.rs index bb00577..dfb889f 100644 --- a/src/embed.rs +++ b/src/embed.rs @@ -24,7 +24,35 @@ use super::{ /// A simple trait for rewriting requests that works with our specific request type pub trait RequestRewriter: Send + Sync { /// Rewrite the given request and return the modified request - fn rewrite_request(&self, request: Request) -> Result; + /// + /// The docroot parameter is used by conditions like ExistenceCondition that need + /// to check for files on the filesystem. + fn rewrite_request( + &self, + request: Request, + docroot: &Path, + ) -> Result; +} + +/// Blanket implementation: any type implementing http_rewriter::Rewriter +/// automatically implements RequestRewriter for our concrete Request type. +impl RequestRewriter for T +where + T: http_rewriter::Rewriter, +{ + fn rewrite_request( + &self, + request: Request, + docroot: &Path, + ) -> Result { + use http_handler::extensions::DocumentRoot; + use http_handler::RequestExt; + let mut request = request; + request.set_document_root(DocumentRoot { + path: docroot.to_path_buf(), + }); + http_rewriter::Rewriter::rewrite(self, request) + } } /// Embed a PHP script into a Rust application to handle HTTP requests. @@ -62,7 +90,7 @@ impl Embed { /// /// ``` /// use std::env::current_dir; - /// use php::Embed; + /// use php_node::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -85,7 +113,7 @@ impl Embed { /// /// ``` /// use std::env::{args, current_dir}; - /// use php::Embed; + /// use php_node::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -109,7 +137,7 @@ impl Embed { /// /// ``` /// use std::env::current_dir; - /// use php::{Embed, Handler, Request, Response}; + /// use php_node::{Embed, Handler, Request, Response}; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -146,7 +174,7 @@ impl Embed { /// /// ```rust /// use std::env::current_dir; - /// use php::Embed; + /// use php_node::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -171,7 +199,7 @@ impl Handler for Embed { /// /// ``` /// use std::{env::temp_dir, fs::File, io::Write}; - /// use php::{Embed, Handler, Request, Response, MockRoot}; + /// use php_node::{Embed, Handler, Request, Response, MockRoot}; /// /// let docroot = MockRoot::builder() /// .file("index.php", "") @@ -210,7 +238,7 @@ impl Handler for Embed { let mut request = request.clone(); if let Some(rewriter) = &self.rewriter { request = rewriter - .rewrite_request(request) + .rewrite_request(request, &self.docroot) .map_err(|e| EmbedRequestError::RequestRewriteError(e.to_string()))?; } diff --git a/src/lib.rs b/src/lib.rs index 23d1a35..7204a16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,8 @@ //! ```rust //! use std::env::{args, current_dir}; //! # use std::path::PathBuf; -//! # use php::MockRoot; -//! use php::{ +//! # use php_node::MockRoot; +//! use php_node::{ //! rewrite::{PathRewriter, Rewriter}, //! Embed, Handler, Request, //! }; @@ -55,7 +55,6 @@ extern crate napi_derive; mod embed; mod exception; mod request_context; -mod rewriter_impl; mod sapi; mod scopes; mod strings; @@ -79,5 +78,4 @@ pub use http_handler::{ pub use embed::{Embed, RequestRewriter}; pub use exception::{EmbedRequestError, EmbedStartError}; pub use request_context::RequestContext; -pub use rewriter_impl::*; pub use test::{MockRoot, MockRootBuilder}; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..beac07e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,58 @@ +use std::{env::current_dir, fs::File, io::Write, path::PathBuf}; + +use bytes::BytesMut; +use php_node::{rewrite::PathRewriter, Embed, Handler, Request, RequestRewriter}; + +#[tokio::main] +async fn main() { + let _temp_file = TempFile::new("index.php", ""); + + let docroot = current_dir().expect("should have current_dir"); + + let rewriter = PathRewriter::new("test", "index").expect("should be valid regex"); + + let maybe_rewriter: Option> = Some(Box::new(rewriter)); + let embed = Embed::new_with_args(docroot, maybe_rewriter, std::env::args()) + .expect("should construct embed"); + + // Build request using the re-exported Request type from http crate + let mut request = Request::new(BytesMut::from("Hello, World!")); + *request.method_mut() = "POST".parse().unwrap(); + *request.uri_mut() = "http://example.com/test.php".parse().unwrap(); + request + .headers_mut() + .insert("Content-Type", "text/html".parse().unwrap()); + request + .headers_mut() + .insert("Content-Length", "13".parse().unwrap()); + + println!("request: {:#?}", request); + + let response = embed + .handle(request.clone()) + .await + .expect("should handle request"); + + println!("response: {:#?}", response); +} + +struct TempFile(PathBuf); + +impl TempFile { + pub fn new(path: P, contents: S) -> Self + where + P: Into, + S: Into, + { + let path = path.into(); + let mut file = File::create(path.clone()).unwrap(); + file.write_all(contents.into().as_bytes()).unwrap(); + Self(path) + } +} + +impl Drop for TempFile { + fn drop(&mut self) { + std::fs::remove_file(&self.0).unwrap(); + } +} diff --git a/src/napi.rs b/src/napi.rs index 956c6ed..e400d99 100644 --- a/src/napi.rs +++ b/src/napi.rs @@ -76,7 +76,8 @@ impl PhpRuntime { let rewriter = if let Some(rewriter_ref) = rewriter { // Dereference to get the actual NapiRewriter and clone it let owned_rewriter = (*rewriter_ref).clone(); - Some(Box::new(NapiRewriterWrapper(owned_rewriter)) as Box) + // Thanks to the blanket impl, NapiRewriter automatically implements RequestRewriter + Some(Box::new(owned_rewriter) as Box) } else { None }; @@ -198,16 +199,3 @@ impl Task for PhpRequestTask { Ok(Into::::into(output)) } } - -// Wrapper to adapt NapiRewriter to RequestRewriter -struct NapiRewriterWrapper(NapiRewriter); - -impl RequestRewriter for NapiRewriterWrapper { - fn rewrite_request( - &self, - request: Request, - ) -> std::result::Result { - // Call the Rewriter trait method explicitly - ::rewrite(&self.0, request) - } -} diff --git a/src/rewriter_impl.rs b/src/rewriter_impl.rs deleted file mode 100644 index 1ceb12c..0000000 --- a/src/rewriter_impl.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Re-export common rewriter types for convenience -pub use http_rewriter::{ - ExistenceCondition, HeaderCondition, HeaderRewriter, HrefRewriter, MethodCondition, - MethodRewriter, NonExistenceCondition, PathCondition, PathRewriter, RewriteError, Rewriter, -}; diff --git a/src/sapi.rs b/src/sapi.rs index 631fda9..3838715 100644 --- a/src/sapi.rs +++ b/src/sapi.rs @@ -303,8 +303,6 @@ pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> u return 0; } - // Fixed body reading bug from FIXME.md #2 - // Now we properly consume from the mutable body instead of cloning RequestContext::current() .map(|ctx| { let body = ctx.request_body_mut(); From be7e2631ec153519d38d4acfab6280129bf10c71 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 22 Oct 2025 22:02:52 -0700 Subject: [PATCH 3/3] Rename Rust library from php-node to php Rename the Rust crate and library from php-node/php_node to php for better clarity and consistency. Updated all references throughout the codebase including doctests. Changes: - Cargo.toml: Changed package name from 'php-node' to 'php' - Cargo.toml: Changed lib name from 'php_node' to 'php' - src/lib.rs: Updated doctest to use 'php::' instead of 'php_node::' - src/embed.rs: Updated all doctests to use 'php::' prefix - src/main.rs: Updated imports to use 'php::' prefix - CLAUDE.md: Updated documentation to reflect single-crate structure and removed references to old multi-crate workspace All tests pass with the new naming. --- CLAUDE.md | 37 +++++++------ Cargo.toml | 6 +-- src/embed.rs | 33 +++++++----- src/lib.rs | 4 +- src/main.rs | 2 +- src/request_context.rs | 118 +++++++---------------------------------- src/sapi.rs | 42 +++++++-------- 7 files changed, 89 insertions(+), 153 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8859b58..609ad12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,17 +37,21 @@ npm run version # Build with proper rpath for linking libphp RUSTFLAGS="-C link-args=-Wl,-rpath,\$ORIGIN" npm run build -# Build specific crate -cargo build --manifest-path crates/php_node/Cargo.toml --release +# Run Rust tests +cargo test + +# Run binary directly +cargo run ``` ## Architecture ### Multi-language Structure -- **Rust** (`/crates`): Core implementation using Cargo workspace - - `lang_handler`: Generic language handler abstractions - - `php`: PHP embedding and SAPI implementation - - `php_node`: NAPI bindings exposing Rust to Node.js +- **Rust** (`/src`): Single-crate implementation + - PHP embedding and SAPI implementation + - NAPI bindings exposing Rust to Node.js (when `napi-support` feature is enabled) + - Binary target for standalone testing (`src/main.rs`) + - Library target for NAPI usage (`src/lib.rs`) - **JavaScript**: Node.js API layer (`index.js`, `index.d.ts`) - **PHP**: Embedded runtime via libphp.{so,dylib} @@ -87,21 +91,22 @@ cargo build --manifest-path crates/php_node/Cargo.toml --release 5. **Platform Support**: x64 Linux, x64/arm64 macOS (pre-built binaries in `/npm`) -6. **Recent Architecture Changes**: - - `lang_handler` crate no longer uses `napi` features directly (removed from dependencies) - - `php` crate uses custom fork of ext-php-rs from platformatic GitHub org +6. **Recent Architecture Changes**: + - Consolidated from multi-crate workspace to single crate named `php` + - NAPI support is now feature-gated with `napi-support` feature + - Binary target supports both library (`rlib`) and dynamic library (`cdylib`) outputs ## Common Tasks ### Adding New NAPI Functions -1. Implement in Rust under `crates/php_node/src/` +1. Implement in Rust under `src/` with `#[cfg(feature = "napi-support")]` 2. Use `#[napi]` attributes for exposed functions/classes 3. Run `npm run build` to regenerate TypeScript definitions ### Modifying Request/Response Handling -- Core logic in `crates/php/src/sapi.rs` +- Core logic in `src/sapi.rs` and `src/embed.rs` - JavaScript wrapper in `index.js` -- Headers handling in `crates/php_node/src/headers.rs` +- Request/response types from `http-handler` crate ### Debugging PHP Issues - Check INTERNALS.md for PHP embedding details @@ -118,7 +123,9 @@ The rewriter system supports Apache mod_rewrite-like functionality: ## Project Files Reference - `index.js`: Main JavaScript API, exports PHP, Request, Response, Headers, Rewriter classes -- `crates/php_node/src/lib.rs`: NAPI bindings entry point -- `crates/php/src/sapi.rs`: PHP SAPI implementation (core request handling) -- `crates/lang_handler/src/`: Generic language handler abstractions (request/response/rewriter) +- `src/lib.rs`: Library entry point, exports core types and NAPI bindings +- `src/main.rs`: Binary entry point for standalone testing +- `src/embed.rs`: Core `Embed` type for handling PHP requests +- `src/sapi.rs`: PHP SAPI implementation (low-level PHP integration) +- `src/runtime.rs`: NAPI runtime implementation (when `napi-support` feature enabled) - `__test__/*.spec.mjs`: Test files for each component \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 551abdb..f95b5fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] edition = "2021" -name = "php-node" +name = "php" version = "1.4.0" authors = ["Platformatic Inc. (https://platformatic.dev)"] license = "MIT" @@ -11,7 +11,7 @@ default = [] napi-support = ["dep:napi", "dep:napi-derive", "dep:napi-build", "http-handler/napi-support", "http-rewriter/napi-support"] [lib] -name = "php_node" +name = "php" crate-type = ["cdylib", "rlib"] path = "src/lib.rs" @@ -26,7 +26,7 @@ hostname = "0.4.1" ext-php-rs = { version = "0.14.0", features = ["embed"] } http-handler = { git = "https://github.com/platformatic/http-handler.git" } # http-handler = { path = "../http-handler" } -http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git", branch = "add-docroot-parameter-support" } +http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git" } # http-rewriter = { path = "../http-rewriter" } libc = "0.2.171" # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix diff --git a/src/embed.rs b/src/embed.rs index dfb889f..14ea8e3 100644 --- a/src/embed.rs +++ b/src/embed.rs @@ -12,7 +12,7 @@ use ext_php_rs::{ zend::{try_catch, try_catch_first, ExecutorGlobals, SapiGlobals}, }; -use http_handler::{Handler, Request, Response}; +use http_handler::{Handler, Request, Response, ResponseBuilderExt}; use super::{ sapi::{ensure_sapi, Sapi}, @@ -90,7 +90,7 @@ impl Embed { /// /// ``` /// use std::env::current_dir; - /// use php_node::Embed; + /// use php::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -113,7 +113,7 @@ impl Embed { /// /// ``` /// use std::env::{args, current_dir}; - /// use php_node::Embed; + /// use php::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -137,7 +137,7 @@ impl Embed { /// /// ``` /// use std::env::current_dir; - /// use php_node::{Embed, Handler, Request, Response}; + /// use php::{Embed, Handler, Request, Response}; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -174,7 +174,7 @@ impl Embed { /// /// ```rust /// use std::env::current_dir; - /// use php_node::Embed; + /// use php::Embed; /// /// let docroot = current_dir() /// .expect("should have current_dir"); @@ -199,7 +199,7 @@ impl Handler for Embed { /// /// ``` /// use std::{env::temp_dir, fs::File, io::Write}; - /// use php_node::{Embed, Handler, Request, Response, MockRoot}; + /// use php::{Embed, Handler, Request, Response, MockRoot}; /// /// let docroot = MockRoot::builder() /// .file("index.php", "") @@ -321,10 +321,13 @@ impl Handler for Embed { let ex = Error::Exception(err); - // Fixed exception handling (FIXME.md #3) by using ResponseExt::set_exception if let Some(ctx) = RequestContext::current() { - ctx.set_response_exception(ex.to_string()); - ctx.set_response_status(500); + let builder = std::mem::replace( + ctx.response_builder_mut(), + http_handler::response::Builder::new(), + ); + let builder = builder.exception(ex.to_string()).status(500); + *ctx.response_builder_mut() = builder; } return Err(EmbedRequestError::Exception(ex.to_string())); @@ -351,10 +354,16 @@ impl Handler for Embed { efree(mimetype.cast::()); } - // Set the final status and content-type header using the new clean API (FIXME.md #4) + // Set the final status and content-type header if let Some(ctx) = RequestContext::current() { - ctx.set_response_status(http_response_code as u16); - ctx.add_response_header("Content-Type", mime); + let builder = std::mem::replace( + ctx.response_builder_mut(), + http_handler::response::Builder::new(), + ); + let builder = builder + .status(http_response_code as u16) + .header("Content-Type", mime); + *ctx.response_builder_mut() = builder; } // Build the final response with accumulated data using the extension system diff --git a/src/lib.rs b/src/lib.rs index 7204a16..6d4c6f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,8 @@ //! ```rust //! use std::env::{args, current_dir}; //! # use std::path::PathBuf; -//! # use php_node::MockRoot; -//! use php_node::{ +//! # use php::MockRoot; +//! use php::{ //! rewrite::{PathRewriter, Rewriter}, //! Embed, Handler, Request, //! }; diff --git a/src/main.rs b/src/main.rs index beac07e..9020f74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{env::current_dir, fs::File, io::Write, path::PathBuf}; use bytes::BytesMut; -use php_node::{rewrite::PathRewriter, Embed, Handler, Request, RequestRewriter}; +use php::{rewrite::PathRewriter, Embed, Handler, Request, RequestRewriter}; #[tokio::main] async fn main() { diff --git a/src/request_context.rs b/src/request_context.rs index 2c09bc7..6b1a9cb 100644 --- a/src/request_context.rs +++ b/src/request_context.rs @@ -1,49 +1,23 @@ -use bytes::BytesMut; use ext_php_rs::zend::SapiGlobals; -use http_handler::request::Parts; -use http_handler::{ - BodyBuffer, HeaderMap, HeaderName, HeaderValue, Request, ResponseBuilderExt, ResponseLog, - StatusCode, -}; +use http_handler::{BodyBuffer, Request}; use std::{ffi::c_void, path::PathBuf}; /// The request context for the PHP SAPI. -/// -/// This has been redesigned to address all issues in FIXME.md: -/// - Uses request Parts to avoid RefUnwindSafe issues (#1) -/// - Stores mutable body for proper consumption (#2) -/// - Uses extension types to accumulate response data (#3, #4) -#[derive(Debug)] pub struct RequestContext { - request_parts: Parts, - request_body: BytesMut, - response_status: StatusCode, - response_headers: HeaderMap, - response_body: BodyBuffer, - response_log: ResponseLog, - response_exception: Option, + request: Request, + response_builder: http_handler::response::Builder, docroot: PathBuf, } impl RequestContext { /// Sets the current request context for the PHP SAPI. - /// - /// Uses into_parts() to avoid RefUnwindSafe issues (FIXME.md #1). pub fn for_request(request: Request, docroot: S) where S: Into, { - // Use into_parts() to avoid RefUnwindSafe issues (FIXME.md #1) - let (parts, body) = request.into_parts(); - let context = Box::new(RequestContext { - request_parts: parts, - request_body: body, - response_status: StatusCode::OK, - response_headers: HeaderMap::new(), - response_body: BodyBuffer::new(), - response_log: ResponseLog::new(), - response_exception: None, + request, + response_builder: http_handler::response::Builder::new(), docroot: docroot.into(), }); let mut globals = SapiGlobals::get_mut(); @@ -76,86 +50,32 @@ impl RequestContext { Some(unsafe { Box::from_raw(ptr as *mut RequestContext) }) } - /// Returns a reference to the request parts. - /// This replaces the old request() method since we now use parts. - pub fn request_parts(&self) -> &Parts { - &self.request_parts - } - - /// Returns a mutable reference to the request body. - /// This allows proper consumption of the body (FIXME.md #2). - pub fn request_body_mut(&mut self) -> &mut BytesMut { - &mut self.request_body + /// Returns a reference to the request. + pub fn request(&self) -> &Request { + &self.request } - /// Returns a reference to the request body. - pub fn request_body(&self) -> &BytesMut { - &self.request_body - } - - /// Add a header to the response. - pub fn add_response_header(&mut self, key: K, value: V) - where - K: TryInto, - V: TryInto, - { - if let (Ok(header_name), Ok(header_value)) = (key.try_into(), value.try_into()) { - self.response_headers.insert(header_name, header_value); - } - } - - /// Set the response status code. - pub fn set_response_status(&mut self, status: u16) { - if let Ok(status_code) = StatusCode::from_u16(status) { - self.response_status = status_code; - } + /// Returns a mutable reference to the request. + pub fn request_mut(&mut self) -> &mut Request { + &mut self.request } - /// Write data to the response body. - pub fn write_response_body(&mut self, data: &[u8]) { - self.response_body.append(data); - } - - /// Write to the response log. - /// This uses extension types to accumulate log data (FIXME.md #4). - pub fn write_response_log(&mut self, data: &[u8]) { - self.response_log.append(data); - } - - /// Set an exception on the response. - /// This stores the exception to be added via ResponseBuilderExt (FIXME.md #3). - pub fn set_response_exception(&mut self, exception: impl Into) { - self.response_exception = Some(exception.into()); + /// Returns a mutable reference to the response builder. + pub fn response_builder_mut(&mut self) -> &mut http_handler::response::Builder { + &mut self.response_builder } /// Build the final response using the accumulated data. - /// This properly uses ResponseBuilderExt for logs and exceptions (FIXME.md #3, #4). - pub fn build_response(self) -> Result { - // Start building the response - let mut builder = http_handler::response::Response::builder().status(self.response_status); - - // Add all headers - for (key, value) in &self.response_headers { - builder = builder.header(key, value); - } - - // Add extensions using ResponseBuilderExt - builder = builder - .body_buffer(self.response_body) - .log(self.response_log.into_bytes()); - - if let Some(exception) = self.response_exception { - builder = builder.exception(exception); - } - - // Get the body buffer from extensions and build final response - let body = builder + pub fn build_response(mut self) -> Result { + // Extract the body buffer from extensions (if any was accumulated) + let body = self + .response_builder .extensions_mut() .and_then(|ext| ext.remove::()) .unwrap_or_default() .into_bytes_mut(); - builder.body(body) + self.response_builder.body(body) } /// Returns the docroot associated with this request context diff --git a/src/sapi.rs b/src/sapi.rs index 3838715..8ca94a6 100644 --- a/src/sapi.rs +++ b/src/sapi.rs @@ -23,6 +23,7 @@ use ext_php_rs::{ use once_cell::sync::OnceCell; use crate::{EmbedRequestError, EmbedStartError, RequestContext}; +use http_handler::ResponseBuilderExt; // This is a helper to ensure that PHP is initialized and deinitialized at the // appropriate times. @@ -267,8 +268,7 @@ pub extern "C" fn sapi_module_ub_write(str: *const c_char, str_length: usize) -> let len = bytes.len(); if let Some(ctx) = RequestContext::current() { - // Use new method name for clarity (FIXME.md #4) - ctx.write_response_body(bytes); + ctx.response_builder_mut().append_body(bytes); } len } @@ -291,8 +291,12 @@ pub extern "C" fn sapi_module_send_header(header: *mut SapiHeader, _server_conte // Header value is None for http version + status line if let Some(value) = header.value() { if let Some(ctx) = RequestContext::current() { - // Use new method that doesn't require mem::replace hacks (FIXME.md #4) - ctx.add_response_header(name, value); + let builder = std::mem::replace( + ctx.response_builder_mut(), + http_handler::response::Builder::new(), + ); + let builder = builder.header(name, value); + *ctx.response_builder_mut() = builder; } } } @@ -305,7 +309,7 @@ pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> u RequestContext::current() .map(|ctx| { - let body = ctx.request_body_mut(); + let body = ctx.request_mut().body_mut(); let actual_length = length.min(body.len()); if actual_length == 0 { return 0; @@ -325,7 +329,7 @@ pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> u #[no_mangle] pub extern "C" fn sapi_module_read_cookies() -> *mut c_char { RequestContext::current() - .map(|ctx| match ctx.request_parts().headers.get("Cookie") { + .map(|ctx| match ctx.request().headers().get("Cookie") { Some(cookie) => estrdup(cookie.to_str().unwrap_or("")), None => std::ptr::null_mut(), }) @@ -373,8 +377,9 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t // } if let Some(ctx) = RequestContext::current() { - let request_parts = ctx.request_parts(); - let headers = &request_parts.headers; + let request = ctx.request(); + let headers = request.headers(); + let uri = request.uri(); // Hack to allow ? syntax for the following code. // At the moment any errors are just swallowed, but these could be @@ -401,21 +406,17 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t std::ptr::null_mut() }; - env_var( - vars, - "REQUEST_SCHEME", - request_parts.uri.scheme_str().unwrap_or("http"), - )?; + env_var(vars, "REQUEST_SCHEME", uri.scheme_str().unwrap_or("http"))?; env_var(vars, "CONTEXT_PREFIX", "")?; env_var(vars, "SERVER_ADMIN", "webmaster@localhost")?; env_var(vars, "GATEWAY_INTERFACE", "CGI/1.1")?; // Laravel seems to think "/register" should be "/index.php/register"? // env_var_c(vars, "PHP_SELF", script_name as *mut c_char)?; - env_var(vars, "PHP_SELF", request_parts.uri.path())?; + env_var(vars, "PHP_SELF", uri.path())?; // TODO: is "/register", should be "/index.php" - env_var(vars, "SCRIPT_NAME", request_parts.uri.path())?; + env_var(vars, "SCRIPT_NAME", uri.path())?; // env_var_c(vars, "SCRIPT_NAME", script_name as *mut c_char)?; env_var_c(vars, "PATH_INFO", script_name as *mut c_char)?; env_var_c(vars, "SCRIPT_FILENAME", script_filename)?; @@ -440,7 +441,7 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t env_var_c(vars, "SERVER_SOFTWARE", inner_sapi.name)?; } - if let Some(socket_info) = request_parts.extensions.get::() { + if let Some(socket_info) = request.extensions().get::() { if let Some(local) = socket_info.local { env_var(vars, "SERVER_ADDR", local.ip().to_string())?; env_var(vars, "SERVER_PORT", local.port().to_string())?; @@ -473,8 +474,7 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_int: c_int) { let message = unsafe { CStr::from_ptr(message) }; if let Some(ctx) = RequestContext::current() { - // Use new method that uses extension system (FIXME.md #4) - ctx.write_response_log(message.to_bytes()); + ctx.response_builder_mut().append_log(message.to_bytes()); } } @@ -486,11 +486,11 @@ pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_i pub fn apache_request_headers() -> Result, String> { let mut headers = HashMap::new(); - let request_parts = RequestContext::current() - .map(|ctx| ctx.request_parts()) + let request = RequestContext::current() + .map(|ctx| ctx.request()) .ok_or("Request context unavailable")?; - for (key, value) in request_parts.headers.iter() { + for (key, value) in request.headers().iter() { headers.insert(key.to_string(), value.to_str().unwrap_or("").to_string()); }