From a3de659cb6222d4f8f971b637c5d21b9e58a5731 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 23 May 2025 18:38:17 +0800 Subject: [PATCH 1/3] Improve safety and test coverage --- .github/workflows/CI.yml | 11 +- .github/workflows/lint.yml | 14 +- crates/lang_handler/src/ffi.rs | 330 ++++++------ crates/lang_handler/src/handler.rs | 7 +- crates/lang_handler/src/headers.rs | 34 +- crates/lang_handler/src/request.rs | 127 +++-- crates/lang_handler/src/response.rs | 22 +- crates/php/src/embed.rs | 769 +++------------------------- crates/php/src/exception.rs | 49 ++ crates/php/src/lib.rs | 18 +- crates/php/src/main.rs | 8 +- crates/php/src/request_context.rs | 154 ++++++ crates/php/src/sapi.rs | 417 +++++++++++++++ crates/php/src/scopes.rs | 28 + crates/php/src/strings.rs | 79 +++ crates/php_node/src/request.rs | 4 +- crates/php_node/src/runtime.rs | 9 +- index.d.ts | 4 +- 18 files changed, 1125 insertions(+), 959 deletions(-) create mode 100644 crates/php/src/exception.rs create mode 100644 crates/php/src/request_context.rs create mode 100644 crates/php/src/sapi.rs create mode 100644 crates/php/src/scopes.rs create mode 100644 crates/php/src/strings.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2a07c9db..4fc5f3c0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -248,6 +248,11 @@ jobs: - name: List packages run: ls -R ./npm shell: bash + # TODO: This should be moved to the test jobs, but needs build deps + # for ext-php-rs to build correctly, including PHP cli + - name: Test crates + shell: bash + run: cargo test - name: Upload target-specific package for ${{ matrix.settings.target }} uses: actions/upload-artifact@v4 with: @@ -313,6 +318,10 @@ jobs: - name: List packages run: ls -R ./npm shell: bash + - name: Give GitHub Actions access to ext-php-rs + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.SECRET_REPO_DEPLOY_KEY }} - name: Test bindings run: pnpm test @@ -381,7 +390,7 @@ jobs: - name: List packages run: ls -R ./npm shell: bash - - name: Test bindings + - name: Test crates and bindings uses: addnab/docker-run-action@v3 with: image: ${{ steps.docker.outputs.IMAGE }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 270edc01..7528e7fa 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -48,13 +48,11 @@ jobs: - name: Cargo fmt run: cargo fmt -- --check - # TODO: Clippy yells a lot. Need to clean things up before turning it on. - # Clippy needs access to install ext-php-rs - # - name: Give GitHub Actions access to ext-php-rs - # uses: webfactory/ssh-agent@v0.5.4 - # with: - # ssh-private-key: ${{ secrets.SECRET_REPO_DEPLOY_KEY }} + - name: Give GitHub Actions access to ext-php-rs + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.SECRET_REPO_DEPLOY_KEY }} - # - name: Clippy - # run: cargo clippy + - name: Clippy + run: cargo clippy diff --git a/crates/lang_handler/src/ffi.rs b/crates/lang_handler/src/ffi.rs index 9304a09a..d84a3ef5 100644 --- a/crates/lang_handler/src/ffi.rs +++ b/crates/lang_handler/src/ffi.rs @@ -1,3 +1,6 @@ +// #![allow(clippy::not_unsafe_ptr_arg_deref)] +#![allow(clippy::missing_safety_doc)] + use std::{ ffi::{self, c_char, CStr, CString}, net::SocketAddr, @@ -68,11 +71,9 @@ pub extern "C" fn lh_headers_new() -> *mut lh_headers_t { /// lh_headers_free(headers); /// ``` #[no_mangle] -pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { +pub unsafe extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { if !headers.is_null() { - unsafe { - drop(Box::from_raw(headers)); - } + drop(Box::from_raw(headers)); } } @@ -85,21 +86,18 @@ pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { /// size_t count = lh_headers_count(headers, "Accept"); /// ``` #[no_mangle] -pub extern "C" fn lh_headers_count( +pub unsafe 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, + let headers = &*headers; + if let Ok(key) = CStr::from_ptr(key).to_str() { + match headers.inner.get(key) { + Some(value) => value.len(), + None => 0, + } + } else { + 0 } } @@ -112,23 +110,18 @@ pub extern "C" fn lh_headers_count( /// const char* value = lh_headers_get(headers, "Accept"); /// ``` #[no_mangle] -pub extern "C" fn lh_headers_get( +pub unsafe 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(value) => value.as_ptr() as *const std::os::raw::c_char, - None => std::ptr::null(), + let headers = &*headers; + if let Ok(key) = std::ffi::CStr::from_ptr(key).to_str() { + match headers.inner.get(key) { + Some(value) => value.as_ptr() as *const std::os::raw::c_char, + None => std::ptr::null(), + } + } else { + std::ptr::null() } } @@ -141,27 +134,22 @@ pub extern "C" fn lh_headers_get( /// const char* value = lh_headers_get_nth(headers, "Accept", 0); /// ``` #[no_mangle] -pub extern "C" fn lh_headers_get_nth( +pub unsafe 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() - }; - - headers - .inner - .get_all(key) - .get(index) - .map(|value| value.as_ptr() as *const std::os::raw::c_char) - .unwrap_or(std::ptr::null()) + let headers = &*headers; + if let Ok(key) = CStr::from_ptr(key).to_str() { + headers + .inner + .get_all(key) + .get(index) + .map(|value| value.as_ptr() as *const std::os::raw::c_char) + .unwrap_or(std::ptr::null()) + } else { + std::ptr::null() + } } /// Set a header with the given key and value. @@ -173,27 +161,17 @@ pub extern "C" fn lh_headers_get_nth( /// lh_headers_set(headers, "Accept", "application/json"); /// ``` #[no_mangle] -pub extern "C" fn lh_headers_set( +pub unsafe 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); + let headers = &mut *headers; + if let Ok(key) = CStr::from_ptr(key).to_str() { + if let Ok(value) = CStr::from_ptr(value).to_str() { + headers.inner.set(key, value.to_string()); + } + } } /// An HTTP request. Includes method, URL, headers, and body. @@ -238,7 +216,7 @@ fn c_char_to_socket_addr(value: *const ffi::c_char) -> Option { /// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); /// ``` #[no_mangle] -pub extern "C" fn lh_request_new( +pub unsafe extern "C" fn lh_request_new( method: *const ffi::c_char, url: *const ffi::c_char, headers: *mut lh_headers_t, @@ -246,17 +224,17 @@ pub extern "C" fn lh_request_new( local_socket: *const ffi::c_char, remote_socket: *const ffi::c_char, ) -> *mut lh_request_t { - let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; - let url_str = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let method = CStr::from_ptr(method).to_string_lossy().into_owned(); + let url_str = 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() }) + Some(CStr::from_ptr(body).to_bytes()) }; let local_socket = c_char_to_socket_addr(local_socket); let remote_socket = c_char_to_socket_addr(remote_socket); - let headers = unsafe { &*headers }; + let headers = &*headers; let request = Request::new( method, url, @@ -280,11 +258,9 @@ pub extern "C" fn lh_request_new( /// lh_request_free(request); /// ``` #[no_mangle] -pub extern "C" fn lh_request_free(request: *mut lh_request_t) { +pub unsafe extern "C" fn lh_request_free(request: *mut lh_request_t) { if !request.is_null() { - unsafe { - drop(Box::from_raw(request)); - } + drop(Box::from_raw(request)); } } @@ -297,8 +273,8 @@ pub extern "C" fn lh_request_free(request: *mut lh_request_t) { /// 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 }; +pub unsafe extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi::c_char { + let request = &*request; CString::new(request.inner.method()).unwrap().into_raw() } @@ -311,8 +287,8 @@ pub extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi: /// 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 }; +pub unsafe extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t { + let request = &*request; Box::into_raw(Box::new(request.inner.url().clone().into())) } @@ -325,8 +301,8 @@ pub extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t /// 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 }; +pub unsafe extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_headers_t { + let request = &*request; Box::into_raw(Box::new(request.inner.headers().clone().into())) } @@ -339,8 +315,8 @@ pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_he /// 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 }; +pub unsafe extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { + let request = &*request; CString::new(request.inner.body()).unwrap().into_raw() } @@ -354,20 +330,18 @@ pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c /// size_t length = lh_request_body_read(request, buffer, 1024); /// ``` #[no_mangle] -pub extern "C" fn lh_request_body_read( +pub unsafe 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 request = &*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); - } + std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut ffi::c_char, buffer, length); length } @@ -421,11 +395,9 @@ pub extern "C" fn lh_request_builder_new() -> *mut lh_request_builder_t { /// lh_request_builder_free(builder); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { +pub unsafe extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { if !builder.is_null() { - unsafe { - drop(Box::from_raw(builder)); - } + drop(Box::from_raw(builder)); } } @@ -438,10 +410,10 @@ pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { /// lh_request_builder_t* builder = lh_request_builder_extend(request); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_extend( +pub unsafe extern "C" fn lh_request_builder_extend( request: *const lh_request_t, ) -> *mut lh_request_builder_t { - let request = unsafe { &*request }; + let request = &*request; Box::into_raw(Box::new(RequestBuilder::extend(&request.inner).into())) } @@ -454,12 +426,12 @@ pub extern "C" fn lh_request_builder_extend( /// lh_request_builder_method(builder, "GET"); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_method( +pub unsafe 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 }; + let method = CStr::from_ptr(method).to_string_lossy().into_owned(); + let builder = &mut *builder; Box::into_raw(Box::new(builder.inner.clone().method(method).into())) } @@ -472,12 +444,12 @@ pub extern "C" fn lh_request_builder_method( /// lh_request_builder_url(builder, "https://example.com"); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_url( +pub unsafe 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 }; + let url = CStr::from_ptr(url).to_string_lossy().into_owned(); + let builder = &mut *builder; Box::into_raw(Box::new(builder.inner.clone().url(&url).unwrap().into())) } @@ -490,14 +462,14 @@ pub extern "C" fn lh_request_builder_url( /// lh_request_builder_header(builder, "Content-Type", "text/plain"); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_header( +pub unsafe 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 }; + let key = CStr::from_ptr(key).to_string_lossy().into_owned(); + let value = CStr::from_ptr(value).to_string_lossy().into_owned(); + let builder = &mut *builder; Box::into_raw(Box::new(builder.inner.clone().header(key, value).into())) } @@ -510,12 +482,12 @@ pub extern "C" fn lh_request_builder_header( /// lh_request_builder_body(builder, "Hello, world!"); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_body( +pub unsafe 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 }; + let body = CStr::from_ptr(body).to_bytes(); + let builder = &mut *builder; Box::into_raw(Box::new(builder.inner.clone().body(body).into())) } @@ -531,11 +503,13 @@ pub extern "C" fn lh_request_builder_body( /// lh_request_t* request = lh_request_builder_build(builder); /// ``` #[no_mangle] -pub extern "C" fn lh_request_builder_build( +pub unsafe 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())) + let builder = Box::from_raw(builder); + Box::into_raw(Box::new( + builder.inner.build().expect("should build request").into(), + )) } /// An HTTP response. Includes status code, headers, and body. @@ -566,13 +540,13 @@ impl From<&lh_response_t> for Response { /// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); /// ``` #[no_mangle] -pub extern "C" fn lh_response_new( +pub unsafe extern "C" fn lh_response_new( status_code: i32, 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 }; + let body_str = CStr::from_ptr(body).to_bytes(); + let headers = &*headers; Box::into_raw(Box::new( Response::new(status_code, headers.into(), body_str, "", None).into(), )) @@ -590,11 +564,9 @@ pub extern "C" fn lh_response_new( /// lh_response_free(response); /// ``` #[no_mangle] -pub extern "C" fn lh_response_free(response: *mut lh_response_t) { +pub unsafe extern "C" fn lh_response_free(response: *mut lh_response_t) { if !response.is_null() { - unsafe { - drop(Box::from_raw(response)); - } + drop(Box::from_raw(response)); } } @@ -607,8 +579,8 @@ pub extern "C" fn lh_response_free(response: *mut lh_response_t) { /// uint16_t status = lh_response_status(response); /// ``` #[no_mangle] -pub extern "C" fn lh_response_status(response: *const lh_response_t) -> i32 { - let response = unsafe { &*response }; +pub unsafe extern "C" fn lh_response_status(response: *const lh_response_t) -> i32 { + let response = &*response; response.inner.status() } @@ -621,8 +593,8 @@ pub extern "C" fn lh_response_status(response: *const lh_response_t) -> i32 { /// 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 }; +pub unsafe extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh_headers_t { + let response = &*response; Box::into_raw(Box::new(response.inner.headers().clone().into())) } @@ -635,8 +607,8 @@ pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh /// 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 }; +pub unsafe extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { + let response = &*response; CString::new(response.inner.body()).unwrap().into_raw() } @@ -690,11 +662,9 @@ pub extern "C" fn lh_response_builder_new() -> *mut lh_response_builder_t { /// lh_response_builder_free(builder); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_free(builder: *mut lh_response_builder_t) { +pub unsafe extern "C" fn lh_response_builder_free(builder: *mut lh_response_builder_t) { if !builder.is_null() { - unsafe { - drop(Box::from_raw(builder)); - } + drop(Box::from_raw(builder)); } } @@ -707,10 +677,10 @@ pub extern "C" fn lh_response_builder_free(builder: *mut lh_response_builder_t) /// lh_response_builder_t* builder = lh_response_builder_extend(response); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_extend( +pub unsafe extern "C" fn lh_response_builder_extend( response: *const lh_response_t, ) -> *mut lh_response_builder_t { - let response = unsafe { &*response }; + let response = &*response; Box::into_raw(Box::new(ResponseBuilder::extend(&response.inner).into())) } @@ -723,11 +693,11 @@ pub extern "C" fn lh_response_builder_extend( /// lh_response_builder_status_code(builder, 200); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_status_code( +pub unsafe extern "C" fn lh_response_builder_status_code( builder: *mut lh_response_builder_t, status_code: i32, ) { - let builder = unsafe { &mut *builder }; + let builder = &mut *builder; builder.inner.status(status_code); } @@ -740,14 +710,14 @@ pub extern "C" fn lh_response_builder_status_code( /// lh_response_builder_header(builder, "Content-Type", "text/plain"); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_header( +pub unsafe 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() }; + let builder = &mut *builder; + let key_str = CStr::from_ptr(key).to_string_lossy().into_owned(); + let value_str = CStr::from_ptr(value).to_string_lossy().into_owned(); builder.inner.header(key_str, value_str); } @@ -760,12 +730,12 @@ pub extern "C" fn lh_response_builder_header( /// lh_response_builder_body(builder, "Hello, world!"); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_body( +pub unsafe 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() }; + let builder = &mut *builder; + let body_str = CStr::from_ptr(body).to_bytes(); builder.inner.body(body_str); } @@ -778,15 +748,15 @@ pub extern "C" fn lh_response_builder_body( /// lh_response_builder_body_write(builder, "Hello, world!", 13); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_body_write( +pub unsafe 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) }; + let builder = &mut *builder; + let data = std::slice::from_raw_parts(data as *const u8, len); builder.inner.body.put(data); - return len; + len } /// Write to the log of the response. @@ -798,16 +768,16 @@ pub extern "C" fn lh_response_builder_body_write( /// lh_response_builder_log_write(builder, "Hello, world!", 13); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_log_write( +pub unsafe 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) }; + let builder = &mut *builder; + let data = std::slice::from_raw_parts(data as *const u8, len); builder.inner.log.put(data); builder.inner.log.put("\n".as_bytes()); - return len; + len } /// Set the exception string of the response. @@ -819,12 +789,12 @@ pub extern "C" fn lh_response_builder_log_write( /// lh_response_builder_exception(builder, "Something went wrong!"); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_exception( +pub unsafe 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() }; + let builder = &mut *builder; + let exception_str = CStr::from_ptr(exception).to_string_lossy().into_owned(); builder.inner.exception(exception_str); } @@ -840,10 +810,10 @@ pub extern "C" fn lh_response_builder_exception( /// lh_response_t* response = lh_response_builder_build(builder); /// ``` #[no_mangle] -pub extern "C" fn lh_response_builder_build( +pub unsafe extern "C" fn lh_response_builder_build( builder: *const lh_response_builder_t, ) -> *mut lh_response_t { - let builder = unsafe { &*builder }; + let builder = &*builder; Box::into_raw(Box::new(builder.inner.build().into())) } @@ -881,8 +851,8 @@ impl From<&lh_url_t> for Url { /// 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() }; +pub unsafe extern "C" fn lh_url_parse(url: *const c_char) -> *mut lh_url_t { + let url = CStr::from_ptr(url).to_string_lossy().into_owned(); let url = Url::parse(&url).unwrap(); Box::into_raw(Box::new(url.into())) } @@ -899,11 +869,9 @@ pub extern "C" fn lh_url_parse(url: *const c_char) -> *mut lh_url_t { /// lh_url_free(url); /// ``` #[no_mangle] -pub extern "C" fn lh_url_free(url: *mut lh_url_t) { +pub unsafe extern "C" fn lh_url_free(url: *mut lh_url_t) { if !url.is_null() { - unsafe { - drop(Box::from_raw(url)); - } + drop(Box::from_raw(url)); } } @@ -916,8 +884,8 @@ pub extern "C" fn lh_url_free(url: *mut lh_url_t) { /// 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 }; +pub unsafe extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.scheme()).unwrap().into_raw() } @@ -930,8 +898,8 @@ pub extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.host_str().unwrap_or("")) .unwrap() .into_raw() @@ -946,8 +914,8 @@ pub extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { + let url = &*url; url.inner.port().unwrap_or(0) } @@ -960,8 +928,8 @@ pub extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { /// 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 }; +pub unsafe extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.domain().unwrap_or("")) .unwrap() .into_raw() @@ -976,8 +944,8 @@ pub extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { + let url = &*url; let origin = match url.inner.origin() { url::Origin::Opaque(_) => { format!("{}://", url.inner.scheme()) @@ -998,8 +966,8 @@ pub extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { + let url = &*url; url.inner.has_authority() } @@ -1012,8 +980,8 @@ pub extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { /// 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 }; +pub unsafe extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.authority()).unwrap().into_raw() } @@ -1026,8 +994,8 @@ pub extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.username()).unwrap().into_raw() } @@ -1040,8 +1008,8 @@ pub extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.password().unwrap_or("")) .unwrap() .into_raw() @@ -1056,8 +1024,8 @@ pub extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.path()).unwrap().into_raw() } @@ -1070,8 +1038,8 @@ pub extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.query().unwrap_or("")) .unwrap() .into_raw() @@ -1086,8 +1054,8 @@ pub extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { + let url = &*url; CString::new(url.inner.fragment().unwrap_or("")) .unwrap() .into_raw() @@ -1102,7 +1070,7 @@ pub extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { /// 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 }; +pub unsafe extern "C" fn lh_url_uri(url: *const lh_url_t) -> *const c_char { + let url = &*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 index e6b0cbc4..40d1d2cd 100644 --- a/crates/lang_handler/src/handler.rs +++ b/crates/lang_handler/src/handler.rs @@ -46,11 +46,14 @@ pub trait Handler { /// # Ok(response) /// # } /// # } - /// let handler = MyHandler; + /// # let handler = MyHandler; + /// # /// let request = Request::builder() /// .method("GET") /// .url("http://example.com").expect("invalid url") - /// .build(); + /// .build() + /// .expect("should build request"); + /// /// let response = handler.handle(request).unwrap(); /// ``` fn handle(&self, request: Request) -> Result; diff --git a/crates/lang_handler/src/headers.rs b/crates/lang_handler/src/headers.rs index adef563b..18f16077 100644 --- a/crates/lang_handler/src/headers.rs +++ b/crates/lang_handler/src/headers.rs @@ -24,7 +24,7 @@ impl From<&Header> for String { /// let mut headers = Headers::new(); /// headers.set("Content-Type", "text/plain"); /// -/// assert_eq!(headers.get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// assert_eq!(headers.get("Content-Type"), Some("text/plain".to_string())); /// ``` #[derive(Debug, Clone)] pub struct Headers(HashMap); @@ -73,7 +73,7 @@ impl Headers { /// headers.add("Accept", "text/plain"); /// headers.add("Accept", "application/json"); /// - /// assert_eq!(headers.get("Accept"), Some(&"application/json".to_string())); + /// assert_eq!(headers.get("Accept"), Some("application/json".to_string())); /// ``` pub fn get(&self, key: K) -> Option where @@ -157,7 +157,7 @@ impl Headers { /// headers.set("Content-Type", "text/plain"); /// headers.set("Content-Type", "text/html"); /// - /// assert_eq!(headers.get("Content-Type"), Some(&"text/html".to_string())); + /// assert_eq!(headers.get("Content-Type"), Some("text/html".to_string())); /// ``` pub fn set(&mut self, key: K, value: V) where @@ -180,10 +180,10 @@ impl Headers { /// headers.add("Accept", "text/plain"); /// headers.add("Accept", "application/json"); /// - /// assert_eq!(headers.get("Accept"), Some(&vec![ + /// assert_eq!(headers.get_all("Accept"), vec![ /// "text/plain".to_string(), /// "application/json".to_string() - /// ])); + /// ]); /// ``` pub fn add(&mut self, key: K, value: V) where @@ -256,7 +256,8 @@ impl Headers { /// # Examples /// /// ``` - /// # use lang_handler::Headers; + /// use lang_handler::Headers; + /// /// let mut headers = Headers::new(); /// headers.set("Content-Type", "text/plain"); /// headers.set("Accept", "application/json"); @@ -267,6 +268,21 @@ impl Headers { self.0.len() } + /// Checks if the headers are empty. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let headers = Headers::new(); + /// + /// assert_eq!(headers.is_empty(), true); + /// ``` + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns an iterator over the headers. /// /// # Examples @@ -285,3 +301,9 @@ impl Headers { self.0.iter() } } + +impl Default for Headers { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs index 44760cec..94c59e68 100644 --- a/crates/lang_handler/src/request.rs +++ b/crates/lang_handler/src/request.rs @@ -22,15 +22,16 @@ use crate::Headers; /// .header("Accept", "application/json") /// .header("Host", "example.com") /// .body("Hello, World!") -/// .build(); +/// .build() +/// .expect("should build request"); /// /// assert_eq!(request.method(), "POST"); /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); -/// assert_eq!(request.headers().get("Accept"), Some(&vec![ +/// assert_eq!(request.headers().get_all("Accept"), vec![ /// "text/html".to_string(), /// "application/json".to_string() -/// ])); -/// assert_eq!(request.headers().get("Host"), Some(&vec!["example.com".to_string()])); +/// ]); +/// assert_eq!(request.headers().get("Host"), Some("example.com".to_string())); /// assert_eq!(request.body(), "Hello, World!"); /// ``` #[derive(Clone, Debug)] @@ -97,12 +98,13 @@ impl Request { /// .header("Content-Type", "text/html") /// .header("Content-Length", 13.to_string()) /// .body("Hello, World!") - /// .build(); + /// .build() + /// .expect("should build request"); /// /// assert_eq!(request.method(), "POST"); /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); - /// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); - /// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["13".to_string()])); + /// assert_eq!(request.headers().get("Content-Type"), Some("text/html".to_string())); + /// assert_eq!(request.headers().get("Content-Length"), Some("13".to_string())); /// assert_eq!(request.body(), "Hello, World!"); /// ``` pub fn builder() -> RequestBuilder { @@ -120,18 +122,20 @@ impl Request { /// .method("GET") /// .url("http://example.com/test.php").expect("invalid url") /// .header("Content-Type", "text/plain") - /// .build(); + /// .build() + /// .expect("should build request"); /// /// let extended = request.extend() /// .method("POST") /// .header("Content-Length", 12.to_string()) /// .body("Hello, World") - /// .build(); + /// .build() + /// .expect("should build request"); /// /// assert_eq!(extended.method(), "POST"); /// assert_eq!(extended.url().as_str(), "http://example.com/test.php"); - /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); - /// assert_eq!(extended.headers().get("Content-Length"), Some(&vec!["12".to_string()])); + /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); + /// assert_eq!(extended.headers().get("Content-Length"), Some("12".to_string())); /// assert_eq!(extended.body(), "Hello, World"); /// ``` pub fn extend(&self) -> RequestBuilder { @@ -201,7 +205,7 @@ impl Request { /// None, /// ); /// - /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); /// ``` pub fn headers(&self) -> &Headers { &self.headers @@ -274,6 +278,21 @@ impl Request { } } +/// Errors which may be produced when building a Request from a RequestBuilder. +#[derive(Debug, PartialEq)] +pub enum RequestBuilderException { + /// Url is required + MissingUrl, +} + +impl std::fmt::Display for RequestBuilderException { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RequestBuilderException::MissingUrl => write!(f, "Expected url to be set"), + } + } +} + /// Builds an HTTP request. /// /// # Examples @@ -287,12 +306,13 @@ impl Request { /// .header("Content-Type", "text/html") /// .header("Content-Length", 13.to_string()) /// .body("Hello, World!") -/// .build(); +/// .build() +/// .expect("should build request"); /// /// assert_eq!(request.method(), "POST"); /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); -/// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); -/// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["13".to_string()])); +/// assert_eq!(request.headers().get("Content-Type"), Some("text/html".to_string())); +/// assert_eq!(request.headers().get("Content-Length"), Some("13".to_string())); /// assert_eq!(request.body(), "Hello, World!"); /// ``` #[derive(Clone)] @@ -340,15 +360,18 @@ impl RequestBuilder { /// "GET".to_string(), /// "http://example.com".parse().unwrap(), /// headers, - /// "Hello, World!" + /// "Hello, World!", + /// None, + /// None /// ); /// /// let extended = RequestBuilder::extend(&request) - /// .build(); + /// .build() + /// .expect("should build request"); /// /// assert_eq!(extended.method(), "GET"); /// assert_eq!(extended.url().as_str(), "http://example.com/"); - /// assert_eq!(extended.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(extended.headers().get("Accept"), Some("text/html".to_string())); /// assert_eq!(extended.body(), "Hello, World!"); /// ``` pub fn extend(request: &Request) -> Self { @@ -357,8 +380,8 @@ impl RequestBuilder { url: Some(request.url().clone()), headers: request.headers().clone(), body: BytesMut::from(request.body()), - local_socket: request.local_socket.clone(), - remote_socket: request.remote_socket.clone(), + local_socket: request.local_socket, + remote_socket: request.remote_socket, } } @@ -371,7 +394,9 @@ impl RequestBuilder { /// /// let request = RequestBuilder::new() /// .method("POST") - /// .build(); + /// .url("http://example.com/test.php").expect("invalid url") + /// .build() + /// .expect("should build request"); /// /// assert_eq!(request.method(), "POST"); /// ``` @@ -389,7 +414,8 @@ impl RequestBuilder { /// /// let request = RequestBuilder::new() /// .url("http://example.com/test.php").expect("invalid url") - /// .build(); + /// .build() + /// .expect("should build request"); /// /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); /// ``` @@ -402,7 +428,7 @@ impl RequestBuilder { self.url = Some(url); Ok(self) } - Err(e) => return Err(e), + Err(e) => Err(e), } } @@ -414,17 +440,19 @@ impl RequestBuilder { /// use lang_handler::RequestBuilder; /// /// let request = RequestBuilder::new() + /// .url("http://example.com/test.php").expect("invalid url") /// .header("Accept", "text/html") - /// .build(); + /// .build() + /// .expect("should build request"); /// - /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(request.headers().get("Accept"), Some("text/html".to_string())); /// ``` pub fn header(mut self, key: K, value: V) -> Self where K: Into, V: Into, { - self.headers.set(key.into(), value.into()); + self.headers.add(key.into(), value.into()); self } @@ -436,8 +464,10 @@ impl RequestBuilder { /// use lang_handler::RequestBuilder; /// /// let request = RequestBuilder::new() + /// .url("http://example.com/test.php").expect("invalid url") /// .body("Hello, World!") - /// .build(); + /// .build() + /// .expect("should build request"); /// /// assert_eq!(request.body(), "Hello, World!"); /// ``` @@ -451,13 +481,19 @@ impl RequestBuilder { /// # Examples /// /// ``` + /// use std::net::SocketAddr; /// use lang_handler::RequestBuilder; /// /// let request = RequestBuilder::new() + /// .url("http://example.com/test.php").expect("invalid url") /// .local_socket("127.0.0.1:8080").expect("invalid local socket") - /// .build(); + /// .build() + /// .expect("should build request"); /// - /// assert_eq!(request.local_socket(), "127.0.0.1:8080"); + /// let expected = "127.0.0.1:8080" + /// .parse::() + /// .expect("should parse"); + /// assert_eq!(request.local_socket(), Some(expected)); /// ``` pub fn local_socket(mut self, local_socket: T) -> Result where @@ -477,13 +513,19 @@ impl RequestBuilder { /// # Examples /// /// ``` + /// use std::net::SocketAddr; /// use lang_handler::RequestBuilder; /// /// let request = RequestBuilder::new() + /// .url("http://example.com/test.php").expect("invalid url") /// .remote_socket("127.0.0.1:8080").expect("invalid remote socket") - /// .build(); + /// .build() + /// .expect("should build request"); /// - /// assert_eq!(request.remote_socket(), "127.0.0.1:8080"); + /// let expected = "127.0.0.1:8080" + /// .parse::() + /// .expect("should parse"); + /// assert_eq!(request.remote_socket(), Some(expected)); /// ``` pub fn remote_socket(mut self, remote_socket: T) -> Result where @@ -506,23 +548,28 @@ impl RequestBuilder { /// use lang_handler::RequestBuilder; /// /// let request = RequestBuilder::new() - /// .build(); + /// .url("http://example.com/test.php").expect("invalid url") + /// .build() + /// .expect("should build request"); /// /// assert_eq!(request.method(), "GET"); - /// assert_eq!(request.url().as_str(), "http://example.com/"); + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); /// assert_eq!(request.body(), ""); /// ``` - pub fn build(self) -> Request { - Request { + pub fn build(self) -> Result { + Ok(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()), + url: self.url.ok_or(RequestBuilderException::MissingUrl)?, headers: self.headers, body: self.body.freeze(), local_socket: self.local_socket, remote_socket: self.remote_socket, - } + }) + } +} + +impl Default for RequestBuilder { + fn default() -> Self { + Self::new() } } diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs index 379476f7..c2c49632 100644 --- a/crates/lang_handler/src/response.rs +++ b/crates/lang_handler/src/response.rs @@ -16,7 +16,7 @@ use crate::Headers; /// .build(); /// /// assert_eq!(response.status(), 200); -/// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(response.body(), "Hello, World!"); /// ``` #[derive(Clone, Debug)] @@ -43,7 +43,7 @@ impl Response { /// 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.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(response.body(), "Hello, World!"); /// assert_eq!(response.log(), "log"); /// assert_eq!(response.exception(), Some(&"exception".to_string())); @@ -82,7 +82,7 @@ impl Response { /// .build(); /// /// assert_eq!(response.status(), 200); - /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(response.body(), "Hello, World!"); /// ``` pub fn builder() -> ResponseBuilder { @@ -107,7 +107,7 @@ impl Response { /// .build(); /// /// assert_eq!(extended.status(), 201); - /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(extended.body(), "Hello, World!"); /// ``` pub fn extend(&self) -> ResponseBuilder { @@ -143,7 +143,7 @@ impl Response { /// .header("Content-Type", "text/plain") /// .build(); /// - /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); /// ``` pub fn headers(&self) -> &Headers { &self.headers @@ -218,7 +218,7 @@ impl Response { /// .build(); /// /// assert_eq!(response.status(), 200); -/// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(response.body(), "Hello, World!"); /// ``` #[derive(Clone, Debug)] @@ -268,7 +268,7 @@ impl ResponseBuilder { /// .build(); /// /// assert_eq!(extended.status(), 201); - /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.headers().get("Content-Type"), Some("text/plain".to_string())); /// assert_eq!(extended.body(), "Hello, World!"); /// ``` pub fn extend(response: &Response) -> Self { @@ -310,7 +310,7 @@ impl ResponseBuilder { /// .header("Content-Type", "text/plain") /// .build(); /// - /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(response.headers().get("Content-Type"), Some("text/plain".to_string())); /// ``` pub fn header(&mut self, key: K, value: V) -> &mut Self where @@ -411,3 +411,9 @@ impl ResponseBuilder { } } } + +impl Default for ResponseBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index f2754b20..7a2958b9 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -1,173 +1,25 @@ use std::{ - collections::HashMap, env::Args, - ffi::{c_char, c_int, c_void, CStr, CString, NulError}, - ops::Deref, - path::{Path, PathBuf, StripPrefixError}, - str::FromStr, - sync::{OnceLock, RwLock}, + ffi::c_char, + path::{Path, PathBuf}, }; -use bytes::{Buf, BufMut}; - use ext_php_rs::{ - builders::{IniBuilder, SapiBuilder}, - embed::{ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup, SapiModule}, error::Error, - exception::register_error_observer, ffi::{ - php_execute_script, php_module_shutdown, php_module_startup, php_register_variable, - php_request_shutdown, php_request_startup, sapi_get_default_content_type, sapi_header_struct, - sapi_send_headers, sapi_shutdown, sapi_startup, zend_eval_string_ex, zend_stream_init_filename, - ZEND_RESULT_CODE_SUCCESS, - }, - prelude::*, - types::{ZendHashTable, ZendStr}, - zend::{ - try_catch, try_catch_first, ExecutorGlobals, ProcessGlobals, SapiGlobals, SapiHeader, - SapiHeaders, + _zend_file_handle__bindgen_ty_1, php_execute_script, sapi_get_default_content_type, + zend_file_handle, zend_stream_init_filename, }, + zend::{try_catch, try_catch_first, ExecutorGlobals, SapiGlobals}, }; -use lang_handler::{Handler, Header, Request, Response, ResponseBuilder}; -use libc::free; - -// This is a helper to ensure that PHP is initialized and deinitialized at the -// appropriate times. -struct Sapi(Box); - -impl Sapi { - 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::>(); - - let mut sapi = SapiBuilder::new("php_lang_handler", "PHP Lang Handler") - .startup_function(sapi_module_startup) - // .shutdown_function(sapi_module_shutdown) - // .activate_function(sapi_module_activate) - .deactivate_function(sapi_module_deactivate) - .ub_write_function(sapi_module_ub_write) - .flush_function(sapi_module_flush) - .send_header_function(sapi_module_send_header) - .read_post_function(sapi_module_read_post) - .read_cookies_function(sapi_module_read_cookies) - .register_server_variables_function(sapi_module_register_server_variables) - .log_message_function(sapi_module_log_message) - // .executable_location(args.get(0)) - .build() - .expect("Failed to build SAPI module"); - - sapi.ini_defaults = Some(sapi_cli_ini_defaults); - sapi.php_ini_path_override = std::ptr::null_mut(); - sapi.php_ini_ignore_cwd = 1; - sapi.additional_functions = std::ptr::null(); - // sapi.phpinfo_as_text = 1; - - let exe_loc = argv.get(0).expect("should have exe location"); - let exe_loc = CString::from_str(exe_loc).expect("should construct exe location cstring"); - sapi.executable_location = exe_loc.as_ptr() as *mut i8; - let mut boxed = Box::new(sapi); - - unsafe { - ext_php_rs_sapi_startup(); - - sapi_startup(boxed.as_mut()); - php_module_startup(boxed.as_mut(), get_module()); - } - - // TODO: Should maybe capture this to store in EmbedException rather than - // writing to the ResponseBuilder here. When php_execute_script fails it - // should read that and could return an error or write it to the - // ResponseBuilder there. - register_error_observer(|_error_type, _file, _line, message| { - RequestContext::current().map(|ctx| { - let message_str = message - .as_str() - .expect("Failed to convert message to string"); - ctx.response_builder().exception(message_str); - }); - }); - - Sapi(boxed) - } +use lang_handler::{Handler, Request, Response}; - fn do_startup(&mut self) -> Result<(), String> { - let sapi = self.0.as_mut(); - let startup = sapi.startup.expect("No startup function"); - if unsafe { startup(sapi) } != ZEND_RESULT_CODE_SUCCESS { - return Err("Failed to start PHP SAPI".to_string()); - } - Ok(()) - } - - pub fn startup() -> Result<(), String> { - match SAPI_INIT.get() { - None => Err("SAPI not initialized".to_string()), - Some(rwlock) => match rwlock.write() { - Err(_) => Err("Failed to lock SAPI instance".to_string()), - Ok(mut sapi) => { - sapi.do_startup()?; - Ok(()) - } - }, - } - } -} - -impl Drop for Sapi { - fn drop(&mut self) { - unsafe { - php_module_shutdown(); - sapi_shutdown(); - - ext_php_rs_sapi_shutdown(); - } - } -} - -static SAPI_INIT: OnceLock> = OnceLock::new(); - -#[derive(Debug)] -pub enum EmbedException { - SapiStartupError, - RequestStartupError, - InvalidCString(NulError), - InvalidStr(std::str::Utf8Error), - HeaderNotFound(String), - ExecuteError, - Exception(String), - Bailout, - ResponseError, - IoError(std::io::Error), - RelativizeError(StripPrefixError), - CanonicalizeError(std::io::Error), -} - -impl std::fmt::Display for EmbedException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EmbedException::SapiStartupError => write!(f, "SAPI startup error"), - EmbedException::RequestStartupError => write!(f, "Request startup error"), - EmbedException::InvalidCString(e) => write!(f, "CString conversion error: {}", e.to_string()), - EmbedException::InvalidStr(e) => write!(f, "String conversion error: {}", e), - EmbedException::HeaderNotFound(header) => write!(f, "Header not found: {}", header), - EmbedException::ExecuteError => write!(f, "Script execution error"), - EmbedException::Exception(e) => write!(f, "Exception thrown: {}", e), - EmbedException::Bailout => write!(f, "PHP bailout"), - EmbedException::ResponseError => write!(f, "Error building response"), - EmbedException::IoError(e) => write!(f, "IO error: {}", e), - EmbedException::RelativizeError(e) => write!(f, "Path relativization error: {}", e), - EmbedException::CanonicalizeError(e) => write!(f, "Path canonicalization error: {}", e), - } - } -} +use crate::{ + sapi::ensure_sapi, + strings::{cstr, nullable_cstr, str_from_cstr, translate_path}, + EmbedException, RequestContext, RequestScope, Sapi, +}; /// Embed a PHP script into a Rust application to handle HTTP requests. #[derive(Debug)] @@ -186,11 +38,15 @@ impl Embed { /// # Examples /// /// ``` + /// use std::env::current_dir; /// use php::Embed; /// - /// let embed = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// let docroot = current_dir() + /// .expect("should have current_dir"); + /// + /// let embed = Embed::new(docroot); /// ``` - pub fn new>(docroot: C) -> Self { + pub fn new>(docroot: C) -> Result { Embed::new_with_argv::(docroot, vec![]) } @@ -199,12 +55,18 @@ impl Embed { /// # Examples /// /// ``` + /// use std::env::{args, current_dir}; /// use php::Embed; /// - /// let args = std::env::args(); - /// let embed = Embed::new_with_args("echo $argv[1];", Some("example.php"), args); + /// let docroot = current_dir() + /// .expect("should have current_dir"); + /// + /// let embed = Embed::new_with_args(docroot, args()); /// ``` - pub fn new_with_args>(docroot: C, args: Args) -> Self { + pub fn new_with_args(docroot: C, args: Args) -> Result + where + C: AsRef, + { Embed::new_with_argv(docroot, args.collect()) } @@ -213,33 +75,50 @@ impl Embed { /// # Examples /// /// ``` + /// use std::env::current_dir; /// 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(); + /// let docroot = current_dir() + /// .expect("should have current_dir"); /// - /// // assert_eq!(response.status(), 200); - /// # // TODO: Uncomment when argv gets passed through correctly. - /// # // assert_eq!(response.body(), "Hello, world!"); + /// let embed = Embed::new_with_argv(docroot, vec![ + /// "foo" + /// ]); /// ``` - pub fn new_with_argv(docroot: C, argv: Vec) -> Self + pub fn new_with_argv(docroot: C, argv: Vec) -> Result where C: AsRef, S: AsRef + std::fmt::Debug, { - SAPI_INIT.get_or_init(|| RwLock::new(Sapi::new(argv))); + ensure_sapi(argv)?; - let docroot = docroot.as_ref().canonicalize().expect("should exist"); + let docroot = docroot + .as_ref() + .canonicalize() + .map_err(|_| EmbedException::DocRootNotFound(docroot.as_ref().display().to_string()))?; - Embed { docroot } + Ok(Embed { docroot }) + } + + /// Get the docroot used for this Embed instance + /// + /// # Examples + /// + /// ```rust + /// use std::env::current_dir; + /// use php::Embed; + /// + /// + /// let docroot = current_dir() + /// .expect("should have current_dir"); + /// + /// let embed = Embed::new(&docroot) + /// .expect("should have constructed Embed"); + /// + /// assert_eq!(embed.docroot(), docroot.as_path()); + /// ``` + pub fn docroot(&self) -> &Path { + self.docroot.as_path() } } @@ -251,16 +130,23 @@ impl Handler for Embed { /// # Examples /// /// ``` + /// use std::env::current_dir; /// use php::{Embed, Handler, Request, Response}; /// - /// let handler = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// let docroot = current_dir() + /// .expect("should have current_dir"); + /// + /// let handler = Embed::new(docroot) + /// .expect("should construct Embed"); /// /// let request = Request::builder() /// .method("GET") /// .url("http://example.com").expect("invalid url") - /// .build(); + /// .build() + /// .expect("should build request"); /// - /// let response = handler.handle(request).unwrap(); + /// let response = handler.handle(request) + /// .expect("should handle request"); /// /// //assert_eq!(response.status(), 200); /// //assert_eq!(response.body(), "Hello, world!"); @@ -271,7 +157,7 @@ impl Handler for Embed { } // Initialize the SAPI module - Sapi::startup().map_err(|_| EmbedException::SapiStartupError)?; + Sapi::startup()?; let url = request.url(); @@ -298,8 +184,6 @@ impl Handler for Embed { // Prepare memory stream of the code let mut file_handle = unsafe { - use ext_php_rs::ffi::{_zend_file_handle__bindgen_ty_1, zend_file_handle, zend_stream}; - let mut file_handle = zend_file_handle { handle: _zend_file_handle__bindgen_ty_1 { fp: std::ptr::null_mut(), @@ -365,9 +249,9 @@ impl Handler for Embed { let ex = Error::Exception(err); - RequestContext::current().map(|ctx| { + if let Some(ctx) = RequestContext::current() { ctx.response_builder().exception(ex.to_string()); - }); + } // TODO: Should exceptions be raised or only captured on // the response builder? @@ -376,7 +260,7 @@ impl Handler for Embed { Ok(()) }) - .map_or_else(|_err| Err(EmbedException::Bailout), |res| res)?; + .unwrap_or(Err(EmbedException::Bailout))?; let (mimetype, http_response_code) = { let globals = SapiGlobals::get(); @@ -406,511 +290,8 @@ impl Handler for Embed { Ok(response_builder.build()) }) - // Convert CatchError to a PhpException - .map_or_else(|_err| Err(EmbedException::Bailout), |res| res)?; + .unwrap_or(Err(EmbedException::Bailout))?; Ok(response) } } - -struct RequestScope(); - -impl RequestScope { - fn new() -> Result { - if unsafe { php_request_startup() } != ZEND_RESULT_CODE_SUCCESS { - return Err(EmbedException::RequestStartupError); - } - - Ok(RequestScope()) - } -} - -impl Drop for RequestScope { - fn drop(&mut self) { - unsafe { - php_request_shutdown(0 as *mut c_void); - } - } -} - -// The request context for the PHP SAPI. -#[derive(Debug)] -struct RequestContext { - request: Request, - response_builder: ResponseBuilder, -} - -impl RequestContext { - // Sets the current request context for the PHP SAPI. - // - // # Examples - // - // ``` - // use php::{Request, RequestContext}; - // - // let request = Request::builder() - // .method("GET") - // .url("http://example.com") - // .build(); - // - // let mut context = RequestContext::new(request); - // context.make_current(); - // - // assert_eq!(context.request().method(), "GET"); - // ``` - fn for_request(request: Request) { - let context = Box::new(RequestContext { - request, - response_builder: ResponseBuilder::new(), - }); - let mut globals = SapiGlobals::get_mut(); - globals.server_context = Box::into_raw(context) as *mut c_void; - } - - // Retrieve a mutable reference to the request context - // - // # Examples - // - // ``` - // use php::{Request, RequestContext}; - // - // let request = Request::builder() - // .method("GET") - // .url("http://example.com") - // .build(); - // - // let mut context = RequestContext::new(request); - // - // SapiGlobals::get_mut().server_context = - // &mut context as *mut RequestContext as *mut c_void; - // - // let current_context = RequestContext::current(); - // assert_eq!(current_context.request().method(), "GET"); - // ``` - pub fn current<'a>() -> Option<&'a mut RequestContext> { - let ptr = { - let globals = SapiGlobals::get(); - globals.server_context as *mut RequestContext - }; - if ptr.is_null() { - return None; - } - - Some(unsafe { &mut *(ptr as *mut RequestContext) }) - } - - pub fn reclaim() -> Option> { - let ptr = { - let mut globals = SapiGlobals::get_mut(); - std::mem::replace(&mut globals.server_context, std::ptr::null_mut()) - }; - if ptr.is_null() { - return None; - } - Some(unsafe { Box::from_raw(ptr as *mut RequestContext) }) - } - - // Returns a reference to the request. - // - // # Examples - // - // ``` - // use php::{Request, RequestContext}; - // - // let request = Request::builder() - // .method("GET") - // .url("http://example.com") - // .build(); - // - // let context = RequestContext::new(request); - // - // assert_eq!(context.request().method(), "GET"); - // ``` - pub fn request(&self) -> &Request { - &self.request - } - - // Returns a mutable reference to the response builder. - // - // # Examples - // - // ``` - // use php::{Request, RequestContext}; - // - // let request = Request::builder() - // .method("GET") - // .url("http://example.com") - // .build(); - // - // let mut context = RequestContext::new(request); - // - // context.response_builder().status(200); - // ``` - pub fn response_builder(&mut self) -> &mut ResponseBuilder { - &mut self.response_builder - } -} - -// -// PHP SAPI Functions -// - -// error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR -static HARDCODED_INI: &str = " - error_reporting=4343 - ignore_repeated_errors=1 - display_errors=0 - display_startup_errors=0 - register_argc_argv=1 - log_errors=1 - implicit_flush=0 - memory_limit=128M - output_buffering=0 - enable_post_data_reading=1 - html_errors=0 - max_execution_time=0 - max_input_time=-1 -"; - -#[no_mangle] -pub extern "C" fn sapi_cli_ini_defaults(configuration_hash: *mut ext_php_rs::types::ZendHashTable) { - let hash = unsafe { &mut *configuration_hash }; - - let config = str::trim(HARDCODED_INI).lines().map(str::trim); - - for line in config { - let mut parts = line.splitn(2, '='); - let key = parts.next().unwrap(); - let value = parts.next().unwrap(); - hash.insert(key, value).unwrap(); - } -} - -#[no_mangle] -pub extern "C" fn sapi_module_startup( - sapi_module: *mut SapiModule, -) -> ext_php_rs::ffi::zend_result { - unsafe { php_module_startup(sapi_module, get_module()) } -} - -#[no_mangle] -pub extern "C" fn sapi_module_deactivate() -> c_int { - { - let mut globals = SapiGlobals::get_mut(); - - globals.server_context = std::ptr::null_mut(); - globals.request_info.argc = 0; - globals.request_info.argv = std::ptr::null_mut(); - - // drop_str(globals.request_info.request_method); - // drop_str(globals.request_info.query_string); - // drop_str(globals.request_info.request_uri); - // drop_str(globals.request_info.path_translated); - // drop_str(globals.request_info.content_type); - // drop_str(globals.request_info.cookie_data); - // drop_str(globals.request_info.php_self); - // drop_str(globals.request_info.auth_user); - // drop_str(globals.request_info.auth_password); - // drop_str(globals.request_info.auth_digest); - } - - // TODO: When _is_ it safe to reclaim the request context? - // RequestContext::reclaim(); - - return 0; -} - -#[no_mangle] -pub extern "C" fn sapi_module_ub_write(str: *const i8, str_length: usize) -> usize { - if str.is_null() || str_length == 0 { - return 0; - } - let bytes = unsafe { std::slice::from_raw_parts(str as *const u8, str_length) }; - let len = bytes.len(); - RequestContext::current().map(|ctx| { - ctx.response_builder().body_write(bytes); - }); - len -} - -#[no_mangle] -pub extern "C" fn sapi_module_flush(_server_context: *mut c_void) { - RequestContext::current().map(|ctx| { - unsafe { sapi_send_headers() }; - let mut globals = SapiGlobals::get_mut(); - globals.headers_sent = 1; - ctx - .response_builder() - .status(globals.sapi_headers.http_response_code); - }); -} - -#[no_mangle] -pub extern "C" fn sapi_module_send_header(header: *mut SapiHeader, _server_context: *mut c_void) { - // Not sure _why_ this is necessary, but it is. - if header.is_null() { - return; - } - - let header = unsafe { &*header }; - let name = header.name(); - - // Header value is None for http version + status line - if let Some(value) = header.value() { - RequestContext::current().map(|ctx| { - ctx.response_builder().header(name, value); - }); - } -} - -#[no_mangle] -pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> usize { - if length == 0 { - return 0; - } - - let body = RequestContext::current() - .map(|ctx| ctx.request().body()) - .unwrap(); - - let length = length.min(body.len()); - if length == 0 { - return 0; - } - - let chunk = body.take(length); - - unsafe { - std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut c_char, buffer, length); - } - length -} - -#[no_mangle] -pub extern "C" fn sapi_module_read_cookies() -> *mut c_char { - SapiGlobals::get().request_info.cookie_data -} - -#[no_mangle] -pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::types::Zval) { - unsafe { - // use ext_php_rs::ffi::php_import_environment_variables; - // if let Some(f) = php_import_environment_variables { - // f(vars); - // } - - let request = RequestContext::current() - .map(|ctx| ctx.request()) - .expect("should have request"); - - let headers = request.headers(); - - for (key, values) in headers.iter() { - let header = match values { - Header::Single(header) => header, - Header::Multiple(headers) => headers.first().expect("should have first header"), - }; - let cgi_key = format!("HTTP_{}", key.to_ascii_uppercase().replace("-", "_")); - php_register_variable(cstr(&cgi_key).unwrap(), cstr(header).unwrap(), vars); - } - - let globals = SapiGlobals::get(); - let req_info = &globals.request_info; - - let cwd = maybe_current_dir().unwrap(); - let cwd_cstr = cstr(cwd.as_os_str().to_str().unwrap()).unwrap(); - - let script_filename = req_info.path_translated; - let script_name = if !req_info.request_uri.is_null() { - req_info.request_uri - } else { - c"".as_ptr() - }; - - php_register_variable( - cstr("REQUEST_SCHEME").unwrap(), - cstr(request.url().scheme()).unwrap(), - vars, - ); - php_register_variable(cstr("CONTEXT_PREFIX").unwrap(), cstr("").unwrap(), vars); - php_register_variable( - cstr("SERVER_ADMIN").unwrap(), - cstr("webmaster@localhost").unwrap(), - vars, - ); - php_register_variable( - cstr("GATEWAY_INTERFACE").unwrap(), - cstr("CGI/1.1").unwrap(), - vars, - ); - - php_register_variable(cstr("PHP_SELF").unwrap(), script_name, vars); - php_register_variable(cstr("SCRIPT_NAME").unwrap(), script_name, vars); - php_register_variable(cstr("SCRIPT_FILENAME").unwrap(), script_filename, vars); - php_register_variable(cstr("PATH_TRANSLATED").unwrap(), script_filename, vars); - php_register_variable(cstr("DOCUMENT_ROOT").unwrap(), cwd_cstr, vars); - php_register_variable(cstr("CONTEXT_DOCUMENT_ROOT").unwrap(), cwd_cstr, vars); - - if !req_info.request_uri.is_null() { - php_register_variable(cstr("REQUEST_URI").unwrap(), req_info.request_uri, vars); - } - - php_register_variable( - cstr("SERVER_PROTOCOL").unwrap(), - cstr("HTTP/1.1").unwrap(), - vars, - ); - - let sapi = SAPI_INIT.get().unwrap(); - php_register_variable( - cstr("SERVER_SOFTWARE").unwrap(), - sapi.read().expect("should read sapi").0.name, - vars, - ); - - if let Some(info) = request.local_socket() { - php_register_variable( - cstr("SERVER_ADDR").unwrap(), - cstr(info.ip().to_string()).unwrap(), - vars, - ); - php_register_variable( - cstr("SERVER_PORT").unwrap(), - cstr(info.port().to_string()).unwrap(), - vars, - ); - } - - if let Some(info) = request.remote_socket() { - php_register_variable( - cstr("REMOTE_ADDR").unwrap(), - cstr(info.ip().to_string()).unwrap(), - vars, - ); - php_register_variable( - cstr("REMOTE_PORT").unwrap(), - cstr(info.port().to_string()).unwrap(), - vars, - ); - } - - if !req_info.request_method.is_null() { - php_register_variable( - cstr("REQUEST_METHOD").unwrap(), - req_info.request_method, - vars, - ); - } - - if !req_info.cookie_data.is_null() { - php_register_variable(cstr("HTTP_COOKIE").unwrap(), req_info.cookie_data, vars); - } - - if !req_info.query_string.is_null() { - php_register_variable(cstr("QUERY_STRING").unwrap(), req_info.query_string, vars); - } - }; -} - -#[no_mangle] -pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_int: c_int) { - let message = unsafe { CStr::from_ptr(message) }; - RequestContext::current().map(|ctx| { - ctx.response_builder().log_write(message.to_bytes()); - }); -} - -// -// PHP Module Functions -// - -#[php_function] -pub fn apache_request_headers() -> Result, String> { - let mut headers = HashMap::new(); - - let request = RequestContext::current() - .map(|ctx| ctx.request()) - .ok_or("Request context unavailable")?; - - for (key, value) in request.headers().iter() { - headers.insert(key.to_string(), value.into()); - } - - Ok(headers) -} - -#[php_module] -pub fn module(module: ModuleBuilder<'_>) -> ModuleBuilder<'_> { - module.function(wrap_function!(apache_request_headers)) -} - -// -// CString helpers -// - -fn default_cstr, V: Into>( - default: S, - maybe: Option, -) -> Result<*mut c_char, EmbedException> { - cstr(match maybe { - Some(v) => v.into(), - None => default.into(), - }) -} - -fn nullable_cstr>(maybe: Option) -> Result<*mut c_char, EmbedException> { - match maybe { - Some(v) => cstr(v.into()), - None => Ok(std::ptr::null_mut()), - } -} - -fn cstr>(s: S) -> Result<*mut c_char, EmbedException> { - CString::new(s.as_ref()) - .map_err(EmbedException::InvalidCString) - .map(|cstr| cstr.into_raw()) -} - -fn str_from_cstr<'a>(ptr: *const c_char) -> Result<&'a str, EmbedException> { - unsafe { CStr::from_ptr(ptr) } - .to_str() - .map_err(EmbedException::InvalidStr) -} - -fn reclaim_str(ptr: *const i8) -> CString { - unsafe { CString::from_raw(ptr as *mut c_char) } -} - -fn drop_str(ptr: *const i8) { - if ptr.is_null() { - return; - } - drop(reclaim_str(ptr)); -} - -fn maybe_current_dir() -> Result { - std::env::current_dir() - .unwrap_or(std::path::PathBuf::from("/")) - .canonicalize() - .map_err(EmbedException::IoError) -} - -fn translate_path(docroot: D, request_uri: P) -> Result -where - D: AsRef, - P: AsRef, -{ - let docroot = docroot.as_ref().to_path_buf(); - let request_uri = request_uri.as_ref(); - let relative_uri = request_uri - .strip_prefix("/") - .map_err(EmbedException::RelativizeError)?; - - match docroot.join(relative_uri).join("index.php").canonicalize() { - Ok(path) => Ok(path), - Err(_) => docroot - .join(relative_uri) - .canonicalize() - .map_err(EmbedException::CanonicalizeError), - } -} diff --git a/crates/php/src/exception.rs b/crates/php/src/exception.rs new file mode 100644 index 00000000..fcdd17ba --- /dev/null +++ b/crates/php/src/exception.rs @@ -0,0 +1,49 @@ +use std::{ffi::NulError, path::StripPrefixError}; + +/// Set of exceptions which may be produced by php::Embed +#[derive(Debug)] +pub enum EmbedException { + DocRootNotFound(String), + SapiNotInitialized, + SapiStartupError, + SapiLockFailed, + SapiMissingStartupFunction, + ExeLocationError, + RequestStartupError, + RequestContextUnavailable, + InvalidCString(NulError), + InvalidStr(std::str::Utf8Error), + HeaderNotFound(String), + ExecuteError, + Exception(String), + Bailout, + ResponseError, + IoError(std::io::Error), + RelativizeError(StripPrefixError), + CanonicalizeError(std::io::Error), +} + +impl std::fmt::Display for EmbedException { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EmbedException::RequestContextUnavailable => write!(f, "Request context unavailable"), + EmbedException::SapiNotInitialized => write!(f, "SAPI has not been initialized"), + EmbedException::SapiLockFailed => write!(f, "Failed to acquire SAPI lock"), + EmbedException::SapiMissingStartupFunction => write!(f, "Missing SAPI startup function"), + EmbedException::ExeLocationError => write!(f, "Error getting executable location"), + EmbedException::DocRootNotFound(docroot) => write!(f, "Document root not found: {}", docroot), + EmbedException::SapiStartupError => write!(f, "SAPI startup error"), + EmbedException::RequestStartupError => write!(f, "Request startup error"), + EmbedException::InvalidCString(e) => write!(f, "CString conversion error: {}", e), + EmbedException::InvalidStr(e) => write!(f, "String conversion error: {}", e), + EmbedException::HeaderNotFound(header) => write!(f, "Header not found: {}", header), + EmbedException::ExecuteError => write!(f, "Script execution error"), + EmbedException::Exception(e) => write!(f, "Exception thrown: {}", e), + EmbedException::Bailout => write!(f, "PHP bailout"), + EmbedException::ResponseError => write!(f, "Error building response"), + EmbedException::IoError(e) => write!(f, "IO error: {}", e), + EmbedException::RelativizeError(e) => write!(f, "Path relativization error: {}", e), + EmbedException::CanonicalizeError(e) => write!(f, "Path canonicalization error: {}", e), + } + } +} diff --git a/crates/php/src/lib.rs b/crates/php/src/lib.rs index 8b32c6c4..3768b029 100644 --- a/crates/php/src/lib.rs +++ b/crates/php/src/lib.rs @@ -1,17 +1,17 @@ #![warn(rust_2018_idioms)] #![warn(clippy::dbg_macro, clippy::print_stdout)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(dead_code)] -#![allow(unused_imports)] -// TODO Because `bindgen` generates codes contains deref nullptr, temporary suppression. -#![allow(deref_nullptr)] -#![allow(clippy::all)] -// #![deny(clippy::all)] mod embed; +mod exception; +mod request_context; +mod sapi; +mod scopes; +mod strings; pub use lang_handler::{Handler, Header, Headers, Request, RequestBuilder, Response, Url}; pub use embed::Embed; +pub use exception::EmbedException; +pub use request_context::RequestContext; +pub(crate) use sapi::Sapi; +pub(crate) use scopes::RequestScope; diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs index 99aed0a6..6e60950e 100644 --- a/crates/php/src/main.rs +++ b/crates/php/src/main.rs @@ -2,7 +2,8 @@ use php::{Embed, Handler, Request}; pub fn main() { let docroot = std::env::current_dir().expect("should have current_dir"); - let embed = Embed::new_with_args(docroot, std::env::args()); + + let embed = Embed::new_with_args(docroot, std::env::args()).expect("should construct embed"); let request = Request::builder() .method("POST") @@ -11,13 +12,14 @@ pub fn main() { .header("Content-Type", "text/html") .header("Content-Length", 13.to_string()) .body("Hello, World!") - .build(); + .build() + .expect("should build request"); println!("request: {:#?}", request); let response = embed .handle(request.clone()) - .expect("failed to handle request"); + .expect("should handle request"); println!("response: {:#?}", response); } diff --git a/crates/php/src/request_context.rs b/crates/php/src/request_context.rs new file mode 100644 index 00000000..031c9b5b --- /dev/null +++ b/crates/php/src/request_context.rs @@ -0,0 +1,154 @@ +use ext_php_rs::zend::SapiGlobals; +use lang_handler::{Request, ResponseBuilder}; +use std::ffi::c_void; + +/// The request context for the PHP SAPI. +#[derive(Debug)] +pub struct RequestContext { + request: Request, + response_builder: ResponseBuilder, +} + +impl RequestContext { + /// Sets the current request context for the PHP SAPI. + /// + /// # Examples + /// + /// ``` + /// use php::{Request, RequestContext}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("should parse url") + /// .build() + /// .expect("should build request"); + /// + /// RequestContext::for_request(request); + /// + /// let context = RequestContext::current() + /// .expect("should acquire current context"); + /// + /// assert_eq!(context.request().method(), "GET"); + /// ``` + pub fn for_request(request: Request) { + let context = Box::new(RequestContext { + request, + response_builder: ResponseBuilder::new(), + }); + let mut globals = SapiGlobals::get_mut(); + globals.server_context = Box::into_raw(context) as *mut c_void; + } + + /// Retrieve a mutable reference to the request context + /// + /// # Examples + /// + /// ``` + /// use php::{Request, RequestContext}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("should parse url") + /// .build() + /// .expect("should build request"); + /// + /// RequestContext::for_request(request); + /// + /// let current_context = RequestContext::current() + /// .expect("should acquire current context"); + /// + /// assert_eq!(current_context.request().method(), "GET"); + /// ``` + pub fn current<'a>() -> Option<&'a mut RequestContext> { + let ptr = { + let globals = SapiGlobals::get(); + globals.server_context as *mut RequestContext + }; + if ptr.is_null() { + return None; + } + + Some(unsafe { &mut *ptr }) + } + + /// Reclaim ownership of the RequestContext. Useful for dropping. + /// + /// # Example + /// + /// ``` + /// use ext_php_rs::zend::SapiGlobals; + /// use php::{Request, RequestContext}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("should parse url") + /// .build() + /// .expect("should build request"); + /// + /// RequestContext::for_request(request); + /// + /// RequestContext::reclaim() + /// .expect("should acquire current context"); + /// + /// assert_eq!(SapiGlobals::get().server_context, std::ptr::null_mut()); + /// ``` + #[allow(dead_code)] + pub fn reclaim() -> Option> { + let ptr = { + let mut globals = SapiGlobals::get_mut(); + std::mem::replace(&mut globals.server_context, std::ptr::null_mut()) + }; + if ptr.is_null() { + return None; + } + Some(unsafe { Box::from_raw(ptr as *mut RequestContext) }) + } + + /// Returns a reference to the request. + /// + /// # Examples + /// + /// ``` + /// use php::{Request, RequestContext}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("should parse url") + /// .build() + /// .expect("should build request"); + /// + /// RequestContext::for_request(request); + /// + /// let context = RequestContext::current() + /// .expect("should acquire current context"); + /// + /// assert_eq!(context.request().method(), "GET"); + /// ``` + pub fn request(&self) -> &Request { + &self.request + } + + /// Returns a mutable reference to the response builder. + /// + /// # Examples + /// + /// ``` + /// use php::{Request, RequestContext}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("should parse url") + /// .build() + /// .expect("should build request"); + /// + /// RequestContext::for_request(request); + /// + /// let mut context = RequestContext::current() + /// .expect("should acquire current context"); + /// + /// context.response_builder().status(200); + /// ``` + pub fn response_builder(&mut self) -> &mut ResponseBuilder { + &mut self.response_builder + } +} diff --git a/crates/php/src/sapi.rs b/crates/php/src/sapi.rs new file mode 100644 index 00000000..f5d9ff39 --- /dev/null +++ b/crates/php/src/sapi.rs @@ -0,0 +1,417 @@ +use std::{ + collections::HashMap, + env::current_exe, + ffi::{c_char, c_int, c_void, CStr}, + sync::RwLock, +}; + +use bytes::Buf; + +use ext_php_rs::{ + builders::SapiBuilder, + embed::SapiModule, + exception::register_error_observer, + ffi::{ + ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup, php_module_shutdown, php_module_startup, + php_register_variable, sapi_send_headers, sapi_shutdown, sapi_startup, + ZEND_RESULT_CODE_SUCCESS, + }, + prelude::*, + zend::{SapiGlobals, SapiHeader}, +}; + +use once_cell::sync::OnceCell; + +use crate::{ + strings::{cstr, maybe_current_dir}, + EmbedException, RequestContext, +}; +use lang_handler::Header; + +// This is a helper to ensure that PHP is initialized and deinitialized at the +// appropriate times. +pub(crate) struct Sapi(Box); + +impl Sapi { + pub fn new(argv: Vec) -> Result + 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::>(); + + let mut sapi = SapiBuilder::new("php_lang_handler", "PHP Lang Handler") + .startup_function(sapi_module_startup) + // .shutdown_function(sapi_module_shutdown) + // .activate_function(sapi_module_activate) + .deactivate_function(sapi_module_deactivate) + .ub_write_function(sapi_module_ub_write) + .flush_function(sapi_module_flush) + .send_header_function(sapi_module_send_header) + .read_post_function(sapi_module_read_post) + .read_cookies_function(sapi_module_read_cookies) + .register_server_variables_function(sapi_module_register_server_variables) + .log_message_function(sapi_module_log_message) + // .executable_location(args.get(0)) + .build() + .map_err(|_| EmbedException::SapiNotInitialized)?; + + sapi.ini_defaults = Some(sapi_cli_ini_defaults); + sapi.php_ini_path_override = std::ptr::null_mut(); + sapi.php_ini_ignore_cwd = 1; + sapi.additional_functions = std::ptr::null(); + // sapi.phpinfo_as_text = 1; + + let exe_loc = argv + .first() + .map(|s| s.to_string()) + .or_else(|| current_exe().ok().map(|p| p.display().to_string())) + .ok_or(EmbedException::ExeLocationError)?; + + sapi.executable_location = cstr(exe_loc)?; + let mut boxed = Box::new(sapi); + + unsafe { + ext_php_rs_sapi_startup(); + + sapi_startup(boxed.as_mut()); + php_module_startup(boxed.as_mut(), get_module()); + } + + // TODO: Should maybe capture this to store in EmbedException rather than + // writing to the ResponseBuilder here. When php_execute_script fails it + // should read that and could return an error or write it to the + // ResponseBuilder there. + register_error_observer(|_error_type, _file, _line, message| { + message + .as_str() + .inspect(|msg| { + if let Some(ctx) = RequestContext::current() { + ctx.response_builder().exception(*msg); + } + }) + .ok(); + }); + + Ok(Sapi(boxed)) + } + + fn do_startup(&mut self) -> Result<(), EmbedException> { + let sapi = self.0.as_mut(); + let startup = sapi + .startup + .ok_or(EmbedException::SapiMissingStartupFunction)?; + if unsafe { startup(sapi) } != ZEND_RESULT_CODE_SUCCESS { + return Err(EmbedException::SapiStartupError); + } + Ok(()) + } + + pub fn startup() -> Result<(), EmbedException> { + SAPI_INIT + .get() + .ok_or(EmbedException::SapiNotInitialized) + .and_then(|rwlock| { + let mut sapi = rwlock.write().map_err(|_| EmbedException::SapiLockFailed)?; + + sapi.do_startup()?; + Ok(()) + }) + } +} + +impl Drop for Sapi { + fn drop(&mut self) { + unsafe { + php_module_shutdown(); + sapi_shutdown(); + + ext_php_rs_sapi_shutdown(); + } + } +} + +pub(crate) static SAPI_INIT: OnceCell> = OnceCell::new(); + +pub fn ensure_sapi(argv: Vec) -> Result<&'static RwLock, EmbedException> +where + S: AsRef + std::fmt::Debug, +{ + SAPI_INIT.get_or_try_init(|| Sapi::new(argv).map(RwLock::new)) +} + +// +// Sapi functions +// + +// error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR +static HARDCODED_INI: &str = " + error_reporting=4343 + ignore_repeated_errors=1 + display_errors=0 + display_startup_errors=0 + register_argc_argv=1 + log_errors=1 + implicit_flush=0 + memory_limit=128M + output_buffering=0 + enable_post_data_reading=1 + html_errors=0 + max_execution_time=0 + max_input_time=-1 +"; + +#[no_mangle] +pub extern "C" fn sapi_cli_ini_defaults(configuration_hash: *mut ext_php_rs::types::ZendHashTable) { + let hash = unsafe { &mut *configuration_hash }; + + let config = str::trim(HARDCODED_INI).lines().map(str::trim); + + for line in config { + if let Some((key, value)) = line.split_once('=') { + // TODO: Capture error somehow? + hash.insert(key, value).ok(); + } + } +} + +#[no_mangle] +pub extern "C" fn sapi_module_startup( + sapi_module: *mut SapiModule, +) -> ext_php_rs::ffi::zend_result { + unsafe { php_module_startup(sapi_module, get_module()) } +} + +#[no_mangle] +pub extern "C" fn sapi_module_deactivate() -> c_int { + { + let mut globals = SapiGlobals::get_mut(); + + globals.server_context = std::ptr::null_mut(); + globals.request_info.argc = 0; + globals.request_info.argv = std::ptr::null_mut(); + + // drop_str(globals.request_info.request_method); + // drop_str(globals.request_info.query_string); + // drop_str(globals.request_info.request_uri); + // drop_str(globals.request_info.path_translated); + // drop_str(globals.request_info.content_type); + // drop_str(globals.request_info.cookie_data); + // drop_str(globals.request_info.php_self); + // drop_str(globals.request_info.auth_user); + // drop_str(globals.request_info.auth_password); + // drop_str(globals.request_info.auth_digest); + } + + // TODO: When _is_ it safe to reclaim the request context? + // RequestContext::reclaim(); + + 0 +} + +#[no_mangle] +pub extern "C" fn sapi_module_ub_write(str: *const i8, str_length: usize) -> usize { + if str.is_null() || str_length == 0 { + return 0; + } + let bytes = unsafe { std::slice::from_raw_parts(str as *const u8, str_length) }; + let len = bytes.len(); + if let Some(ctx) = RequestContext::current() { + ctx.response_builder().body_write(bytes); + } + len +} + +#[no_mangle] +pub extern "C" fn sapi_module_flush(_server_context: *mut c_void) { + if let Some(ctx) = RequestContext::current() { + unsafe { sapi_send_headers() }; + let mut globals = SapiGlobals::get_mut(); + globals.headers_sent = 1; + ctx + .response_builder() + .status(globals.sapi_headers.http_response_code); + } +} + +#[no_mangle] +pub extern "C" fn sapi_module_send_header(header: *mut SapiHeader, _server_context: *mut c_void) { + // Not sure _why_ this is necessary, but it is. + if header.is_null() { + return; + } + + let header = unsafe { &*header }; + let name = header.name(); + + // Header value is None for http version + status line + if let Some(value) = header.value() { + if let Some(ctx) = RequestContext::current() { + ctx.response_builder().header(name, value); + } + } +} + +#[no_mangle] +pub extern "C" fn sapi_module_read_post(buffer: *mut c_char, length: usize) -> usize { + if length == 0 { + return 0; + } + + RequestContext::current() + .map(|ctx| ctx.request().body()) + .map(|body| { + let length = length.min(body.len()); + if length == 0 { + return 0; + } + + let chunk = body.take(length); + + unsafe { + std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut c_char, buffer, length); + } + length + }) + .unwrap_or(0) +} + +#[no_mangle] +pub extern "C" fn sapi_module_read_cookies() -> *mut c_char { + SapiGlobals::get().request_info.cookie_data +} + +#[no_mangle] +pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::types::Zval) { + unsafe { + // use ext_php_rs::ffi::php_import_environment_variables; + // if let Some(f) = php_import_environment_variables { + // f(vars); + // } + + RequestContext::current() + .map(|ctx| ctx.request()) + // Convert to a result so we can use and_then with ? syntax... + .ok_or(EmbedException::RequestContextUnavailable) + .and_then(|request| { + let headers = request.headers(); + + for (key, values) in headers.iter() { + let maybe_header = match values { + Header::Single(header) => Some(header), + Header::Multiple(headers) => headers.first(), + }; + + if let Some(header) = maybe_header { + let cgi_key = format!("HTTP_{}", key.to_ascii_uppercase().replace("-", "_")); + php_register_variable(cstr(&cgi_key)?, cstr(header)?, vars); + } + } + + let globals = SapiGlobals::get(); + let req_info = &globals.request_info; + + let cwd = maybe_current_dir()?; + let cwd_cstr = cstr(cwd.display().to_string())?; + + let script_filename = req_info.path_translated; + let script_name = if !req_info.request_uri.is_null() { + req_info.request_uri + } else { + c"".as_ptr() + }; + + php_register_variable(cstr("REQUEST_SCHEME")?, cstr(request.url().scheme())?, vars); + php_register_variable(cstr("CONTEXT_PREFIX")?, cstr("")?, vars); + php_register_variable(cstr("SERVER_ADMIN")?, cstr("webmaster@localhost")?, vars); + php_register_variable(cstr("GATEWAY_INTERFACE")?, cstr("CGI/1.1")?, vars); + + php_register_variable(cstr("PHP_SELF")?, script_name, vars); + php_register_variable(cstr("SCRIPT_NAME")?, script_name, vars); + php_register_variable(cstr("SCRIPT_FILENAME")?, script_filename, vars); + php_register_variable(cstr("PATH_TRANSLATED")?, script_filename, vars); + php_register_variable(cstr("DOCUMENT_ROOT")?, cwd_cstr, vars); + php_register_variable(cstr("CONTEXT_DOCUMENT_ROOT")?, cwd_cstr, vars); + + if !req_info.request_uri.is_null() { + php_register_variable(cstr("REQUEST_URI")?, req_info.request_uri, vars); + } + + php_register_variable(cstr("SERVER_PROTOCOL")?, cstr("HTTP/1.1")?, vars); + + let sapi = SAPI_INIT.get().ok_or(EmbedException::SapiNotInitialized)?; + + php_register_variable( + cstr("SERVER_SOFTWARE")?, + sapi + .read() + .map_err(|_| EmbedException::SapiLockFailed)? + .0 + .name, + vars, + ); + + if let Some(info) = request.local_socket() { + php_register_variable(cstr("SERVER_ADDR")?, cstr(info.ip().to_string())?, vars); + php_register_variable(cstr("SERVER_PORT")?, cstr(info.port().to_string())?, vars); + } + + if let Some(info) = request.remote_socket() { + php_register_variable(cstr("REMOTE_ADDR")?, cstr(info.ip().to_string())?, vars); + php_register_variable(cstr("REMOTE_PORT")?, cstr(info.port().to_string())?, vars); + } + + if !req_info.request_method.is_null() { + php_register_variable(cstr("REQUEST_METHOD")?, req_info.request_method, vars); + } + + if !req_info.cookie_data.is_null() { + php_register_variable(cstr("HTTP_COOKIE")?, req_info.cookie_data, vars); + } + + if !req_info.query_string.is_null() { + php_register_variable(cstr("QUERY_STRING")?, req_info.query_string, vars); + } + + Ok(()) + }) + // TODO: Capture errors somehow so we can surface them... + .ok(); + }; +} + +#[no_mangle] +pub extern "C" fn sapi_module_log_message(message: *const c_char, _syslog_type_int: c_int) { + let message = unsafe { CStr::from_ptr(message) }; + if let Some(ctx) = RequestContext::current() { + ctx.response_builder().log_write(message.to_bytes()); + } +} + +// +// PHP Module +// + +#[php_function] +pub fn apache_request_headers() -> Result, String> { + let mut headers = HashMap::new(); + + let request = RequestContext::current() + .map(|ctx| ctx.request()) + .ok_or("Request context unavailable")?; + + for (key, value) in request.headers().iter() { + headers.insert(key.to_string(), value.into()); + } + + Ok(headers) +} + +#[php_module] +pub fn module(module: ModuleBuilder<'_>) -> ModuleBuilder<'_> { + module.function(wrap_function!(apache_request_headers)) +} diff --git a/crates/php/src/scopes.rs b/crates/php/src/scopes.rs new file mode 100644 index 00000000..a0b6ea6b --- /dev/null +++ b/crates/php/src/scopes.rs @@ -0,0 +1,28 @@ +use ext_php_rs::ffi::{php_request_shutdown, php_request_startup, ZEND_RESULT_CODE_SUCCESS}; +use std::ffi::c_void; + +use crate::EmbedException; + +/// A scope in which php request activity may occur. This is responsible for +/// starting up and shutting down the php request and cleaning up associated +/// data. +pub(crate) struct RequestScope(); + +impl RequestScope { + /// Starts a new request scope in which a PHP request may operate. + pub fn new() -> Result { + if unsafe { php_request_startup() } != ZEND_RESULT_CODE_SUCCESS { + return Err(EmbedException::RequestStartupError); + } + + Ok(RequestScope()) + } +} + +impl Drop for RequestScope { + fn drop(&mut self) { + unsafe { + php_request_shutdown(std::ptr::null_mut::()); + } + } +} diff --git a/crates/php/src/strings.rs b/crates/php/src/strings.rs new file mode 100644 index 00000000..d2a809e6 --- /dev/null +++ b/crates/php/src/strings.rs @@ -0,0 +1,79 @@ +use std::{ + env::current_dir, + ffi::{c_char, CStr, CString}, + path::{Path, PathBuf}, +}; + +use crate::EmbedException; + +#[allow(dead_code)] +pub(crate) fn default_cstr, V: Into>( + default: S, + maybe: Option, +) -> Result<*mut c_char, EmbedException> { + cstr(match maybe { + Some(v) => v.into(), + None => default.into(), + }) +} + +pub(crate) fn nullable_cstr>( + maybe: Option, +) -> Result<*mut c_char, EmbedException> { + match maybe { + Some(v) => cstr(v.into()), + None => Ok(std::ptr::null_mut()), + } +} + +pub(crate) fn cstr>(s: S) -> Result<*mut c_char, EmbedException> { + CString::new(s.as_ref()) + .map_err(EmbedException::InvalidCString) + .map(|cstr| cstr.into_raw()) +} + +pub(crate) fn str_from_cstr<'a>(ptr: *const c_char) -> Result<&'a str, EmbedException> { + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(EmbedException::InvalidStr) +} + +#[allow(dead_code)] +pub(crate) fn reclaim_str(ptr: *const i8) -> CString { + unsafe { CString::from_raw(ptr as *mut c_char) } +} + +#[allow(dead_code)] +pub(crate) fn drop_str(ptr: *const i8) { + if ptr.is_null() { + return; + } + drop(reclaim_str(ptr)); +} + +pub(crate) fn maybe_current_dir() -> Result { + current_dir() + .unwrap_or(PathBuf::from("/")) + .canonicalize() + .map_err(EmbedException::IoError) +} + +pub(crate) fn translate_path(docroot: D, request_uri: P) -> Result +where + D: AsRef, + P: AsRef, +{ + let docroot = docroot.as_ref().to_path_buf(); + let request_uri = request_uri.as_ref(); + let relative_uri = request_uri + .strip_prefix("/") + .map_err(EmbedException::RelativizeError)?; + + match docroot.join(relative_uri).join("index.php").canonicalize() { + Ok(path) => Ok(path), + Err(_) => docroot + .join(relative_uri) + .canonicalize() + .map_err(EmbedException::CanonicalizeError), + } +} diff --git a/crates/php_node/src/request.rs b/crates/php_node/src/request.rs index d78f04f7..3a9c19c4 100644 --- a/crates/php_node/src/request.rs +++ b/crates/php_node/src/request.rs @@ -111,7 +111,9 @@ impl PhpRequest { } Ok(PhpRequest { - request: builder.build(), + request: builder + .build() + .map_err(|err| Error::from_reason(err.to_string()))?, }) } diff --git a/crates/php_node/src/runtime.rs b/crates/php_node/src/runtime.rs index ab2024a8..6eb424a2 100644 --- a/crates/php_node/src/runtime.rs +++ b/crates/php_node/src/runtime.rs @@ -51,15 +51,16 @@ impl PhpRuntime { /// }); /// ``` #[napi(constructor)] - pub fn new(options: PhpOptions) -> Self { + pub fn new(options: PhpOptions) -> Result { let docroot = options.docroot.clone(); let argv = options.argv.clone(); - let embed = Embed::new_with_argv(docroot, argv); + let embed = + Embed::new_with_argv(docroot, argv).map_err(|err| Error::from_reason(err.to_string()))?; - Self { + Ok(Self { embed: Arc::new(embed), - } + }) } /// Handle a PHP request. diff --git a/index.d.ts b/index.d.ts index cbb7f8e7..401e4b95 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,11 +7,11 @@ export interface PhpRequestSocketOptions { /** The string representation of the local IP address the remote client is connecting on. */ localAddress: string /** The numeric representation of the local port. For example, 80 or 21. */ - localPort: string + localPort: number /** The string representation of the remote IP address. */ remoteAddress: string /** The numeric representation of the remote port. For example, 80 or 21. */ - remotePort: string + remotePort: number } /** Options for creating a new PHP request. */ export interface PhpRequestOptions { From e8b3548c2ec24aad0b36c23569e997cdd54675ff Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 23 May 2025 22:35:37 +0800 Subject: [PATCH 2/3] Improve exceptions --- crates/php/Cargo.toml | 1 + crates/php/src/embed.rs | 2 +- crates/php/src/exception.rs | 44 ++++++++++++++++++------------------- crates/php/src/sapi.rs | 10 +++++++-- crates/php/src/scopes.rs | 2 +- crates/php/src/strings.rs | 34 +++++++++++++++++----------- 6 files changed, 53 insertions(+), 40 deletions(-) diff --git a/crates/php/Cargo.toml b/crates/php/Cargo.toml index 0d1456a2..57c85036 100644 --- a/crates/php/Cargo.toml +++ b/crates/php/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] bytes = "1.10.1" +hostname = "0.4.1" lang_handler = { path = "../lang_handler", features = ["c"] } libc = "0.2.171" once_cell = "1.21.0" diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index 7a2958b9..7f5d1367 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -285,7 +285,7 @@ impl Handler for Embed { .status(http_response_code) .header("Content-Type", mime) }) - .ok_or(EmbedException::ResponseError)? + .ok_or(EmbedException::ResponseBuildError)? }; Ok(response_builder.build()) diff --git a/crates/php/src/exception.rs b/crates/php/src/exception.rs index fcdd17ba..313fdb20 100644 --- a/crates/php/src/exception.rs +++ b/crates/php/src/exception.rs @@ -1,49 +1,47 @@ -use std::{ffi::NulError, path::StripPrefixError}; - /// Set of exceptions which may be produced by php::Embed #[derive(Debug)] pub enum EmbedException { DocRootNotFound(String), SapiNotInitialized, - SapiStartupError, + SapiNotStarted, SapiLockFailed, SapiMissingStartupFunction, - ExeLocationError, - RequestStartupError, + FailedToFindExeLocation, + SapiRequestNotStarted, RequestContextUnavailable, - InvalidCString(NulError), - InvalidStr(std::str::Utf8Error), + CStringEncodeFailed(String), + CStringDecodeFailed(usize), HeaderNotFound(String), - ExecuteError, + // ExecuteError, Exception(String), Bailout, - ResponseError, - IoError(std::io::Error), - RelativizeError(StripPrefixError), - CanonicalizeError(std::io::Error), + ResponseBuildError, + FailedToFindCurrentDirectory, + ExpectedAbsoluteRequestUri(String), + ScriptNotFound(String), } impl std::fmt::Display for EmbedException { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EmbedException::RequestContextUnavailable => write!(f, "Request context unavailable"), - EmbedException::SapiNotInitialized => write!(f, "SAPI has not been initialized"), + EmbedException::SapiNotInitialized => write!(f, "Failed to initialize SAPI"), EmbedException::SapiLockFailed => write!(f, "Failed to acquire SAPI lock"), EmbedException::SapiMissingStartupFunction => write!(f, "Missing SAPI startup function"), - EmbedException::ExeLocationError => write!(f, "Error getting executable location"), + EmbedException::FailedToFindExeLocation => write!(f, "Failed to identify executable location"), EmbedException::DocRootNotFound(docroot) => write!(f, "Document root not found: {}", docroot), - EmbedException::SapiStartupError => write!(f, "SAPI startup error"), - EmbedException::RequestStartupError => write!(f, "Request startup error"), - EmbedException::InvalidCString(e) => write!(f, "CString conversion error: {}", e), - EmbedException::InvalidStr(e) => write!(f, "String conversion error: {}", e), + EmbedException::SapiNotStarted => write!(f, "Failed to start SAPI"), + EmbedException::SapiRequestNotStarted => write!(f, "Failed to start SAPI request"), + EmbedException::CStringEncodeFailed(e) => write!(f, "Failed to encode to cstring: \"{}\"", e), + EmbedException::CStringDecodeFailed(e) => write!(f, "Failed to decode from cstring: {}", e), EmbedException::HeaderNotFound(header) => write!(f, "Header not found: {}", header), - EmbedException::ExecuteError => write!(f, "Script execution error"), + // EmbedException::ExecuteError => write!(f, "Script execution error"), EmbedException::Exception(e) => write!(f, "Exception thrown: {}", e), EmbedException::Bailout => write!(f, "PHP bailout"), - EmbedException::ResponseError => write!(f, "Error building response"), - EmbedException::IoError(e) => write!(f, "IO error: {}", e), - EmbedException::RelativizeError(e) => write!(f, "Path relativization error: {}", e), - EmbedException::CanonicalizeError(e) => write!(f, "Path canonicalization error: {}", e), + EmbedException::ResponseBuildError => write!(f, "Failed to build response"), + EmbedException::FailedToFindCurrentDirectory => write!(f, "Failed to identify current directory"), + EmbedException::ExpectedAbsoluteRequestUri(e) => write!(f, "Expected absolute REQUEST_URI: {}", e), + EmbedException::ScriptNotFound(e) => write!(f, "Script not found: {}", e), } } } diff --git a/crates/php/src/sapi.rs b/crates/php/src/sapi.rs index f5d9ff39..e64f9728 100644 --- a/crates/php/src/sapi.rs +++ b/crates/php/src/sapi.rs @@ -70,7 +70,7 @@ impl Sapi { .first() .map(|s| s.to_string()) .or_else(|| current_exe().ok().map(|p| p.display().to_string())) - .ok_or(EmbedException::ExeLocationError)?; + .ok_or(EmbedException::FailedToFindExeLocation)?; sapi.executable_location = cstr(exe_loc)?; let mut boxed = Box::new(sapi); @@ -106,7 +106,7 @@ impl Sapi { .startup .ok_or(EmbedException::SapiMissingStartupFunction)?; if unsafe { startup(sapi) } != ZEND_RESULT_CODE_SUCCESS { - return Err(EmbedException::SapiStartupError); + return Err(EmbedException::SapiNotStarted); } Ok(()) } @@ -337,6 +337,12 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t php_register_variable(cstr("DOCUMENT_ROOT")?, cwd_cstr, vars); php_register_variable(cstr("CONTEXT_DOCUMENT_ROOT")?, cwd_cstr, vars); + if let Ok(server_name) = hostname::get() { + if let Some(server_name) = server_name.to_str() { + php_register_variable(cstr("SERVER_NAME")?, cstr(server_name)?, vars); + } + } + if !req_info.request_uri.is_null() { php_register_variable(cstr("REQUEST_URI")?, req_info.request_uri, vars); } diff --git a/crates/php/src/scopes.rs b/crates/php/src/scopes.rs index a0b6ea6b..08548a4d 100644 --- a/crates/php/src/scopes.rs +++ b/crates/php/src/scopes.rs @@ -12,7 +12,7 @@ impl RequestScope { /// Starts a new request scope in which a PHP request may operate. pub fn new() -> Result { if unsafe { php_request_startup() } != ZEND_RESULT_CODE_SUCCESS { - return Err(EmbedException::RequestStartupError); + return Err(EmbedException::SapiRequestNotStarted); } Ok(RequestScope()) diff --git a/crates/php/src/strings.rs b/crates/php/src/strings.rs index d2a809e6..b81749a9 100644 --- a/crates/php/src/strings.rs +++ b/crates/php/src/strings.rs @@ -28,14 +28,14 @@ pub(crate) fn nullable_cstr>( pub(crate) fn cstr>(s: S) -> Result<*mut c_char, EmbedException> { CString::new(s.as_ref()) - .map_err(EmbedException::InvalidCString) + .map_err(|_| EmbedException::CStringEncodeFailed(s.as_ref().to_owned())) .map(|cstr| cstr.into_raw()) } pub(crate) fn str_from_cstr<'a>(ptr: *const c_char) -> Result<&'a str, EmbedException> { unsafe { CStr::from_ptr(ptr) } .to_str() - .map_err(EmbedException::InvalidStr) + .map_err(|_| EmbedException::CStringDecodeFailed(ptr.addr())) } #[allow(dead_code)] @@ -53,9 +53,8 @@ pub(crate) fn drop_str(ptr: *const i8) { pub(crate) fn maybe_current_dir() -> Result { current_dir() - .unwrap_or(PathBuf::from("/")) - .canonicalize() - .map_err(EmbedException::IoError) + .and_then(|dir| dir.canonicalize()) + .or(Err(EmbedException::FailedToFindCurrentDirectory)) } pub(crate) fn translate_path(docroot: D, request_uri: P) -> Result @@ -67,13 +66,22 @@ where let request_uri = request_uri.as_ref(); let relative_uri = request_uri .strip_prefix("/") - .map_err(EmbedException::RelativizeError)?; + .map_err(|_| { + let uri = request_uri.display().to_string(); + EmbedException::ExpectedAbsoluteRequestUri(uri) + })?; - match docroot.join(relative_uri).join("index.php").canonicalize() { - Ok(path) => Ok(path), - Err(_) => docroot - .join(relative_uri) - .canonicalize() - .map_err(EmbedException::CanonicalizeError), - } + let exact = docroot + .join(relative_uri); + + exact + .join("index.php") + .canonicalize() + .or_else(|_| { + exact + .canonicalize() + .map_err(|_| { + EmbedException::ScriptNotFound(exact.display().to_string()) + }) + }) } From 0b14d3c48455b8475fb9ce123a5fb6162a0c36d8 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 23 May 2025 22:43:35 +0800 Subject: [PATCH 3/3] Skip cargo test on Linux for now... Because the Linux build happens in a docker task, the PHP headers are not available to build ext-php-rs, which is required to do the cargo test. --- .github/workflows/CI.yml | 1 + crates/php/src/exception.rs | 12 +++++++++--- crates/php/src/strings.rs | 28 ++++++++++------------------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4fc5f3c0..577c260e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -251,6 +251,7 @@ jobs: # TODO: This should be moved to the test jobs, but needs build deps # for ext-php-rs to build correctly, including PHP cli - name: Test crates + if: ${{ contains(matrix.target, 'linux') }} shell: bash run: cargo test - name: Upload target-specific package for ${{ matrix.settings.target }} diff --git a/crates/php/src/exception.rs b/crates/php/src/exception.rs index 313fdb20..59e4e626 100644 --- a/crates/php/src/exception.rs +++ b/crates/php/src/exception.rs @@ -28,7 +28,9 @@ impl std::fmt::Display for EmbedException { EmbedException::SapiNotInitialized => write!(f, "Failed to initialize SAPI"), EmbedException::SapiLockFailed => write!(f, "Failed to acquire SAPI lock"), EmbedException::SapiMissingStartupFunction => write!(f, "Missing SAPI startup function"), - EmbedException::FailedToFindExeLocation => write!(f, "Failed to identify executable location"), + EmbedException::FailedToFindExeLocation => { + write!(f, "Failed to identify executable location") + } EmbedException::DocRootNotFound(docroot) => write!(f, "Document root not found: {}", docroot), EmbedException::SapiNotStarted => write!(f, "Failed to start SAPI"), EmbedException::SapiRequestNotStarted => write!(f, "Failed to start SAPI request"), @@ -39,8 +41,12 @@ impl std::fmt::Display for EmbedException { EmbedException::Exception(e) => write!(f, "Exception thrown: {}", e), EmbedException::Bailout => write!(f, "PHP bailout"), EmbedException::ResponseBuildError => write!(f, "Failed to build response"), - EmbedException::FailedToFindCurrentDirectory => write!(f, "Failed to identify current directory"), - EmbedException::ExpectedAbsoluteRequestUri(e) => write!(f, "Expected absolute REQUEST_URI: {}", e), + EmbedException::FailedToFindCurrentDirectory => { + write!(f, "Failed to identify current directory") + } + EmbedException::ExpectedAbsoluteRequestUri(e) => { + write!(f, "Expected absolute REQUEST_URI: {}", e) + } EmbedException::ScriptNotFound(e) => write!(f, "Script not found: {}", e), } } diff --git a/crates/php/src/strings.rs b/crates/php/src/strings.rs index b81749a9..5efac766 100644 --- a/crates/php/src/strings.rs +++ b/crates/php/src/strings.rs @@ -64,24 +64,16 @@ where { let docroot = docroot.as_ref().to_path_buf(); let request_uri = request_uri.as_ref(); - let relative_uri = request_uri - .strip_prefix("/") - .map_err(|_| { - let uri = request_uri.display().to_string(); - EmbedException::ExpectedAbsoluteRequestUri(uri) - })?; + let relative_uri = request_uri.strip_prefix("/").map_err(|_| { + let uri = request_uri.display().to_string(); + EmbedException::ExpectedAbsoluteRequestUri(uri) + })?; - let exact = docroot - .join(relative_uri); + let exact = docroot.join(relative_uri); - exact - .join("index.php") - .canonicalize() - .or_else(|_| { - exact - .canonicalize() - .map_err(|_| { - EmbedException::ScriptNotFound(exact.display().to_string()) - }) - }) + exact.join("index.php").canonicalize().or_else(|_| { + exact + .canonicalize() + .map_err(|_| EmbedException::ScriptNotFound(exact.display().to_string())) + }) }