use std::cell::Cell;
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::OnceLock;
use claviron_http::ext::{HostName, TlsInfo};
use claviron_runtime::{AppState, RunMode};
use ext_php_rs::types::Zval;
use ext_php_rs::zend::{SapiGlobals, SapiHeader, SapiHeaders, SapiModule};
use http::Version;
use http::header::{CONTENT_TYPE, HeaderName};
use http::status::StatusCode;
use regex::Regex;
use tracing::{debug, error, info, warn};
use super::{
CLAVIRON_THREAD_ID, DOCUMENT_ROOT, HTTP_COOKIE, HTTPS, PATH_INFO,
PATH_TRANSLATED, PHP_SELF, QUERY_STRING, REMOTE_ADDR, REMOTE_PORT,
REQUEST_METHOD, REQUEST_URI, SCRIPT_FILENAME, SCRIPT_NAME, SERVER_ADDR,
SERVER_NAME, SERVER_PORT, SERVER_PROTOCOL, SERVER_SOFTWARE, ServerContext,
build_sapi_module, install_additional_module, register_variable,
register_variable_cstr,
};
use crate::engine::ClavironContext;
use crate::error::SapiGatewayError;
thread_local! {
pub(crate) static PHP_REQUEST_LOGGED_ERROR: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn reset_php_request_error_flag() {
PHP_REQUEST_LOGGED_ERROR.with(|flag| flag.set(false));
}
pub(crate) fn php_request_logged_error() -> bool {
PHP_REQUEST_LOGGED_ERROR.with(Cell::get)
}
const PHP_LOG_MAX_SIZE: usize = 16 * 1024;
pub(super) fn sanitize_log_message(message: &str) -> String {
static HTML_DOCUMENT: OnceLock<Regex> = OnceLock::new();
let regex = HTML_DOCUMENT.get_or_init(|| {
Regex::new(r"(?is)<html(?:\s[^>]*)?>.*?</html\s*>")
.expect("the PHP log HTML filter must compile")
});
let message = regex.replace_all(message, "");
let message = message.trim();
if message.len() <= PHP_LOG_MAX_SIZE {
return message.to_owned();
}
let mut end = PHP_LOG_MAX_SIZE;
while !message.is_char_boundary(end) {
end -= 1;
}
format!("{} [truncated]", &message[..end])
}
struct ServerScopedCstrs {
hostname: CString,
runner: CString,
worker_id: CString,
worker_count: CString,
run_mode: RunMode,
process_id: CString,
software: CString,
}
static SERVER_SCOPED_VARS: OnceLock<ServerScopedCstrs> = OnceLock::new();
pub fn init_server_scoped_vars(state: &AppState) {
let _ = SERVER_SCOPED_VARS.set(ServerScopedCstrs {
hostname: CString::new(state.hostname().to_string())
.unwrap_or_default(),
runner: CString::new(run_identifier(state.run_mode()))
.unwrap_or_default(),
worker_id: CString::new(state.worker_id().unwrap_or(0).to_string())
.unwrap_or_default(),
worker_count: CString::new(state.worker_count().to_string())
.unwrap_or_default(),
run_mode: state.run_mode(),
process_id: CString::new(std::process::id().to_string())
.unwrap_or_default(),
software: CString::new(state.name().to_string()).unwrap_or_default(),
});
}
fn visit_execution_identity(
context: &ClavironContext,
mut visit: impl FnMut(&CStr, &str),
) {
if let Some(scoped) = SERVER_SCOPED_VARS.get() {
visit(c"CLAVIRON_HOSTNAME", &scoped.hostname.to_string_lossy());
visit(c"CLAVIRON_RUNNER", &scoped.runner.to_string_lossy());
visit(c"CLAVIRON_WORKER_ID", &scoped.worker_id.to_string_lossy());
visit(c"CLAVIRON_WORKER_COUNT", &scoped.worker_count.to_string_lossy());
visit(c"CLAVIRON_RUN_MODE", scoped.run_mode.as_env_value());
visit(c"CLAVIRON_PROCESS_ID", &scoped.process_id.to_string_lossy());
if matches!(scoped.run_mode, RunMode::Worker(_)) {
visit(
c"CLAVIRON_WORKER_NUMBER",
&scoped.worker_id.to_string_lossy(),
);
}
} else if let crate::engine::PhpExecution::Request(request) =
&context.execution
{
let runtime = &request.runtime;
visit(c"CLAVIRON_HOSTNAME", &runtime.server_hostname);
visit(c"CLAVIRON_RUNNER", &run_identifier(runtime.server_run_mode));
visit(c"CLAVIRON_RUN_MODE", runtime.server_run_mode.as_env_value());
visit(c"CLAVIRON_PROCESS_ID", &std::process::id().to_string());
let worker = match runtime.server_run_mode {
RunMode::Worker(id) => id.raw(),
_ => 0,
};
visit(c"CLAVIRON_WORKER_ID", &worker.to_string());
visit(
c"CLAVIRON_WORKER_COUNT",
&runtime.server_worker_count.to_string(),
);
if matches!(runtime.server_run_mode, RunMode::Worker(_)) {
visit(c"CLAVIRON_WORKER_NUMBER", &worker.to_string());
}
}
if let crate::engine::PhpExecution::Request(request) = &context.execution {
visit(c"CLAVIRON_PHP_RUNNER", request.runtime.php_runner.as_str());
}
crate::engine::thread::THREAD_ID_CSTR.with(|cell| {
if let Some(value) = cell.get() {
visit(CLAVIRON_THREAD_ID, &value.to_string_lossy());
}
});
if let Some(program) = context.program() {
visit(c"CLAVIRON_PHP_PROGRAM", &program.name);
visit(c"CLAVIRON_PHP_INSTANCE", &program.instance.to_string());
visit(c"CLAVIRON_PHP_MODEL", program.model.as_str());
}
}
fn register_execution_identity(
context: &ClavironContext,
vars: *mut Zval,
) {
visit_execution_identity(context, |key, value| unsafe {
register_variable(key, value, vars);
});
}
pub(crate) struct ClavironSapi(pub(crate) Box<SapiModule>);
unsafe impl Send for ClavironSapi {}
unsafe impl Sync for ClavironSapi {}
impl ClavironSapi {
pub(crate) fn new() -> Result<Self, SapiGatewayError> {
let extensions =
crate::extension::freeze_extensions().map_err(|err| {
SapiGatewayError::Internal(format!(
"freeze PHP extension registry: {err}"
))
})?;
if let Some(module) = super::module::prepare_module(&extensions)
.map_err(|err| {
SapiGatewayError::Internal(format!(
"build PHP host module: {err}"
))
})?
{
install_additional_module(module).map_err(|err| {
SapiGatewayError::Internal(format!(
"install PHP host module: {err}"
))
})?;
}
let mut sapi_module = build_sapi_module::<ClavironContext>(
"claviron",
"Claviron PHP SAPI",
log_message,
|builder| {
builder
.ini_entries(
"max_input_time=-1\ndisplay_errors=0\ndisplay_startup_errors=0\nhtml_errors=0\nlog_errors=1\nerror_log=\n"
)
.ub_write_function(ub_write)
.flush_function(flush)
.send_headers_function(send_headers)
.send_header_function(send_header)
.read_cookies_function(read_cookies)
.register_server_variables_function(register_server_variables)
.getenv_function(claviron_getenv)
}
)
.map_err(|err| SapiGatewayError::Internal(format!("build sapi module: {err}")))?;
sapi_module.additional_functions =
crate::functions::build_function_entries().map_err(|err| {
SapiGatewayError::Internal(format!(
"build sapi functions: {err}"
))
})?;
Ok(Self(Box::new(sapi_module)))
}
}
extern "C" fn log_message(
message: *const c_char,
syslog_type_int: c_int,
) {
if message.is_null() {
return;
}
let message = sanitize_log_message(
&unsafe { CStr::from_ptr(message) }.to_string_lossy(),
);
if syslog_type_int <= 3 {
PHP_REQUEST_LOGGED_ERROR.with(|flag| flag.set(true));
}
let globals = SapiGlobals::get();
if !globals.server_context.is_null() {
let context =
ClavironContext::from_server_context(globals.server_context);
if let Some((application, instance)) = context.application_log_scope() {
match syslog_type_int {
0..=3 => {
error!(runtime = "php", %application, instance, "{message}");
}
4 => {
warn!(runtime = "php", %application, instance, "{message}")
}
5 | 6 => {
info!(runtime = "php", %application, instance, "{message}")
}
7 => {
debug!(runtime = "php", %application, instance, "{message}")
}
_ => {}
}
return;
}
let mut line = message.as_bytes().to_vec();
if !line.ends_with(b"\n") {
line.push(b'\n');
}
if context.write_program_output(true, &line).is_some() {
let _ = context.flush_program_output(true);
return;
}
}
match syslog_type_int {
0..=3 => {
error!("{message}");
}
4 => warn!("{message}"),
5 | 6 => info!("{message}"),
7 => debug!("{message}"),
_ => {}
}
}
extern "C" fn ub_write(
str: *const c_char,
str_length: usize,
) -> usize {
if str.is_null() || str_length == 0 {
return 0;
}
let sapi_globals = SapiGlobals::get();
if sapi_globals.server_context.is_null() {
return str_length;
}
let context =
ClavironContext::from_server_context(sapi_globals.server_context);
if context.is_request_finished() {
return 0;
}
let data =
unsafe { std::slice::from_raw_parts(str.cast::<u8>(), str_length) };
match context.append_output(data) {
true => str_length,
false => {
super::handle_abort_connection();
0
}
}
}
extern "C" fn flush(server_context: *mut c_void) {
if server_context.is_null() {
return;
}
let context = ClavironContext::from_server_context(server_context);
if !context.php_flush() {
super::handle_abort_connection();
}
}
extern "C" fn send_headers(headers: *mut SapiHeaders) -> c_int {
if headers.is_null() {
return 3;
}
let sapi_globals = SapiGlobals::get();
if sapi_globals.server_context.is_null() {
return 2;
}
let context =
ClavironContext::from_server_context(sapi_globals.server_context);
let code = unsafe { (*headers).http_response_code };
if let Ok(code) = u16::try_from(code)
&& let Ok(status) = StatusCode::from_u16(code)
&& let Some(output) = context.response_output()
&& let Ok(mut output) = output.lock()
{
output.status = status;
}
2
}
extern "C" fn send_header(
header: *mut SapiHeader,
server_context: *mut c_void,
) {
if header.is_null() || server_context.is_null() {
return;
}
let Some(sapi_header) = (unsafe { header.as_ref() }) else {
return;
};
let context = ClavironContext::from_server_context(server_context);
let Some(name) = sapi_header.name() else {
return;
};
if name.eq_ignore_ascii_case("Status") {
if let Some(value) = sapi_header.value()
&& let Some(code) = value.split_whitespace().next()
&& let Ok(code) = code.parse::<u16>()
&& let Ok(status) = StatusCode::from_u16(code)
&& let Some(output) = context.response_output()
&& let Ok(mut output) = output.lock()
{
output.status = status;
}
return;
}
if let Some(value) = sapi_header.value() {
context.append_header(name, value);
}
}
extern "C" fn read_cookies() -> *mut c_char {
let sapi_globals = SapiGlobals::get();
if sapi_globals.server_context.is_null() {
return std::ptr::null_mut();
}
ClavironContext::from_server_context(sapi_globals.server_context)
.cookie_header()
.and_then(|cookie| CString::new(cookie).ok())
.map(CString::into_raw)
.unwrap_or_else(std::ptr::null_mut)
}
const PHP_ENV_PASSTHROUGH: &[&str] = &[
"PATH",
"HOME",
"USER",
"LOGNAME",
"SHELL",
"TMPDIR",
"TMP",
"LANG",
"LC_ALL",
"LC_CTYPE",
"LC_MESSAGES",
"LC_COLLATE",
"LC_TIME",
"LC_NUMERIC",
"LC_MONETARY",
"TZ",
"PHPRC",
"PHP_INI_SCAN_DIR",
"PGCLIENTENCODING",
];
extern "C" fn claviron_getenv(
name: *const c_char,
name_len: usize,
) -> *mut c_char {
if name.is_null() {
return std::ptr::null_mut();
}
let name_bytes =
unsafe { std::slice::from_raw_parts(name.cast::<u8>(), name_len) };
let Ok(name_str) = std::str::from_utf8(name_bytes) else {
return std::ptr::null_mut();
};
let sapi_globals = SapiGlobals::get();
if !sapi_globals.server_context.is_null() {
let context =
ClavironContext::from_server_context(sapi_globals.server_context);
if let Ok(overlay) = context.env_overlay.lock()
&& let Some(value) = overlay.get(name_str)
{
return alloc_libc_cstring(value);
}
if let Some(value) = context.environment_value(name_str) {
return alloc_libc_cstring(value);
}
let mut identity = std::ptr::null_mut();
visit_execution_identity(context, |key, value| {
if key.to_bytes() == name_bytes {
identity = alloc_libc_cstring(value);
}
});
if !identity.is_null() {
return identity;
}
}
if PHP_ENV_PASSTHROUGH.contains(&name_str)
&& let Ok(value) = std::env::var(name_str)
{
return alloc_libc_cstring(&value);
}
std::ptr::null_mut()
}
fn alloc_libc_cstring(s: &str) -> *mut c_char {
let len = s.len();
let ptr = unsafe { libc::malloc(len + 1) } as *mut c_char;
if ptr.is_null() {
return std::ptr::null_mut();
}
unsafe {
if len > 0 {
std::ptr::copy_nonoverlapping(
s.as_ptr() as *const c_char,
ptr,
len,
);
}
*ptr.add(len) = 0;
}
ptr
}
extern "C" fn register_server_variables(vars: *mut Zval) {
if vars.is_null() {
return;
}
let sapi_globals = SapiGlobals::get();
if sapi_globals.server_context.is_null() {
return;
}
let context =
ClavironContext::from_server_context(sapi_globals.server_context);
let crate::engine::PhpExecution::Request(request) = &context.execution
else {
register_program_variables(context, vars);
return;
};
let runtime = &request.runtime;
let proto = match request.parts.version {
Version::HTTP_09 => "HTTP/0.9",
Version::HTTP_10 => "HTTP/1.0",
Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2",
Version::HTTP_3 => "HTTP/3",
_ => "HTTP/1.1",
};
let php_self = format!(
"{}{}",
request.sapi_req.script_name,
request.sapi_req.path_info.as_deref().unwrap_or_default()
);
let request_host =
request.parts.extensions.get::<HostName>().map(|h| h.0.to_cow_str());
let scoped = SERVER_SCOPED_VARS.get();
unsafe {
register_variable(PHP_SELF, php_self, vars);
if let Some(scoped) = scoped {
register_variable_cstr(SERVER_SOFTWARE, &scoped.software, vars);
} else {
register_variable(
SERVER_SOFTWARE,
runtime.server_software.clone(),
vars,
);
}
register_variable(SERVER_PROTOCOL, proto, vars);
register_variable(
DOCUMENT_ROOT,
request.sapi_req.document_root.to_string_lossy().as_bytes(),
vars,
);
register_variable(
SCRIPT_FILENAME,
request.sapi_req.script_file.to_string_lossy().as_bytes(),
vars,
);
register_variable(
PATH_TRANSLATED,
request.sapi_req.script_file.to_string_lossy().as_bytes(),
vars,
);
register_variable(
SCRIPT_NAME,
request.sapi_req.script_name.as_str(),
vars,
);
register_variable(
c"DOCUMENT_URI",
request.sapi_req.document_uri.as_str(),
vars,
);
register_variable(
REQUEST_URI,
request.sapi_req.request_uri.as_str(),
vars,
);
register_variable(REQUEST_METHOD, request.parts.method.as_str(), vars);
register_variable(
QUERY_STRING,
request.sapi_req.query_string.as_str(),
vars,
);
if let Some(request_host) = request_host.as_ref() {
register_variable(SERVER_NAME, request_host.as_ref(), vars);
register_variable(c"HTTP_HOST", request_host.as_ref(), vars);
register_variable(
c"HTTP_X_FORWARDED_HOST",
request_host.as_ref(),
vars,
);
}
if let Some(scheme) =
request.conn_ctx.as_ref().map(|conn| conn.scheme()).or_else(|| {
claviron_http::ext::request_scheme(&request.parts.extensions)
})
{
register_variable(c"REQUEST_SCHEME", scheme.as_str(), vars);
register_variable(c"HTTP_X_FORWARDED_PROTO", scheme.as_str(), vars);
register_variable(
HTTPS,
if scheme == http::uri::Scheme::HTTPS { "on" } else { "off" },
vars,
);
}
if let Some(conn) = request.conn_ctx.as_ref() {
register_variable(SERVER_ADDR, conn.local_ip.to_string(), vars);
register_variable(SERVER_PORT, conn.local_port.to_string(), vars);
let remote_ip = conn.real_ip.unwrap_or_else(|| conn.remote.ip());
register_variable(REMOTE_ADDR, remote_ip.to_string(), vars);
register_variable(
REMOTE_PORT,
conn.remote.port().to_string(),
vars,
);
register_variable(c"HTTP_VIA", conn.remote.ip().to_string(), vars);
if let Some(cc) = conn.country_code.as_deref() {
register_variable(c"CLIENT_COUNTRY", cc, vars);
}
register_tls_vars(conn.tls(), vars);
}
if let Some(ct) = request
.parts
.headers
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
register_variable(c"CONTENT_TYPE", ct, vars);
}
register_variable(
c"CONTENT_LENGTH",
request
.request_body
.content_length(&request.parts.headers)
.to_string(),
vars,
);
register_execution_identity(context, vars);
}
if let Some(path_info) = request.sapi_req.path_info.as_deref() {
unsafe {
register_variable(PATH_INFO, path_info, vars);
}
}
for (key, value) in context.environment() {
if let Ok(name) = CString::new(key.as_str()) {
unsafe {
register_variable(name.as_c_str(), value.as_str(), vars);
}
}
}
for (name, value) in request.parts.headers.iter() {
if claviron_php::headers::is_request_denied(name.as_str()) {
continue;
}
let Ok(value) = value.to_str() else {
continue;
};
let Some(cgi_name) = cgi_header_name(name) else {
continue;
};
unsafe {
register_variable(cgi_name.as_c_str(), value, vars);
}
}
if let Some(joined) = context.cookie_header() {
unsafe {
register_variable(HTTP_COOKIE, joined.as_str(), vars);
}
}
}
fn register_program_variables(
context: &ClavironContext,
vars: *mut Zval,
) {
let Some(program) = context.program() else {
return;
};
let script = &program.script;
unsafe {
register_variable(
SCRIPT_FILENAME,
script.as_os_str().as_encoded_bytes(),
vars,
);
register_variable(
SCRIPT_NAME,
script.as_os_str().as_encoded_bytes(),
vars,
);
register_variable(
PHP_SELF,
script.as_os_str().as_encoded_bytes(),
vars,
);
register_execution_identity(context, vars);
if let Some(scoped) = SERVER_SCOPED_VARS.get() {
register_variable_cstr(SERVER_SOFTWARE, &scoped.software, vars);
}
for (key, value) in context.environment() {
if let Ok(key) = CString::new(key.as_str()) {
register_variable(&key, value, vars);
}
}
super::ffi::php_build_argv(std::ptr::null(), vars);
}
}
fn register_tls_vars(
tls: Option<&TlsInfo>,
vars: *mut Zval,
) {
let Some(tls) = tls else {
return;
};
if let Some(protocol) = tls.ssl_protocol() {
unsafe { register_variable(c"SSL_PROTOCOL", protocol, vars) };
}
if let Some(cipher) = tls.ssl_cipher() {
unsafe { register_variable(c"SSL_CIPHER", cipher, vars) };
}
unsafe {
register_variable(c"SSL_CLIENT_VERIFY", tls.ssl_client_verify(), vars)
};
if let Some(cert) = tls.client_cert.as_ref() {
unsafe {
register_variable(
c"SSL_CLIENT_S_DN",
cert.subject_dn.as_str(),
vars,
);
register_variable(
c"SSL_CLIENT_I_DN",
cert.issuer_dn.as_str(),
vars,
);
}
}
}
fn run_identifier(run_mode: RunMode) -> String {
match run_mode {
RunMode::Standalone => "standalone".to_string(),
RunMode::Master => "master".to_string(),
RunMode::Worker(wid) => format!("worker_{}", wid.raw()),
}
}
fn cgi_header_name(name: &HeaderName) -> Option<CString> {
let raw = name.as_str().as_bytes();
let mut cgi_name = Vec::with_capacity(b"HTTP_".len() + raw.len());
cgi_name.extend_from_slice(b"HTTP_");
for byte in raw {
cgi_name.push(match byte {
b'a'..=b'z' => byte - 32,
b'-' => b'_',
_ => *byte,
});
}
CString::new(cgi_name).ok()
}