Cargo.toml
[package]
name = "php_httpd_ext"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
tiny_http = "0.12"src/lib.rs
use ext_php_rs::prelude::*;
use tiny_http::{Header, Response, Server, StatusCode};
use std::io::Read;
fn header(name: &str, value: &str) -> Header {
Header::from_bytes(name.as_bytes(), value.as_bytes()).expect("invalid header")
}
#[php_function]
pub fn httpd_run(
addr: String,
body: String,
max_requests: i64, // 0 or negative => infinite
content_type: Option<String>,
) -> PhpResult<String> {
let server = Server::http(&addr)
.map_err(|e| PhpException::default(format!("failed to bind {}: {}", addr, e)))?;
let mut handled: i64 = 0;
let ct = content_type.unwrap_or_else(|| "text/plain; charset=utf-8".to_string());
for request in server.incoming_requests() {
// 固定レスポンス(逐次処理)
let response = Response::from_string(body.clone())
.with_status_code(StatusCode(200))
.with_header(header("Content-Type", &ct))
.with_header(header("Connection", "close"));
// 送信(エラーはログ文字列として返す方針もあるが、ここでは握りつぶさず例外化)
if let Err(e) = request.respond(response) {
return Err(PhpException::default(format!("respond failed: {}", e)));
}
handled += 1;
if max_requests > 0 && handled >= max_requests {
break;
}
}
Ok(format!(
"httpd_run finished: addr={}, handled={}",
addr, handled
))
}
#[php_function]
pub fn httpd_run_echo(addr: String, max_requests: i64) -> PhpResult<String> {
let server = Server::http(&addr)
.map_err(|e| PhpException::default(format!("failed to bind {}: {}", addr, e)))?;
let mut handled: i64 = 0;
for mut request in server.incoming_requests() {
let url = request.url().to_string();
let method = request.method().as_str().to_string();
let mut out = String::new();
out.push_str("tiny_http + ext-php-rs echo server\n\n");
out.push_str(&format!("method: {}\n", method));
out.push_str(&format!("url: {}\n", url));
out.push_str("\nheaders:\n");
for h in request.headers() {
out.push_str(&format!(" {}: {}\n", h.field.as_str(), h.value.as_str()));
}
// body も読む(大きいボディは注意。ここは最大 64KB まで)
out.push_str("\nbody (up to 65536 bytes):\n");
let mut buf = Vec::new();
let _ = request
.as_reader()
.take(65536)
.read_to_end(&mut buf);
out.push_str(&String::from_utf8_lossy(&buf));
let response = Response::from_string(out)
.with_status_code(StatusCode(200))
.with_header(header("Content-Type", "text/plain; charset=utf-8"))
.with_header(header("Connection", "close"));
if let Err(e) = request.respond(response) {
return Err(PhpException::default(format!("respond failed: {}", e)));
}
handled += 1;
if max_requests > 0 && handled >= max_requests {
break;
}
}
Ok(format!(
"httpd_run_echo finished: addr={}, handled={}",
addr, handled
))
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.name("httpd_ext")
.function(wrap_function!(httpd_run))
.function(wrap_function!(httpd_run_echo))
}php -d extension=target/debug/libphp_httpd_ext.so -r '
echo httpd_run("127.0.0.1:8080", "Hello from PHP extension\n", 3, "text/plain; charset=utf-8"), PHP_EOL;
'
httpd_run finished: addr=127.0.0.1:8080, handled=3