v0.29.6
This commit is contained in:
1 parent
3e33982cd3
commit
ad95bfb050
27 files changed
+309
-80
No files matched your search
@@ -9,7 +9,7 @@ use super::{
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use axum::{body::Body, response::Response};
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream;
|
||||
use futures_util::{stream, StreamExt};
|
||||
use http::{
|
||||
header::{ALLOW, CONTENT_LENGTH, CONTENT_TYPE},
|
||||
HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri,
|
||||
|
||||
@@ -178,9 +178,7 @@ async fn php_response_inner(
|
||||
permit,
|
||||
));
|
||||
let mut response = Response::new(if send_body {
|
||||
Body::from_stream(stream::unfold(rx, |mut rx| async move {
|
||||
rx.recv().await.map(|item| (item, rx))
|
||||
}))
|
||||
Body::from_stream(fastcgi_body_stream(rx))
|
||||
} else {
|
||||
Body::empty()
|
||||
});
|
||||
@@ -189,6 +187,18 @@ async fn php_response_inner(
|
||||
response
|
||||
}
|
||||
|
||||
// tower-http's compression encoder may poll a body again after observing EOF.
|
||||
// An unfused `stream::unfold` panics on that second poll, aborting the response
|
||||
// before the encoder can emit its final gzip/br/zstd bytes.
|
||||
pub(super) fn fastcgi_body_stream(
|
||||
rx: mpsc::Receiver<io::Result<Bytes>>,
|
||||
) -> impl futures_util::Stream<Item = io::Result<Bytes>> {
|
||||
stream::unfold(rx, |mut rx| async move {
|
||||
rx.recv().await.map(|item| (item, rx))
|
||||
})
|
||||
.fuse()
|
||||
}
|
||||
|
||||
pub(super) fn method_not_allowed_response() -> Response {
|
||||
let mut response = status_response(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::*;
|
||||
use super::fastcgi::{
|
||||
fastcgi_params, parse_cgi_headers, read_fcgi_record, write_fcgi_record, FCGI_END_REQUEST,
|
||||
FCGI_PARAMS, FCGI_STDIN, FCGI_STDOUT,
|
||||
fastcgi_body_stream, fastcgi_params, parse_cgi_headers, read_fcgi_record, write_fcgi_record,
|
||||
FCGI_END_REQUEST, FCGI_PARAMS, FCGI_STDIN, FCGI_STDOUT,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use http::{
|
||||
header::{
|
||||
ACCEPT_RANGES, ALLOW, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LENGTH,
|
||||
@@ -15,7 +17,7 @@ use http::{
|
||||
use http_body_util::BodyExt;
|
||||
use std::{collections::BTreeMap, path::Path, time::Duration};
|
||||
use tempfile::TempDir;
|
||||
use tokio::{io::AsyncWriteExt, time::Instant};
|
||||
use tokio::{io::AsyncWriteExt, sync::mpsc, time::Instant};
|
||||
|
||||
fn service(root: &Path, listing: bool) -> CompiledService {
|
||||
Service {
|
||||
|
||||
@@ -20,6 +20,66 @@ async fn fastcgi_fixture(response_body: &'static [u8]) -> std::net::SocketAddr {
|
||||
address
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fastcgi_stream_can_be_polled_repeatedly_after_eof() {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
tx.send(Ok(Bytes::from_static(b"data"))).await.unwrap();
|
||||
drop(tx);
|
||||
let mut body = Box::pin(fastcgi_body_stream(rx));
|
||||
assert_eq!(body.next().await.unwrap().unwrap(), Bytes::from_static(b"data"));
|
||||
assert!(body.next().await.is_none());
|
||||
// Without `fuse` this panics inside futures-util::stream::unfold.
|
||||
assert!(body.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn php_fpm_gzip_response_finishes_after_fastcgi_eof() {
|
||||
use std::io::Read;
|
||||
|
||||
let expected = b"FastCGI compressed response regression: 0123456789 0123456789 0123456789";
|
||||
let tmp = TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.php"), b"<?php echo 'placeholder';").unwrap();
|
||||
let address = fastcgi_fixture(expected).await;
|
||||
let configured = Service {
|
||||
id: "php-compression-regression".into(),
|
||||
name: "PHP compression regression".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.php".into()],
|
||||
php: Php {
|
||||
enabled: true,
|
||||
endpoint: format!("tcp:{address}"),
|
||||
connect_timeout_ms: 500,
|
||||
request_timeout_ms: 5_000,
|
||||
max_concurrent_requests: 1,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
configured.validate().unwrap();
|
||||
let service = configured.compile().unwrap();
|
||||
let response = serve_request(&service, &request(Method::GET, "/"), "/").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let mut accept = HeaderMap::new();
|
||||
accept.insert("accept-encoding", "gzip".parse().unwrap());
|
||||
let settings = crate::compression::Settings {
|
||||
gzip: true,
|
||||
br: false,
|
||||
zstd: false,
|
||||
deflate: false,
|
||||
min_bytes: 32,
|
||||
};
|
||||
let response = crate::compression::encode(response, &accept, &Method::GET, &settings).await;
|
||||
assert_eq!(response.headers()[CONTENT_ENCODING], "gzip");
|
||||
|
||||
let compressed = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let mut decoded = Vec::new();
|
||||
flate2::read::GzDecoder::new(compressed.as_ref())
|
||||
.read_to_end(&mut decoded)
|
||||
.unwrap();
|
||||
assert_eq!(decoded.as_slice(), expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn php_fpm_executes_index_and_try_files_front_controller_without_source_disclosure() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
Reference in new issue
Block a user