Cargo.toml
[package]
name = "php-embed-tinyhttp"
version = "0.1.0"
edition = "2021"
[dependencies]
ext-php-rs = { version = "0.15", features = ["embed"] }
tiny_http = "0.12"
os_pipe = "1.2"
libc = "0.2"
src/main.rs
use ext_php_rs::embed::Embed;
use os_pipe::pipe;
use std::{
env,
ffi::CString,
io::Read,
path::{Path, PathBuf},
};
use std::os::fd::AsRawFd;
fn main() {
let (addr, docroot) = parse_args();
let docroot = std::fs::canonicalize(&docroot).unwrap_or(docroot);
// Embed::run は R: Default を要求するので、戻り値を () に固定する
Embed::run(|| {
if let Err(e) = run_server(&addr, &docroot) {
eprintln!("server error: {e}");
}
});
}
fn parse_args() -> (String, PathBuf) {
let mut addr = "127.0.0.1:8080".to_string();
let mut docroot = PathBuf::from("./public");
let mut it = env::args().skip(1);
while let Some(a) = it.next() {
match a.as_str() {
"--addr" => {
if let Some(v) = it.next() {
addr = v;
}
}
"--docroot" => {
if let Some(v) = it.next() {
docroot = PathBuf::from(v);
}
}
_ => {}
}
}
(addr, docroot)
}
fn run_server(addr: &str, docroot: &Path) -> Result<(), String> {
let server = tiny_http::Server::http(addr)
.map_err(|e| format!("tiny_http bind failed: {e}"))?;
eprintln!("Listening on http://{addr}");
eprintln!("Docroot: {}", docroot.display());
for mut req in server.incoming_requests() {
let method = req.method().as_str().to_string();
let url = req.url().to_string();
let mut _body = Vec::new();
let _ = req.as_reader().read_to_end(&mut _body);
let (path_part, query) = split_url(&url);
let script_path = map_to_script(docroot, &path_part);
let response = match script_path {
Some(script) if script.is_file() => {
set_env_for_request(&method, &url, query, &req);
match capture_php_stdout(&script) {
Ok(out) => tiny_http::Response::from_string(out)
.with_status_code(200)
.with_header(
tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"text/html; charset=utf-8"[..],
)
.unwrap(),
),
Err(err_msg) => tiny_http::Response::from_string(err_msg)
.with_status_code(500)
.with_header(
tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"text/plain; charset=utf-8"[..],
)
.unwrap(),
),
}
}
_ => tiny_http::Response::from_string("Not Found\n")
.with_status_code(404)
.with_header(
tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"text/plain; charset=utf-8"[..],
)
.unwrap(),
),
};
let _ = req.respond(response);
}
Ok(())
}
fn split_url(url: &str) -> (String, &str) {
if let Some((p, q)) = url.split_once('?') {
(p.to_string(), q)
} else {
(url.to_string(), "")
}
}
fn map_to_script(docroot: &Path, path_part: &str) -> Option<PathBuf> {
let mut rel = path_part.trim_start_matches('/').to_string();
if rel.is_empty() {
rel = "index.php".to_string();
}
if rel.ends_with('/') {
rel.push_str("index.php");
}
// 最小・安全寄り:php 以外は不許可
if !rel.ends_with(".php") {
return None;
}
if rel.contains("..") {
return None;
}
Some(docroot.join(rel))
}
fn set_env_for_request(method: &str, uri: &str, query: &str, req: &tiny_http::Request) {
env::set_var("REQUEST_METHOD", method);
env::set_var("REQUEST_URI", uri);
env::set_var("QUERY_STRING", query);
env::set_var("SERVER_PROTOCOL", "HTTP/1.1");
if let Some(h) = req
.headers()
.iter()
.find(|h| h.field.equiv("Host"))
.map(|h| h.value.as_str().to_string())
{
env::set_var("HTTP_HOST", &h);
if let Some((name, port)) = h.split_once(':') {
env::set_var("SERVER_NAME", name);
env::set_var("SERVER_PORT", port);
} else {
env::set_var("SERVER_NAME", &h);
}
}
}
fn capture_php_stdout(script: &Path) -> Result<String, String> {
let script_str = script
.to_str()
.ok_or_else(|| "script path is not valid utf-8".to_string())?;
// NUL 混入を確実に検知(以前の NUL エラー対策)
CString::new(script_str).map_err(|_| "script path contains NUL byte".to_string())?;
let (mut reader, writer) = pipe().map_err(|e| format!("pipe failed: {e}"))?;
let stdout_fd = std::io::stdout().as_raw_fd();
let saved = unsafe { libc::dup(stdout_fd) };
if saved < 0 {
return Err("dup(stdout) failed".to_string());
}
unsafe {
if libc::dup2(writer.as_raw_fd(), stdout_fd) < 0 {
libc::close(saved);
return Err("dup2(pipe->stdout) failed".to_string());
}
}
drop(writer);
// PHP 実行
let r = Embed::run_script(script_str);
// flush + stdout 復元
unsafe {
libc::fflush(std::ptr::null_mut());
libc::dup2(saved, stdout_fd);
libc::close(saved);
}
if let Err(e) = r {
return Err(format!("PHP execution failed: {e:?}"));
}
let mut out = String::new();
reader
.read_to_string(&mut out)
.map_err(|e| format!("read captured stdout failed: {e}"))?;
Ok(out)
}cargo run -- --addr 127.0.0.1:8080 --docroot ./public
<?php
header("Content-Type: text/plain; charset=utf-8");
echo "Hello from embedded PHP!\n";
echo "REQUEST_METHOD=" . ($_SERVER["REQUEST_METHOD"] ?? "") . "\n";
echo "REQUEST_URI=" . ($_SERVER["REQUEST_URI"] ?? "") . "\n";
echo "QUERY_STRING=" . ($_SERVER["QUERY_STRING"] ?? "") . "\n";