1use crate::*;
2
3pub fn mount_php(
18 extensions: &mut Extensions,
19 connection: Connection,
20 capture_fn: impl Fn(&FatRequest, &Host) -> bool + Send + Sync + 'static,
21 path_rewrite: Option<
22 impl Fn(&str, &FatRequest, &Host) -> CompactString + Send + Sync + 'static,
23 >,
24) {
25 type DynPathRewrite =
26 Option<Box<dyn Fn(&str, &FatRequest, &Host) -> CompactString + Send + Sync + 'static>>;
27 let path_rewrite: DynPathRewrite = match path_rewrite {
28 Some(x) => Some(Box::new(x)),
29 None => None,
30 };
31 extensions.add_prepare_fn(
32 Box::new(move |req, host| !host.options.disable_fs && capture_fn(req, host)),
33 prepare!(
34 req,
35 host,
36 path,
37 addr,
38 move |connection: Connection, path_rewrite: DynPathRewrite| {
39 let rewriteen_path = path
40 .and_then(Path::to_str)
41 .into_iter()
42 .zip(path_rewrite.iter())
43 .map(|(path, path_rewrite)| path_rewrite(path, req, host))
44 .next();
45 let path = rewriteen_path
46 .or_else(|| path.and_then(Path::to_str).map(|s| s.to_compact_string()));
47 php(req, host, path.as_deref(), addr, connection.clone()).await
48 }
49 ),
50 extensions::Id::new(-8, "PHP").no_override(),
51 );
52}
53pub async fn mount_php_with_working_directory(
63 extensions: &mut Extensions,
64 connection: Connection,
65 capture: impl Into<String>,
66 working_directory: impl Into<PathBuf>,
67) -> Result<(), io::Error> {
68 let working_directory = tokio::fs::canonicalize(working_directory.into()).await?;
69 let working_directory = working_directory.to_string_lossy().to_compact_string();
70 let capture = capture.into();
71 let rewrite_capture = capture.clone();
72 let file_capture = capture.clone();
73 let file_rewrite_capture = capture.clone();
74 let file_working_directory = working_directory.clone();
75 mount_php(
77 extensions,
78 connection,
79 move |req, _host| {
80 req.uri().path().starts_with(&capture) && req.uri().path().ends_with(".php")
81 },
82 Some(move |_path: &str, request: &FatRequest, _host: &Host| {
83 let path = format!(
84 "/{}",
85 request
86 .uri()
87 .path()
88 .strip_prefix(&rewrite_capture)
89 .expect("failed to strip a prefix we guaranteed the URI path starts with")
90 );
91 let decoded = percent_encoding::percent_decode_str(&path)
92 .decode_utf8()
93 .expect("percent decoding was successful earlier in Kvarn");
94 let p = utils::make_path(
95 &working_directory,
96 "",
97 utils::parse::uri(&decoded).unwrap(),
101 None,
102 );
103 p
104 }),
105 );
106 extensions.add_prepare_fn(
107 Box::new(move |req, host| {
108 !host.options.disable_fs
109 && req.uri().path().starts_with(&file_capture)
110 && !req.uri().path().ends_with(".php")
111 }),
112 prepare!(
113 req,
114 host,
115 _path,
116 _addr,
117 move |file_rewrite_capture: String, file_working_directory: CompactString| {
118 if req.method() != Method::GET && req.method() != Method::HEAD {
119 return default_error_response(StatusCode::METHOD_NOT_ALLOWED, host, None)
120 .await;
121 }
122 let path = format!(
123 "/{}",
124 req.uri()
125 .path()
126 .strip_prefix(file_rewrite_capture)
127 .expect("failed to strip a prefix we guaranteed the URI path starts with")
128 );
129 let decoded = percent_encoding::percent_decode_str(&path)
130 .decode_utf8()
131 .expect("percent decoding was successful earlier in Kvarn");
132 let p = utils::make_path(
133 file_working_directory,
134 "",
135 utils::parse::uri(&decoded).unwrap(),
139 None,
140 );
141 let file = read_file_cached(&p, host.file_cache.as_ref()).await;
142 if let Some(file) = file {
143 FatResponse::new(Response::new(file), comprash::ServerCachePreference::Full)
144 } else {
145 default_error_response(StatusCode::NOT_FOUND, host, None).await
146 }
147 }
148 ),
149 extensions::Id::new(-9, "PHP file server").no_override(),
150 );
151 Ok(())
152}
153fn php<'a>(
154 req: &'a mut FatRequest,
155 host: &'a Host,
156 path: Option<&'a str>,
157 address: SocketAddr,
158 connection: Connection,
159) -> RetFut<'a, FatResponse> {
160 box_fut!({
161 if let Some(path) = path {
165 if tokio::fs::metadata(&path).await.is_err() {
166 return default_error_response(StatusCode::NOT_FOUND, host, None).await;
167 }
168
169 let body = match req.body_mut().read_to_bytes(1024 * 1024 * 16).await {
170 Ok(body) => body,
171 Err(_) => {
172 return FatResponse::cache(
173 default_error(
174 StatusCode::BAD_REQUEST,
175 Some(host),
176 Some("failed to read body".as_bytes()),
177 )
178 .await,
179 )
180 }
181 };
182 let output =
183 match fastcgi::from_prepare(req, &body, Path::new(path), address, connection).await
184 {
185 Ok(vec) => vec,
186 Err(err) => {
187 error!("FastCGI failed. {}", err);
188 return default_error_response(
189 StatusCode::INTERNAL_SERVER_ERROR,
190 host,
191 None,
192 )
193 .await;
194 }
195 };
196 let output = Bytes::copy_from_slice(&output);
197 match async_bits::read::response_php(&output) {
198 Ok(response) => FatResponse::cache(response),
199 Err(err) => {
200 error!("failed to parse response; {}", err.as_str());
201 default_error_response(StatusCode::NOT_FOUND, host, None).await
202 }
203 }
204 } else {
205 error!("Path is none. This is a internal contract error.");
206 default_error_response(StatusCode::INTERNAL_SERVER_ERROR, host, None).await
207 }
208 })
209}
210
211#[cfg(test)]
212mod tests {
213 use kvarn_testing::prelude::*;
214
215 #[tokio::test]
216 async fn no_fs() {
217 let server = ServerBuilder::from(crate::new())
218 .with_options(|options| {
219 options.disable_fs();
220 })
221 .run()
222 .await;
223
224 let response = server.get("index.php").send().await.unwrap();
225 assert_eq!(response.status().as_str(), StatusCode::NOT_FOUND.as_str());
226 }
227}