Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

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:リクエストをパースする

次に、受け取ったリクエストを、サーバーが扱える小さな要素に分解する必要があります。

既存のパーサーを使ってもいいのですが、今回は自作してみましょう。そのためには、リクエストの各部分が何を意味しているのかを理解しておく必要があります。

下の画像が参考になるでしょう:

Rubyで独自のWebサーバーをゼロから構築する方法

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リクエストの構造とそのパース方法を学びました。さらに、レスポンスコードと要求されたファイルの中身(存在する場合)を使ってレスポンスを組み立てる方法も解説しました。

最後に、「ディレクトリトラバーサル」脆弱性とその対策についても触れました。

この記事が役に立ち、新しい学びにつながったなら嬉しいです!下のフォームからニュースレターを購読すれば、最新記事を見逃しませんよ🙂

  1. Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説

    パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。 たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。 Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう! Rubyでのパースの基本:StringScannerクラス

  2. 火曜日のヒント:Plexサーバーで自分だけのSpotify環境を自作しよう

    AppleがLalaを買収して終了させて以来、私はSpotifyなどのストリーミングサービスでその穴を埋めてきました。しかし、どこにも配信されていない(低音質のYouTube版を除けば)手持ちの楽曲がかなりあり、それらをすべてのデバイスで聴きたいと思っていました。そこで登場するのがPlexです。 Plexは、MacやNAS(ネットワーク接続ストレージ)をサーバーとして設定し、自分の音楽コレクションをiPhoneにストリーミングできるソフトウェアパッケージです。外出先からでも利用可能です。セットアップにはネットワークに関する基本的な知識が必要で、ルーターのポート開放方法を把握しておく必要があり