diff --git a/examples/async.rs b/examples/async.rs index 47f5de8e..b1d3b901 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -6,14 +6,12 @@ use std::time::Instant; use ngx::core; use ngx::ffi::{ - ngx_array_push, ngx_command_t, ngx_conf_t, ngx_connection_t, ngx_event_t, ngx_http_handler_pt, - ngx_http_module_t, ngx_http_phases_NGX_HTTP_ACCESS_PHASE, ngx_int_t, ngx_module_t, - ngx_post_event, ngx_posted_events, ngx_posted_next_events, ngx_str_t, ngx_uint_t, + ngx_command_t, ngx_conf_t, ngx_connection_t, ngx_event_t, ngx_http_module_t, ngx_int_t, + ngx_module_t, ngx_post_event, ngx_posted_events, ngx_posted_next_events, ngx_str_t, ngx_uint_t, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_HTTP_MODULE, NGX_LOG_EMERG, }; -use ngx::http::{self, HttpModule, MergeConfigError}; -use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf, NgxHttpCoreModule}; -use ngx::{http_request_handler, ngx_conf_log_error, ngx_log_debug_http, ngx_string}; +use ngx::http::{self, HttpModule, HttpModuleLocationConf, HttpRequestHandler, MergeConfigError}; +use ngx::{ngx_conf_log_error, ngx_log_debug_http, ngx_string}; use tokio::runtime::Runtime; struct Module; @@ -25,18 +23,10 @@ impl http::HttpModule for 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 = &mut *cf; - let cmcf = NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf"); - - let h = ngx_array_push( - &mut cmcf.phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, - ) as *mut ngx_http_handler_pt; - if h.is_null() { - return core::Status::NGX_ERROR.into(); - } - // set an Access phase handler - *h = Some(async_access_handler); - core::Status::NGX_OK.into() + let cf = unsafe { &mut *cf }; + http::add_phase_handler::(cf) + .map_or(core::Status::NGX_ERROR, |_| core::Status::NGX_OK) + .into() } } @@ -139,63 +129,69 @@ impl Drop for RequestCTX { } } -http_request_handler!(async_access_handler, |request: &mut http::Request| { - let co = Module::location_conf(request).expect("module config is none"); +struct AsyncAccessHandler; - ngx_log_debug_http!(request, "async module enabled: {}", co.enable); +impl HttpRequestHandler> for AsyncAccessHandler { + const PHASE: ngx::http::Phases = ngx::http::Phases::Access; - if !co.enable { - return core::Status::NGX_DECLINED; - } + fn handler(request: &mut http::Request) -> Option { + let co = Module::location_conf(request).expect("module config is none"); + + ngx_log_debug_http!(request, "async module enabled: {}", co.enable); - if let Some(ctx) = - unsafe { request.get_module_ctx::(&*addr_of!(ngx_http_async_module)) } - { - if !ctx.done.load(Ordering::Relaxed) { - return core::Status::NGX_AGAIN; + if !co.enable { + return Some(core::Status::NGX_DECLINED.into()); } - return core::Status::NGX_OK; - } + if let Some(ctx) = + unsafe { request.get_module_ctx::(&*addr_of!(ngx_http_async_module)) } + { + if !ctx.done.load(Ordering::Relaxed) { + return Some(core::Status::NGX_AGAIN.into()); + } - let ctx = request.pool().allocate(RequestCTX::default()); - if ctx.is_null() { - return core::Status::NGX_ERROR; + return Some(core::Status::NGX_OK.into()); + } + + let ctx = request.pool().allocate(RequestCTX::default()); + if ctx.is_null() { + return Some(core::Status::NGX_ERROR.into()); + } + request.set_module_ctx(ctx.cast(), unsafe { &*addr_of!(ngx_http_async_module) }); + + let ctx = unsafe { &mut *ctx }; + ctx.event.handler = Some(check_async_work_done); + ctx.event.data = request.connection().cast(); + ctx.event.log = unsafe { (*request.connection()).log }; + unsafe { ngx_post_event(&mut ctx.event, addr_of_mut!(ngx_posted_next_events)) }; + + // Request is no longer needed and can be converted to something movable to the async block + let req = AtomicPtr::new(request.into()); + let done_flag = ctx.done.clone(); + + let rt = ngx_http_async_runtime(); + ctx.task = Some(rt.spawn(async move { + let start = Instant::now(); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let req = unsafe { http::Request::from_ngx_http_request(req.load(Ordering::Relaxed)) }; + // not really thread safe, we should apply all these operation in nginx thread + // but this is just an example. proper way would be storing these headers in the request ctx + // and apply them when we get back to the nginx thread. + req.add_header_out( + "X-Async-Time", + start.elapsed().as_millis().to_string().as_str(), + ); + + done_flag.store(true, Ordering::Release); + // there is a small issue here. If traffic is low we may get stuck behind a 300ms timer + // in the nginx event loop. To workaround it we can notify the event loop using + // pthread_kill( nginx_thread, SIGIO ) to wake up the event loop. (or patch nginx + // and use the same trick as the thread pool) + })); + + Some(core::Status::NGX_AGAIN.into()) } - request.set_module_ctx(ctx.cast(), unsafe { &*addr_of!(ngx_http_async_module) }); - - let ctx = unsafe { &mut *ctx }; - ctx.event.handler = Some(check_async_work_done); - ctx.event.data = request.connection().cast(); - ctx.event.log = unsafe { (*request.connection()).log }; - unsafe { ngx_post_event(&mut ctx.event, addr_of_mut!(ngx_posted_next_events)) }; - - // Request is no longer needed and can be converted to something movable to the async block - let req = AtomicPtr::new(request.into()); - let done_flag = ctx.done.clone(); - - let rt = ngx_http_async_runtime(); - ctx.task = Some(rt.spawn(async move { - let start = Instant::now(); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let req = unsafe { http::Request::from_ngx_http_request(req.load(Ordering::Relaxed)) }; - // not really thread safe, we should apply all these operation in nginx thread - // but this is just an example. proper way would be storing these headers in the request ctx - // and apply them when we get back to the nginx thread. - req.add_header_out( - "X-Async-Time", - start.elapsed().as_millis().to_string().as_str(), - ); - - done_flag.store(true, Ordering::Release); - // there is a small issue here. If traffic is low we may get stuck behind a 300ms timer - // in the nginx event loop. To workaround it we can notify the event loop using - // pthread_kill( nginx_thread, SIGIO ) to wake up the event loop. (or patch nginx - // and use the same trick as the thread pool) - })); - - core::Status::NGX_AGAIN -}); +} extern "C" fn ngx_http_async_commands_set_enable( cf: *mut ngx_conf_t, diff --git a/examples/awssig.rs b/examples/awssig.rs index cbbea102..101b959e 100644 --- a/examples/awssig.rs +++ b/examples/awssig.rs @@ -3,13 +3,12 @@ use std::ffi::{c_char, c_void}; use http::HeaderMap; use ngx::core; use ngx::ffi::{ - ngx_array_push, ngx_command_t, ngx_conf_t, ngx_http_handler_pt, ngx_http_module_t, - ngx_http_phases_NGX_HTTP_PRECONTENT_PHASE, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, + ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_HTTP_MODULE, NGX_HTTP_SRV_CONF, NGX_LOG_EMERG, }; use ngx::http::*; -use ngx::{http_request_handler, ngx_conf_log_error, ngx_log_debug_http, ngx_string}; +use ngx::{ngx_conf_log_error, ngx_log_debug_http, ngx_string}; struct Module; @@ -20,18 +19,10 @@ impl HttpModule for 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 = &mut *cf; - let cmcf = NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf"); - - let h = ngx_array_push( - &mut cmcf.phases[ngx_http_phases_NGX_HTTP_PRECONTENT_PHASE as usize].handlers, - ) as *mut ngx_http_handler_pt; - if h.is_null() { - return core::Status::NGX_ERROR.into(); - } - // set an phase handler - *h = Some(awssigv4_header_handler); - core::Status::NGX_OK.into() + let cf = unsafe { &mut *cf }; + ngx::http::add_phase_handler::(cf) + .map_or(core::Status::NGX_ERROR, |_| core::Status::NGX_OK) + .into() } } @@ -261,82 +252,88 @@ extern "C" fn ngx_http_awssigv4_commands_set_s3_endpoint( ngx::core::NGX_CONF_OK } -http_request_handler!(awssigv4_header_handler, |request: &mut Request| { - // get Module Config from request - let conf = Module::location_conf(request).expect("module conf"); - ngx_log_debug_http!(request, "AWS signature V4 module {}", { - if conf.enable { - "enabled" - } else { - "disabled" - } - }); - if !conf.enable { - return core::Status::NGX_DECLINED; - } +struct AwsSigV4HeaderHandler; - // TODO: build url properly from the original URL from client - let method = request.method(); - if !matches!(method, ngx::http::Method::HEAD | ngx::http::Method::GET) { - return HTTPStatus::FORBIDDEN.into(); - } +impl HttpRequestHandler> for AwsSigV4HeaderHandler { + const PHASE: Phases = Phases::PreContent; - let datetime = chrono::Utc::now(); - let uri = match request.unparsed_uri().to_str() { - Ok(v) => format!("https://{}.{}{}", conf.s3_bucket, conf.s3_endpoint, v), - Err(_) => return core::Status::NGX_DECLINED, - }; + fn handler(request: &mut Request) -> Option { + // get Module Config from request + let conf = Module::location_conf(request).expect("module conf"); + ngx_log_debug_http!(request, "AWS signature V4 module {}", { + if conf.enable { + "enabled" + } else { + "disabled" + } + }); + if !conf.enable { + return Some(core::Status::NGX_DECLINED.into()); + } + + // TODO: build url properly from the original URL from client + let method = request.method(); + if !matches!(method, ngx::http::Method::HEAD | ngx::http::Method::GET) { + return Some(HTTPStatus::FORBIDDEN.into()); + } - let datetime_now = datetime.format("%Y%m%dT%H%M%SZ"); - let datetime_now = datetime_now.to_string(); + let datetime = chrono::Utc::now(); + let uri = match request.unparsed_uri().to_str() { + Ok(v) => format!("https://{}.{}{}", conf.s3_bucket, conf.s3_endpoint, v), + Err(_) => return Some(core::Status::NGX_DECLINED.into()), + }; - let signature = { - // NOTE: aws_sign_v4::AwsSign::new() implementation requires a HeaderMap. - // Iterate over requests headers_in and copy into HeaderMap - // Copy only headers that will be used to sign the request - let mut headers = HeaderMap::new(); - for (name, value) in request.headers_in_iterator() { - if let Ok(name) = name.to_str() { - if name.to_lowercase() == "host" { - if let Ok(value) = http::HeaderValue::from_bytes(value.as_bytes()) { - headers.insert(http::header::HOST, value); - } else { - return core::Status::NGX_DECLINED; + let datetime_now = datetime.format("%Y%m%dT%H%M%SZ"); + let datetime_now = datetime_now.to_string(); + + let signature = { + // NOTE: aws_sign_v4::AwsSign::new() implementation requires a HeaderMap. + // Iterate over requests headers_in and copy into HeaderMap + // Copy only headers that will be used to sign the request + let mut headers = HeaderMap::new(); + for (name, value) in request.headers_in_iterator() { + if let Ok(name) = name.to_str() { + if name.to_lowercase() == "host" { + if let Ok(value) = http::HeaderValue::from_bytes(value.as_bytes()) { + headers.insert(http::header::HOST, value); + } else { + return Some(core::Status::NGX_DECLINED.into()); + } } + } else { + return Some(core::Status::NGX_DECLINED.into()); } - } else { - return core::Status::NGX_DECLINED; } - } - headers.insert("X-Amz-Date", datetime_now.parse().unwrap()); - ngx_log_debug_http!(request, "headers {:?}", headers); - ngx_log_debug_http!(request, "method {:?}", method); - ngx_log_debug_http!(request, "uri {:?}", uri); - ngx_log_debug_http!(request, "datetime_now {:?}", datetime_now); - - let s = aws_sign_v4::AwsSign::new( - method.as_str(), - &uri, - &datetime, - &headers, - "us-east-1", - conf.access_key.as_str(), - conf.secret_key.as_str(), - "s3", - "", - ); - s.sign() - }; + headers.insert("X-Amz-Date", datetime_now.parse().unwrap()); + ngx_log_debug_http!(request, "headers {:?}", headers); + ngx_log_debug_http!(request, "method {:?}", method); + ngx_log_debug_http!(request, "uri {:?}", uri); + ngx_log_debug_http!(request, "datetime_now {:?}", datetime_now); + + let s = aws_sign_v4::AwsSign::new( + method.as_str(), + &uri, + &datetime, + &headers, + "us-east-1", + conf.access_key.as_str(), + conf.secret_key.as_str(), + "s3", + "", + ); + s.sign() + }; - request.add_header_in("authorization", signature.as_str()); - request.add_header_in("X-Amz-Date", datetime_now.as_str()); + request.add_header_in("authorization", signature.as_str()); + request.add_header_in("X-Amz-Date", datetime_now.as_str()); - for (name, value) in request.headers_out_iterator() { - ngx_log_debug_http!(request, "headers_out {name}: {value}",); - } - for (name, value) in request.headers_in_iterator() { - ngx_log_debug_http!(request, "headers_in {name}: {value}",); - } + for (name, value) in request.headers_out_iterator() { + ngx_log_debug_http!(request, "headers_out {name}: {value}",); + } + for (name, value) in request.headers_in_iterator() { + ngx_log_debug_http!(request, "headers_in {name}: {value}",); + } - core::Status::NGX_OK -}); + Some(core::Status::NGX_OK.into()) + } +} diff --git a/examples/curl.rs b/examples/curl.rs index 3d07002f..5f7ba971 100644 --- a/examples/curl.rs +++ b/examples/curl.rs @@ -2,13 +2,11 @@ use std::ffi::{c_char, c_void}; use ngx::core; use ngx::ffi::{ - ngx_array_push, ngx_command_t, ngx_conf_t, ngx_http_handler_pt, ngx_http_module_t, - ngx_http_phases_NGX_HTTP_ACCESS_PHASE, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, + ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_HTTP_MODULE, NGX_LOG_EMERG, }; -use ngx::http::{self, HttpModule, MergeConfigError}; -use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf, NgxHttpCoreModule}; -use ngx::{http_request_handler, ngx_conf_log_error, ngx_log_debug_http, ngx_string}; +use ngx::http::{self, HttpModule, HttpModuleLocationConf, HttpRequestHandler, MergeConfigError}; +use ngx::{ngx_conf_log_error, ngx_log_debug_http, ngx_string}; struct Module; @@ -19,18 +17,10 @@ impl http::HttpModule for 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 = &mut *cf; - let cmcf = NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf"); - - let h = ngx_array_push( - &mut cmcf.phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, - ) as *mut ngx_http_handler_pt; - if h.is_null() { - return core::Status::NGX_ERROR.into(); - } - // set an Access phase handler - *h = Some(curl_access_handler); - core::Status::NGX_OK.into() + let cf = unsafe { &mut *cf }; + http::add_phase_handler::(cf) + .map_or(core::Status::NGX_ERROR, |_| core::Status::NGX_OK) + .into() } } @@ -90,25 +80,31 @@ impl http::Merge for ModuleConfig { } } -http_request_handler!(curl_access_handler, |request: &mut http::Request| { - let co = Module::location_conf(request).expect("module config is none"); +struct CurlRequestHandler; + +impl HttpRequestHandler> for CurlRequestHandler { + const PHASE: ngx::http::Phases = ngx::http::Phases::Access; - ngx_log_debug_http!(request, "curl module enabled: {}", co.enable); + fn handler(request: &mut http::Request) -> Option { + let co = Module::location_conf(request).expect("module config is none"); - match co.enable { - true => { - if request - .user_agent() - .is_some_and(|ua| ua.as_bytes().starts_with(b"curl")) - { - http::HTTPStatus::FORBIDDEN.into() - } else { - core::Status::NGX_DECLINED + ngx_log_debug_http!(request, "curl module enabled: {}", co.enable); + + match co.enable { + true => { + if request + .user_agent() + .is_some_and(|ua| ua.as_bytes().starts_with(b"curl")) + { + Some(http::HTTPStatus::FORBIDDEN.into()) + } else { + Some(core::Status::NGX_DECLINED.into()) + } } + false => Some(core::Status::NGX_DECLINED.into()), } - false => core::Status::NGX_DECLINED, } -}); +} extern "C" fn ngx_http_curl_commands_set_enable( cf: *mut ngx_conf_t, diff --git a/src/http/conf.rs b/src/http/conf.rs index 06426bd4..4449cce3 100644 --- a/src/http/conf.rs +++ b/src/http/conf.rs @@ -207,9 +207,15 @@ pub unsafe trait HttpModuleLocationConf: HttpModule { } mod core { - use crate::ffi::{ - ngx_http_core_loc_conf_t, ngx_http_core_main_conf_t, ngx_http_core_module, - ngx_http_core_srv_conf_t, + use allocator_api2::alloc::AllocError; + + use crate::{ + ffi::{ + ngx_http_core_loc_conf_t, ngx_http_core_main_conf_t, ngx_http_core_module, + ngx_http_core_srv_conf_t, + }, + http::HttpModuleMainConf, + ngx_conf_log_error, }; /// Auxiliary structure to access `ngx_http_core_module` configuration. @@ -229,9 +235,60 @@ mod core { unsafe impl crate::http::HttpModuleLocationConf for NgxHttpCoreModule { type LocationConf = ngx_http_core_loc_conf_t; } + + /// HTTP phases in which a module can register handlers. + #[repr(u32)] + pub enum Phases { + /// Post-read phase + PostRead = crate::ffi::ngx_http_phases_NGX_HTTP_POST_READ_PHASE as _, + /// Server rewrite phase + ServerRewrite = crate::ffi::ngx_http_phases_NGX_HTTP_SERVER_REWRITE_PHASE as _, + /// Find configuration phase + FindConfig = crate::ffi::ngx_http_phases_NGX_HTTP_FIND_CONFIG_PHASE as _, + /// Rewrite phase + Rewrite = crate::ffi::ngx_http_phases_NGX_HTTP_REWRITE_PHASE as _, + /// Post-rewrite phase + PostRewrite = crate::ffi::ngx_http_phases_NGX_HTTP_POST_REWRITE_PHASE as _, + /// Pre-access phase + Preaccess = crate::ffi::ngx_http_phases_NGX_HTTP_PREACCESS_PHASE as _, + /// Access phase + Access = crate::ffi::ngx_http_phases_NGX_HTTP_ACCESS_PHASE as _, + /// Post-access phase + PostAccess = crate::ffi::ngx_http_phases_NGX_HTTP_POST_ACCESS_PHASE as _, + /// Pre-content phase + PreContent = crate::ffi::ngx_http_phases_NGX_HTTP_PRECONTENT_PHASE as _, + /// Content phase + Content = crate::ffi::ngx_http_phases_NGX_HTTP_CONTENT_PHASE as _, + /// Log phase + Log = crate::ffi::ngx_http_phases_NGX_HTTP_LOG_PHASE as _, + } + + /// Register a request handler for a specified phase. + /// This function must be called from the module's `postconfiguration()` function. + pub fn add_phase_handler, R>( + cf: &mut nginx_sys::ngx_conf_t, + ) -> Result<(), AllocError> { + let cmcf = NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf"); + let h: *mut nginx_sys::ngx_http_handler_pt = + unsafe { nginx_sys::ngx_array_push(&mut cmcf.phases[H::PHASE as usize].handlers) as _ }; + if h.is_null() { + ngx_conf_log_error!( + nginx_sys::NGX_LOG_EMERG, + cf, + "failed to register {} handler", + H::name(), + ); + return Err(AllocError); + } + // set an H::PHASE phase handler + unsafe { + *h = Some(H::handler_wrapper); + } + Ok(()) + } } -pub use core::NgxHttpCoreModule; +pub use core::{add_phase_handler, NgxHttpCoreModule, Phases}; #[cfg(ngx_feature = "http_ssl")] mod ssl { diff --git a/src/http/request.rs b/src/http/request.rs index 3671717c..dab1c19a 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -8,6 +8,7 @@ use core::str::FromStr; use crate::core::*; use crate::ffi::*; use crate::http::status::*; +use crate::http::Phases; /// Define a static request handler. /// @@ -85,6 +86,61 @@ macro_rules! http_variable_get { }; } +/// Trait for static request handler. +/// Predefined return types are `Option` +/// and `Result` where `E: ::core::error::Error`. +/// Handler with `Result` return type logs the error message automatically. +pub trait HttpRequestHandler { + /// The phase in which the handler is invoked + const PHASE: Phases; + /// The handler function + fn handler(request: &mut Request) -> ReturnType; + /// Handler name for logging purposes + fn name() -> &'static str { + core::any::type_name::() + } +} + +/// Trait for wrapping a request handler with a C-compatible function. +pub trait HttpHandlerWrapper: HttpRequestHandler { + /// Convert the handler return type to `ngx_int_t`. + fn convert(r: &Request, res: ReturnType) -> ngx_int_t; + /// The C-compatible handler wrapper function. + /// + /// # Safety + /// + /// The caller has provided a valid non-null pointer to an `ngx_http_request_t`. + unsafe extern "C" fn handler_wrapper(r: *mut ngx_http_request_t) -> ngx_int_t { + let r = unsafe { Request::from_ngx_http_request(r) }; + let res = Self::handler(r); + Self::convert(r, res) + } +} + +/// Implementation of `HttpHandlerWrapper` for `Option`. +impl HttpHandlerWrapper> for T +where + T: HttpRequestHandler>, +{ + fn convert(_r: &Request, res: Option) -> ngx_int_t { + res.unwrap_or(NGX_ERROR as _) + } +} + +/// Implementation of `HttpHandlerWrapper` for `Result`. +impl HttpHandlerWrapper> for T +where + T: HttpRequestHandler>, + E: fmt::Display, +{ + fn convert(r: &Request, res: Result) -> ngx_int_t { + res.unwrap_or_else(|err| { + crate::ngx_log_error!(NGX_LOG_ERR, r.log(), "{err}"); + NGX_ERROR as _ + }) + } +} + /// Wrapper struct for an [`ngx_http_request_t`] pointer, providing methods for working with HTTP /// requests. /// diff --git a/src/http/status.rs b/src/http/status.rs index 8b067cb6..a545f65b 100644 --- a/src/http/status.rs +++ b/src/http/status.rs @@ -36,6 +36,12 @@ impl From for Status { } } +impl From for ngx_int_t { + fn from(val: HTTPStatus) -> Self { + val.0 as _ + } +} + impl From for ngx_uint_t { fn from(val: HTTPStatus) -> Self { val.0