v0.29.5
This commit is contained in:
1 parent
dd8c195600
commit
3e33982cd3
20 files changed
+287
-52
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.29.4",
|
||||
"version": "0.29.5",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
@@ -25,7 +25,7 @@ use tokio::{
|
||||
const FCGI_VERSION_1: u8 = 1;
|
||||
const FCGI_BEGIN_REQUEST: u8 = 1;
|
||||
pub(super) const FCGI_END_REQUEST: u8 = 3;
|
||||
const FCGI_PARAMS: u8 = 4;
|
||||
pub(super) const FCGI_PARAMS: u8 = 4;
|
||||
pub(super) const FCGI_STDIN: u8 = 5;
|
||||
pub(super) const FCGI_STDOUT: u8 = 6;
|
||||
const FCGI_STDERR: u8 = 7;
|
||||
|
||||
@@ -106,6 +106,38 @@ async fn serve_inner(
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
// An existing PHP script followed by PATH_INFO is a valid virtual PHP
|
||||
// request, even though the full URL is not a filesystem path. Dispatch it
|
||||
// before try_files can rewrite /static.php/skins/... to /index.php (as in
|
||||
// Roundcube 1.7), while still applying the original path's rewrites and
|
||||
// access controls above.
|
||||
if let Ok(components) = path_components(&rewritten_path) {
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
if let Some(php) = php_for_path(service, &PathBuf::from(&script_path)) {
|
||||
return php_response(
|
||||
service,
|
||||
php,
|
||||
method,
|
||||
headers,
|
||||
uri,
|
||||
script_path,
|
||||
Some(path_info),
|
||||
rewritten_query,
|
||||
body,
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (effective_path, effective_query) = match resolve_try_files(
|
||||
service,
|
||||
&rewritten_path,
|
||||
@@ -140,6 +172,13 @@ async fn serve_inner(
|
||||
if matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) =>
|
||||
{
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let public_path = PathBuf::from(&script_path);
|
||||
if let Some(php) = php_for_path(service, &public_path) {
|
||||
return php_response(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
include!("tests/part_01.rs");
|
||||
include!("tests/part_02.rs");
|
||||
include!("tests/part_03.rs");
|
||||
include!("tests/part_04.rs");
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use super::fastcgi::{
|
||||
fastcgi_params, parse_cgi_headers, read_fcgi_record, write_fcgi_record, FCGI_END_REQUEST,
|
||||
FCGI_STDIN, FCGI_STDOUT,
|
||||
FCGI_PARAMS, FCGI_STDIN, FCGI_STDOUT,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
use http::{
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// Decode the actual FastCGI PARAMS records received by a mock PHP-FPM peer.
|
||||
// This tests the Web Service dispatch and parameter generation together.
|
||||
fn decode_fastcgi_params(mut bytes: &[u8]) -> BTreeMap<String, String> {
|
||||
fn read_len(bytes: &mut &[u8]) -> usize {
|
||||
let prefix = bytes[0];
|
||||
if prefix & 0x80 == 0 {
|
||||
*bytes = &bytes[1..];
|
||||
prefix as usize
|
||||
} else {
|
||||
let value = u32::from_be_bytes(bytes[..4].try_into().unwrap()) & 0x7fff_ffff;
|
||||
*bytes = &bytes[4..];
|
||||
value as usize
|
||||
}
|
||||
}
|
||||
let mut result = BTreeMap::new();
|
||||
while !bytes.is_empty() {
|
||||
let name_len = read_len(&mut bytes);
|
||||
let value_len = read_len(&mut bytes);
|
||||
let name = std::str::from_utf8(&bytes[..name_len]).unwrap().to_owned();
|
||||
let value = std::str::from_utf8(&bytes[name_len..name_len + value_len]).unwrap().to_owned();
|
||||
bytes = &bytes[name_len + value_len..];
|
||||
result.insert(name, value);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn fastcgi_fixture_record_params() -> (
|
||||
std::net::SocketAddr,
|
||||
tokio::sync::oneshot::Receiver<BTreeMap<String, String>>,
|
||||
) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
let (mut stream, _) = listener.accept().await.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut params = Vec::new();
|
||||
loop {
|
||||
let (kind, content) = read_fcgi_record(&mut stream, deadline).await.unwrap();
|
||||
if kind == FCGI_PARAMS {
|
||||
params.extend_from_slice(&content);
|
||||
}
|
||||
if kind == FCGI_STDIN && content.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = tx.send(decode_fastcgi_params(¶ms));
|
||||
write_fcgi_record(&mut stream, FCGI_STDOUT,
|
||||
b"Status: 200 OK\r\nContent-Type: text/css\r\n\r\nPHP-STATIC-OK").await.unwrap();
|
||||
write_fcgi_record(&mut stream, FCGI_STDOUT, &[]).await.unwrap();
|
||||
write_fcgi_record(&mut stream, FCGI_END_REQUEST, &[0u8; 8]).await.unwrap();
|
||||
stream.flush().await.unwrap();
|
||||
});
|
||||
(address, rx)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn roundcube_static_php_path_info_runs_before_front_controller_try_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("static.php"), b"<?php // static asset handler").unwrap();
|
||||
std::fs::write(tmp.path().join("index.php"), b"<?php // front controller").unwrap();
|
||||
// This is the Roundcube v1.7 URL layout; /static.php/skins/... is
|
||||
// virtual PATH_INFO, not a readable directory on disk.
|
||||
let uri = "/static.php/skins/elastic/images/favicon.ico?s=1783249197";
|
||||
let (address, received) = fastcgi_fixture_record_params().await;
|
||||
let mut configured = Service {
|
||||
id: "roundcube".into(),
|
||||
name: "Roundcube".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.php".into()],
|
||||
try_files: vec!["$uri".into(), "$uri/".into(), "/index.php?$query_string".into()],
|
||||
php: Php {
|
||||
enabled: true,
|
||||
path_info: true,
|
||||
endpoint: format!("tcp:{address}"),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
configured.validate().unwrap();
|
||||
let service = configured.compile().unwrap();
|
||||
let response = serve_request(&service, &request(Method::GET, uri),
|
||||
"/static.php/skins/elastic/images/favicon.ico").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.headers()[CONTENT_TYPE], "text/css");
|
||||
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "PHP-STATIC-OK");
|
||||
let params = received.await.unwrap();
|
||||
assert_eq!(params["SCRIPT_FILENAME"].as_str(), tmp.path().join("static.php").to_string_lossy().as_ref());
|
||||
assert_eq!(params["SCRIPT_NAME"], "/static.php");
|
||||
assert_eq!(params["PATH_INFO"], "/skins/elastic/images/favicon.ico");
|
||||
assert_eq!(params["QUERY_STRING"], "s=1783249197");
|
||||
assert_eq!(params["REQUEST_URI"], uri);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn without_php_path_info_try_files_keeps_legacy_front_controller_fallback() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("static.php"), b"<?php // static asset handler").unwrap();
|
||||
std::fs::write(tmp.path().join("index.php"), b"<?php // front controller").unwrap();
|
||||
let (address, received) = fastcgi_fixture_record_params().await;
|
||||
let mut configured = Service {
|
||||
id: "roundcube-no-path-info".into(),
|
||||
name: "Roundcube without PATH_INFO".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
try_files: vec!["$uri".into(), "/index.php?$query_string".into()],
|
||||
php: Php {
|
||||
enabled: true,
|
||||
path_info: false,
|
||||
endpoint: format!("tcp:{address}"),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
configured.validate().unwrap();
|
||||
let service = configured.compile().unwrap();
|
||||
let uri = "/static.php/plugins/zipdownload/zipdownload.min.js?s=1783249197";
|
||||
let response = serve_request(&service, &request(Method::GET, uri),
|
||||
"/static.php/plugins/zipdownload/zipdownload.min.js").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let _ = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let params = received.await.unwrap();
|
||||
assert_eq!(params["SCRIPT_FILENAME"].as_str(), tmp.path().join("index.php").to_string_lossy().as_ref());
|
||||
assert_eq!(params["SCRIPT_NAME"], "/index.php");
|
||||
assert!(!params.contains_key("PATH_INFO"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user