diff --git a/crates/lang_handler/src/ffi.rs b/crates/lang_handler/src/ffi.rs index a92e65b2..9304a09a 100644 --- a/crates/lang_handler/src/ffi.rs +++ b/crates/lang_handler/src/ffi.rs @@ -1,6 +1,6 @@ use std::{ - ffi, - ffi::{c_char, CStr, CString}, + ffi::{self, c_char, CStr, CString}, + net::SocketAddr, }; use bytes::{Buf, BufMut}; @@ -216,6 +216,20 @@ impl From<&lh_request_t> for Request { } } +fn c_char_to_socket_addr(value: *const ffi::c_char) -> Option { + if value.is_null() { + return None; + } + + // Convert to &str + let value = match unsafe { CStr::from_ptr(value) }.to_str() { + Err(_) => return None, + Ok(s) => s, + }; + + value.parse::().ok() +} + /// Create a new `lh_request_t`. /// /// # Examples @@ -229,6 +243,8 @@ pub extern "C" fn lh_request_new( url: *const ffi::c_char, headers: *mut lh_headers_t, body: *const ffi::c_char, + local_socket: *const ffi::c_char, + remote_socket: *const ffi::c_char, ) -> *mut lh_request_t { let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; let url_str = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; @@ -238,8 +254,17 @@ pub extern "C" fn lh_request_new( } else { Some(unsafe { CStr::from_ptr(body).to_bytes() }) }; + let local_socket = c_char_to_socket_addr(local_socket); + let remote_socket = c_char_to_socket_addr(remote_socket); let headers = unsafe { &*headers }; - let request = Request::new(method, url, headers.into(), body.unwrap_or(&[])); + let request = Request::new( + method, + url, + headers.into(), + body.unwrap_or(&[]), + local_socket, + remote_socket, + ); Box::into_raw(Box::new(request.into())) } diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs index 1cd95947..44760cec 100644 --- a/crates/lang_handler/src/request.rs +++ b/crates/lang_handler/src/request.rs @@ -1,4 +1,7 @@ -use std::fmt::Debug; +use std::{ + fmt::Debug, + net::{AddrParseError, SocketAddr}, +}; use bytes::{Bytes, BytesMut}; use url::{ParseError, Url}; @@ -37,6 +40,8 @@ pub struct Request { headers: Headers, // TODO: Support Stream bodies when napi.rs supports it body: Bytes, + local_socket: Option, + remote_socket: Option, } unsafe impl Sync for Request {} @@ -57,14 +62,25 @@ impl Request { /// "POST".to_string(), /// "http://example.com/test.php".parse().unwrap(), /// headers, - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None, /// ); - pub fn new>(method: String, url: Url, headers: Headers, body: T) -> Self { + 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, } } @@ -133,7 +149,9 @@ impl Request { /// "POST".to_string(), /// "http://example.com/test.php".parse().unwrap(), /// Headers::new(), - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None, /// ); /// /// assert_eq!(request.method(), "POST"); @@ -153,7 +171,9 @@ impl Request { /// "POST".to_string(), /// "http://example.com/test.php".parse().unwrap(), /// Headers::new(), - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None, /// ); /// /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); @@ -176,7 +196,9 @@ impl Request { /// "POST".to_string(), /// "http://example.com/test.php".parse().unwrap(), /// headers, - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None, /// ); /// /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); @@ -196,7 +218,9 @@ impl Request { /// "POST".to_string(), /// "http://example.com/test.php".parse().unwrap(), /// Headers::new(), - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None, /// ); /// /// assert_eq!(request.body(), "Hello, World!"); @@ -204,6 +228,50 @@ impl Request { 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 + } } /// Builds an HTTP request. @@ -233,6 +301,8 @@ pub struct RequestBuilder { url: Option, headers: Headers, body: BytesMut, + local_socket: Option, + remote_socket: Option, } impl RequestBuilder { @@ -251,6 +321,8 @@ impl RequestBuilder { url: None, headers: Headers::new(), body: BytesMut::with_capacity(1024), + local_socket: None, + remote_socket: None, } } @@ -285,6 +357,8 @@ impl RequestBuilder { url: Some(request.url().clone()), headers: request.headers().clone(), body: BytesMut::from(request.body()), + local_socket: request.local_socket.clone(), + remote_socket: request.remote_socket.clone(), } } @@ -372,6 +446,58 @@ impl RequestBuilder { self } + /// Sets the local socket of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .local_socket("127.0.0.1:8080").expect("invalid local socket") + /// .build(); + /// + /// assert_eq!(request.local_socket(), "127.0.0.1:8080"); + /// ``` + pub fn local_socket(mut self, local_socket: T) -> Result + where + T: Into, + { + match local_socket.into().parse() { + Err(e) => Err(e), + Ok(local_socket) => { + self.local_socket = Some(local_socket); + Ok(self) + } + } + } + + /// Sets the remote socket of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .remote_socket("127.0.0.1:8080").expect("invalid remote socket") + /// .build(); + /// + /// assert_eq!(request.remote_socket(), "127.0.0.1:8080"); + /// ``` + pub fn remote_socket(mut self, remote_socket: T) -> Result + where + T: Into, + { + match remote_socket.into().parse() { + Err(e) => Err(e), + Ok(remote_socket) => { + self.remote_socket = Some(remote_socket); + Ok(self) + } + } + } + /// Builds the request. /// /// # Examples @@ -395,6 +521,8 @@ impl RequestBuilder { .unwrap_or_else(|| Url::parse("http://example.com").unwrap()), headers: self.headers, body: self.body.freeze(), + local_socket: self.local_socket, + remote_socket: self.remote_socket, } } } diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index ca86dfad..f2754b20 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -727,11 +727,6 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t c"".as_ptr() }; - // php_register_variable(cstr("PATH").unwrap(), cstr("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin").unwrap(), vars); - // php_register_variable(cstr("SERVER_SIGNATURE").unwrap(), cstr(" - // Apache/2.4.62 (Debian) Server at localhost Port 8080 - - // ").unwrap(), vars); php_register_variable( cstr("REQUEST_SCHEME").unwrap(), cstr(request.url().scheme()).unwrap(), @@ -773,15 +768,31 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t vars, ); - // TODO: REMOTE_ADDR, REMOTE_PORT + if let Some(info) = request.local_socket() { + php_register_variable( + cstr("SERVER_ADDR").unwrap(), + cstr(info.ip().to_string()).unwrap(), + vars, + ); + php_register_variable( + cstr("SERVER_PORT").unwrap(), + cstr(info.port().to_string()).unwrap(), + vars, + ); + } - // TODO: This should pull from the _real_ headers - php_register_variable(cstr("HTTP_HOST").unwrap(), c"localhost:3000".as_ptr(), vars); - php_register_variable(cstr("SERVER_NAME").unwrap(), c"localhost".as_ptr(), vars); - php_register_variable(cstr("SERVER_ADDR").unwrap(), c"172.19.0.2".as_ptr(), vars); - php_register_variable(cstr("SERVER_PORT").unwrap(), c"3000".as_ptr(), vars); - php_register_variable(cstr("REMOTE_ADDR").unwrap(), c"192.168.65.1".as_ptr(), vars); - php_register_variable(cstr("REMOTE_PORT").unwrap(), c"21845".as_ptr(), vars); + if let Some(info) = request.remote_socket() { + php_register_variable( + cstr("REMOTE_ADDR").unwrap(), + cstr(info.ip().to_string()).unwrap(), + vars, + ); + php_register_variable( + cstr("REMOTE_PORT").unwrap(), + cstr(info.port().to_string()).unwrap(), + vars, + ); + } if !req_info.request_method.is_null() { php_register_variable( diff --git a/crates/php_node/src/request.rs b/crates/php_node/src/request.rs index 74e7ccb2..d78f04f7 100644 --- a/crates/php_node/src/request.rs +++ b/crates/php_node/src/request.rs @@ -1,11 +1,25 @@ use std::collections::HashMap; 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 remote IP address. + pub remote_address: String, + /// The numeric representation of the remote port. For example, 80 or 21. + pub remote_port: u16, +} + /// Options for creating a new PHP request. #[napi(object)] #[derive(Default)] @@ -20,6 +34,8 @@ pub struct PhpRequestOptions { pub headers: Option>>, /// The body for the request. pub body: Option, + /// The socket information for the request. + pub socket: Option, } /// A PHP request. @@ -61,17 +77,28 @@ impl PhpRequest { /// }); /// ``` #[napi(constructor)] - pub fn constructor(options: PhpRequestOptions) -> Self { + pub fn constructor(options: PhpRequestOptions) -> Result { let mut builder: RequestBuilder = Request::builder() .method(options.method) - .url(options.url) - .expect("invalid url"); + .url(&options.url) + .map_err(|_| Error::from_reason(format!("Invalid URL \"{}\"", options.url)))?; + + if let Some(socket) = options.socket { + let local_socket = format!("{}:{}", socket.local_address, socket.local_port); + let remote_socket = format!("{}:{}", socket.remote_address, socket.remote_port); + + builder = builder + .local_socket(&local_socket) + .map_err(|_| Error::from_reason(format!("Invalid local socket \"{}\"", local_socket)))? + .remote_socket(&remote_socket) + .map_err(|_| Error::from_reason(format!("Invalid remote socket \"{}\"", remote_socket)))?; + } if let Some(headers) = options.headers { for key in headers.keys() { - let values = headers - .get(key) - .expect(format!("missing header values for key: {}", key).as_str()); + let values = headers.get(key).ok_or_else(|| { + Error::from_reason(format!("Missing header values for key \"{}\"", key)) + })?; for value in values { builder = builder.header(key.clone(), value.clone()) @@ -83,9 +110,9 @@ impl PhpRequest { builder = builder.body(body.as_ref()) } - PhpRequest { + Ok(PhpRequest { request: builder.build(), - } + }) } /// Get the HTTP method for the request. diff --git a/crates/php_node/src/response.rs b/crates/php_node/src/response.rs index 6aa39b19..48494e99 100644 --- a/crates/php_node/src/response.rs +++ b/crates/php_node/src/response.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use napi::bindgen_prelude::*; +use napi::Result; use php::Response; @@ -52,15 +53,15 @@ impl PhpResponse { /// }); /// ``` #[napi(constructor)] - pub fn constructor(options: PhpResponseOptions) -> Self { + pub fn constructor(options: PhpResponseOptions) -> Result { let mut builder = Response::builder(); builder.status(options.status); if let Some(headers) = options.headers { for key in headers.keys() { - let values = headers - .get(key) - .expect(format!("missing header values for key: {}", key).as_str()); + let values = headers.get(key).ok_or_else(|| { + Error::from_reason(format!("Missing header values for key \"{}\"", key)) + })?; for value in values { builder.header(key.clone(), value.clone()); @@ -80,9 +81,9 @@ impl PhpResponse { builder.exception(exception); } - PhpResponse { + Ok(PhpResponse { response: builder.build(), - } + }) } /// Get the HTTP status code for the response. diff --git a/crates/php_node/src/runtime.rs b/crates/php_node/src/runtime.rs index 24783eb1..ab2024a8 100644 --- a/crates/php_node/src/runtime.rs +++ b/crates/php_node/src/runtime.rs @@ -12,7 +12,7 @@ use crate::{PhpRequest, PhpResponse}; #[derive(Clone, Default)] pub struct PhpOptions { /// The command-line arguments for the PHP instance. - pub argv: Option>, + pub argv: Vec, /// The document root for the PHP instance. pub docroot: String, } @@ -55,10 +55,7 @@ impl PhpRuntime { let docroot = options.docroot.clone(); let argv = options.argv.clone(); - let embed = match argv { - Some(argv) => Embed::new_with_argv(docroot, argv), - None => Embed::new(docroot), - }; + let embed = Embed::new_with_argv(docroot, argv); Self { embed: Arc::new(embed), diff --git a/demo/index.html b/demo/index.html index d3968c7b..2e3c1dcd 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,6 +7,6 @@

PHP Node

-

Try a request to /info to see the phpinfo() output.

+

Try a request to /info.php to see the phpinfo() output.

diff --git a/demo/server.js b/demo/server.js index f6afd3dd..07f4b0f0 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,8 +1,10 @@ -import { join, resolve } from 'path' -import { readFileSync } from 'fs' -import { createServer } from 'http' +import { argv, cwd } from 'node:process' +import { join, resolve } from 'node:path' +import { readFileSync } from 'node:fs' +import { createServer } from 'node:http' +import { strictEqual } from 'node:assert' + import { Php, Request } from '../index.js' -import { strictEqual } from 'assert' // Read homepage html const { dirname } = import.meta @@ -10,8 +12,8 @@ const homepage = readFileSync(join(dirname, 'index.html'), 'utf8') // Create reusable PHP instance const php = new Php({ - file: 'index.php', - code: readFileSync(resolve(dirname, 'index.php'), 'utf8') + argv, + docroot: cwd() }) const server = createServer(async (req, res) => { @@ -24,8 +26,8 @@ const server = createServer(async (req, res) => { const url = urlForRequest(req) - // Every page except /info should show the homepage. - if (url.pathname !== '/info') { + // Every page except /info.php should show the homepage. + if (url.pathname !== '/info.php') { res.writeHead(200, { 'Content-Type': 'text/html' }) @@ -37,7 +39,8 @@ const server = createServer(async (req, res) => { method: req.method, url: url.href, headers: fixHeaders(req.headers), - body: Buffer.concat(chunks) + body: Buffer.concat(chunks), + socket: req.socket }) try { @@ -54,7 +57,7 @@ const server = createServer(async (req, res) => { server.listen(3000, async () => { const { port } = server.address() - const url = `http://localhost:${port}/info` + const url = `http://localhost:${port}/info.php` const res = await fetch(url, { method: 'POST', diff --git a/index.d.ts b/index.d.ts index 4b3a4395..cbb7f8e7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,6 +3,16 @@ /* auto-generated by NAPI-RS */ +export interface PhpRequestSocketOptions { + /** 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. */ + localPort: string + /** The string representation of the remote IP address. */ + remoteAddress: string + /** The numeric representation of the remote port. For example, 80 or 21. */ + remotePort: string +} /** Options for creating a new PHP request. */ export interface PhpRequestOptions { /** The HTTP method for the request. */ @@ -17,6 +27,8 @@ export interface PhpRequestOptions { headers?: Record> /** The body for the request. */ body?: Uint8Array + /** The socket information for the request. */ + socket?: PhpRequestSocketOptions } /** Options for creating a new PHP response. */ export interface PhpResponseOptions { @@ -38,7 +50,7 @@ export interface PhpResponseOptions { /** Options for creating a new PHP instance. */ export interface PhpOptions { /** The command-line arguments for the PHP instance. */ - argv?: Array + argv: Array /** The document root for the PHP instance. */ docroot: string }