3章
RackとRailsの関係
UnicornやPumaのようなアプリケーションサーバとRailsのようなフレームワーク間のインターフェースを共通化した仕様・実装となるライブラリ。
Rackによって、この二者の間のスムーズなやり取りを行う。
Rackインターフェースがあるから、多少Webサーバーとフレームワークが違ってもいい感じにしてくれる。
Rackに必要なインターフェース
以下の規約に則る必要がある。
call
というメソッドを定義する。
call
メソッドは慣例的にenv
あるいはenvironment
と命名する引数を1つ受け取る。
call
メソッドは次の値を戻り値として返す。
- HTTPのステータスコードを表す数値オブジェクト
- HTTPヘッダーを表すハッシュオブジェクト
- レスポンスボディとなる文字列を含んだ配列風オブジェクト
1
2
3
|
def call(env)
[status, headers, body]
end
|
Rackを使ってアプリケーションを起動させる
Rackは$ gem install rack
でインストールする。$ rackup -v
でバージョン確認。
→run
メソッドが使えるようになる。
具体的には例えばこんな感じ。
1
2
3
4
5
6
7
8
|
class App
def call(env)
status = 200
headers = { "Content-Type" => "text/plain" }
body = ["sample"]
[status, headers, body]
end
end
|
↓Rackが利用するエントリーポイント用ファイル
(一般的にconfig.ru
というファイル名)
require "rack"
require_relative "app"
run App.new
require_relative
現在のファイルからの相対パスで require します。
require と require_relative って何が違うの? - Qiita
module function Kernel.#require_relative (Ruby 2.3.0)
1
2
3
4
5
6
7
8
9
|
$ rackup
Puma starting in single mode...
* Version 4.3.5 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
|
http://127.0.0.1:9292/
にアクセスするとこんな感じ。
1
2
3
|
Use Ctrl-C to stop
127.0.0.1 - - [02/Sep/2020:19:51:29 +0900] "GET / HTTP/1.1" 200 6 0.0057
127.0.0.1 - - [02/Sep/2020:19:51:29 +0900] "GET /favicon.ico HTTP/1.1" 200 6 0.0010
|
rackup
コマンド→config.ru
を読み込み→./app.rb
を呼び出してApp.new
をrunする(callメソッドを呼び出す)
Rails の初期化プロセス - Railsガイド
Rails と Rack - Railsガイド
Rackミドルウェア
Rackミドルウェアを使うと、HTTPリクエスト / HTTPレスポンスの間で処理を追加することができる。
例えば……Rack::Runtime
を使う場合
1
2
3
4
5
|
require "rack"
require_relative "app"
use Rack::Runtime
run App.new
|
こんな感じでuse
を付けるだけで、レスポンスヘッダーに"X-Runtime"
が追加される。
他にもミドルウェアはいろいろある。詳しくは↓
https://github.com/rack/rack-contrib
Rackミドルウェアを作成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class SimpleMiddleware
def initialize(app)
@app = app
# Middlewareとして使われるクラスの初期引数には何が渡されるのか?
puts "*" * 30
puts "* #{self.class} initialize(app = #{app.class})"
puts "*" * 30
end
def call(env)
status, headers, body = @app.call(env)
puts "*" * 30
puts "* #{self.class} call(body = #{body})"
puts "*" * 30
return [status, headers, body]
end
end
|
require "rack"
require_relative "app"
require_relative "simple_middleware"
use Rack::Runtime
use SimpleMiddleware
run App.new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$ rackup
******************************
* SimpleMiddleware initialize(app = App)
******************************
Puma starting in single mode...
* Version 4.3.5 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
******************************
* SimpleMiddleware call(body = ["sample"])
******************************
127.0.0.1 - - [02/Sep/2020:20:26:46 +0900] "GET / HTTP/1.1" 200 6 0.0055
******************************
* SimpleMiddleware call(body = ["sample"])
******************************
127.0.0.1 - - [02/Sep/2020:20:26:46 +0900] "GET /favicon.ico HTTP/1.1" 200 6 0.0007
|
- ミドルウェアは最初にRackを起動した時点でinitializeされる
- その引数にはconfig.ruで定義された
App.new
が渡される(セットされる)
- リクエストを送るたびに
各Middleware#call
が実行され、レスポンスを返している
- 実行順はリクエストとレスポンスの入れ子型のイメージ
- ミドルウェアに
env
を渡しながらRackに到達し、#call
の戻り値をもらいながらレスポンスを返す
RailsでRackを使う
1
2
3
4
5
|
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application
|
1
2
3
4
5
|
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
|
こんな感じで定義している。
config.ru
があるので、rakeupコマンドで直接起動させることもできる。
1
2
3
4
5
6
7
8
|
$ bundle exec rackup
Puma starting in single mode...
* Version 4.3.5 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
|
ミドルウェアの確認。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run HelloRails::Application.routes
|
自作でRackミドルウェアを作成する場合、わかりやすくlib/middlewares
に置く。
development環境でのみ起動させたい場合はconfig/environments/development.rb
などの環境別のファイルに追加する。
どの環境でも使用する場合は、config/application.rb
に追加する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
require_relative 'boot'
require 'rails/all'
require 'midlewares/hoge_middleware' # 追加
Bundler.require(*Rails.groups)
module HelloRails
class Application < Rails::Application
config.load_defaults 6.0
config.middleware.use HogeMiddleware # 追加
end
end
|
この場合、HogeMiddlewareは既存のミドルウェアの上(内側)に積まれる。
読み込みの順番を変えたい場合、以下のように設定する。
- config.middleware.use
- config.middleware.insert_after
- config.middleware.insert_before
- config.middleware.delete
1
|
config.middleware.insert_after Rack::ETag, HogeMiddleware # 追加
|
こうすると、Rack::ETagの後にHogeMiddlewareが呼ばれる。
データベース
rails db:rollback STEP=1
のようにSTEP
を付けるとステップ分のマイグレーションファイルをロールバックする。
rails db:migrate:status
マイグレーションファイルの状態を確認する。
rails db:seed:replant
レコードを削除してからseed.rbを読み込む。
rails db:prepare
DBが存在する場合はマイグレーションを、存在しない場合はDBのセットアップを行う。楽。(= bin/setup
)
複数データベース
Rails6から対応した。
今のところ必要性がそこまでピンとこないので省略。
秘密情報
以前は環境変数経由で定義されることが多かった。
Rails 4.1
config/secret.yml
が生成されるようになったので、環境ごとに値を一括管理できるようになった。
が、秘密情報は環境変数というのは変わらず。
1
2
3
4
5
6
|
development:
secret_key_base: b8f81b...0f8dee
test:
secret_key_base: 6d2a31...3abc7e
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"]>
|
Rails.application.secrets.secret_key_base
で取得可能。
Rails 5.1, 5.2
省略。
Rails 6.0
rails credentials:edit --environment staging
コマンドでconfig/credentials/staging.key
とconfig/credentials/staging.yml.enc
が生成される(ついでにstaging.keyは.gitignoreってくれる)。
ただし、credentialsの読み込みは、credentials/{RAILS_ENV}.yml.enc
→なかったらcredentials/credentials.yml.enc
なので、どちらかに書いてあればOKというわけではない。重複内容も別ファイルに書く必要がある。
同じcredentialsファイルを別の環境でも読みたいときは、以下のように設定する。
1
2
3
4
|
Rails.application.configure do
config.credentials.content_path = Rails.root.join('config', 'credentials', 'development.yml.enc')
config.credentials.key_path = Rails.root.join('config', 'credentials', 'development.key')
end
|
Early Hints
ブラウザでアクセスした際、
- ブラウザがリクエスト送信
- サーバーからHTMLのレスポンスを受け取る
- HTMLのパースを開始
- アセットをリクエスト
- サーバーからアセットのレスポンスを受け取る
- アセットのパースを開始
という順番になるので、1, 2間と4, 5間で待ち時間が発生する。「始めからわかりきったアセットは、もっと早めにもらいたくね?」から生まれた。
HTTP/1.1 103 Early Hints
Link: </assets/application.debug-....css>; rel=preload; as=style
HTTP/1.1 103 Early Hints
Link: </pack/js/application-....js>; rel=preload; as=script
HTTP/1.1 200 OK
...(HTMLのレスポンス)
こんな感じで事前にアセットのリクエスト先を連絡してもらえるので、2.の前にアセット用のリクエストを送信することができる嬉しい!
ただし、現時点では203に対応したメジャーなブラウザがない&未対応ブラウザに103を含む複数レスポンスを送信するとセキュリティ上の問題があるので、リバースプロキシを介するのが現実的。
【ポイント】 リバースプロキシの基本的な仕組み
まあ、なんだかんだ現実的にこれが有用かどうかはうーんって感じ。HTTP2の機能だし。
使う前には、本当にそれがいいのか?を調べること。
Content Security Policy(CSP)
XSSの脆弱性による影響範囲を少なくするための仕組み。
@post.body.html_safe
のように、ユーザーが入力したタグをそのまま出力したとする。以下のようなJSが送られるとヤバイ。
1
2
|
<script src="http://trap.example.com/dangerjs"></script>
<script>alert(document.cookie)</script>
|
CSPを使うとこのようなXSSを無効にできる。
Content-Security-Policy: script-src 'self' www.google-analytics.com
このようにホワイトリスト形式で、許可したいドメインを指定する。
'self'
はレスポンスを送ったサーバーと同一オリジン(ドメイン・ポート番号・URLが同一)であるという意味。
CSPをRailsで使うには
config/initializers/content_security_policy.rb
のコメントアウトを外すことで設定できる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # If you are using webpack-dev-server then specify webpack-dev-server host
# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# Set the nonce only to specific directives
# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
|
コントローラごとに設定することもできる。
1
2
3
4
5
|
class PostController < ApplicationController
content_security_policy do |p|
p.script_src :self, :https, "https://example.com"
end
end
|
Content-Security-Policy-Report-Onlyを使う
ただし、いきなりSCPを有効にすると、本来指定しないといけないURLを指定し忘れることもある。
なので、まずはContent-Security-Policy-Report-Only
ヘッダを指定するとよい。これを適用すると、違反(指定漏れ)していた場合report-uriディレクティブで指定したURLに送信してくれる。
設定は以下。
1
2
3
4
5
|
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.report_uri "/csp-violation-report-endpoint"
end
Rails.application.config.content_security_policy_report_only = true
|
nonceを使う
より細かい指定ができる。
Content-Security-Policy: script-src 'nonce-2726c7f26c'
これを指定すると、以下のようなnonce属性があるscriptのみ実行できるようになる。
1
2
3
|
<script nonce="2726c7f26c">
alert("実行できる")
</script>
|
Railsで指定する場合はこんな感じ。
1
2
3
4
5
|
# scriptとstyleにnonceを利用
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# scriptのみnonceを利用
Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
|
ビューファイルでは以下のようにすると、対応したnonceがタグに追加される。
<%= javascript_tag nonce: true do %>
alert("Hello, World!")
<% end %>
<%= javascript_pack_tag "script", nonce: true %>
rails_ujsなどを利用している場合、csp_meta_tag
メソッドでnonceをHTMLに埋め込む必要がある。
1
2
3
|
<head>
<%= csp_meta_tag %>
</head>
|