diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a0117d9b..ea64d4b4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -25,48 +25,81 @@ jobs: fail-fast: false matrix: settings: - - host: macos-latest + - host: macos-13 target: x86_64-apple-darwin + os: macos + arch: x86_64 build: yarn build --target x86_64-apple-darwin - - host: windows-latest - build: yarn build --target x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc - - host: windows-latest - build: | - yarn build --target i686-pc-windows-msvc - yarn test - target: i686-pc-windows-msvc - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - build: yarn build --target x86_64-unknown-linux-gnu - - host: macos-latest + setup: | + brew install autoconf automake libtool + - host: macos-15 target: aarch64-apple-darwin + os: macos + arch: aarch64 build: yarn build --target aarch64-apple-darwin + setup: | + brew install autoconf automake libtool - host: ubuntu-latest - target: aarch64-unknown-linux-gnu - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - build: yarn build --target aarch64-unknown-linux-gnu - - host: ubuntu-latest - target: armv7-unknown-linux-gnueabihf + target: x86_64-unknown-linux-gnu + os: linux + arch: x86_64 + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build: yarn build --target x86_64-unknown-linux-gnu + # build: yarn build setup: | sudo apt-get update - sudo apt-get install gcc-arm-linux-gnueabihf -y - build: yarn build --target armv7-unknown-linux-gnueabihf + sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + # build: yarn build --target aarch64-unknown-linux-gnu + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: armv7-unknown-linux-gnueabihf + # setup: | + # sudo apt-get update + # sudo apt-get install gcc-arm-linux-gnueabihf libssl-dev -y + # build: yarn build --target armv7-unknown-linux-gnueabihf - host: ubuntu-latest - target: armv7-unknown-linux-musleabihf - build: yarn build --target armv7-unknown-linux-musleabihf - - host: ubuntu-latest - target: aarch64-unknown-linux-musl + target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: |- set -e && - rustup target add aarch64-unknown-linux-musl && - yarn build --target aarch64-unknown-linux-musl - - host: windows-latest - target: aarch64-pc-windows-msvc - build: yarn build --target aarch64-pc-windows-msvc - name: stable - ${{ matrix.settings.target }} - node@20 + rustup target add x86_64-unknown-linux-musl && + yarn build --target x86_64-unknown-linux-musl + setup: | + sudo apt-get update + sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: armv7-unknown-linux-musleabihf + # build: yarn build --target armv7-unknown-linux-musleabihf + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: aarch64-unknown-linux-musl + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + # build: |- + # set -e && + # rustup target add aarch64-unknown-linux-musl && + # yarn build --target aarch64-unknown-linux-musl + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: windows-latest + # target: aarch64-pc-windows-msvc + # build: yarn build --target aarch64-pc-windows-msvc + # - host: windows-latest + # target: x86_64-pc-windows-msvc + # build: yarn build --target x86_64-pc-windows-msvc + # - host: windows-latest + # target: i686-pc-windows-msvc + # build: | + # yarn build --target i686-pc-windows-msvc + # yarn test + name: stable - ${{ matrix.settings.target }} - node@22 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 @@ -74,7 +107,7 @@ jobs: uses: actions/setup-node@v4 if: ${{ !matrix.settings.docker }} with: - node-version: 20 + node-version: 22 cache: yarn - name: Install uses: dtolnay/rust-toolchain@stable @@ -100,19 +133,35 @@ jobs: run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - - name: Setup node x86 - if: matrix.settings.target == 'i686-pc-windows-msvc' - run: yarn config set supportedArchitectures.cpu "ia32" + - name: Fetch SPC + shell: bash + run: ./scripts/fetch-spc.sh + - name: Cache PHP Download + id: cache-php-download + uses: actions/cache@v4 + with: + path: downloads + key: php-download-${{ matrix.settings.target }}-${{ matrix.settings.host }} + - name: Fetch PHP and extensions + if: steps.cache-php-download.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-php.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Cache PHP + id: cache-php-build + uses: actions/cache@v4 + with: + path: | + buildroot + source + key: php-build-${{ matrix.settings.target }}-${{ matrix.settings.host }} + - name: Build PHP + if: steps.cache-php-build.outputs.cache-hit != 'true' shell: bash + run: ./scripts/build-php.sh - name: Install dependencies run: yarn install - - name: Setup node x86 - uses: actions/setup-node@v4 - if: matrix.settings.target == 'i686-pc-windows-msvc' - with: - node-version: 20 - cache: yarn - architecture: x86 - name: Build in docker uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} @@ -340,48 +389,48 @@ jobs: name: bindings-universal-apple-darwin path: ${{ env.APP_NAME }}.*.node if-no-files-found: error - publish: - name: Publish - runs-on: ubuntu-latest - needs: - - test-macOS-windows-binding - - test-linux-x64-gnu-binding - - test-linux-aarch64-gnu-binding - - test-linux-aarch64-musl-binding - - test-linux-arm-gnueabihf-binding - - universal-macOS - steps: - - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - - name: Install dependencies - run: yarn install - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - name: Move artifacts - run: yarn artifacts - - name: List packages - run: ls -R ./npm - shell: bash - - name: Publish - run: | - npm config set provenance true - if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --access public - elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --tag next --access public - else - echo "Not a release, skipping publish" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # publish: + # name: Publish + # runs-on: ubuntu-latest + # needs: + # - test-macOS-windows-binding + # - test-linux-x64-gnu-binding + # - test-linux-aarch64-gnu-binding + # - test-linux-aarch64-musl-binding + # - test-linux-arm-gnueabihf-binding + # - universal-macOS + # steps: + # - uses: actions/checkout@v4 + # - name: Setup node + # uses: actions/setup-node@v4 + # with: + # node-version: 20 + # cache: yarn + # - name: Install dependencies + # run: yarn install + # - name: Download all artifacts + # uses: actions/download-artifact@v4 + # with: + # path: artifacts + # - name: Move artifacts + # run: yarn artifacts + # - name: List packages + # run: ls -R ./npm + # shell: bash + # - name: Publish + # run: | + # npm config set provenance true + # if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; + # then + # echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + # npm publish --access public + # elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; + # then + # echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + # npm publish --tag next --access public + # else + # echo "Not a release, skipping publish" + # fi + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 4f6f4b78..add4fce9 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,8 @@ Cargo.lock crates/php/buildroot crates/php/downloads crates/php/source + +buildroot +downloads +source +spc diff --git a/Cargo.toml b/Cargo.toml index d98574bb..9dc13c56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/lang_handler", "crates/php", "crates/php_node" ] diff --git a/__test__/index.spec.mjs b/__test__/index.spec.mjs index 0a783448..a5e59cd1 100644 --- a/__test__/index.spec.mjs +++ b/__test__/index.spec.mjs @@ -1,7 +1,93 @@ import test from 'ava' -import { handleRequest } from '../index.js' +import { Php, Request } from '../index.js' -test('sum from native', (t) => { - t.is(handleRequest(), "Hello, World!") +test('input/output streams work', async (t) => { + const php = new Php({ + argv: process.argv, + file: 'index.php', + code: ` + if (file_get_contents('php://input') == 'Hello, from Node.js!') { + echo 'Hello, from PHP!'; + } + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php', + body: Buffer.from('Hello, from Node.js!') + }) + + const res = await php.handleRequest(req) + t.is(res.status, 200) + t.is(res.body.toString('utf8'), 'Hello, from PHP!') +}) + +test('logs work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + error_log('Hello, from error_log!'); + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php' + }) + + const res = await php.handleRequest(req) + t.is(res.status, 200) + t.is(res.log.toString('utf8'), 'Hello, from error_log!\n') +}) + +test('exceptions work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + throw new Exception('Hello, from PHP!'); + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php' + }) + + await t.throwsAsync(php.handleRequest(req), { + message: 'Hello, from PHP!' + }) +}) + +test('request headers work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + foreach($_SERVER as $key => $val) { + if ($key == 'argv') { + for ($i = 0; $i < count($val); $i++) { + $arg = $val[$i]; + echo "argv[$i]: $arg\n"; + } + continue; + } + + echo "$key: $val\n"; + } + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php', + headers: { + 'X-Test': ['Hello, from Node.js!'] + } + }) + + const res = await php.handleRequest(req) + console.log(res) + t.is(res.status, 200) + t.is(res.body.toString(), 'Hello, from PHP!') }) diff --git a/crates/lang_handler/Cargo.toml b/crates/lang_handler/Cargo.toml new file mode 100644 index 00000000..df1d4cfd --- /dev/null +++ b/crates/lang_handler/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2021" +name = "lang_handler" +version = "0.0.0" + +[lib] +name = "lang_handler" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[features] +default = [] +c = [] + +[build-dependencies] +cbindgen = "0.28.0" + +[dependencies] +bytes = "1.10.1" +url = "2.5.4" diff --git a/crates/lang_handler/build.rs b/crates/lang_handler/build.rs new file mode 100644 index 00000000..3f9ef3b6 --- /dev/null +++ b/crates/lang_handler/build.rs @@ -0,0 +1,24 @@ +extern crate cbindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("../../..") + .canonicalize() + .unwrap(); + + let header_path = out_dir + .join("lang_handler.h"); + + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_include_guard("LANG_HANDLER_H") + .with_language(cbindgen::Language::C) + .generate() + .expect("Unable to generate bindings") + .write_to_file(header_path); +} diff --git a/crates/lang_handler/src/ffi.rs b/crates/lang_handler/src/ffi.rs new file mode 100644 index 00000000..eae9f594 --- /dev/null +++ b/crates/lang_handler/src/ffi.rs @@ -0,0 +1,1020 @@ +use std::{ffi, ffi::{CStr, CString, c_char}}; + +use bytes::{Buf, BufMut}; + +use crate::{Headers, Request, RequestBuilder, Response, ResponseBuilder, Url}; + +/// Reclaim a string allocated by the library. +/// +/// # Examples +/// +/// ```c +/// const char* rust_string = get_string_from_rust(); +/// // Return ownership of the string to Rust and drop +/// lh_reclaim_str(rust_string); +/// ``` +#[no_mangle] +pub extern "C" fn lh_reclaim_str(url: *const c_char) { + unsafe { + drop(CString::from_raw(url as *mut c_char)); + } +} + +/// A multi-map of HTTP headers. Each key can have multiple values. +#[allow(non_camel_case_types)] +pub struct lh_headers_t { + inner: Headers, +} + +/// Convert a `Headers` into a `lh_headers_t`. +impl From for lh_headers_t { + fn from(inner: Headers) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_headers_t` into a `Headers`. +impl From<&lh_headers_t> for Headers { + fn from(headers: &lh_headers_t) -> Headers { + headers.inner.clone() + } +} + +/// Create a new `lh_headers_t`. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_new() -> *mut lh_headers_t { + let headers = Headers::new(); + Box::into_raw(Box::new(headers.into())) +} + +/// Free a `lh_headers_t`. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// +/// // Do something... +/// +/// lh_headers_free(headers); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { + if !headers.is_null() { + unsafe { + drop(Box::from_raw(headers)); + } + } +} + +/// Get the number of headers with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// size_t count = lh_headers_count(headers, "Accept"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_count(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> usize { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(value) => value.len(), + None => 0 + } +} + +/// Get the value of the last header with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// const char* value = lh_headers_get(headers, "Accept"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_get(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if values.len() > 0 { + values[values.len() - 1].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +/// Get the value of the nth header with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// const char* value = lh_headers_get_nth(headers, "Accept", 0); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_get_nth(headers: *const lh_headers_t, key: *const std::os::raw::c_char, index: usize) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if index < values.len() { + values[index].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +/// Set a header with the given key and value. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// lh_headers_set(headers, "Accept", "application/json"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_set(headers: *mut lh_headers_t, key: *const std::os::raw::c_char, value: *const std::os::raw::c_char) { + let headers = unsafe { + assert!(!headers.is_null()); + &mut *headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap().to_string() + }; + let value = unsafe { + assert!(!value.is_null()); + std::ffi::CStr::from_ptr(value).to_str().unwrap().to_string() + }; + headers.inner.set(key, value); +} + +/// An HTTP request. Includes method, URL, headers, and body. +#[allow(non_camel_case_types)] +pub struct lh_request_t { + inner: Request, +} + +/// Convert a `Request` into a `lh_request_t`. +impl From for lh_request_t { + fn from(inner: Request) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_request_t` into a `Request`. +impl From<&lh_request_t> for Request { + fn from(request: &lh_request_t) -> Request { + request.inner.clone() + } +} + +/// Create a new `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_new( + method: *const ffi::c_char, + url: *const ffi::c_char, + headers: *mut lh_headers_t, + body: *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() }; + let url = Url::parse(&url_str).unwrap(); + let body = if body.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(body).to_bytes() }) + }; + let headers = unsafe { &*headers }; + let request = Request::new(method, url, headers.into(), body.unwrap_or(&[])); + Box::into_raw(Box::new(request.into())) +} + +/// Free a `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// +/// // Do something... +/// +/// lh_request_free(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_free(request: *mut lh_request_t) { + if !request.is_null() { + unsafe { + drop(Box::from_raw(request)); + } + } +} + +/// Get the method of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// const char* method = lh_request_method(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + CString::new(request.inner.method()).unwrap().into_raw() +} + +/// Get the URL of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_url_t* url = lh_request_url(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.url().clone().into())) +} + +/// Get the headers of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_headers_t* headers = lh_request_headers(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_headers_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.headers().clone().into())) +} + +/// Get the body of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// const char* body = lh_request_body(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + CString::new(request.inner.body()).unwrap().into_raw() +} + +/// Read from the body of the request into a buffer. Consumes that many bytes from the body. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// char buffer[1024]; +/// size_t length = lh_request_body_read(request, buffer, 1024); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_body_read(request: *const lh_request_t, buffer: *mut ffi::c_char, length: usize) -> usize { + let request = unsafe { &*request }; + let body = request.inner.body(); + + let length = length.min(body.len()); + let chunk = body.take(length); + + unsafe { + std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut ffi::c_char, buffer, length); + } + length +} + +/// An HTTP request builder. Includes method, URL, headers, and body. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_request_builder_t { + inner: RequestBuilder, +} + +/// Convert a `RequestBuilder` into a `lh_request_builder_t`. +impl From for lh_request_builder_t { + fn from(inner: RequestBuilder) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_request_builder_t` into a `RequestBuilder`. +impl From<&lh_request_builder_t> for RequestBuilder { + fn from(builder: &lh_request_builder_t) -> RequestBuilder { + builder.inner.clone() + } +} + +/// Create a new `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_new() -> *mut lh_request_builder_t { + Box::into_raw(Box::new(RequestBuilder::new().into())) +} + +/// Free a `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// +/// // Do something... +/// +/// lh_request_builder_free(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} + +/// Create a new `lh_request_builder_t` from an existing `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_request_builder_t* builder = lh_request_builder_extend(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_extend(request: *const lh_request_t) -> *mut lh_request_builder_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(RequestBuilder::extend(&request.inner).into())) +} + +/// Set the method of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_method(builder, "GET"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_method( + builder: *mut lh_request_builder_t, + method: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().method(method).into())) +} + +/// Set the URL of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_url(builder, "https://example.com"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_url( + builder: *mut lh_request_builder_t, + url: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().url(&url).unwrap().into())) +} + +/// Add a header to the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_header(builder, "Content-Type", "text/plain"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_header( + builder: *mut lh_request_builder_t, + key: *const ffi::c_char, + value: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let key = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().header(key, value).into())) +} + +/// Set the body of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_body(builder, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_body( + builder: *mut lh_request_builder_t, + body: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let body = unsafe { CStr::from_ptr(body).to_bytes() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().body(body).into())) +} + +/// Build a `lh_request_t` from a `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// +/// // Populate the builder data... +/// +/// lh_request_t* request = lh_request_builder_build(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_build(builder: *mut lh_request_builder_t) -> *mut lh_request_t { + let builder = unsafe { Box::from_raw(builder) }; + Box::into_raw(Box::new(builder.inner.build().into())) +} + +/// An HTTP response. Includes status code, headers, and body. +#[allow(non_camel_case_types)] +pub struct lh_response_t { + inner: Response, +} + +/// Convert a `Response` into a `lh_response_t`. +impl From for lh_response_t { + fn from(inner: Response) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_response_t` into a `Response`. +impl From<&lh_response_t> for Response { + fn from(response: &lh_response_t) -> Response { + response.inner.clone() + } +} + +/// Create a new `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { + let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; + let headers = unsafe { &*headers }; + Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str, "", None).into())) +} + +/// Free a `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// +/// // Do something... +/// +/// lh_response_free(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_free(response: *mut lh_response_t) { + if !response.is_null() { + unsafe { + drop(Box::from_raw(response)); + } + } +} + +/// Get the status code of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// uint16_t status = lh_response_status(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_status(response: *const lh_response_t) -> u16 { + let response = unsafe { &*response }; + response.inner.status() +} + +/// Get the headers of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// lh_headers_t* headers = lh_response_headers(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh_headers_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(response.inner.headers().clone().into())) +} + +/// Get the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// const char* body = lh_response_body(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { + let response = unsafe { &*response }; + CString::new(response.inner.body()).unwrap().into_raw() +} + +/// An HTTP response builder. Includes status, headers, body, log, and exception string. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_response_builder_t { + inner: ResponseBuilder, +} + +/// Convert a `ResponseBuilder` into a `lh_response_builder_t`. +impl From for lh_response_builder_t { + fn from(inner: ResponseBuilder) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_response_builder_t` into a `ResponseBuilder`. +impl From<&lh_response_builder_t> for ResponseBuilder { + fn from(builder: &lh_response_builder_t) -> ResponseBuilder { + builder.inner.clone() + } +} + +/// Create a new `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_new() -> *mut lh_response_builder_t { + Box::into_raw(Box::new(ResponseBuilder::new().into())) +} + +/// Free a `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// +/// // Do something... +/// +/// lh_response_builder_free(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_free(builder: *mut lh_response_builder_t) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} + +/// Create a new `lh_response_builder_t` from an existing `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// lh_response_builder_t* builder = lh_response_builder_extend(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_extend(response: *const lh_response_t) -> *mut lh_response_builder_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(ResponseBuilder::extend(&response.inner).into())) +} + +/// Set the status code of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_status_code(builder, 200); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_status_code(builder: *mut lh_response_builder_t, status_code: u16) { + let builder = unsafe { &mut *builder }; + builder.inner.status(status_code); +} + +/// Add a header to the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_header(builder, "Content-Type", "text/plain"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_header(builder: *mut lh_response_builder_t, key: *const c_char, value: *const c_char) { + let builder = unsafe { &mut *builder }; + let key_str = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value_str = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + builder.inner.header(key_str, value_str); +} + +/// Set the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_body(builder, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_body(builder: *mut lh_response_builder_t, body: *const c_char) { + let builder = unsafe { &mut *builder }; + let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; + builder.inner.body(body_str); +} + +/// Write to the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_body_write(builder, "Hello, world!", 13); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_body_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.body.put(data); + return len; +} + +/// Write to the log of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_log_write(builder, "Hello, world!", 13); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_log_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.log.put(data); + builder.inner.log.put("\n".as_bytes()); + return len; +} + +/// Set the exception string of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_exception(builder, "Something went wrong!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_exception(builder: *mut lh_response_builder_t, exception: *const c_char) { + let builder = unsafe { &mut *builder }; + let exception_str = unsafe { CStr::from_ptr(exception).to_string_lossy().into_owned() }; + builder.inner.exception(exception_str); +} + +/// Build a `lh_response_t` from a `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// +/// // Populate the builder data... +/// +/// lh_response_t* response = lh_response_builder_build(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_build(builder: *const lh_response_builder_t) -> *mut lh_response_t { + let builder = unsafe { &*builder }; + Box::into_raw(Box::new(builder.inner.build().into())) +} + +/// An HTTP URL. Includes scheme, host, port, domain, origin, authority, username, password, path, query, fragment, and URI. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_url_t { + inner: Url, +} + +/// Convert a `Url` into a `lh_url_t`. +impl From for lh_url_t { + fn from(inner: Url) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_url_t` into a `Url`. +impl From<&lh_url_t> for Url { + fn from(url: &lh_url_t) -> Url { + url.inner.clone() + } +} + +/// Parse a URL into a `lh_url_t`. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_parse(url: *const c_char) -> *mut lh_url_t { + let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let url = Url::parse(&url).unwrap(); + Box::into_raw(Box::new(url.into())) +} + +/// Free a `lh_url_t`. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// +/// // Do something... +/// +/// lh_url_free(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_free(url: *mut lh_url_t) { + if !url.is_null() { + unsafe { + drop(Box::from_raw(url)); + } + } +} + +/// Get the scheme of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* scheme = lh_url_scheme(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.scheme()).unwrap().into_raw() +} + +/// Get the host of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* host = lh_url_host(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.host_str().unwrap_or("")).unwrap().into_raw() +} + +/// Get the port of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// uint16_t port = lh_url_port(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { + let url = unsafe { &*url }; + url.inner.port().unwrap_or(0) +} + +/// Get the domain of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* domain = lh_url_domain(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.domain().unwrap_or("")).unwrap().into_raw() +} + +/// Get the origin of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* origin = lh_url_origin(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + let origin = match url.inner.origin() { + url::Origin::Opaque(_) => { + format!("{}://", url.inner.scheme()) + }, + url::Origin::Tuple(scheme, host, port) => { + format!("{}://{}:{}", scheme, host, port) + } + }; + CString::new(origin.as_str()).unwrap().into_raw() +} + +/// Check if the URL has an authority. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// bool has_authority = lh_url_has_authority(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { + let url = unsafe { &*url }; + url.inner.has_authority() +} + +/// Get the authority of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* authority = lh_url_authority(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.authority()).unwrap().into_raw() +} + +/// Get the username of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* username = lh_url_username(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.username()).unwrap().into_raw() +} + +/// Get the password of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* password = lh_url_password(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.password().unwrap_or("")).unwrap().into_raw() +} + +/// Get the path of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* path = lh_url_path(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.path()).unwrap().into_raw() +} + +/// Get the query of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* query = lh_url_query(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.query().unwrap_or("")).unwrap().into_raw() +} + +/// Get the fragment of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* fragment = lh_url_fragment(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.fragment().unwrap_or("")).unwrap().into_raw() +} + +/// Get the URI of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* uri = lh_url_uri(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_uri(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.as_str()).unwrap().into_raw() +} diff --git a/crates/lang_handler/src/handler.rs b/crates/lang_handler/src/handler.rs new file mode 100644 index 00000000..25756588 --- /dev/null +++ b/crates/lang_handler/src/handler.rs @@ -0,0 +1,57 @@ +use crate::{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 { + 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").expect("invalid url") + /// .build(); + /// 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 new file mode 100644 index 00000000..9d08944d --- /dev/null +++ b/crates/lang_handler/src/headers.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; + +/// 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(&vec!["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()) + } + + /// Returns the values associated with a header field. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// assert_eq!(headers.get("Accept"), Some(&vec![ + /// "text/plain".to_string(), + /// "application/json".to_string() + /// ])); + /// ``` + pub fn get(&self, key: K) -> Option<&Vec> + where + K: AsRef, + { + self.0.get(key.as_ref()) + } + + /// Sets a header field with the given value. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Content-Type", "text/plain"); + /// assert_eq!(headers.get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// ``` + pub fn set(&mut self, key: K, value: V) + where + K: Into, + V: Into, + { + self.0 + .entry(key.into()) + .or_insert_with(Vec::new) + .push(value.into()); + } + + /// 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()); + } + + /// Returns an iterator over the headers. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// for (key, values) in headers.iter() { + /// println!("{}: {:?}", key, values); + /// } + /// ``` + pub fn iter(&self) -> impl Iterator)> { + self.0.iter() + } + + /// Returns an iterator over the header values. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// for value in headers.iter_values() { + /// println!("{:?}", value); + /// } + /// ``` + pub fn iter_values(&self) -> impl Iterator { + self.0.values().flatten() + } +} diff --git a/crates/lang_handler/src/lib.rs b/crates/lang_handler/src/lib.rs new file mode 100644 index 00000000..d28698ed --- /dev/null +++ b/crates/lang_handler/src/lib.rs @@ -0,0 +1,14 @@ +#[cfg(feature = "c")] +mod ffi; +mod handler; +mod headers; +mod request; +mod response; + +#[cfg(feature = "c")] +pub use ffi::*; +pub use handler::Handler; +pub use headers::Headers; +pub use request::{Request, RequestBuilder}; +pub use response::{Response, ResponseBuilder}; +pub use url::Url; diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs new file mode 100644 index 00000000..9938c13b --- /dev/null +++ b/crates/lang_handler/src/request.rs @@ -0,0 +1,395 @@ +use std::fmt::Debug; + +use bytes::{Bytes, BytesMut}; +use url::{ParseError, Url}; + +use crate::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").expect("invalid url") +/// .header("Accept", "text/html") +/// .header("Accept", "application/json") +/// .header("Host", "example.com") +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(request.method(), "POST"); +/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); +/// assert_eq!(request.headers().get("Accept"), Some(&vec![ +/// "text/html".to_string(), +/// "application/json".to_string() +/// ])); +/// assert_eq!(request.headers().get("Host"), Some(&vec!["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, +} + +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!" + /// ); + pub fn new>(method: String, url: Url, headers: Headers, body: T) -> Self { + Self { + method, + url, + headers, + body: body.into() + } + } + + /// 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").expect("invalid url") + /// .header("Content-Type", "text/html") + /// .header("Content-Length", 13.to_string()) + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(request.method(), "POST"); + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); + /// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["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").expect("invalid url") + /// .header("Content-Type", "text/plain") + /// .build(); + /// + /// let extended = request.extend() + /// .method("POST") + /// .header("Content-Length", 12.to_string()) + /// .body("Hello, World") + /// .build(); + /// + /// assert_eq!(extended.method(), "POST"); + /// assert_eq!(extended.url().as_str(), "http://example.com/test.php"); + /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.headers().get("Content-Length"), Some(&vec!["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!" + /// ); + /// + /// 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!" + /// ); + /// + /// 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!" + /// ); + /// + /// assert_eq!(request.headers().get("Accept"), Some(&vec!["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!" + /// ); + /// + /// assert_eq!(request.body(), "Hello, World!"); + /// ``` + pub fn body(&self) -> Bytes { + self.body.clone() + } +} + +/// Builds an HTTP request. +/// +/// # Examples +/// +/// ``` +/// use lang_handler::{Request, RequestBuilder}; +/// +/// let request = Request::builder() +/// .method("POST") +/// .url("http://example.com/test.php").expect("invalid url") +/// .header("Content-Type", "text/html") +/// .header("Content-Length", 13.to_string()) +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(request.method(), "POST"); +/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); +/// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); +/// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["13".to_string()])); +/// assert_eq!(request.body(), "Hello, World!"); +/// ``` +#[derive(Clone)] +pub struct RequestBuilder { + method: Option, + url: Option, + headers: Headers, + body: BytesMut, +} + +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), + } + } + + /// 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!" + /// ); + /// + /// let extended = RequestBuilder::extend(&request) + /// .build(); + /// + /// assert_eq!(extended.method(), "GET"); + /// assert_eq!(extended.url().as_str(), "http://example.com/"); + /// assert_eq!(extended.headers().get("Accept"), Some(&vec!["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().clone()), + headers: request.headers().clone(), + body: BytesMut::from(request.body()), + } + } + + /// Sets the method of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .method("POST") + /// .build(); + /// + /// 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").expect("invalid url") + /// .build(); + /// + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); + /// ``` + pub fn url(mut self, url: T) -> Result + where + T: Into + { + match url.into().parse() { + Ok(url) => { + self.url = Some(url); + Ok(self) + }, + Err(e) => return Err(e), + } + } + + /// Sets a header of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .header("Accept", "text/html") + /// .build(); + /// + /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// ``` + pub fn header(mut self, key: K, value: V) -> Self + where + K: Into, + V: Into + { + self.headers.set(key.into(), value.into()); + self + } + + /// Sets the body of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(request.body(), "Hello, World!"); + /// ``` + pub fn body>(mut self, body: T) -> Self { + self.body = body.into(); + self + } + + /// Builds the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .build(); + /// + /// assert_eq!(request.method(), "GET"); + /// assert_eq!(request.url().as_str(), "http://example.com/"); + /// assert_eq!(request.body(), ""); + /// ``` + pub fn build(self) -> Request { + Request { + method: self.method.unwrap_or_else(|| "GET".to_string()), + // TODO: This is wrong. Return a Result instead. + url: self.url.unwrap_or_else(|| Url::parse("http://example.com").unwrap()), + headers: self.headers, + body: self.body.freeze(), + } + } +} diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs new file mode 100644 index 00000000..982c6d91 --- /dev/null +++ b/crates/lang_handler/src/response.rs @@ -0,0 +1,396 @@ +use bytes::{Bytes, BytesMut}; + +use crate::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(&vec!["text/plain".to_string()])); +/// assert_eq!(response.body(), "Hello, World!"); +/// ``` +#[derive(Clone, Debug)] +pub struct Response { + status: u16, + 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(&vec!["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: u16, 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(&vec!["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(&vec!["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) -> u16 { + 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(&vec!["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(&vec!["text/plain".to_string()])); +/// assert_eq!(response.body(), "Hello, World!"); +/// ``` +#[derive(Clone)] +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, + } + } + + /// 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(&vec!["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: u16) -> &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(&vec!["text/plain".to_string()])); + /// ``` + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + K: Into, + V: Into, + { + self.headers.set(key, value); + 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 + } + + /// 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 + } + + /// 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 + } + + /// 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(), + } + } +} diff --git a/crates/php/Cargo.toml b/crates/php/Cargo.toml index 40d6e144..70b12fc3 100644 --- a/crates/php/Cargo.toml +++ b/crates/php/Cargo.toml @@ -11,6 +11,11 @@ path = "src/lib.rs" name = "php-main" path = "src/main.rs" +[dependencies] +lang_handler = { path = "../lang_handler", features = ["c"] } +libc = "0.2.171" +once_cell = "1.21.0" + [build-dependencies] autotools = "0.2" bindgen = "0.69.4" diff --git a/crates/php/build.rs b/crates/php/build.rs index 6cce4a9a..db07d485 100644 --- a/crates/php/build.rs +++ b/crates/php/build.rs @@ -1,197 +1,29 @@ use std::{ - collections::HashSet, + collections::HashMap, env, ffi::OsStr, - fmt::{Debug, Display}, + fmt::Debug, path::PathBuf, process::Command }; // use autotools::Config; use bindgen::Builder; -use downloader::{Download, Downloader}; -use file_mode::ModePath; - -fn maybe_windowsify(path: T) -> String where T: Display { - match env::var("CARGO_CFG_TARGET_OS").as_deref() { - Ok("windows") => format!("{}.exe", path), - _ => path.to_string() - } -} - -fn spc_url() -> String { - let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - let url = format!("https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-{}-{}", os, arch); - - maybe_windowsify(url) -} - -fn get_spc() -> PathBuf { - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - - let filename = maybe_windowsify("spc"); - let spc = out_path.join(filename.clone()); - - println!("cargo:rerun-if-changed={}", spc.to_str().unwrap()); - - if spc.exists() { - return spc; - } - println!("spc is not present"); - - let mut downloader = Downloader::builder() - .download_folder(&out_path) - .build() - .unwrap(); - - let dl = Download::new(&spc_url()) - .file_name(filename.as_ref()); - - downloader.download(&vec![dl]).unwrap(); - - // Make the file executable - spc.set_mode("a+x").unwrap(); - spc -} fn main() { - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - let current_dir = env::current_dir().unwrap(); - - // Any commented out ones I was not able to get to build for some reason... - let available_extensions = HashSet::from([ - "amqp", - "apcu", - "ast", - "bcmath", - "bz2", - "calendar", - "ctype", - // "curl", - // "dba", - // "dio", - // "dom", - // "ds", - // "event", - // "exif", - // "ffi", - // "fileinfo", - // "filter", - // "ftp", - // "gd", - // "gettext", - // "glfw", - // "gmp", - // "gmssl", - // "grpc", - // "iconv", - // "igbinary", - // "imagick", - // "imap", - // "inotify", - // "intl", - // "ldap", - // "libxml", - // "mbregex", - // "mbstring", - // "mcrypt", - // "memcache", - // "memcached", - // "mongodb", - // "msgpack", - // "mysqli", - // "mysqlnd", - // "oci8", - // "opcache", - // "openssl", - // "parallel", // Requires zts - // "password-argon2", - // "pcntl", - // "pdo", - // "pdo_mysql", - // "pdo_pgsql", - // "pdo_sqlite", - // "pdo_sqlsrv", - // "pgsql", - // "phar", - // "posix", - // "protobuf", - // "rar", - // "rdkafka", - // "readline", - // "redis", - // "session", - // "shmop", - // "simdjson", - // "simplexml", - // "snappy", - // "soap", - // "sockets", - // "sodium", - // "spx", - // "sqlite3", - // "sqlsrv", - // "ssh2", - // "swoole", // Mutually exclusive with pdo_* - // "swoole-hook-mysql", // Mutually exclusive with pdo_* - // "swoole-hook-pgsql", // Mutually exclusive with pdo_* - // "swoole-hook-sqlite", // Mutually exclusive with pdo_* - // "swow", - // "sysvmsg", - // "sysvsem", - // "sysvshm", - // "tidy", - // "tokenizer", - // "uuid", - // "uv", - // "xdebug", - // "xhprof", - // "xlswriter", - // "xml", - // "xmlreader", - // "xmlwriter", - // "xsl", - // "yac", - // "yaml", - // "zip", - // "zlib", - // "zstd" - ]); + println!("cargo:rerun-if-changed=build.rs"); - // TODO: Make an actually reasonable selection of default extensions - let extensions = env::var("PHP_EXTENSIONS").unwrap_or_else( - |_| available_extensions.iter() - .map(|s| s.to_string()) - .collect::>() - .join(",") - ); + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let project_root = crate_dir.join("../.."); - let spc = get_spc(); + let spc = project_root.join("spc"); let spc_cmd = spc.to_str().unwrap(); - // Skip download and build of PHP if a build is already present - // TODO: Probably need to detect modification date somehow... - if !current_dir.join("buildroot/lib/libphp.a").exists() { - // Download PHP and requested extensions - execute_command(&[ - spc_cmd, - "download", - &format!("--for-extensions={}", extensions.clone()), - "--retry=10", - "--prefer-pre-built", - "--with-php=8.4" - ]); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - // Build in embed mode - execute_command(&[ - spc_cmd, - "build", - &extensions, - "--build-embed", - // "--enable-zts" - ]); - } + let extensions_sh = project_root.join("scripts/extensions.sh"); + let extensions_cmd = extensions_sh.to_str().unwrap(); + let extensions = execute_command(&[extensions_cmd], None); // Get the includes let includes = execute_command(&[ @@ -199,7 +31,7 @@ fn main() { "spc-config", &extensions, "--includes" - ]); + ], None); // Get the libs let libs = execute_command(&[ @@ -207,16 +39,16 @@ fn main() { "spc-config", &extensions, "--libs" - ]); + ], None); // Include main headers - let includes = includes.split(' ').collect::>(); + let mut includes = includes.split(' ').collect::>(); for dir in includes.iter() { println!("cargo:include={}", &dir[2..]); } // Include SAPI headers - let sapi_include = current_dir + let sapi_include = project_root .join("buildroot/include/php/sapi") .canonicalize() .unwrap(); @@ -225,7 +57,12 @@ fn main() { // Link libraries let libs = libs.split(' ').collect::>(); + let mut is_framework = false; for dir in libs.iter() { + if is_framework { + is_framework = false; + println!("cargo:rustc-link-lib=framework={}", dir); + } if dir.starts_with("-L") { println!("cargo:rustc-link-search={}", &dir[2..]); } @@ -233,10 +70,23 @@ fn main() { println!("cargo:rustc-link-lib={}", &dir[2..]); } if dir.starts_with("-framework") { - println!("cargo:rustc-link-lib=framework={}", &dir[11..]); + is_framework = true; } } + // Locate lang_handler header and lib + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("../../..") + .canonicalize() + .unwrap(); + + println!("cargo:rustc-link-search={}", out_dir.display()); + println!("cargo:rustc-link-lib=lang_handler"); + println!("cargo:include={}", out_dir.display()); + + let lang_handler_include_flag = format!("-I{}", out_dir.display()); + includes.push(lang_handler_include_flag.as_str()); + let mut builder = cc::Build::new(); for include in &includes { builder.flag(include); @@ -247,6 +97,13 @@ fn main() { println!("cargo:rerun-if-changed=src/php_wrapper.h"); + let sw_vers = execute_command(&[ + "sw_vers", + "-productVersion" + ], None); + + println!("cargo:env=MACOSX_DEPLOYMENT_TARGET={}", sw_vers); + let mut builder = Builder::default() // .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .header("src/php_wrapper.c") @@ -257,6 +114,12 @@ fn main() { .blocklist_function("zend_startup") // Block the `zend_random_bytes_insecure` because it fails checks. .blocklist_item("zend_random_bytes_insecure") + .opaque_type("lh_request_t") + .opaque_type("lh_response_t") + .opaque_type("lh_request_builder_t") + .opaque_type("lh_response_builder_t") + .opaque_type("lh_headers_t") + .opaque_type("lh_url_t") .clang_args(&includes) .derive_default(true); @@ -275,17 +138,25 @@ fn main() { .expect("Unable to write output file"); } -fn execute_command + Debug>(argv: &[S]) -> String { +fn execute_command + Debug>(argv: &[S], env: Option>) -> String { let mut command = Command::new(&argv[0]); + if let Some(env) = env { + command.envs(env); + } command.args(&argv[1..]); + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let project_root = crate_dir.join("../.."); + command.current_dir(project_root); + let result = command .output() .unwrap_or_else(|e| panic!("Execute command {:?} failed with: {:?}", &argv, e)); if !result.status.success() { + let out = String::from_utf8(result.stdout).unwrap(); let err = String::from_utf8(result.stderr).unwrap(); - panic!("Execute command {:?} failed with output: {}", &argv, err); + panic!("Execute command {:?} failed with stdout: {}, stderr: {}", &argv, out, err); } String::from_utf8(result.stdout).unwrap().trim().to_owned() diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index e279ab59..5e7e7855 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -1,96 +1,194 @@ -use std::{env::Args, ffi::{CStr, CString}}; +use std::{ + env::Args, + ffi::{c_void, c_char, CStr, CString} +}; +use std::sync::OnceLock; -use crate::{sys, Request, Response}; +use lang_handler::{Handler, Request, Response}; -pub struct Embed; +use crate::sys; -fn args_to_c(args: Args) -> (i32, *mut *mut std::os::raw::c_char) { - let mut c_args = Vec::new(); - let mut c_ptrs = Vec::new(); +// This is a helper to ensure that PHP is initialized and deinitialized at the +// appropriate times. +struct PhpInit; - for arg in args { - let c_arg = std::ffi::CString::new(arg).unwrap(); - let c_ptr = c_arg.clone().into_raw(); - c_args.push(c_arg); - c_ptrs.push(c_ptr); - } - - let c_args = c_args - .into_iter() - .map(|c_arg| c_arg.into_raw()) - .collect::>(); - - let mut c_ptrs = c_ptrs - .into_iter() - .collect::>(); +impl PhpInit { + pub fn new(argv: Vec) -> Self + where + S: AsRef + { + let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect(); + let argc = argv.len() as i32; + let mut argv_ptrs = argv + .iter() + .map(|v| v.as_ptr() as *mut c_char) + .collect::>(); - (c_args.len() as i32, c_ptrs.as_mut_ptr()) + unsafe { + sys::php_http_init(argc, argv_ptrs.as_mut_ptr()); + } + PhpInit + } } -impl Embed { - pub fn new() -> Self { - Embed::new_with_c_args(0, std::ptr::null_mut()) +impl Drop for PhpInit { + fn drop(&mut self) { + unsafe { + sys::php_http_destruct(); + } } +} - pub fn new_with_args(args: Args) -> Self { - let argv: Vec = args.collect(); - Embed::new_with_argv(argv) - } +static PHP_INIT: OnceLock = OnceLock::new(); + +/// Embed a PHP script into a Rust application to handle HTTP requests. +/// +/// # Examples +/// +/// ``` +/// use php::{Embed, Handler, Request, Response}; +/// +/// let handler = Embed::new("echo 'Hello, world!';", Some("example.php")); +/// +/// let request = Request::builder() +/// .method("GET") +/// .url("http://example.com").expect("invalid url") +/// .build(); +/// +/// let response = handler.handle(request).unwrap(); +/// +/// assert_eq!(response.status(), 200); +/// assert_eq!(response.body(), "Hello, world!"); +/// ``` +#[derive(Debug, Clone)] +pub struct Embed { + code: String, + filename: Option, +} + +unsafe impl Send for Embed {} +unsafe impl Sync for Embed {} - pub fn new_with_argv(argv: Vec) -> Self +impl Embed { + /// Creates a new `Embed` instance. + /// + /// # Examples + /// + /// ``` + /// use php::Embed; + /// + /// let embed = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// ``` + pub fn new(code: C, filename: Option) -> Self where - S: AsRef, + C: Into, + F: Into { - let mut c_args = Vec::new(); - let mut c_ptrs = Vec::new(); - - for arg in argv { - let c_arg = CString::new(arg.as_ref()).unwrap(); - let c_ptr = c_arg.clone().into_raw(); - c_args.push(c_arg); - c_ptrs.push(c_ptr); - } - - let c_args = c_args - .into_iter() - .map(|c_arg| c_arg.into_raw()) - .collect::>(); - - let mut c_ptrs = c_ptrs - .into_iter() - .collect::>(); - - Embed::new_with_c_args(c_args.len() as i32, c_ptrs.as_mut_ptr()) + Embed::new_with_argv::(code, filename, vec![]) } - fn new_with_c_args(argc: i32, argv: *mut *mut std::os::raw::c_char) -> Self { - unsafe { sys::php_embed_init(argc, argv); } - Embed + /// Creates a new `Embed` instance with command-line arguments. + /// + /// # Examples + /// + /// ``` + /// use php::Embed; + /// + /// let args = std::env::args(); + /// let embed = Embed::new_with_args("echo $argv[1];", Some("example.php"), args); + /// ``` + pub fn new_with_args(code: C, filename: Option, args: Args) -> Self + where + C: Into, + F: Into + { + Embed::new_with_argv(code, filename, args.collect()) } - pub fn handle_request(&self, code: C, filename: Option, request: Request) -> Response + /// Creates a new `Embed` instance with command-line arguments. + /// + /// # Examples + /// + /// ``` + /// use php::{Embed, Handler, Request, Response}; + /// + /// let embed = Embed::new_with_argv("echo $_SERVER['argv'][0];", Some("example.php"), vec![ + /// "Hello, world!" + /// ]); + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("invalid url") + /// .build(); + /// + /// let response = embed.handle(request).unwrap(); + /// + /// assert_eq!(response.status(), 200); + /// # // TODO: Uncomment when argv gets passed through correctly. + /// # // assert_eq!(response.body(), "Hello, world!"); + /// ``` + pub fn new_with_argv(code: C, filename: Option, argv: Vec) -> Self where - C: AsRef, + C: Into, F: Into, + S: AsRef + std::fmt::Debug, { - let code = CString::new(code.as_ref()) + PHP_INIT.get_or_init(|| PhpInit::new(argv)); + + Embed { + code: code.into(), + filename: filename.map(|v| v.into()) + } + } +} + +impl Handler for Embed { + type Error = String; + + /// Handles an HTTP request. + /// + /// # Examples + /// + /// ``` + /// use php::{Embed, Handler, Request, Response}; + /// + /// let handler = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("invalid url") + /// .build(); + /// + /// let response = handler.handle(request).unwrap(); + /// + /// assert_eq!(response.status(), 200); + /// assert_eq!(response.body(), "Hello, world!"); + /// ``` + fn handle(&self, request: Request) -> Result { + let code = CString::new(self.code.clone()) .unwrap(); - let filename = filename - .map(|v| CString::new(v.into())) + let filename = self.filename + .as_ref() + .map(|v| CString::new(v.clone())) .unwrap_or(CString::new("")) .unwrap(); - unsafe { - sys::php_http_handle_request(code.as_ptr(), filename.as_ptr(), *request) - }.into() - } -} + let mut request: lang_handler::lh_request_t = request.into(); + let request = &mut request as *mut _ as *mut sys::lh_request_t; -impl Drop for Embed { - fn drop(&mut self) { - unsafe { - sys::php_embed_shutdown(); + let response = unsafe { + sys::php_http_handle_request(code.as_ptr(), filename.as_ptr(), request) + }; + + if response.is_null() { + return Err("Failed to handle request".into()); } + + let response = unsafe { + &*(response as *mut lang_handler::lh_response_t) + }; + + Ok(response.into()) } } diff --git a/crates/php/src/lib.rs b/crates/php/src/lib.rs index 8b341815..6938770f 100644 --- a/crates/php/src/lib.rs +++ b/crates/php/src/lib.rs @@ -11,11 +11,9 @@ // #![deny(clippy::all)] mod embed; -mod request; -mod response; mod sys; +pub use lang_handler::{Handler, Headers, Request, Response, RequestBuilder, Url}; + pub use embed::Embed; -pub use request::Request; -pub use response::Response; pub use self::sys::*; diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs index 790e01db..e3c46b6e 100644 --- a/crates/php/src/main.rs +++ b/crates/php/src/main.rs @@ -1,24 +1,26 @@ -use php::{Embed, Request}; +use php::{Embed, Request, Handler}; pub fn main() { - let embed = Embed::new_with_args(std::env::args()); + let code = " + http_response_code(123); + header('Content-Type: text/plain'); + echo file_get_contents(\"php://input\"); + "; + let filename = Some("test.php"); + let embed = Embed::new_with_args(code, filename, std::env::args()); let request = Request::builder() - .method("GET") - .path("/test.php") + .method("POST") + .url("http://example.com/test.php").expect("invalid url") + .header("Content-Type", "text/html") + .header("Content-Length", 13.to_string()) .body("Hello, World!") .build(); - println!("method: {}", request.method()); - println!("path: {}", request.path()); - println!("body: {}", request.body()); + println!("request: {:#?}", request); - let response = embed.handle_request( - ";echo 'Hello, World!';", - Some("test.php"), - request.clone() - ); + let response = embed.handle(request.clone()) + .expect("failed to handle request"); - println!("Request: {:?}", request); - println!("Response: {:?}", response); + println!("response: {:#?}", response); } diff --git a/crates/php/src/php_wrapper.c b/crates/php/src/php_wrapper.c index 6637e4cb..dd643103 100644 --- a/crates/php/src/php_wrapper.c +++ b/crates/php/src/php_wrapper.c @@ -1,6 +1,8 @@ // TODO: Review these dependencies. They were all auto-added by IDE completion. // There's likely consolidated headers to get things from. #include "SAPI.h" +#include "TSRM.h" +#include "php.h" #include "php_main.h" #include "zend.h" #include "zend_alloc.h" @@ -10,6 +12,9 @@ #include "zend_property_hooks.h" #include "zend_types.h" #include "zend_variables.h" +#include "zend_exceptions.h" +#include "lang_handler.h" +#include "php_ini_builder.h" #include #include @@ -21,338 +26,191 @@ #include #include -typedef struct string_array { - size_t buffer_size; - size_t used_size; - size_t count; - char* buffer; - size_t* offsets; -} string_array; - -string_array* string_array_new(size_t initial_size) { - string_array* arr = (string_array*)malloc(sizeof(string_array)); - if (!arr) return NULL; - - arr->buffer_size = initial_size; - arr->used_size = 0; - arr->count = 0; - arr->buffer = (char*)malloc(initial_size * sizeof(char)); - arr->offsets = (size_t*)malloc(initial_size * sizeof(size_t)); - - if (!arr->buffer || !arr->offsets) { - free(arr->buffer); - free(arr->offsets); - free(arr); - return NULL; - } +typedef struct php_server_context_s { + lh_request_t* request; + lh_response_builder_t* response_builder; +} php_server_context_t; - return arr; -} -bool string_array_grow(string_array* arr, size_t new_size) { - char* new_buffer = (char*)realloc(arr->buffer, new_size * sizeof(char)); - size_t* new_offsets = (size_t*)realloc(arr->offsets, new_size * sizeof(size_t)); - - if (!new_buffer || !new_offsets) { - free(new_buffer); - free(new_offsets); - return false; - } +static const char HARDCODED_INI[] = + "display_errors=0\n" + "register_argc_argv=1\n" + "log_errors=1\n" + "implicit_flush=1\n" + "memory_limit=128MB\n" + "output_buffering=0\n"; - arr->buffer = new_buffer; - arr->offsets = new_offsets; - arr->buffer_size = new_size; +int php_http_startup(sapi_module_struct* sapi_module) { + struct php_ini_builder ini_builder; + php_ini_builder_init(&ini_builder); + php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); + sapi_module->ini_entries = php_ini_builder_finish(&ini_builder); + php_ini_builder_deinit(&ini_builder); - return true; + return php_module_startup(sapi_module, NULL); } -bool string_array_add(string_array* arr, const char* str) { - size_t str_len = strlen(str) + 1; // include the null terminator - if (arr->used_size + str_len > arr->buffer_size) { - if (!string_array_grow(arr, (arr->buffer_size + str_len) * 2)) { - return false; - } - } +int php_http_deactivate() { + php_server_context_t* context = SG(server_context); + if (!context) return SUCCESS; - arr->offsets[arr->count] = arr->used_size; - strcpy(&arr->buffer[arr->used_size], str); - arr->used_size += str_len; - arr->count++; + SG(server_context) = NULL; - return true; -} -const char* string_array_get(string_array* arr, size_t index) { - if (index >= arr->count) { - return NULL; - } - return &arr->buffer[arr->offsets[index]]; -} -bool string_array_remove(string_array* arr, size_t index) { - if (index >= arr->count) { - return false; - } + SG(request_info).argc = 0; + SG(request_info).argv = NULL; - size_t start_offset = arr->offsets[index]; - size_t next_offset = (index + 1 < arr->count) ? arr->offsets[index + 1] : arr->used_size; - size_t length_to_move = arr->used_size - next_offset; + // request + if (SG(request_info).request_method != NULL) { + lh_reclaim_str(SG(request_info).request_method); + } - memmove(&arr->buffer[start_offset], &arr->buffer[next_offset], length_to_move); - arr->used_size -= (next_offset - start_offset); + // url + if (SG(request_info).path_translated != NULL) { + lh_reclaim_str(SG(request_info).path_translated); + } + if (SG(request_info).query_string != NULL) { + lh_reclaim_str(SG(request_info).query_string); + } + if (SG(request_info).request_uri != NULL) { + lh_reclaim_str(SG(request_info).request_uri); + } - for (size_t i = index; i < arr->count - 1; ++i) { - arr->offsets[i] = arr->offsets[i + 1] - (next_offset - start_offset); + // headers + if (SG(request_info).content_type != NULL) { + lh_reclaim_str(SG(request_info).content_type); + } + if (SG(request_info).cookie_data != NULL) { + lh_reclaim_str(SG(request_info).cookie_data); } - arr->count--; - return true; + return SUCCESS; } -void string_array_free(string_array* arr) { - free(arr->buffer); - free(arr->offsets); - free(arr); + +size_t php_http_ub_write(const char* str, size_t len) { + php_server_context_t* context = SG(server_context); + return lh_response_builder_body_write(context->response_builder, str, len); } +void php_http_flush() { + if (!SG(headers_sent)) { + sapi_send_headers(); + SG(headers_sent) = 1; + } +} -/** - * A header key/value pair. - */ -typedef struct php_http_header { - const char* key; - string_array* values; -} php_http_header; - -php_http_header* php_http_header_init(php_http_header* self, const char* key, string_array* values) { - self->key = key; - self->values = values; - return self; +void php_http_send_header( + sapi_header_struct* sapi_header, + __attribute__((unused)) void* server_context +) { + // Not sure _why_ this is necessary, but it is. + if (sapi_header == NULL) return; + // printf("Header: %s\n", sapi_header->header); } -php_http_header* php_http_header_new(const char* key) { - php_http_header* self = (php_http_header*)malloc(sizeof(php_http_header)); - if (!self) return NULL; - string_array* values = string_array_new(1); - if (!values) return NULL; +int php_http_send_headers(sapi_headers_struct* sapi_headers) { + // php_server_context_t* context = SG(server_context); + sapi_header_struct* h; + zend_llist_position pos; - return php_http_header_init(self, key, values); -} -const char* php_http_header_key(php_http_header* self) { - return self->key; -} -string_array* php_http_header_values(php_http_header* self) { - return self->values; -} -bool php_http_header_add_value(php_http_header* self, const char* value) { - return string_array_add(self->values, value); -} -const char* php_http_header_get_value(php_http_header* self, size_t index) { - return string_array_get(self->values, index); -} -size_t php_http_header_value_count(php_http_header* self) { - return self->values->count; -} -bool php_http_header_remove_value(php_http_header* self, size_t index) { - return string_array_remove(self->values, index); -} -void php_http_header_free(php_http_header* self) { - free(self); + h = zend_llist_get_first_ex(&sapi_headers->headers, &pos); + while (h) { + if ( h->header_len > 0 ) { + // printf("Header: %s\n", h->header); + } + h = zend_llist_get_next_ex(&sapi_headers->headers, &pos); + } + return SAPI_HEADER_SENT_SUCCESSFULLY; } -/** - * A collection of headers. - */ -typedef struct php_http_headers { - size_t allocated; - size_t count; - php_http_header* headers; -} php_http_headers; - -bool php_http_headers_grow(php_http_headers* self, size_t count) { - size_t new_size = (self->allocated + count) * sizeof(php_http_header); - self->headers = (php_http_header*)realloc(self->headers, new_size); - if (self->headers == NULL) return false; - self->allocated += count; - return true; +size_t php_http_read_post(char* buffer, size_t count_bytes) { + php_server_context_t* context = SG(server_context); + return lh_request_body_read(context->request, buffer, count_bytes); } -php_http_headers* php_http_headers_init(php_http_headers* self) { - self->allocated = 0; - self->count = 0; - self->headers = NULL; - return self; -} -php_http_headers* php_http_headers_new(size_t count) { - php_http_headers* self = (php_http_headers*)malloc(sizeof(php_http_headers)); - if (self == NULL) return NULL; - - php_http_headers_init(self); - if (count > 0 && !php_http_headers_grow(self, count)) { - free(self); - return NULL; - } - return self; -} -void php_http_headers_free(php_http_headers* self) { - free(self->headers); - free(self); -} -bool php_http_headers_has_room(php_http_headers* self, size_t count) { - return self->allocated - self->count >= count; -} -size_t php_http_headers_count(php_http_headers* self) { - return self->count; -} -php_http_header* php_http_headers_get(php_http_headers* self, size_t index) { - return &self->headers[index]; +char* php_http_read_cookies() { + return SG(request_info).cookie_data; } -int php_http_headers_find_index(php_http_headers* self, const char* key) { - for (int i = 0; i < (int)self->count; i++) { - if (strcmp(self->headers[i].key, key) == 0) { - return i; - } - } - return -1; + +void php_http_register_server_variables(zval* track_vars_array) { + php_import_environment_variables(track_vars_array); } -php_http_header* php_http_headers_find(php_http_headers* self, const char* key) { - int index = php_http_headers_find_index(self, key); - if (index == -1) return NULL; - return &self->headers[index]; + +void php_http_log_message( + const char* message, + __attribute__((unused)) int syslog_type_int +) { + php_server_context_t* context = SG(server_context); + size_t len = strlen(message); + lh_response_builder_log_write(context->response_builder, message, len); } -php_http_header* php_http_headers_remove(php_http_headers* self, const char* key) { - int i = php_http_headers_find_index(self, key); - if (i == -1) return NULL; - php_http_header* header = &self->headers[i]; - for (int j = i; j < (int)self->count - 1; j++) { - self->headers[j] = self->headers[j + 1]; - } - self->count--; +static sapi_module_struct php_http_sapi_module = { + "php-http", /* name */ + "PHP/HTTP", /* pretty name */ - return header; -} -php_http_headers* php_http_headers_push(php_http_headers* self, const char* key, const char* value) { - php_http_header* header = php_http_headers_find(self, key); - if (header != NULL) { - if (!php_http_header_add_value(header, value)) { - return NULL; - } - return self; - } + php_http_startup, /* startup */ + php_module_shutdown_wrapper, /* shutdown */ - if (!php_http_headers_has_room(self, 1) && !php_http_headers_grow(self, 1)) { - return NULL; - } + NULL, /* activate */ + php_http_deactivate, /* deactivate */ - string_array* values = string_array_new(1); - if (!values) return NULL; + php_http_ub_write, /* unbuffered write */ + php_http_flush, /* flush */ - string_array_add(values, value); - header = &self->headers[self->count++]; - php_http_header_init(header, key, values); + NULL, /* get uid */ + NULL, /* getenv */ - return self; -} + php_error, /* error handler */ -/** - * An incoming request. - */ -typedef struct php_http_request { - const char* method; - const char* path; - php_http_headers headers; - const char* body; -} php_http_request; - -php_http_request* php_http_request_init(php_http_request* self) { - self->method = NULL; - self->path = NULL; - php_http_headers_init(&self->headers); - self->body = NULL; - return self; -} -php_http_request* php_http_request_new() { - php_http_request* self = (php_http_request*)malloc(sizeof(php_http_request)); - if (self == NULL) return NULL; + NULL, /* header handler */ + php_http_send_headers, /* send headers handler */ + php_http_send_header, /* send header handler */ - return php_http_request_init(self); -} -void php_http_request_free(php_http_request* self) { - free((void*)self->method); - free((void*)self->path); - php_http_headers_free(&self->headers); - free((void*)self->body); - free(self); -} -bool php_http_request_set_method(php_http_request* self, const char* method) { - self->method = strdup(method); - return true; -} -const char* php_http_request_get_method(php_http_request* self) { - return self->method; -} -bool php_http_request_set_path(php_http_request* self, const char* path) { - self->path = strdup(path); - return true; -} -const char* php_http_request_get_path(php_http_request* self) { - return self->path; -} -bool php_http_request_set_body(php_http_request* self, const char* body) { - self->body = strdup(body); - return true; -} -const char* php_http_request_get_body(php_http_request* self) { - return self->body; -} + php_http_read_post, /* read POST data */ + php_http_read_cookies, /* read Cookies */ -/** - * An outgoing response. - */ -typedef struct php_http_response { - int status; - php_http_headers headers; - const char* body; -} php_http_response; - -php_http_response* php_http_response_init(php_http_response* self) { - self->status = 0; - php_http_headers_init(&self->headers); - self->body = NULL; - return self; -} -php_http_response* php_http_response_new() { - php_http_response* self = (php_http_response*)malloc(sizeof(php_http_response)); - if (self == NULL) return NULL; + php_http_register_server_variables, /* register server variables */ + php_http_log_message, /* Log message */ - return php_http_response_init(self); -} -void php_http_response_free(php_http_response* self) { - php_http_headers_free(&self->headers); - free((void*)self->body); - free(self); -} -bool php_http_response_set_status(php_http_response* self, int status) { - self->status = status; - return true; -} -int php_http_response_get_status(php_http_response* self) { - return self->status; -} -bool php_http_response_set_body(php_http_response* self, const char* body) { - self->body = strdup(body); - return true; -} -const char* php_http_response_get_body(php_http_response* self) { - return self->body; + NULL, /* Get request time */ + NULL, /* Child terminate */ + + STANDARD_SAPI_MODULE_PROPERTIES +}; + +zend_result php_http_init(int argc, char** argv) { +#ifdef ZTS + php_tsrm_startup(); +#endif + + if (argc > 0) { + php_http_sapi_module.executable_location = argv[0]; + } + sapi_startup(&php_http_sapi_module); + + php_module_startup(&php_http_sapi_module, NULL); + + return SUCCESS; } -php_http_headers* php_http_response_get_headers(php_http_response* self) { - return &self->headers; + +zend_result php_http_destruct() { + // Why is this needed??? + // sapi_module.flush = NULL; + + // php_http_sapi_module.shutdown(&php_http_sapi_module); + php_module_shutdown(); + + sapi_shutdown(); + +#ifdef ZTS + tsrm_shutdown(); +#endif + + return SUCCESS; } /** * TODO: - * - Learn how php_stream works and adapt to tokio streams? * - Provide high-performance PSR-7 implementation on libuv? - * - Use thread pool, or let wattpm handle it? - * - Don't need multiple instances of the native module and PHP runtime. * * NOTES: * @@ -365,78 +223,95 @@ php_http_headers* php_http_response_get_headers(php_http_response* self) { * Each SAPI request is handled in an isolated PHP context, but code compilation * can be shared making spin up quick. Each of these contexts is single-threaded. */ -php_http_response* php_http_handle_request(const char* code, const char* filename, php_http_request* request) { - php_http_response* response = php_http_response_new(); - if (response == NULL) return NULL; - - // Teardown initial request. - php_request_shutdown((void*) 0); - - // Set up $_SERVER values. - // SG(request_info).script_filename = estrdup(filename); - // SG(request_info).php_self = estrdup(request->path); - SG(request_info).request_method = estrdup(request->method); - // SG(request_info).request_body = - SG(request_info).path_translated = estrdup(request->path); - // SG(request_info).query_string = estrdup(request->query_string); - // SG(request_info).request_uri = estrdup(request->request_uri); - // SG(request_info).cookie_data = estrdup(request->cookie_data); - // SG(request_info).content_type = estrdup(request->content_type); - // SG(request_info).content_length = estrdup(request->content_length); - // SG(request_info).server_software = estrdup(request->server_software); // wattpm? - // SG(request_info).gateway_interface = estrdup(request->gateway_interface); // CGI/1.1 - // SG(request_info).request_time_float = request->request_time_float; - // SG(request_info).document_root = estrdup(request->document_root); - // SG(request_info).remote_addr = estrdup(request->remote_addr); - // SG(request_info).remote_host = estrdup(request->remote_host); - // SG(request_info).remote_port = estrdup(request->remote_port); - // SG(request_info).remote_user = estrdup(request->remote_user); - // SG(request_info).server_port = estrdup(request->server_port); - // SG(request_info).script_name = estrdup(request->script_name); - // SG(request_info).php_auth_digest = estrdup(request->php_auth_digest); - // SG(request_info).php_auth_user = estrdup(request->php_auth_user); - // SG(request_info).php_auth_pw = estrdup(request->php_auth_pw); - // SG(request_info).auth_type = estrdup(request->auth_type); - // SG(request_info).path_info = estrdup(request->path_info); - - // SG(server_context) needs to be non-zero size because...reasons. ¯\_(ツ)_/¯ - SG(server_context) = (void*)(1); // Sigh. - - // Needs to be set _after_ php_request_startup, also because reasons. - SG(request_info).proto_num = 110; - - // Start new request now that we've setup the environment fully. - if (php_request_startup() == FAILURE) { - return NULL; +lh_response_t* php_http_handle_request(const char* code, const char* filename, lh_request_t* request) { + lh_response_builder_t* response_builder = lh_response_builder_new(); + + if (php_http_sapi_module.startup(&php_http_sapi_module) == FAILURE) { +#ifdef ZTS + tsrm_shutdown(); +#endif + return lh_response_builder_build(response_builder); } - // php_embed_module.flush = + zend_first_try { + // This is where we store the stuff for associating callbacks with this request. + php_server_context_t context = { + .request = request, + .response_builder = response_builder + }; + + SG(server_context) = &context; + + SG(options) |= SAPI_OPTION_NO_CHDIR; + SG(headers_sent) = 0; + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + + // Reset state + SG(sapi_headers).http_response_code = 200; + + // Set up superglobals + SG(request_info).request_method = lh_request_method(request); + + lh_url_t* url = lh_request_url(request); + SG(request_info).path_translated = (char*) lh_url_path(url); + SG(request_info).query_string = (char*) lh_url_query(url); + SG(request_info).request_uri = (char*) lh_url_uri(url); + // TODO: Add auth fields + + lh_headers_t* headers = lh_request_headers(request); + + const char* content_type = lh_headers_get(headers, "Content-Type"); + if (content_type == NULL) { + SG(request_info).content_type = content_type; + } + + const char* content_length = lh_headers_get(headers, "Content-Length"); + if (content_length != NULL) { + SG(request_info).content_length = strtoll(content_length, NULL, 10); + } + + const char* cookie = lh_headers_get(headers, "Cookie"); + SG(request_info).cookie_data = (char*) cookie; + + // Start new request now that we've setup the environment fully. + if (php_request_startup() == FAILURE) { + return lh_response_builder_build(response_builder); + } - zval retval; - zend_try { - // size_t len = strlen(code); - // zend_eval_stringl_ex((char*)code, len, &retval, filename, true); + // Needs to be set _after_ php_request_startup, also because reasons. + SG(request_info).proto_num = 110; - zend_eval_string((char*)code, NULL, filename); + size_t len = strlen(code); + zend_eval_stringl_ex((char*)code, len, NULL, filename, false); if (EG(exception)) { - // Can't call zend_clear_exception because there isn't a current - // execution stack (ie, `EG(current_execute_data)`) - zend_object* e = EG(exception); + zval rv; + zend_class_entry* exception_ce = zend_get_exception_base(EG(exception)); + zval *msg = zend_read_property_ex(exception_ce, EG(exception), ZSTR_KNOWN(ZEND_STR_MESSAGE), /* silent */ false, &rv); + + SG(sapi_headers).http_response_code = 500; + lh_response_builder_exception(response_builder, Z_STRVAL_P(msg)); + + zend_object_release(EG(exception)); EG(exception) = NULL; - // TODO: do something with the error... - zval_ptr_dtor((zval*)e); + EG(exit_status) = 1; } - } zend_catch { - return NULL; - } zend_end_try(); - // Populate response fields - php_http_headers_push(&response->headers, "Content-Type", "text/plain"); - php_http_response_set_status(response, SG(sapi_headers).http_response_code); + const char* mime = SG(sapi_headers).mimetype; + if (mime == NULL) { + mime = "text/plain"; + } + lh_response_builder_header(response_builder, "Content-Type", mime); + lh_response_builder_status_code(response_builder, SG(sapi_headers).http_response_code); + + php_request_shutdown(NULL); + lh_headers_free(headers); - // TODO: Need to figure out how php://output works. - response->body = "Hello, World!"; + php_header(); + php_output_flush_all(); + } zend_end_try(); - return response; + return lh_response_builder_build(response_builder); } diff --git a/crates/php/src/request.rs b/crates/php/src/request.rs deleted file mode 100644 index 792b5ec4..00000000 --- a/crates/php/src/request.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{env::Args, ffi::{CStr, CString}, ops::Deref}; - -use crate::sys; - -#[derive(Debug, Clone)] -pub struct Request { - request: *mut sys::php_http_request -} - -impl Deref for Request { - type Target = *mut sys::php_http_request; - - fn deref(&self) -> &Self::Target { - &self.request - } -} - -impl From<*mut sys::php_http_request> for Request { - fn from(request: *mut sys::php_http_request) -> Self { - Request { request } - } -} - -impl Request { - pub fn new() -> Self { - unsafe { sys::php_http_request_new() }.into() - } - - pub fn builder() -> RequestBuilder { - RequestBuilder::new() - } - - // Method - pub fn set_method(&self, method: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(method.as_ref()).unwrap(); - sys::php_http_request_set_method(self.request, str.as_ptr()); - } - } - pub fn method(&self) -> String { - unsafe { - let method = sys::php_http_request_get_method(self.request); - CStr::from_ptr(method).to_string_lossy().into_owned() - } - } - - // URI - pub fn set_path(&self, path: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(path.as_ref()).unwrap(); - sys::php_http_request_set_path(self.request, str.as_ptr()); - } - } - pub fn path(&self) -> String { - unsafe { - let path = sys::php_http_request_get_path(self.request); - CStr::from_ptr(path).to_string_lossy().into_owned() - } - } - - // Body - // TODO: Streaming bodies with futures::Stream - pub fn set_body(&self, body: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(body.as_ref()).unwrap(); - sys::php_http_request_set_body(self.request, str.as_ptr()); - } - } - pub fn body(&self) -> String { - unsafe { - let body = sys::php_http_request_get_body(self.request); - CStr::from_ptr(body).to_string_lossy().into_owned() - } - } -} - -pub struct RequestBuilder { - request: Request -} - -impl RequestBuilder { - pub fn new() -> Self { - RequestBuilder { - request: Request::new() - } - } - - pub fn method(self, method: T) -> Self - where - T: AsRef - { - self.request.set_method(method); - self - } - - pub fn path(self, path: T) -> Self - where - T: AsRef - { - self.request.set_path(path); - self - } - - pub fn body(self, body: T) -> Self - where - T: AsRef - { - self.request.set_body(body); - self - } - - pub fn build(self) -> Request { - self.request - } -} diff --git a/crates/php/src/response.rs b/crates/php/src/response.rs deleted file mode 100644 index 2a8cb235..00000000 --- a/crates/php/src/response.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{env::Args, ffi::{CStr, CString}, ops::Deref}; - -use crate::sys; - -#[derive(Debug, Clone)] -pub struct Response { - response: *mut sys::php_http_response -} - -impl Deref for Response { - type Target = *mut sys::php_http_response; - - fn deref(&self) -> &Self::Target { - &self.response - } -} - -impl From<*mut sys::php_http_response> for Response { - fn from(response: *mut sys::php_http_response) -> Self { - Response { - response - } - } -} - -impl Response { - pub fn status(&self) -> i32 { - unsafe { - sys::php_http_response_get_status(self.response) - } - } - - pub fn set_status(&self, status: i32) { - unsafe { - sys::php_http_response_set_status(self.response, status); - } - } - - pub fn body(&self) -> String { - unsafe { - let body = sys::php_http_response_get_body(self.response); - CStr::from_ptr(body).to_string_lossy().into_owned() - } - } - - pub fn set_body(&self, body: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(body.as_ref()).unwrap(); - sys::php_http_response_set_body(self.response, str.as_ptr()); - } - } -} diff --git a/crates/php_node/src/headers.rs b/crates/php_node/src/headers.rs new file mode 100644 index 00000000..49aece7a --- /dev/null +++ b/crates/php_node/src/headers.rs @@ -0,0 +1,181 @@ +use std::ptr; + +use napi::Result; +use napi::{sys, sys::{napi_env, napi_value}}; +use napi::bindgen_prelude::*; + +use php::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")] +pub struct PhpHeaders { + headers: Headers +} + +impl PhpHeaders { + // Create a new PHP headers instance. + pub fn new(headers: Headers) -> Self { + PhpHeaders { + headers + } + } +} + +#[napi] +impl PhpHeaders { + /// Create a new PHP headers instance. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// ``` + #[napi(constructor)] + pub fn constructor() -> Self { + PhpHeaders { + headers: Headers::new() + } + } + + /// Get the 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.get('Accept')) { + /// console.log(mime); + /// } + /// ``` + #[napi] + pub fn get(&self, key: String) -> Option> { + self.headers.get(&key).map(|v| v.to_owned()) + } + + /// 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) + } + + /// Remove a header key/value pair. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// headers.remove('Content-Type'); + /// ``` + #[napi] + pub fn remove(&mut self, key: String) { + self.headers.remove(&key) + } + + /// 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 [key, values] of headers.entries()) { + /// console.log(`${key}: ${values.join(', ')}`); + /// } + /// ``` + #[napi] + pub fn entries(&self) -> Vec>> { + self.headers.iter().map(|(k, v)| Entry(k.to_owned(), v.to_owned())).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 key of headers.keys()) { + /// console.log(key); + /// } + /// ``` + #[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.headers.iter_values().map(|v| v.to_owned()).collect() + } +} diff --git a/crates/php_node/src/lib.rs b/crates/php_node/src/lib.rs index 4ed698c6..26ade1fc 100644 --- a/crates/php_node/src/lib.rs +++ b/crates/php_node/src/lib.rs @@ -1,35 +1,12 @@ #[macro_use] extern crate napi_derive; -use php::{Embed, Request}; - -#[napi] -pub fn handle_request() { - let embed = Embed::new(); - - let request = Request::builder() - .method("GET") - .path("/test.php") - .body("Hello, World!") - .build(); - - println!("=== request ==="); - println!("method: {}", request.method()); - println!("path: {}", request.path()); - println!("body: {}", request.body()); - println!(""); - - let response = embed.handle_request( - " - echo 'Hello, World!\n'; - http_response_code(400); - echo $_SERVER['REQUEST_METHOD'] . '\n'; - ", - Some("test.php"), - request.clone() - ); - - println!("\n=== response ==="); - println!("status: {:?}", response.status()); - println!("body: {:?}", response.body()); -} +mod headers; +mod runtime; +mod request; +mod response; + +pub use headers::PhpHeaders; +pub use runtime::PhpRuntime; +pub use request::PhpRequest; +pub use response::PhpResponse; diff --git a/crates/php_node/src/request.rs b/crates/php_node/src/request.rs new file mode 100644 index 00000000..093d6603 --- /dev/null +++ b/crates/php_node/src/request.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; + +use napi::bindgen_prelude::*; + +use php::{Request, RequestBuilder}; + +use crate::PhpHeaders; + +/// Options for creating a new PHP request. +#[napi(object)] +#[derive(Default)] +pub struct PhpRequestOptions { + /// The HTTP method for the request. + pub method: String, + /// The URL for the request. + pub url: String, + /// The headers for the request. + pub headers: Option>>, + /// The body for the request. + pub body: 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 { + request: Request +} + +#[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) -> Self { + let mut builder: RequestBuilder = Request::builder() + .method(options.method) + .url(options.url).expect("invalid url"); + + 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()); + + for value in values { + builder = builder.header(key.clone(), value.clone()) + } + } + } + + if let Some(body) = options.body { + builder = builder.body(body.as_ref()) + } + + PhpRequest { + request: builder.build() + } + } + + /// 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 new file mode 100644 index 00000000..89f97124 --- /dev/null +++ b/crates/php_node/src/response.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use napi::bindgen_prelude::*; + +use php::Response; + +use crate::PhpHeaders; + +/// Options for creating a new PHP response. +#[napi(object)] +pub struct PhpResponseOptions { + /// The HTTP status code for the response. + pub status: u32, + /// 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: PhpResponseOptions) -> Self { + let mut builder = Response::builder(); + builder.status(options.status as u16); + + 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()); + + for value in values { + builder.header(key.clone(), value.clone()); + } + } + } + + 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); + } + + 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/runtime.rs b/crates/php_node/src/runtime.rs new file mode 100644 index 00000000..5ba63a0c --- /dev/null +++ b/crates/php_node/src/runtime.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use napi::{Error, Task, Env, Result}; +use napi::bindgen_prelude::*; + +use php::{Embed, Handler, Request, Response}; + +use crate::{PhpRequest, PhpResponse}; + +/// Options for creating a new PHP instance. +#[napi(object)] +#[derive(Clone, Default)] +pub struct PhpOptions { + /// The command-line arguments for the PHP instance. + pub argv: Option>, + /// The PHP code to embed. + pub code: String, + /// The filename for the PHP code. + pub file: Option +} + +/// 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); +/// ``` +#[napi(js_name = "Php")] +pub struct PhpRuntime { + embed: Arc +} + +#[napi] +impl PhpRuntime { + /// Create a new PHP instance. + /// + /// # Examples + /// + /// ```js + /// const php = new Php({ + /// code: 'echo "Hello, world!";' + /// }); + /// ``` + #[napi(constructor)] + pub fn new(options: PhpOptions) -> Self { + let code = options.code.clone(); + let filename = options.file.clone(); + let argv = options.argv.clone(); + + // TODO: Need to figure out how to send an Embed across threads + // so we can reuse the same Embed instance for multiple requests. + let embed = match argv { + Some(argv) => Embed::new_with_argv(code, filename, argv), + None => Embed::new(code, filename) + }; + + Self { + embed: Arc::new(embed) + } + } + + /// Handle a PHP request. + /// + /// # 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); + /// ``` + #[napi] + pub fn handle_request(&self, request: &PhpRequest) -> AsyncTask { + AsyncTask::new(PhpRequestTask { + embed: self.embed.clone(), + request: request.into() + }) + } + + /// Handle a PHP request synchronously. + /// + /// # Examples + /// + /// ```js + /// const php = new Php({ + /// code: 'echo "Hello, world!";' + /// }); + /// + /// const response = php.handleRequestSync(new Request({ + /// method: 'GET', + /// url: 'http://example.com' + /// })); + /// + /// console.log(response.status); + /// console.log(response.body); + /// ``` + #[napi] + pub fn handle_request_sync(&self, request: &PhpRequest) -> Result { + self.embed + .handle(request.into()) + .map_err(|err| Error::from_reason(err)) + .map(PhpResponse::new) + } +} + +// Task container to run a PHP request in a worker thread. +pub struct PhpRequestTask { + embed: Arc, + request: Request +} + +impl Task for PhpRequestTask { + type Output = Response; + type JsValue = PhpResponse; + + // Handle the PHP request in the worker thread. + fn compute(&mut self) -> Result { + self.embed + .handle(self.request.clone()) + .map_err(|err| Error::from_reason(err)) + } + + // 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)) + } +} diff --git a/index.d.ts b/index.d.ts index 8c2ea76f..ade64d2d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,4 +3,411 @@ /* auto-generated by NAPI-RS */ -export declare function handleRequest(): void +/** Options for creating a new PHP instance. */ +export interface PhpOptions { + /** The command-line arguments for the PHP instance. */ + argv?: Array + /** The PHP code to embed. */ + code: string + /** The filename for the PHP code. */ + file?: string +} +/** Options for creating a new PHP request. */ +export interface PhpRequestOptions { + /** The HTTP method for the request. */ + method: string + /** The URL for the request. */ + url: string + /** The headers for the request. */ + headers?: Record> + /** The body for the request. */ + body?: Uint8Array +} +/** 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?: Record> + /** The body for the response. */ + body?: Uint8Array + /** The log for the response. */ + log?: Uint8Array + /** The exception for the response. */ + exception?: string +} +export type PhpHeaders = Headers +/** + * A multi-map of HTTP headers. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * const contentType = headers.get('Content-Type'); + * ``` + */ +export declare class Headers { + /** + * Create a new PHP headers instance. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * ``` + */ + constructor() + /** + * Get the 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.get('Accept')) { + * console.log(mime); + * } + * ``` + */ + get(key: string): Array | null + /** + * Set a header key/value pair. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * ``` + */ + set(key: string, value: string): void + /** + * Remove a header key/value pair. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.remove('Content-Type'); + * ``` + */ + remove(key: string): void + /** + * 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 [key, values] of headers.entries()) { + * console.log(`${key}: ${values.join(', ')}`); + * } + * ``` + */ + entries(): Array + /** + * 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 key of headers.keys()) { + * console.log(key); + * } + * ``` + */ + keys(): Array + /** + * 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); + * } + * ``` + */ + values(): Array +} +export type PhpRuntime = Php +/** + * 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({ + * code: 'echo "Hello, world!";' + * }); + * ``` + */ + constructor(options: PhpOptions) + /** + * Handle a PHP request. + * + * # 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); + * ``` + */ + handleRequest(request: PhpRequest): Promise + /** + * Handle a PHP request synchronously. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * + * 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 PhpRequest = Request +/** + * 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 { + /** + * 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]) + * }); + * ``` + */ + constructor(options: PhpRequestOptions) + /** + * Get the HTTP method for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * method: 'GET' + * }); + * + * console.log(request.method); + * ``` + */ + get method(): string + /** + * Get the URL for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: 'http://example.com' + * }); + * + * console.log(request.url); + * ``` + */ + get url(): string + /** + * 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); + * } + * ``` + */ + get headers(): Headers + /** + * Get the body for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(request.body); + * ``` + */ + get body(): Buffer +} +export type PhpResponse = Response +/** A PHP response. */ +export declare class Response { + /** + * 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]) + * }); + * ``` + */ + constructor(options: PhpResponseOptions) + /** + * Get the HTTP status code for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * status: 200 + * }); + * + * console.log(response.status); + * ``` + */ + get status(): number + /** + * 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); + * } + * ``` + */ + get headers(): Headers + /** + * Get the body for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(response.body); + * ``` + */ + get body(): Buffer + /** + * Get the log for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * log: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(response.log); + * ``` + */ + get log(): Buffer + /** + * Get the exception for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * exception: 'An error occurred' + * }); + * + * console.log(response.exception); + * ``` + */ + get exception(): string | null +} diff --git a/index.js b/index.js index a75ed774..1ddb6f55 100644 --- a/index.js +++ b/index.js @@ -310,6 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { handleRequest } = nativeBinding +const { Headers, Php, Request, Response } = nativeBinding -module.exports.handleRequest = handleRequest +module.exports.Headers = Headers +module.exports.Php = Php +module.exports.Request = Request +module.exports.Response = Response diff --git a/scripts/build-php.sh b/scripts/build-php.sh new file mode 100755 index 00000000..456afe8e --- /dev/null +++ b/scripts/build-php.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +EXTENSIONS=${1:-$(source ./scripts/extensions.sh)} + +OS=${2:-$(uname -s | tr '[:upper:]' '[:lower:]')} +ARCH=${3:-$(uname -m)} + +if [[ "$ARCH" == "arm64" ]]; then + ARCH="aarch64" +elif [[ "$ARCH" == "amd64" ]]; then + ARCH="x86_64" +fi + +if [[ "$OS" == "darwin" ]]; then + OS="macos" + # export MACOSX_DEPLOYMENT_TARGET=$(rustc --target ${ARCH}-apple-darwin --print deployment-target) +elif [[ "$OS" == "linux" ]]; then + export PATH="/usr/local/musl/bin:$PATH" + export CC="${ARCH}-linux-musl-gcc" + export AR="${ARCH}-linux-musl-ar" + if [[ "$(ldd --version 2>&1)" != *"musl"* ]]; then + export SPC_LIBC="glibc" + else + export SPC_LIBC="musl" + fi +fi + +# Ensure it is built with debug symbols when DEBUG is set +if [[ -n "${DEBUG:-}" ]]; then + SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-cgi --disable-phpdbg --enable-debug" +fi + +./spc build ${EXTENSIONS} --build-embed --enable-zts --no-strip --debug diff --git a/scripts/download-php.sh b/scripts/download-php.sh new file mode 100755 index 00000000..1de0ebee --- /dev/null +++ b/scripts/download-php.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PHP_VERSION=${1:-8.4} +EXTENSIONS=${2:-$(source ./scripts/extensions.sh)} + +./spc download --prefer-pre-built --with-php=${PHP_VERSION} --retry=10 --for-extensions=${EXTENSIONS} diff --git a/scripts/extensions.sh b/scripts/extensions.sh new file mode 100755 index 00000000..b78ab5d8 --- /dev/null +++ b/scripts/extensions.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +printf "amqp" +printf ",apcu" +# printf ",ast" +printf ",bcmath" +printf ",bz2" +printf ",calendar" +printf ",ctype" +printf ",curl" +printf ",dba" +# printf ",dio" +printf ",dom" +printf ",ds" +# printf ",enchant" +printf ",event" +printf ",exif" +# printf ",ffi" +printf ",fileinfo" +printf ",filter" +printf ",ftp" +printf ",gd" +printf ",gettext" +# printf ",glfw" +printf ",gmp" +# printf ",gmssl" +# printf ",grpc" +printf ",iconv" +printf ",igbinary" +# printf ",imagick" +# printf ",imap" +# printf ",inotify" +printf ",intl" +# printf ",ldap" +# printf ",libxml" +printf ",mbregex" +printf ",mbstring" +# printf ",mcrypt" +printf ",memcache" +# printf ",memcached" +# printf ",mongodb" +# printf ",msgpack" +printf ",mysqli" +printf ",mysqlnd" +# printf ",oci8" +printf ",opcache" +printf ",openssl" +# printf ",opentelemetry" +# printf ",parallel" +printf ",password-argon2" +# printf ",pcntl" +printf ",pdo" +printf ",pdo_mysql" +# printf ",pdo_pgsql" +printf ",pdo_sqlite" +# printf ",pdo_sqlsrv" +# printf ",pgsql" +printf ",phar" +printf ",posix" +# printf ",protobuf" +printf ",rar" +# printf ",rdkafka" +# printf ",readline" +printf ",redis" +printf ",session" +printf ",shmop" +printf ",simdjson" +printf ",simplexml" +# printf ",snappy" +printf ",soap" +# printf ",sockets" +# printf ",sodium" +# printf ",spx" +printf ",sqlite3" +printf ",sqlsrv" +# printf ",swoole" +# printf ",swoole-hook-mysql" +# printf ",swoole-hook-pgsql" +# printf ",swoole-hook-sqlite" +printf ",swow" +printf ",sysvmsg" +printf ",sysvsem" +printf ",sysvshm" +printf ",tidy" +printf ",tokenizer" +printf ",uuid" +# printf ",uv" +# printf ",xdebug" +printf ",xhprof" +# printf ",xlswriter" +printf ",xml" +printf ",xmlreader" +printf ",xmlwriter" +# printf ",xsl" +printf ",yac" +printf ",yaml" +printf ",zip" +printf ",zlib" +# printf ",zstd" +printf "\n" diff --git a/scripts/fetch-spc.sh b/scripts/fetch-spc.sh new file mode 100755 index 00000000..b58c183d --- /dev/null +++ b/scripts/fetch-spc.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +OS=${1:-$(uname -s | tr '[:upper:]' '[:lower:]')} +ARCH=${2:-$(uname -m)} + +if [[ "$OS" == "darwin" ]]; then + OS="macos" +fi + +if [[ "$ARCH" == "arm64" ]]; then + ARCH="aarch64" +elif [[ "$ARCH" == "amd64" ]]; then + ARCH="x86_64" +fi + +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-${OS}-${ARCH} +chmod +x ./spc +./spc doctor --auto-fix diff --git a/yarn.lock b/yarn.lock index fc9213f1..0f8ae0e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,6 +88,15 @@ __metadata: languageName: node linkType: hard +"@platformatic/php@workspace:.": + version: 0.0.0-use.local + resolution: "@platformatic/php@workspace:." + dependencies: + "@napi-rs/cli": "npm:^2.18.4" + ava: "npm:^6.0.1" + languageName: unknown + linkType: soft + "@rollup/pluginutils@npm:^5.1.3": version: 5.1.4 resolution: "@rollup/pluginutils@npm:5.1.4" @@ -1171,15 +1180,6 @@ __metadata: languageName: node linkType: hard -"php-stackable@workspace:.": - version: 0.0.0-use.local - resolution: "php-stackable@workspace:." - dependencies: - "@napi-rs/cli": "npm:^2.18.4" - ava: "npm:^6.0.1" - languageName: unknown - linkType: soft - "picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1"