Rust 初心者です。
簡単な Web サーバを作ってみました。
ググるとかなり Web サーバの作例が出てきますね。
TcpListener / TcpStream / thread あたりはなんとなくわかりました。
あとは TcpStream が std:: io::{Read, Write} を実装しているので、前回の手法を使って拡張してみました。
以下、エッセンスを紹介します。
Web アプリ部分とサーバ部分
Web アプリ部分は、ブラウザから受け取った HTTP Request を表現する Request 型と、ブラウザに返す HTTP Response を表現する Response 型を使ってこのような形で書きます。
// Webアプリ部分
fn web_app(req:&Request, rsp:&mut Response) -> Result<StatusCode> {
// [MEMO] 実際はリクエストされたパス req.path() に応じて処理するが、
// 簡単なサンプルを示す。
// コンテンツタイプの設定
rsp.set_header("Content-Type", "text/html");
// 以下、ブラウザに渡す HTML の内容
rsp.print("<h1>Working!</h1>")?;
// 現在の日時
let local_datetime: chrono::DateTime<chrono::Local> = chrono::Local::now();
rsp.print(&format!("<p>current time = {}", local_datetime))?;
// リクエストされたパスの文字列
let path = req.path();
rsp.print(&format!("<p>path={}", path))?;
// ユーザエージェントの文字列
if let Some(user_agent) = req.header("User-Agent") {
rsp.print(&format!("<p>user_agent={}", user_agent))?;
}
rsp.print("<hr><div align=\"right\">Powered by hoge server</div>")?;
// ステータスコードは 200
Ok(StatusCode::Ok)
}
ブラウザで接続するとこんな表示になります。
(http://localhost:8080/cgi-bin/hoge.pl に接続した結果)
XSS の脆弱性がありそうなので、ブラウザの URL にスクリプトを混入させてみました。
(http://localhost:8080/<script>alert(1);</script> に接続した結果)
ちゃんとエスケープされました。format! マクロがやってくれたようです。もしくはブラウザかも。
続いてサーバ部分です。
HTTP/1.0 しばり、つまり、キープアライブなし。
あとトランスファーエンコーディングなし、キャッシュコントロールなし、です。
// ループバックアドレスの 8080 番ポートで待ち受ける
const PORT:&str = "127.0.0.1:8080";
use std::{
net::{TcpListener, TcpStream},
thread,
};
// TCPストリームハンドラ
fn tcp_stream_handler (mut stream: TcpStream) -> Result<()> {
// HTTPリクエストメッセージの読み出し
let request = BufReader::new(&stream).read_request()?;
if cfg!(debug_assertions) {
println!("{:?}", request);
}
// HTTPレスポンスの作成
let mut response = Response::new();
let status_code = web_app(&request, &mut response)?;
// web_app から返った HTTPステータスコードを設定
response.set_status_code(status_code);
// HTTPレスポンスメッセージの書き出し
stream.write_response(response)?;
stream.flush()?;
Ok(())
} // ここで stream はクローズされる
fn main() {
eprintln!("# Listening {}...", PORT);
let listener = match TcpListener::bind(PORT) {
Ok(l) => l,
Err(e) => {
eprintln!("[ERROR] {:?}", e);
return
}
};
while let Ok((stream, _)) = listener.accept() {
eprintln!("# Connected!");
thread::spawn(||
if let Err(e) = tcp_stream_handler(stream) {
eprintln!("# [ERROR] {:?}", e);
Err(e)
} else {
Ok(())
}
);
}
}
Request 型と Response 型
Request 型は次のようにしました。メソッドは Enum 型を使いました。
#[derive(Debug)]
pub struct Request {
method: Method,
path: String,
version: String,
headers: HashMap<String, String>,
body: Vec<u8>,
}
impl Request {
pub fn new() -> Self {
Request {
method: Method::GET,
path: String::from("/"),
version: String::from("HTTP/1.0"),
headers: HashMap::new(),
body: vec![],
}
}
pub fn method (&self) -> Method {
self.method
}
// 以下省略
}
#[derive(Debug,PartialEq,Clone,Copy)]
pub enum Method {
UNKNOWN,
GET,
POST,
HEAD,
// *ここにメソッドを追加する
}
impl std::convert::From<String> for Method {
fn from(method: String) -> Self {
match method.to_uppercase().as_str() {
"GET" => Method::GET,
"POST" => Method::POST,
"HEAD" => Method::HEAD,
// *ここにメソッドを追加する
_ => Method::UNKNOWN,
}
}
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// 以下省略
Response 型は次のように与えました。ステータスコードは Enum 型を使いました。
// HTTP Response メッセージを表現する型
#[derive(Debug)]
pub struct Response {
version: String,
status_code: StatusCode,
headers: HashMap<String, String>,
body: Vec<u8>,
}
impl Response {
pub fn new() -> Self {
// デフォルトでキャッシュコントロールなし、コネクションはクローズとする
let mut headers:HashMap<String, String> = HashMap::new();
headers.insert(String::from("Cache-Control"), String::from("\"no-cache\""));
headers.insert(String::from("Connection"), String::from("close"));
headers.insert(String::from("Server"), String::from("hogehoge/0.1"));
Response {
version: String::from("HTTP/1.0"), // HTTP/1.0 固定
status_code: StatusCode::Ok,
headers: headers,
body: vec![],
}
}
// ステータスコードの設定
pub fn set_status_code(&mut self, status_code:StatusCode) {
self.status_code = status_code
}
// ヘッダの設定
pub fn set_header(&mut self, name:&str, value:&str) {
self.headers.insert(String::from(name), String::from(value));
}
// 文字列を出力
pub fn print(&mut self, s: &str) -> Result<usize> {
let n = self.body.write(s.as_bytes())?;
Ok(n)
}
// 以下省略
}
// ステータスコードを表現する型
#[derive(Debug,PartialEq)]
pub enum StatusCode {
Ok,
BadRequest,
NotFound,
MethodNotAllowed,
InternalServerError,
// ★ここにステータスコードを追加する
}
impl StatusCode {
fn to_string(&self) -> String {
String::from(match self {
StatusCode::Ok => "OK",
StatusCode::BadRequest => "Bad Request",
StatusCode::NotFound => "Not Found",
StatusCode::MethodNotAllowed => "Method Not Allowed",
StatusCode::InternalServerError => "Internal Server Error",
// ★ここにステータスコードを追加する
})
}
}
impl std::fmt::Display for StatusCode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// 以下省略
Request の読み出し
TCPハンドラのこの部分です。
// HTTPリクエストメッセージの読み出し
let request = BufReader::new(&stream).read_request()?;
前回と同様にトレイトを使って既存型の BufReader にメソッド read_request を追加しました。
impl<R:Read> HttpRead for std::io::BufReader<R>{}
// HTTPリクエスト対応 Read トレイト
pub trait HttpRead: std::io::Read {
fn read_request(&mut self) -> Result<Request> {
// リクエストラインの読み出し
let request_line = self.read_items_in_line()?;
if request_line.len() != 3 {
return Err(Error::MalformedRequestError)
}
let method = Method::from(request_line[0].clone());
if method == Method::UNKNOWN {
return Err(Error::MalformedRequestError)
}
// ヘッダの読み出し
let mut headers:HashMap<String, String> = HashMap::new();
loop {
if let Some((name, value)) = self.read_header()? {
headers.insert(name, value); // ヘッダ名 name は Upper Case になっていることに注意
} else {
// Noneのとき=ヘッダを読めだせないとき=空の行のときにループをぬける
break
}
}
// Content-Length の読み出し
let mut clen:usize = 0;
if let Some(clen_str) = headers.get("CONTENT-LENGTH") {
clen = clen_str.parse()?;
}
// ボディの読み出し
let mut body:Vec<u8> = vec![];
if clen > 0 {
if let Some(n) = self.read_nbytes(clen, &mut body)? {
if n != clen {
// content-length の値とあわないときエラー
return Err(Error::MalformedRequestError)
}
} else {
// None のときエラー
return Err(Error::MalformedRequestError)
}
}
Ok(Request{
method: method,
path: request_line[1].clone(),
version: request_line[2].clone(),
headers: headers,
body: body,
})
}
// ヘッダの読み出し
fn read_header (&mut self) -> ResultOption<(String, String)> {
let mut name_bytes:Vec<u8> = vec![];
let mut value_bytes:Vec<u8> = vec![];
let mut mode = 0; // mode = 0 : ヘッダ名、mode = 1 : ヘッダ値
loop {
if let Some(c) = self.read_1byte()? { // [1]
if mode == 0 { // ヘッダ名のモードのとき
if c == COLON { // コロンがきたのでヘッダ値のモードへ
mode = 1;
continue
} else if c == SP || c == HT { // 先頭の空白は無視したい
if name_bytes.len() > 0 {
name_bytes.push(c);
continue
}
} else if c == CR {
if name_bytes.len() > 0 { // ヘッダ名の途中で CR がきたらエラー
return Err(Error::MalformedRequestError)
}
// ヘッダ名はまだ表れていない
// 次の 1 バイトが LF かチェック
if let Some(c) = self.read_1byte()? { // [2]
if c != LF {
// CR の次が LF ではなかったときはエラー
return Err(Error::MalformedRequestError)
} else {
// CR LF が現れたのでループを抜ける
break
}
} else {
// None、読みだせなかった時はループをぬける
break
}
} else if c == LF { // いきなり LF のときはエラー
return Err(Error::MalformedRequestError)
} else {
// それ以外は保管
name_bytes.push(c);
continue
}
} else { // ヘッダ値のモードのとき
if c == SP || c == HT {
if value_bytes.len() > 0 { // 先頭の空白は無視したい
value_bytes.push(c);
continue
}
} else if c == CR {
// 次の 1 バイトが LF かチェック
if let Some(c) = self.read_1byte()? { // [2]
if c != LF {
// CR の次が LF ではなかったときはエラー
return Err(Error::MalformedRequestError)
} else {
// CR LF が現れたのでループを抜ける
break
}
} else {
// None、読みだせなかった時はループをぬける
break
}
} else if c == LF { // いきなり LF のときはエラー
return Err(Error::MalformedRequestError)
} else {
// それ以外の時は保管
value_bytes.push(c);
continue
}
}
} else {
// None、読みだせなかった時はループをぬける
break
}
}
if name_bytes.len() < 1 { // ヘッダ名のバイト列がないときは None を返す
return Ok(None)
}
let binding = String::from_utf8(name_bytes)?;
let name = binding.trim_end().to_uppercase(); // 空白を削除しアッパーケースに変換
let binding = String::from_utf8(value_bytes)?;
let value = binding.trim_end(); // 空白を削除
Ok(Some((String::from(name), String::from(value))))
}
// nバイトを読みだすデフォルトメソッド
// エラーではないが 1 バイトも読みだせなかった時は Ok(None) が返ることに注意
fn read_nbytes (&mut self, size:usize, out:&mut Vec<u8>) -> ResultOption<usize> {
// 省略
前回紹介したものは省略しました。
Response の出力
TCPハンドラのこの部分です。
// HTTPレスポンスメッセージの書き出し
stream.write_response(response)?;
stream.flush()?;
ここも TcpStream にメソッドを追加しました。
// TcpStream に適用
impl HttpWrite for std::net::TcpStream{}
// HTTPレスポンス対応 Writeトレイト
pub trait HttpWrite: std::io::Write {
fn write_response(&mut self, response:Response) -> Result<()> {
// ステータスラインの出力
self.write(format!("{version} {status_code} {msg}\r\n",
version=response.version,
status_code=response.status_code,
msg=response.status_code.to_string()).as_bytes())?;
// ヘッダの出力
for (name, value) in response.headers {
self.write(format!("{}: {}\r\n", name, value).as_bytes())?;
}
// 空行の出力
self.write("\r\n".as_bytes())?;
// レスポンスボディの出力
if response.body.len() > 0 {
self.write(&response.body)?;
}
Ok(())
}
}
ちょっとしたWebサーバを作ってみました。このくらいあれば、とりあえず動くWebアプリは作れそうな予感。
- この続きとして、残りのメソッドとステータスコードの拡充、HTML ファイル対応、Python を使った CGI スクリプト機能、MD (Markdown) 形式のコンテンツを HTML にレンダリングして表示する機能あたりを作ろうかなと思ってます。
- さすがにこんなレベルのサーバでお客様に出してくのは厳しいですね。ログ機能と、パフォーマンスチューニング機能と、Gracefull shutdown は欲しいなぁ。割り込み対応とスレッドプールのモニタが必要だけど実装がめんどくさそう。
- そこそこ使えるフレームワークを勉強した方が早いですかね。それはそれで面倒くさそうですが。