ripht_php_sapi/sapi/
mod.rs1use std::ffi::CString;
18use std::sync::OnceLock;
19
20use thiserror::Error;
21
22#[cfg(feature = "tracing")]
23use tracing::{error, info, trace};
24
25pub(crate) mod callbacks;
26mod executor;
27pub(crate) mod ffi;
28pub(crate) mod server_context;
29pub(crate) mod server_vars;
30
31pub use executor::{ExecutionError, Executor};
32pub(crate) use server_vars::{ServerVars, ServerVarsCString};
33
34use crate::execution::{ExecutionContext, ExecutionHooks, ExecutionResult};
35
36static PHP_INIT_RESULT: OnceLock<Result<(), SapiError>> = OnceLock::new();
37
38pub(crate) static SAPI_NAME: &[u8] = b"ripht\0";
39pub(crate) static SAPI_PRETTY_NAME: &[u8] = b"Ripht PHP SAPI\0";
40pub(crate) static SERVER_SOFTWARE: &str =
41 concat!("Ripht/", env!("CARGO_PKG_VERSION"));
42static INI_ENTRIES: &[u8] = b"\
43variables_order=EGPCS\n\
44request_order=GP\n\
45output_buffering=4096\n\
46implicit_flush=0\n\
47html_errors=0\n\
48display_errors=1\n\
49log_errors=1\n\
50\0";
51
52#[derive(Debug, Clone, Error)]
54#[non_exhaustive]
55pub enum SapiError {
56 #[error("PHP engine not initialized")]
57 NotInitialized,
58
59 #[error("PHP initialization failed: {0}")]
60 InitializationFailed(String),
61
62 #[error("INI key contains null byte")]
63 InvalidIniKey,
64
65 #[error("INI value contains null byte")]
66 InvalidIniValue,
67
68 #[error("Failed to set INI: {0}")]
69 IniSetFailed(String),
70
71 #[error(
72 "PHP library not found. Build PHP with --enable-embed=static and set RIPHT_PHP_SAPI_PREFIX"
73 )]
74 LibraryNotFound,
75}
76
77pub struct RiphtSapi {
79 _marker: std::marker::PhantomData<*mut ()>,
80}
81
82impl RiphtSapi {
83 #[must_use]
85 pub fn instance() -> Self {
86 Self::init().expect("SAPI initialization failure")
87 }
88
89 fn init() -> Result<Self, SapiError> {
90 let init_result = PHP_INIT_RESULT.get_or_init(|| {
91 #[cfg(feature = "tracing")]
92 info!("Initializing RiphtSapi");
93
94 unsafe {
97 ffi::sapi_module.name = SAPI_NAME.as_ptr() as *mut _;
98 ffi::sapi_module.pretty_name =
99 SAPI_PRETTY_NAME.as_ptr() as *mut _;
100
101 ffi::sapi_module.startup = Some(callbacks::ripht_sapi_startup);
103 ffi::sapi_module.shutdown =
104 Some(callbacks::ripht_sapi_shutdown);
105 ffi::sapi_module.activate =
106 Some(callbacks::ripht_sapi_activate);
107 ffi::sapi_module.deactivate =
108 Some(callbacks::ripht_sapi_deactivate);
109
110 ffi::sapi_module.ub_write =
111 Some(callbacks::ripht_sapi_ub_write);
112 ffi::sapi_module.flush = Some(callbacks::ripht_sapi_flush);
113
114 ffi::sapi_module.send_headers =
115 Some(callbacks::ripht_sapi_send_headers);
116 ffi::sapi_module.send_header =
117 Some(callbacks::ripht_sapi_send_header);
118
119 ffi::sapi_module.read_post =
120 Some(callbacks::ripht_sapi_read_post);
121 ffi::sapi_module.read_cookies =
122 Some(callbacks::ripht_sapi_read_cookies);
123
124 ffi::sapi_module.register_server_variables =
125 Some(callbacks::ripht_sapi_register_server_variables);
126
127 ffi::sapi_module.log_message =
128 Some(callbacks::ripht_sapi_log_message);
129 ffi::sapi_module.get_request_time =
130 Some(callbacks::ripht_sapi_get_request_time);
131 ffi::sapi_module.getenv = Some(callbacks::ripht_sapi_getenv);
132
133 ffi::sapi_module.php_ini_ignore = 0;
134 ffi::sapi_module.php_ini_ignore_cwd = 1;
135
136 ffi::sapi_module.input_filter =
137 Some(callbacks::ripht_sapi_input_filter);
138 ffi::sapi_module.default_post_reader =
139 Some(callbacks::ripht_sapi_default_post_reader);
140 ffi::sapi_module.treat_data =
141 Some(callbacks::ripht_sapi_treat_data);
142
143 ffi::sapi_module.ini_entries = INI_ENTRIES.as_ptr() as *const _;
144
145 #[cfg(feature = "tracing")]
146 trace!("Starting SAPI");
147
148 ffi::sapi_startup(&mut ffi::sapi_module);
149
150 #[cfg(feature = "tracing")]
151 trace!("Initializing SAPI module");
152
153 let result = ffi::php_module_startup(
154 &mut ffi::sapi_module,
155 std::ptr::null_mut(),
156 );
157
158 if result == ffi::FAILURE {
159 #[cfg(feature = "tracing")]
160 error!("SAPI module startup failed");
161
162 ffi::sapi_shutdown();
163
164 Err(SapiError::InitializationFailed(
165 "SAPI module initialization failed".to_string(),
166 ))
167 } else {
168 #[cfg(feature = "tracing")]
169 info!("SAPI module initialized");
170 Ok(())
171 }
172 }
173 });
174
175 match init_result {
176 Ok(()) => Ok(Self {
177 _marker: std::marker::PhantomData,
178 }),
179 Err(e) => Err(e.clone()),
182 }
183 }
184
185 pub fn shutdown() {
187 unsafe {
188 ffi::php_module_shutdown();
189 ffi::sapi_shutdown();
190 }
191 }
192
193 pub fn set_ini(
194 &self,
195 key: impl Into<Vec<u8>>,
196 value: impl Into<Vec<u8>>,
197 ) -> Result<(), SapiError> {
198 let k_str = key.into();
199 let v_str = value.into();
200
201 let key_cstr = CString::new(k_str.clone())
202 .map_err(|_| SapiError::InvalidIniKey)?;
203 let value_cstr = CString::new(v_str.clone())
204 .map_err(|_| SapiError::InvalidIniValue)?;
205
206 unsafe {
207 let init = ffi::zend_string_init_interned
208 .expect("zend_string_init_interned is null");
209
210 let name = init(key_cstr.as_ptr(), k_str.len(), true);
211
212 if name.is_null() {
213 return Err(SapiError::IniSetFailed(
214 String::from_utf8(k_str).unwrap_or_default(),
215 ));
216 }
217
218 let result = ffi::zend_alter_ini_entry_chars(
219 name,
220 value_cstr.as_ptr(),
221 v_str.len(),
222 ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
223 ffi::ZEND_INI_STAGE_RUNTIME,
224 );
225
226 if result != ffi::SUCCESS {
227 return Err(SapiError::IniSetFailed(
228 String::from_utf8(k_str).unwrap_or_default(),
229 ));
230 }
231
232 Ok(())
233 }
234 }
235
236 pub fn get_ini(&self, key: &str) -> Option<String> {
237 #[cfg(feature = "tracing")]
238 trace!(ini_key = key, "Getting INI value");
239
240 let key_cstr = CString::new(key).ok()?;
241
242 unsafe {
243 let ptr = ffi::zend_ini_string(key_cstr.as_ptr(), key.len(), 0);
244 if ptr.is_null() {
245 None
246 } else {
247 Some(
248 std::ffi::CStr::from_ptr(ptr)
249 .to_string_lossy()
250 .into_owned(),
251 )
252 }
253 }
254 }
255
256 pub fn executor(&self) -> Result<Executor<'_>, SapiError> {
257 Executor::new(self)
258 }
259
260 pub fn execute(
261 &self,
262 ctx: ExecutionContext,
263 ) -> Result<ExecutionResult, ExecutionError> {
264 self.executor()
265 .map_err(|_| ExecutionError::NotInitialized)?
266 .execute(ctx)
267 }
268
269 pub fn execute_streaming<F>(
270 &self,
271 ctx: ExecutionContext,
272 on_output: F,
273 ) -> Result<ExecutionResult, ExecutionError>
274 where
275 F: FnMut(&[u8]) + 'static,
276 {
277 self.executor()
278 .map_err(|_| ExecutionError::NotInitialized)?
279 .execute_streaming(ctx, on_output)
280 }
281
282 pub fn execute_with_hooks<H: ExecutionHooks + 'static>(
283 &self,
284 ctx: ExecutionContext,
285 hooks: H,
286 ) -> Result<ExecutionResult, ExecutionError> {
287 self.executor()
288 .map_err(|_| ExecutionError::NotInitialized)?
289 .execute_with_hooks(ctx, hooks)
290 }
291
292 pub fn is_initialized(&self) -> bool {
293 PHP_INIT_RESULT
294 .get()
295 .map(|result| result.is_ok())
296 .unwrap_or(false)
297 }
298}