Cargo.toml
src/lib.rs
use std::net::SocketAddr;
use std::collections::BTreeMap;
use base64::Engine;
use bytes::Bytes;
use serde::Serialize;
use ext_php_rs::prelude::*;
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
use warp::http::{HeaderMap, Method, StatusCode};
use warp::path::FullPath;
fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
PhpException::default(e.to_string())
}
#[derive(Serialize)]
struct DebugRequest {
method: String,
path: String,
query: String,
headers: BTreeMap<String, String>,
body: DebugBody,
}
#[derive(Serialize)]
#[serde(tag = "type", content = "value")]
enum DebugBody {
Empty,
Utf8(String),
Base64(String),
}
fn headers_to_map(headers: HeaderMap) -> BTreeMap<String, String> {
let mut map = BTreeMap::new();
for (name, value) in headers.iter() {
let key = name.to_string();
let val = match value.to_str() {
Ok(s) => s.to_string(),
Err(_) => {
// 非UTF-8は bytes を base64 表現で入れる(ヘッダは基本UTF-8想定だが安全側)
let b64 = base64::engine::general_purpose::STANDARD.encode(value.as_bytes());
format!("base64:{b64}")
}
};
map.insert(key, val);
}
map
}
fn bytes_to_body(body: Bytes) -> DebugBody {
if body.is_empty() {
return DebugBody::Empty;
}
match std::str::from_utf8(body.as_ref()) {
Ok(s) => DebugBody::Utf8(s.to_string()),
Err(_) => {
let b64 = base64::engine::general_purpose::STANDARD.encode(body.as_ref());
DebugBody::Base64(b64)
}
}
}
async fn serve_once(
host: String,
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// /debug に来たリクエストを“解析してそのまま返す”
let debug = warp::path("debug")
.and(warp::path::end())
.and(warp::method())
.and(warp::path::full())
.and(
warp::query::raw()
.or_else(|_| async { Ok::<(String,), std::convert::Infallible>((String::new(),)) }),
)
.and(warp::header::headers_cloned())
.and(warp::body::content_length_limit(1024 * 1024).and(warp::body::bytes()))
.and_then(
|method: Method,
path: FullPath,
query: String,
headers: HeaderMap,
body: Bytes| async move {
let payload = DebugRequest {
method: method.to_string(),
path: path.as_str().to_string(),
query,
headers: headers_to_map(headers),
body: bytes_to_body(body),
};
let json = serde_json::to_string_pretty(&payload).unwrap_or_else(|e| {
format!(r#"{{"error":"serde_json failed","detail":"{}"}}"#, e)
});
Ok::<_, std::convert::Infallible>(warp::reply::with_status(
warp::reply::with_header(json, "content-type", "application/json; charset=utf-8"),
StatusCode::OK,
))
},
);
// それ以外は通常応答
let root = warp::path::end().map(|| "Hello, World!".to_string());
let routes = debug.or(root);
let tower_svc = warp::service(routes);
let hyper_svc = TowerToHyperService::new(tower_svc);
let addr: SocketAddr = format!("{host}:{port}").parse()?;
let listener = TcpListener::bind(addr).await?;
eprintln!("listening on http://{addr}");
loop {
let (stream, _peer) = listener.accept().await?;
let io = TokioIo::new(stream);
let svc = hyper_svc.clone();
tokio::spawn(async move {
if let Err(err) = http1::Builder::new().serve_connection(io, svc).await {
eprintln!("error serving connection: {err}");
}
});
}
}
#[php_function]
pub fn warp_http_serve(host: String, port: i64) -> PhpResult<()> {
if port < 0 || port > 65535 {
return Err(php_err("port must be in 0..65535"));
}
let port = port as u16;
// PHP 呼び出しをブロックして Rust 側のサーバを回す(最小例)
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(php_err)?;
rt.block_on(async move { serve_once(host, port).await })
.map_err(php_err)?;
Ok(())
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(warp_http_serve))
}cargo build
php -d extension=target/debug/libphp_warp_server.so -r 'warp_http_serve("127.0.0.1", 3000);'
curl -s "http://127.0.0.1:3000/debug?x=1&y=2" -H "X-Test: 123" -d "name=masaki" | jq .