Rubyで独自のWebサーバーをゼロから構築する方法
Rubyで自分だけのWebサーバーをゼロから作ったことはありますか?
実はRubyには、すでに多くのWebサーバーが存在します。例えば:
- Puma
- Thin
- Unicorn
しかし、シンプルなWebサーバーの仕組みを深く理解したいなら、自分で一から作ってみるのは最高の学習教材になります。
この記事では、その方法をステップバイステップで解説していきます。
ステップ1:接続を待ち受ける
さて、どこから始めましょうか?
まず必要なのは、TCPポート80で新しい接続を待ち受けることです。
Rubyによるネットワークプログラミングについては別記事で詳しく解説しているので、ここでは仕組みの詳細説明は省略します。
コードだけ先にお見せしましょう:
require 'socket'
server = TCPServer.new('localhost', 80)
loop {
client = server.accept
request = client.readpartial(2048)
puts request
}
このコードを実行すると、ポート80で接続を受け付けるサーバーが起動します。まだ大したことはできませんが、リクエストがどのような形で届くのかを目で確認できます。
注意:Linux/Mac環境でポート80を使用するにはroot権限が必要です。代わりに1024以上のポート番号を使うのも良いでしょう。私は8080がおすすめです🙂
リクエストを簡単に生成するには、ブラウザやcurlなどのツールを使えばOKです。
実行すると、サーバー側には次のような出力が表示されます:
GET / HTTP/1.1 Host: localhost User-Agent: curl/7.49.1 Accept: */*
これがHTTPリクエストです。HTTPとは、WebブラウザとWebサーバー間の通信に使われるプレーンテキスト形式のプロトコルです。
公式のプロトコル仕様はこちらで確認できます:https://tools.ietf.org/html/rfc7230
ステップ2:リクエストをパースする
次に、受け取ったリクエストを、サーバーが扱える小さな要素に分解する必要があります。
既存のパーサーを使ってもいいのですが、今回は自作してみましょう。そのためには、リクエストの各部分が何を意味しているのかを理解しておく必要があります。
下の画像が参考になるでしょう:

GETリクエストの構造
ヘッダーはブラウザキャッシュ、バーチャルホスティング、データ圧縮などに使われますが、基本的な実装では無視してもサーバーとして十分機能します。
シンプルなHTTPパーサーを作るには、「リクエストデータが改行(\r\n)で区切られている」という事実を利用できます。シンプルさを保つため、エラーチェックや妥当性検証は行いません。
私が書いたコードがこちらです:
def parse(request)
method, path, version = request.lines[0].split
{
path: path,
method: method,
headers: parse_headers(request)
}
end
def parse_headers(request)
headers = {}
request.lines[1..-1].each do |line|
return headers if line == "\r\n"
header, value = line.split
header = normalize(header)
headers[header] = value
end
def normalize(header)
header.gsub(":", "").downcase.to_sym
end
end
これで、パース済みのリクエストデータをハッシュとして取得できます。リクエストが扱いやすい形になったので、次はクライアントへのレスポンス作成に取り掛かりましょう。
ステップ3:レスポンスの準備と送信
レスポンスを作るには、要求されたリソースが存在するかどうかを確認する必要があります。つまり、ファイルが存在するかチェックするわけです。
そのために書いたコードがこちら:
SERVER_ROOT = "/tmp/web-server/"
def prepare_response(request)
if request.fetch(:path) == "/"
respond_with(SERVER_ROOT + "index.html")
else
respond_with(SERVER_ROOT + request.fetch(:path))
end
end
def respond_with(path)
if File.exists?(path)
send_ok_response(File.binread(path))
else
send_file_not_found
end
end
ここでは2つの処理が行われています:
- まず、パスが
/の場合はindex.htmlを返すものとみなします。 - 次に、要求されたファイルが見つかれば、OKレスポンス(200)とともにファイルの中身を送信します。
ファイルが見つからない場合は、おなじみの404 Not Foundレスポンスを返します。
よく使われるHTTPレスポンスコード一覧
参考までに、主要なステータスコードをまとめました。
| コード | 説明 |
|---|---|
| 200 | OK(成功) |
| 301 | Moved Permanently(恒久的な移動) |
| 302 | Found(一時的な移動) |
| 304 | Not Modified(未更新) |
| 400 | Bad Request(不正なリクエスト) |
| 401 | Unauthorized(認証が必要) |
| 403 | Forbidden(アクセス禁止) |
| 404 | Not Found(ページが見つからない) |
| 500 | Internal Server Error(サーバー内部エラー) |
| 502 | Bad Gateway(不正なゲートウェイ) |
Responseクラスとメソッド
前の例で使った「send」系メソッドはこちらです:
def send_ok_response(data) Response.new(code: 200, data: data) end def send_file_not_found Response.new(code: 404) end
そして、Responseクラスは以下のようになります:
class Response
attr_reader :code
def initialize(code:, data: "")
@response =
"HTTP/1.1 #{code}\r\n" +
"Content-Length: #{data.size}\r\n" +
"\r\n" +
"#{data}\r\n"
@code = code
end
def send(client)
client.write(@response)
end
end
レスポンスはテンプレートと文字列補間を組み合わせて組み立てています。
あとは、すべてを接続待ち受けのloop内でつなぎ合わせれば、動作するサーバーの完成です。
loop {
client = server.accept
request = client.readpartial(2048)
request = RequestParser.new.parse(request)
response = ResponsePreparer.new.prepare(request)
puts "#{client.peeraddr[3]} #{request.fetch(:path)} - #{response.code}"
response.send(client)
client.close
}
SERVER_ROOTディレクトリ配下にHTMLファイルをいくつか置いて、ブラウザからアクセスしてみてください。画像を含むその他の静的ファイルも問題なく配信できるはずです。
もちろん、本物のWebサーバーには今回カバーしなかった機能がまだたくさんあります。
以下はその一部です。ぜひ練習課題として自分で実装してみてください(練習こそ上達への近道です!):
- バーチャルホスティング
- MIMEタイプの判定
- データ圧縮
- アクセス制御
- マルチスレッド対応
- リクエストのバリデーション
- クエリ文字列のパース
- POSTボディのパース
- ブラウザキャッシュ(304レスポンス)
- リダイレクト
セキュリティから学ぶ教訓
ユーザーからの入力を受け取って何らかの処理をするのは、常に危険が伴います。この小さなWebサーバープロジェクトでは、ユーザー入力に相当するのがHTTPリクエストです。
実は、このコードには「ディレクトリトラバーサル(path traversal)」と呼ばれる脆弱性が潜んでいます。攻撃者は、SERVER_ROOTディレクトリの外にあるファイルも含めて、Webサーバーを実行しているユーザーがアクセス可能な任意のファイルを読めてしまうのです。
問題となっているのは、この一行です:
File.binread(path)
実際にこの脆弱性を突いてみると、挙動がよくわかります。ただし、ほとんどのHTTPクライアント(curlを含む)はURLを前処理して脆弱性を引き起こす部分を取り除いてしまうため、「手動で」HTTPリクエストを送る必要があります。
使えるツールの一つがnetcatです。
攻撃の例はこんな感じです:
$ nc localhost 8080 GET ../../etc/passwd HTTP/1.1
Unix系システムであれば、/etc/passwdファイルの中身が返ってきます。これが成立する理由は、ドット2つ(..)で一つ上のディレクトリに移動できるため、SERVER_ROOTディレクトリを「脱出」できてしまうからです。
対策の一つは、連続するドットを1つに「圧縮」することです:
path.gsub!(/\.+/, ".")
セキュリティを考えるときは、常に「ハッカーの帽子」をかぶり、自分の対策を破る方法を探す癖をつけましょう。例えば、単にpath.gsub!("..", ".")としただけなら、ドット3つ(...)を使えば回避できてしまいます。
完成版のコード
この記事ではコードがあちこちに散らばっているので、完成して動くコードが欲しい方のために…
こちらがリンクです:
https://gist.github.com/matugm/efe0a1c4fc53310f7ac93dcd1f041f6c#file-web-server-rb
ぜひ活用してください!
まとめ
この記事では、新しい接続を待ち受ける方法、HTTPリクエストの構造とそのパース方法を学びました。さらに、レスポンスコードと要求されたファイルの中身(存在する場合)を使ってレスポンスを組み立てる方法も解説しました。
最後に、「ディレクトリトラバーサル」脆弱性とその対策についても触れました。
この記事が役に立ち、新しい学びにつながったなら嬉しいです!下のフォームからニュースレターを購読すれば、最新記事を見逃しませんよ🙂
-
Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説
パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。 たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。 Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう! Rubyでのパースの基本:StringScannerクラス
-
火曜日のヒント:Plexサーバーで自分だけのSpotify環境を自作しよう
AppleがLalaを買収して終了させて以来、私はSpotifyなどのストリーミングサービスでその穴を埋めてきました。しかし、どこにも配信されていない(低音質のYouTube版を除けば)手持ちの楽曲がかなりあり、それらをすべてのデバイスで聴きたいと思っていました。そこで登場するのがPlexです。 Plexは、MacやNAS(ネットワーク接続ストレージ)をサーバーとして設定し、自分の音楽コレクションをiPhoneにストリーミングできるソフトウェアパッケージです。外出先からでも利用可能です。セットアップにはネットワークに関する基本的な知識が必要で、ルーターのポート開放方法を把握しておく必要があり