Rackを動かす:自作Ruby HTTPサーバーでRailsアプリを実行する方法
Ruby Magicシリーズでは、ソフトウェアを分解して内部の仕組みを学ぶのが大好きです。大切なのはプロセスそのものであり、最終的な成果物は本番環境で使うものではありません。目的は、Ruby言語や人気ライブラリの内部的な動作への理解を深めることにあります。新しい記事は月に1回ほど公開しているので、この手の話題がお好きな方はぜひニュースレターをご購読ください。
以前のRuby Magicでは、わずか30行のコードでRuby製のHTTPサーバーを実装しました。大量のコードを書くことなく、HTTP GETリクエストを処理し、シンプルなRackアプリケーションを配信することができました。今回は、この自作サーバーをさらに発展させます。完成すれば、Railsの有名な「15分ブログ」を配信できるWebサーバーができあがり、投稿の作成・更新・削除が可能になります。
前回のおさらい
前回は、サンプルアプリケーションとしてRack::Lobsterを配信できる最低限のサーバーを実装しました。その動作の流れは以下の通りです。
- TCPサーバーを開き、リクエストが届くのを待機します。
- リクエストが届くと、リクエストライン(
GET /?flip=left HTTP/1.1\r\n)を解析し、メソッド(GET)、パス(/)、クエリパラメータ(flip=left)を取得します。 - メソッド、パス、クエリ文字列をRackアプリに渡すと、ステータス、レスポンスヘッダー、レスポンスボディの3つ組が返ってきます。
- これらを使ってHTTPレスポンスを組み立ててブラウザへ送り返し、接続を閉じて次のリクエストを待ちます。
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
#1
while session = server.accept
request = session.gets
puts request
#2
method, full_path = request.split(' ')
path, query = full_path.split('?')
#3
status, headers, body = app.call({
'REQUEST_METHOD' => method,
'PATH_INFO' => path,
'QUERY_STRING' => query
})
#4
session.print "HTTP/1.1 #{status}\r\n"
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
session.print "\r\n"
body.each do |part|
session.print part
end
session.close
end今回も前回書いたコードの続きから始めます。一緒に進めたい方は、こちらが前回の最終コードです。
RackとRailsの関係
RailsやSinatraといったRubyフレームワークは、Rackインターフェースの上に構築されています。現在サーバーのテストに使っているRack::Lobsterのインスタンスと同じように、RailsのRails.applicationもRackアプリケーションオブジェクトです。理論上は、私たちのサーバーでも既にRailsアプリケーションを動かせるはずです。
それを確かめるために、シンプルなRailsアプリケーションを用意しました。サーバーと同じディレクトリにクローンしましょう。
$ ls
http_server.rb
$ git clone https://github.com/jeffkreeftmeijer/wups.git blog
Cloning into 'blog'...
remote: Counting objects: 162, done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 162 (delta 32), reused 162 (delta 32), pack-reused 0
Receiving objects: 100% (162/162), 29.09 KiB | 0 bytes/s, done.
Resolving deltas: 100% (32/32), done.
Checking connectivity... done.
$ ls
blog http_server.rb
次に、サーバー側でrackとrack/lobsterの代わりにRailsアプリケーションのenvironmentファイルをrequireし、app変数にはRack::Lobster.newの代わりにRails.applicationを代入します。
# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
app = Rails.application
server = TCPServer.new 5678
# ...サーバーを起動して https://localhost:5678 を開いてみると、まだ完全には動いていないことがわかります。サーバー自体はクラッシュしませんが、ブラウザには内部サーバーエラーが表示されます。
サーバーのログを確認すると、rack.inputというものが不足していることが判明しました。前回の実装で手を抜いた部分があるため、このRailsアプリケーションを動かすまでにはもう少し作業が必要なのです。
$ ruby http_server.rb
GET / HTTP/1.1
Error during failsafe response: Missing rack.input
...
http_server.rb:15:in `<main>'
Rack環境(Rack environment)とは
サーバーを実装した当時、私たちはRack環境について深く考えることなく、Rackアプリケーションを正しく配信するために必要な変数の大半を無視していました。結果としてREQUEST_METHOD、PATH_INFO、QUERY_STRINGの3つの変数だけを実装しましたが、シンプルなRackアプリであればそれで十分だったのです。
先ほどの例外からもわかるように、Railsはrack.inputを必要とします。これは生のHTTP POSTデータの入力ストリームとして使われるものです。さらに、サーバーのポート番号やリクエストのCookieデータなど、渡すべき変数が他にもいくつかあります。
幸い、RackにはRack::Lintという便利なツールがあり、Rack環境のすべての変数が存在し、かつ有効であることを確認できます。Rails.applicationを引数にRack::Lint.newを呼び出してRailsアプリをラップすれば、これを使ってサーバーをテストできます。
# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678
# ...Rack::Lintは、環境変数が欠けていたり不正だったりすると例外を投げます。現状のままサーバーを再起動して https://localhost:5678 を開くと、サーバーがクラッシュし、最初のエラーとしてSERVER_NAME変数が設定されていないことを指摘されます。
~/Appsignal/http-server (master) $ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)
...
from http_server.rb:15:in `<main>'
投げられるエラーを一つずつ修正していけば、Rack::Lintがクラッシュしなくなるまで変数を追加し続けることができます。Rack::Lintが必要とする各変数を見ていきましょう。
SERVER_NAME:サーバーのホスト名。今はローカルでしか動かさないので「localhost」を使用します。SERVER_PORT:サーバーが稼働しているポート番号。ポート番号(5678)はハードコードされているので、そのままRack環境に渡します。rack.version:対象とするRackプロトコルのバージョン番号を整数の配列で表したもの。執筆時点では[1,3]です。rack.input:生のHTTP POSTデータを格納する入力ストリーム。詳細は後述しますが、当面は空のStringIOインスタンス(ASCII-8BITエンコーディング)を渡します。rack.errors:Rack::Loggerが書き込むためのエラーストリーム。ここでは$stderrを使用します。rack.multithread:このサーバーはシングルスレッドなのでfalseを設定します。rack.multiprocess:このサーバーは単一プロセスで動作するので、これもfalseです。rack.run_once:このサーバーは1プロセスで複数の連続リクエストを処理できるので、これもfalseです。rack.url_scheme:SSL非対応のため、「https」ではなく「http」を設定します。
すべての不足変数を追加すると、Rack::Lintからもう一つ問題を指摘されます。
$ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env variable QUERY_STRING has non-string value nil (Rack::Lint::LintError)
...
from http_server.rb:18:in `<main>'
リクエストにクエリ文字列が含まれていない場合、QUERY_STRINGとしてnilを渡してしまいますが、これは許可されていません。その場合、Rackは代わりに空文字列を期待します。不足していた変数を実装し、クエリ文字列の扱いを修正すると、環境は次のようになります。
# http_server.rb
# ...
method, full_path = request.split(' ')
path, query = full_path.split('?')
input = StringIO.new
input.set_encoding 'ASCII-8BIT'
status, headers, body = app.call({
'REQUEST_METHOD' => method,
'PATH_INFO' => path,
'QUERY_STRING' => query || '',
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => '5678',
'rack.version' => [1,3],
'rack.input' => input,
'rack.errors' => $stderr,
'rack.multithread' => false,
'rack.multiprocess' => false,
'rack.run_once' => false,
'rack.url_scheme' => 'http'
})
session.print "HTTP/1.1 #{status}\r\n"
# ...サーバーを再起動してもう一度 https://localhost:5678 にアクセスすると、Railsの「Yay! You're on Rails!」ページが表示されるはずです。つまり、自作サーバー上で本物のRailsアプリケーションが動いているのです!
HTTP POSTボディの解析
このアプリケーションはインデックスページだけではありません。 https://localhost:5678/posts にアクセスすると、空の投稿リストが表示されます。新規投稿フォームに入力して「Create Post」ボタンを押すと、ActionController::InvalidAuthenticityToken例外に遭遇します。
認証トークン(authenticity token)はフォーム送信時に一緒に送信され、リクエストが信頼できるソースから来たものかどうかを検証するために使われます。ところが現状のサーバーはPOSTデータを完全に無視しているため、トークンが送られず、リクエストを検証できないのです。
最初にHTTPサーバーを実装したとき、session.getsで最初の行(リクエストライン)を取得し、そこからHTTPメソッドとパスを解析しました。しかし、リクエストライン以外の部分はすべて無視していました。
POSTデータを抽出できるようにするには、まずHTTPリクエストの構造を理解する必要があります。実際の例を見ると、その構造はHTTPレスポンスによく似ていることがわかります。
POST /posts HTTP/1.1\r\n
Host: localhost:5678\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: en-us\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Origin: https://localhost:5678\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14\r\n
Cookie: _wups_session=LzE0Z2hSZFNseG5TR3dEVEwzNE52U0lFa0pmVGlQZGtZR3AveWlyMEFvUHRPeXlQUzQ4L0xlKzNLVWtqYld2cjdiWkpmclZIaEhJd1R6eDhaZThFbVBlN2p6QWpJdllHL2F4Z3VseUZ6NU1BRTU5Y1crM2lLRVY0UzdSZkpwYkt2SGFLZUQrYVFvaFE0VjZmZlIrNk5BPT0tLUpLTHQvRHQ0T3FycWV0ZFZhVHZWZkE9PQ%3D%3D--4ef4508c936004db748da10be58731049fa190ee\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
Referer: https://localhost:5678/posts/new\r\n
Content-Length: 369\r\n
\r\n
utf8=%E2%9C%93&authenticity_token=3fu7e8v70K0h9o%2FGNiXxaXSVg3nZ%2FuoL60nlhssUEHpQRz%2BM4ZIHjQduQMexvXrNoC2pjmhNPI4xNNA0Qkh5Lg%3D%3D&post%5Btitle%5D=My+first+post&post%5Bcreated_at%281i%29%5D=2017&post%5Bcreated_at%282i%29%5D=1&post%5Bcreated_at%283i%29%5D=23&post%5Bcreated_at%284i%29%5D=18&post%5Bcreated_at%285i%29%5D=47&post%5Bbody%5D=It+works%21&commit=Create+Post
レスポンスと同様に、HTTPリクエストは以下の要素で構成されています。
- リクエストライン(
POST /posts HTTP/1.1\r\n):メソッドトークン(POST)、リクエストURI(/posts/)、HTTPバージョン(HTTP/1.1)で構成され、行末を示すCRLF(キャリッジリターン\r+ラインフィード\n)が続きます。 - ヘッダー行(
Host: localhost:5678\r\n):ヘッダーキー、コロン、値、CRLFの順に並びます。 - リクエストラインとヘッダーをボディから区切る空行(ダブルCRLF)(
\r\n\r\n)。 - URLエンコードされたPOSTボディ。
session.getsでリクエストの最初の行(リクエストライン)を読み取った後、残りはヘッダー行とボディです。ヘッダー行を取得するには、空行(\r\n)が見つかるまでセッションから行を読み込み続けます。
各ヘッダー行は、最初のコロンで分割します。コロンの前がキー、後ろが値です。値には#stripを適用して、末尾の改行を取り除きます。
ボディを取得するためにリクエストから何バイト読み込むべきかを知るには、「Content-Length」ヘッダーを使用します。これはブラウザがリクエスト送信時に自動的に付加するものです。
# http_server.rb
# ...
headers = {}
while (line = session.gets) != "\r\n"
key, value = line.split(':', 2)
headers[key] = value.strip
end
body = session.read(headers["Content-Length"].to_i)
# ...次に、空のオブジェクトを送る代わりに、リクエスト経由で受け取ったボディを含むStringIOインスタンスを送るようにします。また、リクエストヘッダーからCookieを解析できるようになったので、HTTP_COOKIE変数としてRack環境に追加すれば、リクエストの認証チェックを通過できるようになります。
# http_server.rb
# ...
status, headers, body = app.call({
# ...
'REMOTE_ADDR' => '127.0.0.1',
'HTTP_COOKIE' => headers['Cookie'],
'rack.version' => [1,3],
'rack.input' => StringIO.new(body),
'rack.errors' => $stderr,
# ...
})
# ...これで完成です。サーバーを再起動してもう一度フォームを送信してみると、ブログの最初の投稿が正常に作成されたことが確認できるはずです!
今回はWebサーバーを大幅にアップグレードしました。RackアプリからのGETリクエストを受け付けるだけでなく、POSTリクエストを処理する完全なRailsアプリを配信できるようになりました。しかも、合計コード量はまだ50行にも達していません!
改良版サーバーで遊んでみたい方は、こちらがコードです。さらに詳しく知りたい場合や具体的な質問がある場合は、@AppSignalまでお気軽にお知らせください。
-
macOSで未検証のアプリを実行する3つの方法【セキュリティ設定・Homebrew対応】
AppleはApp Storeから承認済みのアプリのみをダウンロードしてほしいと考えていますが、それが常に可能とは限りません。Web上で見つけた便利なアプリが未承認だった場合、macOSはその起動を自動的にブロックします。このセキュリティ機能は善意によるものですが、サードパーティ製アプリを利用したい場合には、これを回避する必要があります。幸い、Macで未検証のアプリを実行するのはそれほど難しくありません。ただし、始める前に覚えておいてほしいのは、このセキュリティ対策には理由があるということです。信頼できる提供元からのアプリだけをインストールするようにしましょう。そうしないと、Mac用アンチウイ
-
Windows 11でAndroidアプリを実行する方法|Amazon Appstoreの導入手順とサイドロード術
Windows 11のPCでもAndroidアプリが使えたら便利だと思いませんか?その夢はついに現実になりました。Windows 11では、サードパーティ製ソフトやエミュレーターを別途用意しなくても、Androidアプリを簡単に実行できます。鍵となるのが「Amazon Appstore」。Microsoft Storeからインストールすれば、厳選されたカタログの中からお気に入りのアプリやゲームを閲覧・インストールできるようになります。ただし注意点として、この機能はリリース当初、米国でのみサポートされていました。 スマートフォンだけでなくWindows PCでもアプリを活用したい——これは多く