Note

3年後の自分のために書いています

Rails の API モードでセッションを有効にする

巷には config/application.rb 内で Rails.app_class.config.api_only = false にすればできる、的な記事が溢れているが、不要な middleware まで読み込みたくない。

環境

差分

デフォルトの CookieStore を使う場合

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CookieStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end

config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware が必要というのが分からずハマった。

👆 しないでも _session_id は保存されたが、session[:user_id] などの情報が保持されなかった。(再度リクエストした時に取得できなかった)

Rails と Rack - Railsガイド

  • ActionDispatch::ContentSecurityPolicy::Middleware

Content-Security-Policyヘッダー設定用のDSLを提供します。

とのこと。これを使うことで _session_id が暗号化(?) されていた。

api_only = false にすると {app_name}_session というキーに暗号化された値が入る(_session_id にも入るけど)

CacheStore を使う場合 ~memory_store の場合~

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CacheStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index dc78c00..cbbc46a 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -14,18 +14,11 @@ Rails.application.configure do
   # Show full error reports.
   config.consider_all_requests_local = true

-  # Enable/disable caching. By default caching is disabled.
-  # Run rails dev:cache to toggle caching.
-  if Rails.root.join('tmp', 'caching-dev.txt').exist?
-    config.cache_store = :memory_store
-    config.public_file_server.headers = {
-      'Cache-Control' => "public, max-age=#{2.days.to_i}"
-    }
-  else
-    config.action_controller.perform_caching = false
-
-    config.cache_store = :null_store
-  end
+  # Enable caching.
+  config.cache_store = :memory_store
+  config.public_file_server.headers = {
+    'Cache-Control' => "public, max-age=#{2.hour.to_i}"
+  }

   # Print deprecation notices to the Rails logger.
   config.active_support.deprecation = :log

Rails.app_class.config.cache_store に何も指定しなければ、デフォルトの :file_store になり、 path には "#{root}/tmp/cache/" が使われる。(Rails.app_class.config.cache_store = :file_store と書いた場合は明示的に path の指定が必要になる)

Redis を使いたい場合はここを :redis_cache_store にすれば OK。

api_only の設定による middleware の差分

Rails.app_class.config.api_only が true の場合と false の場合の差分。

$ rails middleware で確認できる。

false の方がたくさん middleware が使われている。

--- tmp/api_true.txt 2020-05-05 05:16:23.000000000 +0900
+++ tmp/api_false.txt 2020-05-05 05:18:06.000000000 +0900
@@ -3,6 +3,7 @@
 use ActionDispatch::Static
 use ActionDispatch::Executor
 use Rack::Runtime
+use Rack::MethodOverride
 use ActionDispatch::RequestId
 use ActionDispatch::RemoteIp
 use Rails::Rack::Logger
@@ -12,7 +13,12 @@
 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 RailsCsrfProtectionApi::Application.routes

セッションストレージについて

Rails 的には CookieStore 推奨っぽい

あらゆるセッションは、セッション固有のIDをcookieに保存します (注意: RailsでセッションIDをURLで渡すことはセキュリティ上の危険があるため許可されません。セッションIDは必ずcookieで渡さなくてはなりません)。

多くのセッションストアでは、このIDは単にサーバー上のセッションデータ (データベーステーブルなど) を検索するために使われています。ただしCookieStoreは例外的にcookie自身にすべてのセッションデータを保存します(必要であればセッションIDも利用できます)。そしてRailsではCookieStoreがデフォルトで使われ、かつRailsでの推奨セッションストアでもあります。CookieStoreの利点は、非常に軽量であることと、新規Webアプリケーションでセッションを利用するための準備がまったく不要である点です。cookieデータは改竄防止のために暗号署名が与えられています。さらにcookie自身も暗号化されているので、内容を他人に読まれないようになっています。(改ざんされたcookieRailsが拒否します)

以下も参考になりそう。

Rails セキュリティガイド#セッションストレージ

参考

WebRTC + Firebase のチュートリアルやってみた

See. https://webrtc.org/getting-started/firebase-rtc-codelab

ほぼ写経しただけ、👆 のチュートリアルの通りに書いても動かなかったので、サンプルリポジトリ の方を参考にした。

成果物

ブラウザで動くシンプルな 1 対 1 のビデオ通話アプリケーション。 チュートリアルではローカルでの動作確認までしか書いてないけど、せっかくなので Firebase Hosting にデプロイまでした。

使い方

  1. Open camera & microphone でカメラとマイクを許可
  2. Create room を押下すると Room ID が発行される
  3. 別の PC で Join room を押下し、Room ID を入力すると通話ができる
  4. Hang up を押下すると通話終了し、その Room ID は使えなくなる

同一 LAN 内じゃないと動作しなかった :sob:

いろいろ試した結果以下のような結果になった。

最初は OS やブラウザのせいかと思っていたが、途中からもしかしてネットワーク周りが原因では?と思い、最終的に家用 Wifi ルータとモバイルルータ使って LAN を分けて試した結果、Mac + Chrome x Mac + Chrome 環境でも LAN が違うと通信できないことが分かった。

現状 Google が提供している STUN サーバなるものを利用しているが、LAN 外のホストと通信するには TURN サーバなるものを用意しないといけないかもしれない。

ref. そして壁の向こうへ。 NAT/Firewallを越えて通信しよう―WebRTC入門2016

STUNでNATを越えられないとき

NATにはグローバルIPアドレスを共有するだけでなく、セキュリティ対策としての役割もあります。内部の端末を隠したり、通信できるポートを制限したり、一種の簡易Firewallとして利用されているケースもあります。その場合はFirewallの場合と同じく、次に説明するTURNを利用する必要があります。

またNATの構造によっては、接続先によって(今回の場合、STUNサーバーとPeer-to-Peerの通信相手)別のポートが割り当てられる Symmetric NAT という物があるようです。この場合もSTUNの仕組みでは通信することができません。やはりTURNの出番ということになります。

メモ

  • PC(Mac)同士だと動いたけどスマホiOS)では相手の画面が表示されない不具合が出た
  • Firestore は Room ID と offer の管理に使ってるだけ、あとはほぼ JS から呼べる MediaDevices と WebRTC の API で実装できてる
  • ICE と SDP あとで調べる

参考

gem 'her' コードリーディング

と言いつつ途中から依存してる 'faraday' のコードリーディングがメインになった。

her 側で faraday_options 引数として渡した Hash を faraday 側では url という名前で扱っており、混乱したので記事にしておく。

バージョン

  • her (1.1.0)
  • faraday (0.17.3)

url のみ渡して setup

以下を実行した時に何が起こるか。

require 'her'
Her::API.setup url: "https://api.example.com"

Her::API.setup -> Her::API#initialize -> Her::API#setup と処理が流れていく。

Her::API#setup 内で Faraday.new する時 の実引数 faraday_options{ url: "https://api.example.com" } という Hash になる。

Faraday.new の時には実引数 faraday_options は仮引数 url として渡ってくる、第二引数 options は渡してないので nil になる。(ここの名前の紛らわしさで混乱した)

Faraday.new 内の Faraday::Connection.new で呼ばれた Faraday::Connection#initialize の仮引数 url は Hash なので、ここ で値を抜き取られて url 変数にセット、最終的には #url_prefix= で url_prefix にセットされる。(呼び出し側のここインスタンスメソッドの呼び出しを url_prefix= とするとローカル変数への代入になってしまうので self.url_prefix= としている。)

戻り値を her_api とすると her_api.connectin.url_prefix のようにアクセスできる。

url の他にもオプション渡して setup

README#ssl を参考に。

require 'her'
Her::API.setup url: "https://api.example.com", ssl: { ca_path: "/usr/lib/ssl/certs" }

この場合 Faraday.new の仮引数 url{ url: "https://api.example.com", ssl: { ca_path: "/usr/lib/ssl/certs" } } } のような Hash となる。options は引き続き nil、さあこれでも動くのか。

結論からいうと渡ってきた url が Hash の場合は ここoptions に代入してオプションとしても扱えるようにしてる。わお。

一応 コメントにも書いてあるのね。わお。

Can also be the options Hash.

ぶっちゃけここの柔軟性は不要だと思うけど、歴史的経緯があるのかな。

メモ

  • 仮引数が定義側、実引数が呼び出し側
  • faraday は Faraday::Options で Hash を Struct に変換してメソッド呼び出しできるようにしてるの面白かった

参考

gem 'pg' コードリーディング

なぜだか急に pg gem の実装が気になった。

# コネクション張って SELECT するだけのコード
require 'pg'

conn = PG.connect(dbname: 'hoge')
conn.exec('SELECT * FROM fuga') do |result|
  result.each do |row|
    puts row
  end
end

👆 のコードを実行した時に何が起こっているかを追う。

バージョン

pg 1.2.3

PG.connect

PG.connect の定義は lib/pg.rb にある

はいはい PG::Connectionインスタンスを返してるのね。って PG::Connection クラス 見てみたらインスタンスメソッド一つもないし、example コードPG::Connection.open も定義されてない。

ここでハマってしまったが、結論から言うと全て ext/ 以下の C 言語のコードで定義されていた。

実装的には ext/pg_connection.c にあった。

まず .open 含むいくつかの特異メソッドは .new の alias だった

alias 張るのに使われてる SINGLETON_ALIAS は自前で定義したマクロだった。(内部的にはrb_define_alias 使ってる)

alias 張られてるクラスの rb_cPGconnPG::Connection なのではと思って読んでたら、ビンゴ。

rb_cPGconn 変数代入してるとこ 見ると、 rb_define_class_underrb_mPG に所属する定数として定義されていて、rb_mPG 変数代入してるとこ(C 拡張の初期化時に呼ばれる)を見ると、rb_define_module でモジュール PG として定義されていた。

で initialize の処理は rb_define_methodpgconn_init として定義されていた。

pgconn_init これが読みたかったんだよう 😭(ドキュメントもバッチリ書いてある…)

とりあえずいろいろやって conn オブジェクトを返してるね。(ここまで読んで満足してる)

conn.exec

ここpgconn_async_exec として定義されてる。

pgconn_async_exec これが読みたかry

大事そうな処理としては postgres にクエリ投げてるっぽい pgconn_send_query と非同期に結果取得してるっぽい pgconn_get_last_result かな。

pgconn_get_last_resultPG::Result が返る。(PG::Result の定義は ここ っぽいけど力尽きました)

所感

  • 途中から PostgreSQL とか pg gem とかどうでもよくなってた
  • ruby の C API の読み方がちょっと分かって良かった

参考

Docker 学習ログ

Docker 分からなさすぎるのでコツコツと学ぶ。 間違ってるところは随時直していく。

利用するメリット

  • 開発環境やテスト環境が構築・配布しやすくなる
    • Docker を実行できる環境さえあれば OS の違いも吸収する
  • VM(仮想化 OS)よりも起動が速い
    • VM(仮想化 OS)は OS 部分から仮想化する(VM の数だけゲスト OS が必要)のに対し、Docker は OS のカーネル部分はホスト OS のものを共有して利用するため

ソースコード

ref. 「Moby」ベースとなったオープンソース版Dockerの最新状況

バージョニング

https://www.publickey1.jp/blog/17/docker_v1703.html

前回のバージョンはDocker 1.13でしたが、今回からバージョン番号の制度が変更になり、バージョン番号は「年2桁.月2桁.リビジョン」となり…

2020 年 4 月現在の最新バージョンが 19.03.8 なので 👆ともちょっとずれてそうだけど…

基礎技術

Docker Engine
---
Linux VM(Docker for Mac に同梱) ← これがホスト OS の役割を果たす
---
macOS

コンポーネント

Docker Engine

  • クライアント(CLI
  • サーバ(Docker デーモン)
  • クライアントからコマンド叩くことでローカルやリモートの Docker デーモンと通信するアーキテクチャになっている

Docker Compose

  • Docker Engine とは独立したソフトウェア(Docker for Mac だと同梱されてるけど)
  • yaml ファイル一つで複数コンテナを定義できる
  • 環境変数、ポート設定なども yaml に記述
  • 同じ compose ファイル内であれば自動的に同じネットワークに繋がるので内部的なポート設定は不要

イメージとは

  • オブジェクト指向でいうとクラス
  • Dockerfile からビルドされるやつ($ docker build
  • Registry(DockerHub)を使ってイメージそのものを共有可能

コンテナとは

ref. https://stackoverflow.com/questions/23735149/what-is-the-difference-between-a-docker-image-and-a-container

ボリュームとは

  • データを永続化できる場所で、コンテナから見ると外部 HDD のようなイメージ
    • ボリュームを使わないとコンテナ破棄時にコンテナ内のデータは消える
    • DB(Postgres や MySQL)のコンテナを利用する時などによく利用する
  • 実体はホスト(Docker が動いてるコンピュータ)にファイルとして保存されている
    • 名前付きボリュームの場合は、Docker 内のリソースとして管理されるので正確な保存場所は分からないっぽい?
    • 名前つけずにパス指定した場合はそのパスにファイルが作られる
  • Docker におけるデータ永続化の方法としては バインドマウント なるものもあるらしいが、ボリュームの方が推奨されてるっぽい
  • また docker-compose では COPY や ADD が使えないが、volumes を使って代用できる
    • 👆 ができるのはボリュームには以下の仕様があるため
  • 参考

Dockerfile

docker-compose.yml

  • volumes
    • https://docs.docker.com/compose/compose-file/#volumes
    • https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/#volumes (非公式っぽい日本語訳ドキュメント)
    • SOURCE:TARGETの形式で指定して、SOURCE にはホスト側のパスまたはボリューム名を指定、TARGET にはボリュームをマウントするコンテナのパスを指定する
      • 例: namedvolume:/var/lib/mysql, ./cache:/tmp/cache
    • 名前付きボリュームを使うにはトップ・レベルの volumes キーを指定する必要がある
    • たまに見かける SOURCE:TARGET:cached などの cached オプションはパフォーマンス向上のためのオプションっぽいが、最新の公式ドキュメントには記載が見当たらない

コマンド

その他

  • コンテナ削除しても、イメージ削除しても上手く動かない… :sob: って時はボリューム削除してみるべし
  • docker-compose.yml で image を指定しておくと、ローカルに同名イメージがあればそちらを使ってくれる
    •   services:
          web:
              build: .
              image: shopify-app
              # ...
      
    • 普通に image に指定してある名前(ここでいうと shopify-app)でタグ付けしてやれば同名の image を指定している service 全てで同じイメージを参照する挙動だった(_{service} はつけてもつけなくても動く)、、賢い :sob:
  • docker-compose.yml のボリュームマウント時に一部のディレクトリだけマウントしない方法
    • マウント後に無名ボリュームで一部のディレクトリだけ再マウントしてやれば OK

参考

非効率なコードレビューをなくしたいというポエムを書くためのメモ

前提

  • 現在 Web 業界で主流(?)な 100 % PR を完成させてから(マージできる状態にしてから)In Review にするやり方は非効率
  • PR はざっくりできた 〜 In Review までがけっこう作業ある(commit 粒度の確認、YARD などのコメント、GitHub の 0 コメ、動作確認手順記載)
  • レビュー時に設計レベルのちゃぶ台返しをされた時に時間的無駄や、精神的疲労が蓄積される
  • 👆 のしんどさは主にジュニア~メンバークラスの人々が感じることが多く、シニアになると指摘される数が減り修正も早いのであまり危機意識を感じなくなるのではないか(だから自分が考えてみる)

こうした方がよいのではないかという案

  • 新機能や広範囲に渡るリファクタリングはモブ設計(クラスやインターフェースの設計)を行い、可能ならばテストまで書く
  • 実装は一人に任せてもよいし、新規の外部 API などを叩くのであれば、モブで続けてもよい
  • レビューは実装でバグやパフォーマンス的に問題になる部分のみする(N+1、O(n2) など)、基本は動いていれば OK
  • 改行などの指摘は linter が通っていればスルーする
    • どうしても気になるならばレビュアーが別途 PR を作る or 「どうしても気に入らなかったら revert してください」と commit してしまう
  • typoGitHub の suggestion を使う or 自明ならばレビュアーが commit
  • コンフリクトも自明ならばレビュアーが commit しても良い
  • どうしても設計レベルの指摘を行う場合は代替の PR もしくはペアプログラミングで修正する

メモ

  • どこまでモブでやるかの線引き難しい、どこまでが設計でどこから実装なのかはっきりさせたい(例: 関数名を考えるのは設計か?引数と返り値を考えるのは実装か?)
  • 設計部分をモブでやることでジュニアの自力で考える力がつかないのではないか?
    • 自力で叩き台まで作って PR などにして相談する、という時も必要だと思う、その際も 100 % 作りきる前に相談するのが良い
    • また効率的なレビューをした結果、時間が空くはずなのでその分を自学に充てるのもあり

触発された記事とか

Pull Request レビューの限界|qsona|note

むしろ、根底に関わるような重要な設計相談はそれ以前に会話なりモブプログラミングなりですべき

コードレビューで指摘するたびにいちいちイライラされるプログラマーには致命的なバグでもない限り何も言わないのが正解だと思いますか? - Quora

動いているなら何も言わないのが正解のひとつと思います。 また、かわりに自分が直してあげたり、ペアプログラミングした方がいいでしょう。

【コードメモ】JS で Object を forEach する方法

$ node
> let obj = { hoge: "hogehoge", fuga: "fugafuga" }
> Object.keys(obj).forEach(key => console.log(key, obj[key]))
hoge hogehoge
fuga fugafuga

参考

How to loop through a plain JavaScript object with the objects as members? - Stack Overflow

Object.keys() - JavaScript | MDN