diff --git a/.github/workflows/nginx.yaml b/.github/workflows/nginx.yaml index f38b663c..0ebba793 100644 --- a/.github/workflows/nginx.yaml +++ b/.github/workflows/nginx.yaml @@ -54,6 +54,7 @@ env: load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_shared_dict_module.so; + load_module ${{ github.workspace }}/nginx/objs/ngx_http_subrequest_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_upstream_custom_module.so; OPENSSL_VERSION: '3.0.16' diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 521487b6..1aa4c572 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -56,6 +56,11 @@ name = "shared_dict" path = "shared_dict.rs" crate-type = ["cdylib"] +[[example]] +name = "subrequest" +path = "subrequest.rs" +crate-type = ["cdylib"] + [features] default = ["export-modules", "ngx/vendored"] # Generate `ngx_modules` table with module exports diff --git a/examples/config b/examples/config index 6b763652..e6bd0562 100644 --- a/examples/config +++ b/examples/config @@ -47,6 +47,14 @@ if [ $HTTP = YES ]; then ngx_rust_module fi + if :; then + ngx_module_name=ngx_http_subrequest_module + ngx_module_libs= + ngx_rust_target_name=subrequest + + ngx_rust_module + fi + if :; then ngx_module_name=ngx_http_upstream_custom_module ngx_module_libs= diff --git a/examples/subrequest.rs b/examples/subrequest.rs new file mode 100644 index 00000000..7623f1a3 --- /dev/null +++ b/examples/subrequest.rs @@ -0,0 +1,281 @@ +use core::fmt::Display; + +use ngx::core::Status; +use ngx::http::subrequest::{SubRequestBuilder, SubRequestError}; +use ngx::http::{ + HTTPStatus, HttpModule, HttpModuleLocationConf, HttpPhase, HttpRequestHandler, + IntoHandlerStatus, Merge, MergeConfigError, Request, add_phase_handler, +}; +use ngx::{ngx_log_debug_http, ngx_log_error}; + +use nginx_sys::{ + NGX_CONF_TAKE1, NGX_ERROR, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_LOG_ERR, + ngx_command_t, ngx_conf_t, ngx_flag_t, ngx_http_complex_value_t, ngx_http_module_t, + ngx_http_request_t, ngx_http_send_response, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, +}; + +const NGX_CONF_UNSET_FLAG: ngx_flag_t = nginx_sys::NGX_CONF_UNSET as _; + +struct SampleHandler; + +enum SampleHandlerError { + ContextAllocation, + SubRequestCreation(String), + SubRequest(ngx_int_t), + Response(ngx_int_t), +} + +impl From> for SampleHandlerError +where + E: Display, +{ + fn from(e: SubRequestError) -> Self { + SampleHandlerError::SubRequestCreation(e.to_string()) + } +} + +impl Display for SampleHandlerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SampleHandlerError::ContextAllocation => { + write!(f, "context allocation failed") + } + SampleHandlerError::SubRequestCreation(e) => { + write!(f, "subrequest creation failed: {}", e) + } + SampleHandlerError::SubRequest(rc) => { + write!(f, "subrequest failed with return code: {}", rc) + } + SampleHandlerError::Response(rc) => { + write!(f, "response creation failed with return code: {}", rc) + } + } + } +} + +impl IntoHandlerStatus for SampleHandlerError { + fn into_handler_status(self, r: &Request) -> ngx_int_t { + ngx_log_error!(NGX_LOG_ERR, r.log(), "subrequest example: {self}"); + Status::NGX_ERROR.into() + } +} + +impl HttpRequestHandler for SampleHandler { + const PHASE: HttpPhase = HttpPhase::Access; + type Output = Result; + + fn handler(request: &mut Request) -> Self::Output { + let co = Module::location_conf(request).expect("module config is none"); + ngx_log_debug_http!(request, "subrequest module enabled: {}", co.enable); + + if co.enable != 1 { + return Ok(Status::NGX_DECLINED); + } + + let rptr: *mut ngx_http_request_t = request.as_mut(); + + match SRCtx::get(request) { + Some(ctx) => ctx.rc.map_or( + // `ctx` has been created but not filled yet - subrequest is still in progress + Ok(Status::NGX_AGAIN), + // `ctx` has been created and filled - subrequest is completed + |rc| { + let status = ctx.status.0; + let msg = format!("subrequest completed with HTTP status: {status}, rc: {rc}"); + ngx_log_debug_http!(request, "{msg}"); + + if status >= nginx_sys::NGX_HTTP_SPECIAL_RESPONSE as _ { + Ok(Status::from(ctx.status)) + } else if rc == nginx_sys::NGX_OK as _ && ctx.out.is_some() { + let outbuf = unsafe { &*ctx.out.unwrap().buf }; + let mut ct = ctx.ct; + let mut cv: ngx_http_complex_value_t = unsafe { core::mem::zeroed() }; + cv.value = ngx_str_t { + len: unsafe { outbuf.last.offset_from(outbuf.pos) } as _, + data: outbuf.pos as _, + }; + let resp_rc = unsafe { + ngx_http_send_response(rptr, status, &raw mut ct, &raw mut cv) + }; + if resp_rc == nginx_sys::NGX_OK as _ { + Ok(Status::from(ctx.status)) + } else { + Err(SampleHandlerError::Response(resp_rc)) + } + } else if rc == nginx_sys::NGX_OK as _ { + Ok(Status::from(ctx.status)) + } else if let Ok(http_status) = HTTPStatus::try_from(rc) { + Ok(Status::from(http_status)) + } else { + Err(SampleHandlerError::SubRequest(rc)) + } + }, + ), + None => { + if SRCtx::create(request).is_some() { + let uri: &str = co.uri.to_str().unwrap_or("/proxy"); + + SubRequestBuilder::new(request, uri)? + .args("arg1=val1&arg2=val2")? + .init(|sr| { + ngx_log_debug_http!( + sr, + "initializing subrequest with URI: {}", + sr.path() + ); + sr.add_header_in("X-SubRequest", "1").ok_or("cannot add header") + }) + .handler(sr_handler) + .in_memory() + .waited() + .build()?; + + Ok(Status::NGX_AGAIN) + } else { + Err(SampleHandlerError::ContextAllocation) + } + } + } + } +} + +struct SRCtx<'r> { + rc: Option, + status: HTTPStatus, + out: Option<&'r nginx_sys::ngx_chain_t>, + ct: ngx_str_t, +} + +impl SRCtx<'_> { + fn create(request: &mut Request) -> Option<&mut Self> { + let ctx_ref = unsafe { request.pool().allocate_with_cleanup(Self::default).ok()?.as_mut() }; + request.set_module_ctx(ctx_ref as *mut _ as _, Module::module()); + Some(ctx_ref) + } + + fn get(request: &Request) -> Option<&Self> { + request.get_module_ctx::(Module::module()) + } + + fn get_mut(request: &mut Request) -> Option<&mut Self> { + request.get_module_ctx_mut::(Module::module()) + } +} + +impl Default for SRCtx<'_> { + fn default() -> Self { + Self { rc: None, status: HTTPStatus(NGX_ERROR as _), out: None, ct: ngx_str_t::empty() } + } +} + +fn sr_handler(r: &mut Request, mut rc: ngx_int_t) -> ngx_int_t { + let newctx = SRCtx { + rc: Some(rc), + status: r.status(), + // SAFETY: `r.as_ref().out` is valid as long as the main request is not finalized, + // and the subrequest is always finalized before the main request. + out: core::ptr::NonNull::new(r.as_ref().out).map(|out| unsafe { out.as_ref() }), + ct: r.as_ref().headers_out.content_type, + }; + if let Some(ctx) = SRCtx::get_mut(r.main_mut()) { + *ctx = newctx; + } else { + ngx_log_error!(nginx_sys::NGX_LOG_ERR, r.log(), "subrequest: context not found"); + rc = NGX_ERROR as _; + } + rc +} + +static NGX_HTTP_SUBREQUEST_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { + preconfiguration: None, + postconfiguration: Some(Module::postconfiguration), + create_main_conf: None, + init_main_conf: None, + create_srv_conf: None, + merge_srv_conf: None, + create_loc_conf: Some(Module::create_loc_conf), + merge_loc_conf: Some(Module::merge_loc_conf), +}; + +#[cfg(feature = "export-modules")] +ngx::ngx_modules!(ngx_http_subrequest_module); + +#[used] +#[allow(non_upper_case_globals)] +#[cfg_attr(not(feature = "export-modules"), unsafe(no_mangle))] +pub static mut ngx_http_subrequest_module: ngx_module_t = ngx_module_t { + ctx: &raw const NGX_HTTP_SUBREQUEST_MODULE_CTX as _, + commands: unsafe { &raw mut NGX_HTTP_SUBREQUEST_COMMANDS[0] }, + type_: nginx_sys::NGX_HTTP_MODULE as _, + ..ngx_module_t::default() +}; + +struct Module; + +impl HttpModule for Module { + fn module() -> &'static ngx_module_t { + unsafe { &*::core::ptr::addr_of!(ngx_http_subrequest_module) } + } + + unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t { + // SAFETY: this function is called with non-NULL cf always + let cf = unsafe { &mut *cf }; + add_phase_handler::(cf) + .map_or(nginx_sys::NGX_ERROR as _, |_| nginx_sys::NGX_OK as _) + } +} + +#[derive(Debug)] +struct ModuleConfig { + enable: ngx_flag_t, + uri: ngx_str_t, +} + +impl Default for ModuleConfig { + fn default() -> Self { + Self { enable: NGX_CONF_UNSET_FLAG, uri: ngx_str_t::empty() } + } +} + +impl Merge for ModuleConfig { + fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> { + if self.enable == NGX_CONF_UNSET_FLAG { + if prev.enable != NGX_CONF_UNSET_FLAG { + self.enable = prev.enable; + } else { + self.enable = 0; + } + } + if self.uri.data.is_null() { + self.uri = prev.uri; + } + if self.enable == 1 && self.uri.data.is_null() { + self.uri = ngx::ngx_string!("/proxy"); + } + Ok(()) + } +} + +unsafe impl HttpModuleLocationConf for Module { + type LocationConf = ModuleConfig; +} + +static mut NGX_HTTP_SUBREQUEST_COMMANDS: [ngx_command_t; 3] = [ + ngx_command_t { + name: ngx::ngx_string!("subrequest"), + type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, + set: Some(nginx_sys::ngx_conf_set_flag_slot), + conf: NGX_HTTP_LOC_CONF_OFFSET, + offset: core::mem::offset_of!(ModuleConfig, enable), + post: core::ptr::null_mut(), + }, + ngx_command_t { + name: ngx::ngx_string!("subrequest_uri"), + type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, + set: Some(nginx_sys::ngx_conf_set_str_slot), + conf: NGX_HTTP_LOC_CONF_OFFSET, + offset: core::mem::offset_of!(ModuleConfig, uri), + post: core::ptr::null_mut(), + }, + ngx_command_t::empty(), +]; diff --git a/examples/t/subrequest.t b/examples/t/subrequest.t new file mode 100644 index 00000000..1be078e1 --- /dev/null +++ b/examples/t/subrequest.t @@ -0,0 +1,79 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc + +# Tests for ngx-rust example modules. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(2) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + subrequest on; + } + + location /non_existing { + subrequest on; + subrequest_uri /non_existing_upstream; + } + + location /proxy { + internal; + proxy_pass http://127.0.0.1:8081; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + set $reply 'Invalid'; + if ($http_x_subrequest) { + set $reply 'Hello from backend'; + } + return 200 $reply; + } + } +} + +EOF + +$t->write_file('index.html', ''); +$t->run(); + +like(http_get('/'), + qr/200 OK.*Hello from backend/s, + 'subrequest'); +like(http_get('/non_existing'), qr/404 Not Found/s, + 'subrequest to non-existing upstream'); diff --git a/src/core/pool.rs b/src/core/pool.rs index efb283e0..b710479f 100644 --- a/src/core/pool.rs +++ b/src/core/pool.rs @@ -5,7 +5,7 @@ use core::ptr::{self, NonNull}; use nginx_sys::{ NGX_ALIGNMENT, ngx_buf_t, ngx_create_temp_buf, ngx_palloc, ngx_pcalloc, ngx_pfree, - ngx_pmemalign, ngx_pnalloc, ngx_pool_cleanup_add, ngx_pool_t, + ngx_pmemalign, ngx_pnalloc, ngx_pool_cleanup_add, ngx_pool_cleanup_t, ngx_pool_t, }; use crate::allocator::{AllocError, Allocator, dangling_for_layout}; @@ -200,25 +200,37 @@ impl Pool { Some(MemoryBuffer::from_ngx_buf(buf)) } - /// Adds a cleanup handler for a value in the memory pool. + /// Allocates memory for a value and adds a cleanup handler to the memory pool. /// - /// Returns `Ok(())` if the cleanup handler is successfully added, or `Err(())` if the cleanup - /// handler cannot be added. + /// The value is created by calling the provided closure `f`. If allocation fails, + /// the closure is not called. + /// + /// Returns `Ok(NonNull)` if the allocation and cleanup handler addition are successful, + /// or `Err(AllocError)` if allocation fails. /// /// # Safety - /// This function is marked as unsafe because it involves raw pointer manipulation. - unsafe fn add_cleanup_for_value(&self, value: *mut T) -> Result<(), ()> { - let cln = unsafe { ngx_pool_cleanup_add(self.0.as_ptr(), 0) }; - if cln.is_null() { - return Err(()); - } - - unsafe { - (*cln).handler = Some(cleanup_type::); - (*cln).data = value as *mut c_void; + /// The returned pointer must not outlive the pool, must not be freed manually + /// (as it has a cleanup handler), and must not be accessed after the pool is destroyed. + /// In case of types with non-standard alignment, the returned pointer may not be properly + /// aligned. + pub unsafe fn allocate_with_cleanup T>( + &self, + f: F, + ) -> Result, AllocError> { + let cln = unsafe { + ngx_pool_cleanup_add(self.0.as_ptr(), mem::size_of::()).as_mut().ok_or(AllocError)? + }; + // 'data' may be NULL only if `T` is zero-sized. In that case, no real value is stored, + // so we can just use the cleanup structure itself as a placeholder. + // Note that zero-sized `T` may implement `Drop`, and this implementation will be + // called at cleanup time. + if cln.data.is_null() { + cln.data = ptr::from_mut(cln).cast(); } - - Ok(()) + cln.handler = Some(cleanup_type::); + // `data` points to the memory allocated for the value by `ngx_pool_cleanup_add()` + unsafe { ptr::write(cln.data as *mut T, f()) }; + NonNull::new(cln.data.cast()).ok_or(AllocError) } /// Allocates memory from the pool of the specified size. @@ -274,16 +286,66 @@ impl Pool { /// allocation or cleanup handler addition fails. pub fn allocate(&self, value: T) -> *mut T { unsafe { - let p = self.alloc(mem::size_of::()) as *mut T; - ptr::write(p, value); - if self.add_cleanup_for_value(p).is_err() { - ptr::drop_in_place(p); - return ptr::null_mut(); - }; - p + match self.allocate_with_cleanup(|| value) { + Err(_) => ptr::null_mut(), + Ok(mut ptr) => ptr.as_mut(), + } } } + /// Runs the cleanup handler for a value and removes it. + /// + /// Returns `Some(())` if the value was successfully removed, + /// or `None` if the value was not found. + /// + /// # Safety + /// The caller must ensure that `value` is a valid pointer to a value that has an + /// associated cleanup handler in the pool. + pub unsafe fn remove(&mut self, value: *const T) -> Option<()> { + // Comparing function pointers is generally unreliable, but in this specific + // case we can assume that the same function pointer was used when adding the cleanup + // handler. + #[allow(unpredictable_function_pointer_comparisons)] + self.remove_cleanup_if(|cln| { + cln.handler == Some(cleanup_type::) && core::ptr::addr_eq(cln.data, value) + }) + .map(|cln| { + unsafe { cln.handler.unwrap()(cln.data) }; + }) + } + + /// Internal method to find and remove a cleanup handler matching a predicate. + /// + /// Returns `Some(&ngx_pool_cleanup_t)` if a matching handler is found and removed, + /// or `None` if not found. + fn remove_cleanup_if( + &mut self, + predicate: impl Fn(&ngx_pool_cleanup_t) -> bool, + ) -> Option<&ngx_pool_cleanup_t> { + let mut top = ngx_pool_cleanup_t { + handler: None, + data: core::ptr::null_mut(), + next: unsafe { self.0.as_mut().cleanup }, + }; + + let mut prev = &mut top; + let top_ptr = prev as *const _; + + while let Some(cln) = unsafe { prev.next.as_mut() } { + if predicate(cln) { + if core::ptr::eq(prev, top_ptr) { + unsafe { self.0.as_mut().cleanup = cln.next }; + } else { + prev.next = cln.next; + } + return Some(cln); + } + prev = cln; + } + + None + } + /// Resizes a memory allocation in place if possible. /// /// If resizing is requested for the last allocation in the pool, it may be @@ -321,15 +383,13 @@ impl Pool { /// Cleanup handler for a specific type `T`. /// -/// This function is called when cleaning up a value of type `T` in an FFI context. +/// This function is used as a type-specific cleanup handler for the `Pool`. +/// The handler is added to the pool's cleanup chain when allocating a value of type `T` +/// with a cleanup. It is called when the pool is destroyed or when the value is removed, +/// and it drops the value of type `T`. /// /// # Safety -/// This function is marked as unsafe due to the raw pointer manipulation and the assumption that -/// `data` is a valid pointer to `T`. -/// -/// # Arguments -/// -/// * `data` - A raw pointer to the value of type `T` to be cleaned up. +/// `data` should be a valid, writable, and properly aligned pointer to `T`. unsafe extern "C" fn cleanup_type(data: *mut c_void) { unsafe { ptr::drop_in_place(data as *mut T); diff --git a/src/http/mod.rs b/src/http/mod.rs index 00c329a8..44f282f0 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -4,6 +4,9 @@ mod request; mod status; mod upstream; +/// HTTP subrequest builder and handler. +pub mod subrequest; + pub use conf::*; pub use module::*; pub use request::*; diff --git a/src/http/request.rs b/src/http/request.rs index 64d7e6e8..6cf18ab3 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -85,7 +85,8 @@ macro_rules! http_variable_get { /// in the `into_handler_status` method. /// /// There are predefined implementations for `ngx_int_t`, [`Status`], [`HTTPStatus`], -/// [`Option`] with value type implementing [`IntoHandlerStatus`]. +/// [`Option`] with value type implementing [`IntoHandlerStatus`], +/// and [`Result`] with value and error types implementing [`IntoHandlerStatus`]. pub trait IntoHandlerStatus where Self: Sized, @@ -104,6 +105,16 @@ where } } +impl IntoHandlerStatus for Result +where + T: IntoHandlerStatus, + E: IntoHandlerStatus, +{ + fn into_handler_status(self, r: &Request) -> ngx_int_t { + self.map_or_else(|err| err.into_handler_status(r), |val| val.into_handler_status(r)) + } +} + impl IntoHandlerStatus for ngx_int_t { #[inline] fn into_handler_status(self, _r: &Request) -> ngx_int_t { @@ -195,12 +206,44 @@ impl Request { unsafe { &mut *r.cast::() } } + /// Create a const [`Request`] from a const [`ngx_http_request_t`]. + /// + /// # Safety + /// + /// The caller has provided a valid non-null pointer to a valid `ngx_http_request_t` + /// which shares the same representation as `Request`. + pub unsafe fn from_const_ngx_http_request<'a>(r: *const ngx_http_request_t) -> &'a Request { + unsafe { &*r.cast::() } + } + /// Is this the main request (as opposed to a subrequest)? pub fn is_main(&self) -> bool { let main = self.0.main.cast(); core::ptr::eq(self, main) } + /// Get a mutable reference to the main request. + /// + /// If this is already the main request, returns `self`; otherwise returns + /// a mutable reference to the associated main request. Since nginx processes subrequests + /// sequentially, it is safe to return a mutable reference to the main request even + /// if `self` is a subrequest. + pub fn main_mut(&mut self) -> &mut Request { + if self.is_main() { self } else { unsafe { Request::from_ngx_http_request(self.0.main) } } + } + + /// Get a reference to the main request. + /// + /// If this is already the main request, returns `self`; otherwise returns + /// a reference to the associated main request. + pub fn main(&self) -> &Request { + if self.is_main() { + self + } else { + unsafe { Request::from_const_ngx_http_request(self.0.main) } + } + } + /// Request pool. pub fn pool(&self) -> Pool { // SAFETY: This request is allocated from `pool`, thus must be a valid pool. @@ -248,6 +291,14 @@ impl Request { unsafe { ctx.as_ref() } } + /// Get mutable Module context + pub fn get_module_ctx_mut(&mut self, module: &ngx_module_t) -> Option<&mut T> { + let ctx = self.get_module_ctx_ptr(module).cast::(); + // SAFETY: ctx is either NULL or allocated with ngx_p(c)alloc and + // explicitly initialized by the module + unsafe { ctx.as_mut() } + } + /// Sets the value as the module's context. /// /// See @@ -292,11 +343,38 @@ impl Request { } } + /// Get HTTP status of response. + pub fn status(&self) -> HTTPStatus { + self.0.headers_out.status.try_into().unwrap_or(HTTPStatus(0)) + } + /// Set HTTP status of response. pub fn set_status(&mut self, status: HTTPStatus) { self.0.headers_out.status = status.into(); } + /// Clean up request headers_in and initialize it with ngx_list_init + pub fn init_headers_in(&mut self, n: ngx_uint_t) -> Option<()> { + unsafe { + nginx_sys::ngx_explicit_memzero( + &raw mut self.0.headers_in as _, + core::mem::size_of::(), + ); + let rc = nginx_sys::ngx_list_init( + &raw mut self.0.headers_in.headers, + self.0.pool, + n, + core::mem::size_of::(), + ); + if rc != NGX_OK as _ { + return None; + } + self.0.headers_in.content_length_n = -1; + self.0.headers_in.keep_alive_n = -1; + } + Some(()) + } + /// Add header to the `headers_in` object. /// /// See @@ -383,62 +461,6 @@ impl Request { Status::NGX_DONE } - /// Send a subrequest - pub fn subrequest( - &self, - uri: &str, - module: &ngx_module_t, - post_callback: unsafe extern "C" fn( - *mut ngx_http_request_t, - *mut c_void, - ngx_int_t, - ) -> ngx_int_t, - ) -> Status { - let uri_ptr = unsafe { &mut ngx_str_t::from_str(self.0.pool, uri) as *mut _ }; - // ------------- - // allocate memory and set values for ngx_http_post_subrequest_t - let sub_ptr = self.pool().alloc(core::mem::size_of::()); - - // assert!(sub_ptr.is_null()); - let post_subreq = - sub_ptr as *const ngx_http_post_subrequest_t as *mut ngx_http_post_subrequest_t; - unsafe { - (*post_subreq).handler = Some(post_callback); - // WARN: safety! ensure that ctx is already set - (*post_subreq).data = self.get_module_ctx_ptr(module); - } - // ------------- - - let mut psr: *mut ngx_http_request_t = core::ptr::null_mut(); - let r = unsafe { - ngx_http_subrequest( - (self as *const Request as *mut Request).cast(), - uri_ptr, - core::ptr::null_mut(), - &raw mut psr, - sub_ptr as *mut _, - NGX_HTTP_SUBREQUEST_WAITED as _, - ) - }; - - // previously call of ngx_http_subrequest() would ensure that the pointer is not null - // anymore - let sr = unsafe { &mut *psr }; - - /* - * allocate fake request body to avoid attempts to read it and to make - * sure real body file (if already read) won't be closed by upstream - */ - sr.request_body = - self.pool().alloc(core::mem::size_of::()) as *mut _; - - if sr.request_body.is_null() { - return Status::NGX_ERROR; - } - sr.set_header_only(1 as _); - Status(r) - } - /// Iterate over headers_in /// each header item is (&str, &str) (borrowed) pub fn headers_in_iterator(&self) -> NgxListIterator<'_> { diff --git a/src/http/status.rs b/src/http/status.rs index a545f65b..72207a87 100644 --- a/src/http/status.rs +++ b/src/http/status.rs @@ -7,7 +7,7 @@ use crate::ffi::*; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct HTTPStatus(pub ngx_uint_t); -/// A possible error value when converting a `HTTPStatus` from a `u16` or `&str` +/// A possible error value when converting a `HTTPStatus` from a `usize`, `isize` or `&[u8]`. /// /// This error indicates that the supplied input was not a valid number, was less /// than 100, or was greater than 599. @@ -48,32 +48,48 @@ impl From for ngx_uint_t { } } -impl fmt::Debug for HTTPStatus { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} +impl TryFrom for HTTPStatus { + type Error = InvalidHTTPStatusCode; -impl HTTPStatus { - /// Convets a u16 to a status code. #[inline] - pub fn from_u16(src: u16) -> Result { - if !(100..600).contains(&src) { + fn try_from(value: usize) -> Result { + if !(100..600).contains(&value) { return Err(InvalidHTTPStatusCode::new()); } + Ok(HTTPStatus(value)) + } +} + +impl TryFrom for HTTPStatus { + type Error = InvalidHTTPStatusCode; + + #[inline] + fn try_from(value: isize) -> Result { + let value: usize = value.try_into().map_err(|_| InvalidHTTPStatusCode::new())?; + Self::try_from(value) + } +} + +impl TryFrom for HTTPStatus { + type Error = InvalidHTTPStatusCode; - Ok(HTTPStatus(src.into())) + #[inline] + fn try_from(value: u16) -> Result { + Self::try_from(value as usize) } +} + +impl TryFrom<&[u8]> for HTTPStatus { + type Error = InvalidHTTPStatusCode; - /// Converts a &[u8] to a status code. - pub fn from_bytes(src: &[u8]) -> Result { - if src.len() != 3 { + fn try_from(value: &[u8]) -> Result { + if value.len() != 3 { return Err(InvalidHTTPStatusCode::new()); } - let a = src[0].wrapping_sub(b'0') as u16; - let b = src[1].wrapping_sub(b'0') as u16; - let c = src[2].wrapping_sub(b'0') as u16; + let a = value[0].wrapping_sub(b'0') as u16; + let b = value[1].wrapping_sub(b'0') as u16; + let c = value[2].wrapping_sub(b'0') as u16; if a == 0 || a > 5 || b > 9 || c > 9 { return Err(InvalidHTTPStatusCode::new()); diff --git a/src/http/subrequest.rs b/src/http/subrequest.rs new file mode 100644 index 00000000..a458d7ed --- /dev/null +++ b/src/http/subrequest.rs @@ -0,0 +1,362 @@ +use core::convert::Infallible; +use core::ffi::c_void; +use core::fmt::Display; +use core::ptr; + +use nginx_sys::{ngx_http_post_subrequest_t, ngx_http_request_t, ngx_int_t, ngx_str_t, ngx_uint_t}; + +use crate::allocator::AllocError; +use crate::http::{IntoHandlerStatus, Request}; +use crate::ngx_log_debug_http; + +/// Default type for the subrequest initializer function +pub type SubRequestDefInit = fn(&mut Request) -> Result<(), Infallible>; +/// Default type for the subrequest post-completion handler function +pub type SubRequestDefHandler = fn(&mut Request, ngx_int_t) -> ngx_int_t; + +/// A builder for creating and initiating HTTP subrequests. +/// +/// `SubRequestBuilder` provides a fluent API for constructing nginx subrequests +/// ([`ngx_http_subrequest()`][nginx-dev-guide]). It handles URI and argument allocation from +/// the request pool, optional subrequest initialization, post-completion handlers, and +/// the various subrequest flags (`in_memory`, `waited`, `cloned`, `background`). +/// +/// The builder is consumed by [`build`](Self::build), which creates the subrequest and +/// schedules it for processing. The caller typically returns `NGX_AGAIN` to suspend the +/// main request until the subrequest completes. +/// +/// By default, the builder discards the parent request body in the subrequest (use +/// [`keep_body`](Self::keep_body) to preserve it) and initializes the subrequest's input +/// headers list with a capacity of 4 (use [`init_headers_in`](Self::init_headers_in) to +/// change or set to 0 to inherit the parent's headers without modification). +/// +/// [nginx-dev-guide]: https://nginx.org/en/docs/dev/development_guide.html#http_subrequests +/// +/// # Examples +/// +/// A minimal subrequest with no handler: +/// +/// ```no_run +/// # use ngx::http::subrequest::SubRequestBuilder; +/// # fn example(request: &mut ngx::http::Request) -> Result<(), Box> { +/// SubRequestBuilder::new(request, "/proxy")? +/// .build()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// A fully configured subrequest with query arguments, an initializer that adds custom +/// headers, a post-subrequest handler, in-memory buffering, and the waited flag: +/// +/// ```no_run +/// # use ngx::http::subrequest::SubRequestBuilder; +/// # use nginx_sys::ngx_int_t; +/// fn sr_handler(r: &mut ngx::http::Request, rc: ngx_int_t) -> ngx_int_t { +/// ngx::ngx_log_debug_http!(r, "subrequest completed with rc: {rc}"); +/// rc +/// } +/// +/// # fn example(request: &mut ngx::http::Request) -> Result<(), Box> { +/// SubRequestBuilder::new(request, "/proxy")? +/// .args("arg1=val1&arg2=val2")? +/// .init(|sr| { +/// sr.add_header_in("X-SubRequest", "1").ok_or("cannot add header") +/// }) +/// .handler(sr_handler) +/// .in_memory() +/// .waited() +/// .build()?; +/// # Ok(()) +/// # } +/// ``` +pub struct SubRequestBuilder<'r, I = SubRequestDefInit, H = SubRequestDefHandler> { + request: &'r mut Request, + uri: ngx_str_t, + args: Option, + flags: ngx_uint_t, + keep_body: bool, + init_headers: ngx_uint_t, + init: Option, + handler: Option, +} + +/// An error type for subrequest operations. +#[derive(Debug)] +pub enum SubRequestError { + /// Indicates that the subrequest allocation failed. + Alloc, + /// Indicates that the subrequest creation failed. + Create, + /// Indicates that the subrequest header initialization failed. + HeaderInit, + /// Indicates that the subrequest initialization failed. + Init(E), +} + +impl From for SubRequestError { + fn from(_: AllocError) -> Self { + Self::Alloc + } +} + +impl Display for SubRequestError +where + E: Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SubRequestError::Alloc => { + write!(f, "subrequest: allocation failed") + } + SubRequestError::Create => { + write!(f, "subrequest: creation failed") + } + SubRequestError::HeaderInit => { + write!(f, "subrequest: header initialization failed") + } + SubRequestError::Init(e) => { + write!(f, "subrequest: initialization failed: {}", e) + } + } + } +} + +impl core::error::Error for SubRequestError where E: Display + core::fmt::Debug {} + +impl<'r> SubRequestBuilder<'r> { + /// Creates a new [`SubRequestBuilder`] with the specified URI. + /// + /// The URI string is copied into memory allocated from the request pool. + /// Returns [`SubRequestError::Alloc`] if the pool allocation fails. + pub fn new(request: &'r mut Request, uri: &str) -> Result { + let uri = unsafe { ngx_str_t::from_bytes(request.pool().as_ptr(), uri.as_bytes()) } + .ok_or(SubRequestError::Alloc)?; + Ok(Self { + request, + uri, + args: None, + flags: 0, + keep_body: false, + init_headers: 4, + init: None, + handler: None, + }) + } +} + +impl<'r, I, E, H, O> SubRequestBuilder<'r, I, H> +where + I: FnOnce(&mut Request) -> Result<(), E>, + H: FnOnce(&mut Request, ngx_int_t) -> O, + O: IntoHandlerStatus, +{ + /// Sets the query string arguments for the subrequest. + /// + /// The arguments string (e.g. `"arg1=val1&arg2=val2"`) is copied into memory allocated + /// from the request pool. Returns [`SubRequestError::Alloc`] if the allocation fails. + pub fn args(mut self, args: &str) -> Result { + let args = unsafe { ngx_str_t::from_bytes(self.request.pool().as_ptr(), args.as_bytes()) } + .ok_or(SubRequestError::Alloc)?; + self.args = Some(args); + Ok(self) + } + + /// Sets an initializer function to modify the subrequest before it is initiated. + /// + /// The initializer runs after `ngx_http_subrequest()` creates the subrequest but before + /// nginx begins processing it. Use this to set up headers, discard the request body, + /// or perform other per-subrequest initialization. + /// + /// The function receives a mutable reference to the subrequest and must return + /// `Result<(), E>`. If it returns an error, [`build`](Self::build) fails with + /// [`SubRequestError::Init`]. + pub fn init(self, init: IT) -> SubRequestBuilder<'r, IT, H> + where + IT: FnOnce(&mut Request) -> Result<(), ET>, + { + SubRequestBuilder:: { + request: self.request, + uri: self.uri, + args: self.args, + flags: self.flags, + keep_body: self.keep_body, + init_headers: self.init_headers, + init: Some(init), + handler: self.handler, + } + } + + /// Sets a post-subrequest handler function. + /// + /// The handler is invoked by nginx when the subrequest completes. It receives a mutable + /// reference to the subrequest and the completion result code (`ngx_int_t`). The handler + /// is the place to inspect the subrequest response status and headers, read the buffered + /// output (when combined with [`in_memory`](Self::in_memory)), and propagate results + /// back to the main request via its module context. + /// + /// The handler is allocated from the request pool and wrapped in an + /// `ngx_http_post_subrequest_t` callback. It is called exactly once. + pub fn handler(self, handler: HT) -> SubRequestBuilder<'r, I, HT> + where + HT: FnOnce(&mut Request, ngx_int_t) -> OT, + OT: IntoHandlerStatus, + { + SubRequestBuilder:: { + request: self.request, + uri: self.uri, + args: self.args, + flags: self.flags, + keep_body: self.keep_body, + init_headers: self.init_headers, + init: self.init, + handler: Some(handler), + } + } + + /// Sets the subrequest to store its output in memory. + /// + /// When enabled, the response body is captured in the subrequest's `out` chain instead + /// of being sent to the client connection. This is typically combined with a + /// [post-subrequest handler](Self::handler) that reads the buffered response body. + /// + /// Corresponds to the `NGX_HTTP_SUBREQUEST_IN_MEMORY` flag. + pub fn in_memory(mut self) -> Self { + self.flags |= nginx_sys::NGX_HTTP_SUBREQUEST_IN_MEMORY as ngx_uint_t; + self + } + + /// Sets the subrequest to be waited. + /// + /// When enabled, the subrequest's `done` flag is set even if the subrequest is not + /// active when it is finalized. This is typically combined with [`in_memory`](Self::in_memory) + /// to ensure the main request resumes processing after the subrequest completes. + /// + /// Corresponds to the `NGX_HTTP_SUBREQUEST_WAITED` flag. + pub fn waited(mut self) -> Self { + self.flags |= nginx_sys::NGX_HTTP_SUBREQUEST_WAITED as ngx_uint_t; + self + } + + /// Sets the subrequest to be a clone of its parent. + /// + /// A cloned subrequest is started at the same location and proceeds from the same + /// phase as the parent request, rather than being looked up by URI. This is useful + /// when the subrequest must inherit the parent's location configuration. + /// + /// Corresponds to the `NGX_HTTP_SUBREQUEST_CLONE` flag. + pub fn cloned(mut self) -> Self { + self.flags |= nginx_sys::NGX_HTTP_SUBREQUEST_CLONE as ngx_uint_t; + self + } + + /// Sets the subrequest to run in the background. + /// + /// A background subrequest does not block any other subrequests or the main request, + /// allowing them to proceed independently. However, the client connection is kept open + /// until the background subrequest completes. + /// + /// Corresponds to the `NGX_HTTP_SUBREQUEST_BACKGROUND` flag. + pub fn background(mut self) -> Self { + self.flags |= nginx_sys::NGX_HTTP_SUBREQUEST_BACKGROUND as ngx_uint_t; + self + } + + /// Keep the request body in the subrequest. + /// By default, the request body is discarded in the subrequest. + pub fn keep_body(mut self) -> Self { + self.keep_body = true; + self + } + + /// Sets the number of headers to initialize in the subrequest. + /// By default, 4 headers are initialized, which is sufficient for most use cases. + /// Setting this to 0 keeps all headers from the main request, + /// but they cannot be modified in the subrequest. + pub fn init_headers_in(mut self, count: ngx_uint_t) -> Self { + self.init_headers = count; + self + } + + /// Builds and initiates the subrequest. + /// + /// This consumes the builder, allocates the post-subrequest callback (if a + /// [`handler`](Self::handler) was set), calls `ngx_http_subrequest()` to create + /// the subrequest, and then runs the [initializer](Self::init) (if set). + /// + /// # Errors + /// + /// Returns [`SubRequestError::Alloc`] if pool allocation for the handler or callback + /// structure fails, [`SubRequestError::Create`] if `ngx_http_subrequest()` returns a + /// non-`NGX_OK` status, or [`SubRequestError::Init`] if the initializer closure + /// returns an error. + pub fn build(mut self) -> Result<(), SubRequestError> { + let sr_args_ptr = self.args.as_mut().map_or(ptr::null_mut(), ptr::from_mut); + + let psr_ptr: *mut ngx_http_post_subrequest_t = if self.handler.is_some() { + let pool = self.request.pool(); + + let ctx = unsafe { pool.allocate_with_cleanup(|| self.handler) }?; + + let psr = unsafe { + pool.allocate_with_cleanup(|| ngx_http_post_subrequest_t { + handler: Some(sr_handler::), + data: ctx.as_ptr() as _, + }) + }?; + psr.as_ptr() as _ + } else { + ptr::null_mut() + }; + + let mut sr_ptr: *mut ngx_http_request_t = core::ptr::null_mut(); + + let rc = unsafe { + nginx_sys::ngx_http_subrequest( + self.request.as_mut() as *mut _ as _, + &raw mut self.uri, + sr_args_ptr, + &raw mut sr_ptr, + psr_ptr, + self.flags as ngx_uint_t, + ) + }; + if rc != nginx_sys::NGX_OK as _ { + return Err(SubRequestError::Create); + } + + if !self.keep_body { + (unsafe { *sr_ptr }).request_body = ptr::null_mut(); + } + + let sr = unsafe { Request::from_ngx_http_request(sr_ptr) }; + + if self.init_headers > 0 { + sr.init_headers_in(self.init_headers).ok_or(SubRequestError::HeaderInit)?; + } + + if let Some(init) = self.init { + init(sr).map_err(SubRequestError::Init)?; + } + Ok(()) + } +} + +extern "C" fn sr_handler( + r: *mut ngx_http_request_t, + data: *mut c_void, + rc: ngx_int_t, +) -> ngx_int_t +where + H: FnOnce(&mut Request, ngx_int_t) -> O, + O: IntoHandlerStatus, +{ + let request = unsafe { Request::from_ngx_http_request(r) }; + ngx_log_debug_http!(request, "subrequest handler called with rc: {rc}"); + // SAFETY: `data` is a pointer to an `Option` that is valid as long as the main request + // is not finalized, and the subrequest is always finalized before the main request. + if let Some(handler) = unsafe { &mut *(data as *mut Option) }.take() { + (handler)(request, rc).into_handler_status(request) + } else { + rc + } +} diff --git a/src/log.rs b/src/log.rs index e461d6bb..432901d5 100644 --- a/src/log.rs +++ b/src/log.rs @@ -153,7 +153,7 @@ macro_rules! ngx_log_debug { #[macro_export] macro_rules! ngx_log_debug_http { ( $request:expr, $($arg:tt)+ ) => { - let log = unsafe { (*$request.connection()).log }; + let log = $request.log(); $crate::ngx_log_debug!(mask: $crate::log::DebugMask::Http, log, $($arg)+); } }