1use std::path::{Path, PathBuf};
7
8use crate::execution::ExecutionContext;
9use crate::sapi::ServerVars;
10
11#[cfg(feature = "tracing")]
12use tracing::debug;
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum WebRequestError {
18 MissingMethod,
19 InvalidMethod(String),
20 ScriptNotFound(PathBuf),
21}
22
23impl std::fmt::Display for WebRequestError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::MissingMethod => write!(f, "HTTP method not specified"),
27 Self::InvalidMethod(m) => write!(f, "Invalid HTTP method: {}", m),
28 Self::ScriptNotFound(path) => {
29 write!(f, "Script not found: {}", path.display())
30 }
31 }
32 }
33}
34
35impl std::error::Error for WebRequestError {}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Method {
40 Get,
41 Post,
42 Put,
43 Delete,
44 Patch,
45 Head,
46 Options,
47}
48
49impl TryFrom<&str> for Method {
50 type Error = String;
51
52 fn try_from(value: &str) -> Result<Self, Self::Error> {
53 match value.to_uppercase().as_str() {
54 "GET" => Ok(Method::Get),
55 "POST" => Ok(Method::Post),
56 "PUT" => Ok(Method::Put),
57 "DELETE" => Ok(Method::Delete),
58 "PATCH" => Ok(Method::Patch),
59 "HEAD" => Ok(Method::Head),
60 "OPTIONS" => Ok(Method::Options),
61 _ => Err(format!("Invalid HTTP method: {}", value)),
62 }
63 }
64}
65
66impl TryFrom<String> for Method {
67 type Error = String;
68
69 fn try_from(value: String) -> Result<Self, Self::Error> {
70 Method::try_from(value.as_str())
71 }
72}
73
74impl std::fmt::Display for Method {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80impl Method {
81 pub fn as_str(&self) -> &'static str {
82 match &self {
83 Method::Get => "GET",
84 Method::Post => "POST",
85 Method::Put => "PUT",
86 Method::Delete => "DELETE",
87 Method::Patch => "PATCH",
88 Method::Head => "HEAD",
89 Method::Options => "OPTIONS",
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
99pub struct WebRequest {
100 https: bool,
101 body: Vec<u8>,
102 server_port: u16,
103 remote_port: u16,
104 uri: Option<String>,
105 remote_addr: String,
106 server_addr: String,
107 method: Option<Method>,
108 server_name: String,
109 server_protocol: String,
110 path_info: Option<String>,
111 headers: Vec<(String, String)>,
112 cookies: Vec<(String, String)>,
113 document_root: Option<PathBuf>,
114 env_vars: Vec<(String, String)>,
115 ini_overrides: Vec<(String, String)>,
116}
117
118impl Default for WebRequest {
119 fn default() -> Self {
120 Self {
121 uri: None,
122 method: None,
123 server_name: "localhost".to_string(),
124 server_port: 80,
125 server_protocol: "HTTP/1.1".to_string(),
126 remote_addr: "127.0.0.1".to_string(),
127 remote_port: 0,
128 server_addr: "127.0.0.1".to_string(),
129 https: false,
130 headers: Vec::new(),
131 cookies: Vec::new(),
132 body: Vec::new(),
133 document_root: None,
134 path_info: None,
135 env_vars: Vec::new(),
136 ini_overrides: vec![
137 ("log_errors".to_string(), "1".to_string()),
138 ("html_errors".to_string(), "0".to_string()),
139 ("request_order".to_string(), "GP".to_string()),
140 ("display_errors".to_string(), "1".to_string()),
141 ("implicit_flush".to_string(), "0".to_string()),
142 ("variables_order".to_string(), "EGPCS".to_string()),
143 ("output_buffering".to_string(), "4096".to_string()),
144 ],
145 }
146 }
147}
148
149impl WebRequest {
150 #[must_use]
151 pub fn new(method: Method) -> Self {
152 Self {
153 method: Some(method),
154 ..Default::default()
155 }
156 }
157
158 #[must_use]
159 pub fn get() -> Self {
160 Self::new(Method::Get)
161 }
162
163 #[must_use]
164 pub fn post() -> Self {
165 Self::new(Method::Post)
166 }
167
168 #[must_use]
169 pub fn put() -> Self {
170 Self::new(Method::Put)
171 }
172
173 #[must_use]
174 pub fn delete() -> Self {
175 Self::new(Method::Delete)
176 }
177
178 #[must_use]
179 pub fn patch() -> Self {
180 Self::new(Method::Patch)
181 }
182
183 #[must_use]
184 pub fn head() -> Self {
185 Self::new(Method::Head)
186 }
187
188 #[must_use]
189 pub fn options() -> Self {
190 Self::new(Method::Options)
191 }
192
193 #[must_use]
194 pub fn with_uri(mut self, uri: impl Into<String>) -> Self {
195 self.uri = Some(uri.into());
196 self
197 }
198
199 #[must_use]
200 pub fn with_header(
201 mut self,
202 name: impl Into<String>,
203 value: impl Into<String>,
204 ) -> Self {
205 self.headers
206 .push((name.into(), value.into()));
207 self
208 }
209
210 #[must_use]
211 pub fn with_headers<I, K, V>(mut self, iter: I) -> Self
212 where
213 I: IntoIterator<Item = (K, V)>,
214 K: Into<String>,
215 V: Into<String>,
216 {
217 self.headers.extend(
218 iter.into_iter()
219 .map(|(k, v)| (k.into(), v.into())),
220 );
221 self
222 }
223
224 #[must_use]
225 pub fn with_cookie(
226 mut self,
227 name: impl Into<String>,
228 value: impl Into<String>,
229 ) -> Self {
230 self.cookies
231 .push((name.into(), value.into()));
232 self
233 }
234
235 #[must_use]
236 pub fn with_cookies<I, K, V>(mut self, iter: I) -> Self
237 where
238 I: IntoIterator<Item = (K, V)>,
239 K: Into<String>,
240 V: Into<String>,
241 {
242 self.cookies.extend(
243 iter.into_iter()
244 .map(|(k, v)| (k.into(), v.into())),
245 );
246 self
247 }
248
249 #[must_use]
250 pub fn with_body(mut self, bytes: impl Into<Vec<u8>>) -> Self {
251 self.body = bytes.into();
252 self
253 }
254
255 #[must_use]
256 pub fn with_content_type(self, ct: impl Into<String>) -> Self {
257 self.with_header("Content-Type", ct)
258 }
259
260 #[must_use]
261 pub fn with_raw_cookie_header(
262 self,
263 cookie_string: impl Into<String>,
264 ) -> Self {
265 self.with_header("Cookie", cookie_string)
266 }
267
268 #[must_use]
269 pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
270 self.server_name = name.into();
271 self
272 }
273
274 #[must_use]
275 pub fn with_server_port(mut self, port: u16) -> Self {
276 self.server_port = port;
277 self
278 }
279
280 #[must_use]
281 pub fn with_server_protocol(mut self, proto: impl Into<String>) -> Self {
282 self.server_protocol = proto.into();
283 self
284 }
285
286 #[must_use]
287 pub fn with_remote_addr(mut self, addr: impl Into<String>) -> Self {
288 self.remote_addr = addr.into();
289 self
290 }
291
292 #[must_use]
293 pub fn with_remote_port(mut self, port: u16) -> Self {
294 self.remote_port = port;
295 self
296 }
297
298 #[must_use]
299 pub fn with_server_addr(mut self, addr: impl Into<String>) -> Self {
300 self.server_addr = addr.into();
301 self
302 }
303
304 #[must_use]
305 pub fn with_https(mut self, enabled: bool) -> Self {
306 self.https = enabled;
307 self
308 }
309
310 #[must_use]
311 pub fn with_document_root(mut self, path: impl Into<PathBuf>) -> Self {
312 self.document_root = Some(path.into());
313 self
314 }
315
316 #[must_use]
317 pub fn with_path_info(mut self, path: impl Into<String>) -> Self {
318 self.path_info = Some(path.into());
319 self
320 }
321
322 #[must_use]
323 pub fn with_env(
324 mut self,
325 key: impl Into<String>,
326 value: impl Into<String>,
327 ) -> Self {
328 self.env_vars
329 .push((key.into(), value.into()));
330
331 self
332 }
333
334 #[must_use]
335 pub fn with_envs<I, K, V>(mut self, iter: I) -> Self
336 where
337 I: IntoIterator<Item = (K, V)>,
338 K: Into<String>,
339 V: Into<String>,
340 {
341 self.env_vars.extend(
342 iter.into_iter()
343 .map(|(k, v)| (k.into(), v.into())),
344 );
345
346 self
347 }
348
349 #[must_use]
350 pub fn with_ini(
351 mut self,
352 key: impl Into<String>,
353 value: impl Into<String>,
354 ) -> Self {
355 self.ini_overrides
356 .push((key.into(), value.into()));
357
358 self
359 }
360
361 #[must_use]
362 pub fn with_ini_overrides<I, K, V>(mut self, iter: I) -> Self
363 where
364 I: IntoIterator<Item = (K, V)>,
365 K: Into<String>,
366 V: Into<String>,
367 {
368 self.ini_overrides.extend(
369 iter.into_iter()
370 .map(|(k, v)| (k.into(), v.into())),
371 );
372
373 self
374 }
375
376 pub fn build(
377 self,
378 script_path: impl AsRef<Path>,
379 ) -> Result<ExecutionContext, WebRequestError> {
380 let method = self
381 .method
382 .ok_or(WebRequestError::MissingMethod)?;
383
384 #[cfg(feature = "tracing")]
385 debug!(
386 method = %method,
387 uri = ?self.uri,
388 "Building Web request"
389 );
390
391 let script_path = script_path
392 .as_ref()
393 .to_path_buf();
394
395 if !script_path.exists() {
396 return Err(WebRequestError::ScriptNotFound(script_path));
397 }
398
399 let script_filename = std::fs::canonicalize(&script_path)
400 .unwrap_or_else(|_| script_path.clone());
401
402 let uri = self.uri.unwrap_or_else(|| {
403 format!(
404 "/{}",
405 script_filename
406 .file_name()
407 .map(|s| s
408 .to_string_lossy()
409 .into_owned())
410 .unwrap_or_default()
411 )
412 });
413
414 let document_root = self
415 .document_root
416 .unwrap_or_else(|| {
417 script_filename
418 .parent()
419 .map(|p| p.to_path_buf())
420 .unwrap_or_else(|| PathBuf::from("/"))
421 });
422
423 let (path, query_string) = parse_uri(&uri);
424
425 let mut vars = ServerVars::web_defaults();
426
427 vars.request_method(method.as_str())
428 .request_uri(&uri)
429 .query_string(&query_string.unwrap_or_default())
430 .script_filename(&script_filename)
431 .script_name(&path)
432 .document_root(&document_root)
433 .server_name(&self.server_name)
434 .server_port(self.server_port)
435 .server_addr(&self.server_addr)
436 .remote_addr(&self.remote_addr)
437 .remote_port(self.remote_port)
438 .server_protocol(&self.server_protocol)
439 .https(self.https);
440
441 if let Some(ref path_info) = self.path_info {
442 vars.path_info(path_info, &document_root);
443 }
444
445 if !self.cookies.is_empty() {
446 let cookie_str = self
447 .cookies
448 .iter()
449 .map(|(k, v)| format!("{}={}", k, v))
450 .collect::<Vec<_>>()
451 .join("; ");
452
453 vars.cookies(&cookie_str);
454 }
455
456 let mut has_content_type = false;
457 let mut has_content_length = false;
458
459 for (name, value) in &self.headers {
460 if name.eq_ignore_ascii_case("Content-Type") {
461 has_content_type = true;
462 } else if name.eq_ignore_ascii_case("Content-Length") {
463 has_content_length = true;
464 }
465 vars.http_header(name, value);
466 }
467
468 if !has_content_type && !self.body.is_empty() {
469 vars.content_type("application/octet-stream");
470 }
471
472 if !has_content_length && !self.body.is_empty() {
473 vars.content_length(self.body.len());
474 }
475
476 Ok(ExecutionContext {
477 script_path,
478 server_vars: vars,
479 input: self.body,
480 env_vars: self.env_vars,
481 ini_overrides: self.ini_overrides,
482 log_to_stderr: false,
483 })
484 }
485}
486
487fn parse_uri(uri: &str) -> (String, Option<String>) {
488 match uri.find('?') {
489 Some(pos) => (uri[..pos].to_string(), Some(uri[pos + 1..].to_string())),
490 None => (uri.to_string(), None),
491 }
492}
493
494#[cfg(feature = "http")]
495mod http_compat {
496 use super::*;
497
498 pub fn from_http_request<B: AsRef<[u8]>>(
499 req: http::Request<B>,
500 script_path: impl AsRef<Path>,
501 ) -> Result<ExecutionContext, WebRequestError> {
502 let (parts, body) = req.into_parts();
503 from_http_parts(parts, body.as_ref().to_vec(), script_path)
504 }
505
506 pub fn from_http_parts(
507 parts: http::request::Parts,
508 body: Vec<u8>,
509 script_path: impl AsRef<Path>,
510 ) -> Result<ExecutionContext, WebRequestError> {
511 let method = Method::try_from(parts.method.as_str()).map_err(|_| {
512 WebRequestError::InvalidMethod(parts.method.to_string())
513 })?;
514
515 let mut builder =
516 WebRequest::new(method).with_uri(parts.uri.to_string());
517
518 for (name, value) in parts.headers.iter() {
519 if let Ok(value_str) = value.to_str() {
520 builder = builder.with_header(name.as_str(), value_str);
521 }
522 }
523
524 builder = builder.with_body(body);
525
526 builder.build(script_path)
527 }
528}
529
530#[cfg(feature = "http")]
531pub use http_compat::{from_http_parts, from_http_request};
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use std::path::PathBuf;
537
538 fn php_script_path(name: &str) -> PathBuf {
539 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
540 .join("tests/php_scripts")
541 .join(name)
542 }
543
544 #[test]
545 fn test_web_request_sets_log_to_stderr_false() {
546 let script_path = php_script_path("hello.php");
547 let ctx = WebRequest::get()
548 .build(&script_path)
549 .expect("failed to build Web request");
550
551 assert!(
552 !ctx.log_to_stderr,
553 "Web request should set log_to_stderr to false"
554 );
555 }
556
557 #[test]
558 fn test_web_execution_captures_messages() {
559 use crate::RiphtSapi;
560
561 let sapi = RiphtSapi::instance();
562 let script_path = php_script_path("error_log_test.php");
563
564 let ctx = WebRequest::get()
565 .build(&script_path)
566 .expect("failed to build Web request");
567
568 let result = sapi
569 .execute(ctx)
570 .expect("execution should succeed");
571
572 assert!(
573 result.all_messages().any(|_| true),
574 "Web execution should capture error_log messages"
575 );
576
577 assert!(
578 result
579 .all_messages()
580 .any(|m| m
581 .message
582 .contains("Test error log message")),
583 "Should contain the error_log message"
584 );
585 }
586}