Rails Good Parts
Railsにまつわる便利機能・デザインパターンはたくさんありすぎて、正直覚えきれないので、 個人的に役立ったものまとめていこうと思います。
ActiveModel
validation
バリデーションを特定のコンテキストでのみ実行する方法。
class Person include ActiveModel::Validations attr_accessor :name validates_presence_of :name, on: :new end person = Person.new person.valid? # => true person.valid?(:new) # => false
ActionView
disable_with
二重サブミット防止するために、Javascriptを書く必要なし!
http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-submit_tag
submit_tag "Complete sale", data: { disable_with: "Submitting..." }
Patterns
Form Object
特定のコンテキストでしか利用しないバリデーション(例: 特定のユーザロールのみ、入力を必須としたいテキストフィールド)をModelレイヤで実装すると、複雑化することが多いように思います。 このようなケースでは、Form Object の導入は一つの選択肢になります。 blog.sundaycoding.com
PackerのAnsible RemoteでSSH ForwardAgentする
PackerのAnsible Remote ProvisionerでAMIをビルドしていますが、 SSH ForwardAgentできずにだいぶハマったので、備忘のためにメモしておきます。
やりたいこと
githubのプライベートリポジトリをansibleでcloneしたい。 公開鍵認証する必要あるが、private key はアップロードしたくないので、forward agent で解決したい。
なお、packerのバージョンは0.11.0です。
やったこと
packer.json
{ "variables": { "aws_access_key": "", "aws_secret_key": "" }, "builders": [{ "type": "amazon-ebs", "access_key": "{{user `aws_access_key`}}", "secret_key": "{{user `aws_secret_key`}}", "region": "ap-northeast-1", "source_ami": "ami-xxxxxxx", "instance_type": "t2.small", "ssh_username": "ec2-user" }], "provisioners": [{ "type": "ansible", "user" : "ec2-user", "sftp_command" : "/usr/libexec/openssh/sftp-server -e", "playbook_file": "ansible/playbook.yml", "ansible_env_vars": [ "ANSIBLE_HOST_KEY_CHECKING=False", "ANSIBLE_SSH_ARGS='-o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s'"] }] }
ここではANSIBLE_SSH_ARGS環境変数に -o ForwardAgent=yes をセットしています。
ansible.cfg
[defaults] sudo_flags=-HE
このオプションによりsudoでも環境変数を引き継ぐようになります。 sudoのmanには以下のように書かれています。
-E The -E (preserve environment) option will override the env_reset option in sudoers(5). It is only available when either the matching command has the SETENV tag or the setenv option is set in sudoers(5). sudo will return an error if the -E option is specified and the user does not have permission to preserve the environment.
これでANSIBLE_SSH_ARGSが有効になりました。
role
- name: git clone repository git: repo: 'git@github.com:organization/your-awesome-repo.git' dest: '/home/yourname/dir' accept_hostkey: yes version: master
あとはcloneするだけ。
References
Multithreaded Programming - Operating System Concepts Chapter 4
ちょっとしたメモ。
Operating System Concepts の Chapter 4 の冒頭に
A thread is a basic unit of CPU utilization; it comprises a thread ID, a program counter, a register set, and a stack. It shares with other threads belonging to the same process its code section, data section, and other operating-system resources, such as open files and signals.
出典: https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html
と書かれている。
ざっくりいうと、プログラムカウンタ、レジスタ、スタックはそれぞれで保持するけど、データセクション(含 グローバル変数)、オープンファイルやシグナルは共有するということ。
グローバル変数について実際に確認してみる。
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <pthread.h> int x = 0; pthread_t tid[2]; void* incr() { x = x + 1; printf("%d\n", x); } int main() { int i = 0; int err; while(i < 10) { err = pthread_create(&(tid[i]), NULL, &incr, NULL); if (err != 0) printf("can't create thread\n"); i++; } sleep(1); }
$ gcc -pthread thread.c && ./a.out 1 2 3 4 5 6 7 8 9 10
確かに、スレッドで共有されていることが確認できる。
次に、fork(2)で同じようなコードを書いてみる。
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <pthread.h> int x = 0; void* incr() { x = x + 1; printf("%d\n", x); } int main() { int i = 0; int err; pid_t pid; while(i < 10) { pid = fork(); // child if (pid == 0) { incr(); exit(0); } // parent i++; } sleep(1); }
$ gcc fork.c && ./a.out 1 1 1 1 1 1 1 1 1 1
これは共有されない。
Action Mailerでfromフィールドに差出人名を表示したい
よく見かけるAction Mailer のサンプルはこんな感じだと思います。
mail from: 'noreply@example.com', to: 'foobar@example.com', subject: 'Hi'
このケースでは差出人は noreply@example.com となるんですが、時にサービス名などを設定したくなることもあると思います。 今回はActionMailerでどうやるといいの? って話です。
ドキュメント
まずはrails/actionmailer at master · rails/rails · GitHubを探しますが、特に方法は書かれていません。
ActionMailerはMailのラッパーなので、GitHub - mikel/mail: A Really Ruby Mail Libraryに目を通すと、それらしきサンプルが見つかります。
mail = Mail.new do to 'nicolas@test.lindsaar.net.au' from 'Mikel Lindsaar <mikel@test.lindsaar.net.au>' subject 'First multipart email sent with Mail' end
"Mikel Lindsaar"の部分を置き換える形で運用を始めたところ、ごく稀にエラーが発生してメールが送信できない問題にぶつかりました。
An ArgumentError occurred in *****#create: An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address. app/controllers/*****.rb:123:in `****************'
原因
ArgumentErrorはmail/check_delivery_params.rb at df48a05a7fb5a4271e6df12da7afb26a53494a18 · mikel/mail · GitHubにてraiseされていました。smtp_envelope_from がblankだとこのエラーが発生します。
いろいろとテストした結果、少なくとも ドット「.」や括弧「(」「)」が表示名に含まれている場合にエラーが再現することが確認できました。
name = 'kotaroi.(test)' mail = Mail.new do from "#{name} <noreply@example.com>" to 'foobar@example.com' subject 'This is a test email' end mail.smtp_envelope_from # => nil
ということで、rfcを読み解きます。本来は運用前に調べておくべき話ですね(汗
RFC
obsoleteやupdateがたくさんあるので、どれを読むべきか把握するのが大変でした。。 今回読むべきは RFC 5322 - Internet Message Format で、FromフィールドはこのRFCで規定されています。
3.6.2. Originator Fields The originator fields of a message consist of the from field, the sender field (when applicable), and optionally the reply-to field. The from field consists of the field name "From" and a comma- separated list of one or more mailbox specifications.
と記載されており、セクションを少し遡ると、
3.4. Address Specification Addresses occur in several message header fields to indicate senders and recipients of messages. An address may either be an individual mailbox, or a group of mailboxes. address = mailbox / group mailbox = name-addr / addr-spec name-addr = [display-name] angle-addr angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr group = display-name ":" [group-list] ";" [CFWS] display-name = phrase mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
と書かれています。最終的に
atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" atom = [CFWS] 1*atext [CFWS] dot-atom-text = 1*atext *("." 1*atext) dot-atom = [CFWS] dot-atom-text [CFWS] specials = "(" / ")" / ; Special characters that do "<" / ">" / ; not appear in atext "[" / "]" / ":" / ";" / "@" / "\" / "," / "." / DQUOTE
と
qtext = %d33 / ; Printable US-ASCII %d35-91 / ; characters not including %d93-126 / ; "\" or the quote character obs-qtext qcontent = qtext / quoted-pair quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] (中略) word = atom / quoted-string phrase = 1*word / obs-phrase
に辿り着きました。
これにより、表示名(display-name)は quoted-string でない場合は specials を含めてはならないことを確認できました。裏を返せば、specials を使いたい場合は quoted-string とすればよいです。
次は「Ascii以外はどうするの」という疑問が出てきますが、RFC 6532 - Internationalized Email Headers にてUTF-8が使えるよう拡張されています。
3.2. Syntax Extensions to RFC 5322 The preceding changes mean that the following constructs now allow UTF-8: 1. Unstructured text, used in header fields like "Subject:" or "Content-description:". 2. Any construct that uses atoms, including but not limited to the local parts of addresses and Message-IDs. This includes addresses in the "for" clauses of "Received:" header fields. 3. Quoted strings. 4. Domains.
コード
specials が含まれるか否かを判定し、含まれる場合は quoted-string(つまりダブルクォートで囲む)とし、さらにダブルクォートとバックスラッシュをエスケープすればよさそうです。
def envelope_display_name(display_name) name = display_name.dup # Special characters if name && name =~ /[\(\)<>\[\]:;@\\,\."]/ # escape double-quote and backslash name.gsub!(/\\/, '\\') name.gsub!(/"/, '\"') # enclose name = '"' + name + '"' end name end name = envelope_display_name('display name here') mail from: "#{name} <noreply@example.com>", to: 'foobar@example.com', subject: 'Hi'
日本語(UTF8-non-ascii)が含まれていても、問題なく動作することを確認済みです。
まとめ
ActionMailerで差出人名を変更する方法を調べました。 本当は mail gem の実装まで確認しておいたほうがいいんですが、時間が限られてるのでまた今度。。
bundler環境で動いてるunicornでgem が更新されない話
TL;DR
Unicornをpreload_app=falseで運用してる場合は、
before_exec do |server| ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile") end
の設定をした上で、SIGUSR2 を使いましょう。 capistrano3-unicornを利用しているなら、下記の通りです。
after 'deploy:publishing', 'deploy:restart' namespace :deploy do task :restart do invoke 'unicorn:restart' end end
発生していた問題
新しいgemをGemfileに追加してcapistranoでデプロイするも、なぜか新しいgemを認識してくれない。
## unicorn.log E, [2016-09-08T15:02:36.583698 #5571] ERROR -- : uninitialized constant ExceptionNotifier::Rake (NameError) /home/myapp/myapp/releases/20160908060023/config/initializers/exception_notification.rb:10:in `<top (required)>' (以下略)
設定
preload_appはfalseで運用しているため、capistrano3-unicornのREADMEに従って、config/deploy.rbには下記の通り記述しています。
after 'deploy:publishing', 'deploy:restart' namespace :deploy do task :restart do invoke 'unicorn:reload' end end
この設定により cap production deploy を実行すると、SIGHUP をunicornに送ることになります。
Running /usr/bin/env kill -s HUP `cat /home/myapp/myapp/current/tmp/pids/unicorn.pid` on ***.***.***.***
原因
2つほど誤解(というか理解不足)がありました。
誤解その1
http://unicorn.bogomips.org/Sandbox.html に従って、before_exec で ENV["BUNDLE_GEMFILE"] をセットしていました。
before_exec do |server| ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile") end
が、これがうまく動作しません。
UnicornでSIGHUPをハンドリングしている箇所を読んで見ると、preload_app=false では before_exec を実行しないことが判明。。。
誤解その2
そうは言っても https://unicorn.bogomips.org/SIGNALS.html のHUPには
When reloading the application, Gem.refresh will be called so updated code for your application can pick up newly installed RubyGems.
と書かれているじゃないか、と思うわけです。
実際、unicornのコードやログを見ても、Gem.refresh を呼び出しているようにも見えます。
I, [2016-09-09T11:49:21.648309 #5967] INFO -- : worker=0 spawned pid=5967 I, [2016-09-09T11:49:21.667362 #5967] INFO -- : Refreshing Gem list I, [2016-09-09T11:49:23.931829 #30636] INFO -- : reaped #<Process::Status: pid 14112 exit 0> worker=1 I, [2016-09-09T11:49:23.933808 #5970] INFO -- : worker=1 spawned pid=5970 I, [2016-09-09T11:49:23.934109 #5970] INFO -- : Refreshing Gem list
が、どうも疑わしいので Bundler のコードを追いかけてみました。
# Because Bundler has a static view of what specs are available, # we don't #refresh, so stub it out. def replace_refresh gem_class = (class << Gem; self; end) redefine_method(gem_class, :refresh) {} end
空っぽに置き換えられていることが判明。。。
どうすればよいか?
USR2 シグナルを使えばOKです。
Unicornのmasterプロセスは USR2 シグナルを受取ると、自身をforkして unicornコマンドをexecします。
I, [2016-09-09T16:54:40.831653 #10698] INFO -- : executing ["/home/myapp/myapp/shared/bundle/ruby/2.2.0/bin/unicorn", "-c", "/home/myapp/myapp/current/config/unicorn/sandbox.rb", "-E", "deployment", "-D", {12=>#<Kgio::TCPServer:fd 12>}] (in /home/myapp/myapp/releases/20160909075156)
コードを見ると、execの直前でbefore_execを実行しているので、
before_exec do |server| ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile") end
でBUNDLE_GEMFILEをセットしておけばよいです。これで新しいGemfileをexecするプロセスで認識できます(この時点でcurrent symlinkは切り替え済み)。
なお、execで実行されるunicornのbinはGem.bin_pathを呼び出すのですが、BundlerがこのコードによりGem.bin_pathを書き換えているので新しいgemを認識できるということのようです。(詳しく追ってないので、もしかしたら間違っているかも)
REST API
REST APIをつくるときのリファレンスをまとめておく場所(の予定)
W3C Status Code
HTTP/1.1: Status Code Definitions
jsonapi
Blogs
Best Practices for Designing a Pragmatic RESTful API | Vinay Sahni
クリックジャッキングとX-Frame-Options
クリックジャッキングとX-Frame-Optionsについて調べたことをまとめます。
クリックジャッキング
クリックジャッキング攻撃とは、ユーザを視覚的にだまして正常に見えるウェブページ上のコンテンツをクリックさせ、別のウェブページのコンテンツをクリックさせる攻撃のことである。その結果、ユーザが公開するつもりのないプライバシー情報を公開させられたり、意図しない情報を登録させられたりするなどの被害を受ける可能性がある。
出典: クリックジャッキング - IPA 独立行政法人 情報処理推進機構 PDF
悪意あるサイトはiframe を透明化して、攻撃対象のサイトを読み込みます。 X-Frame-Options レスポンスヘッダを設定することで、対策をすることができます。
X-Frame-Options
3種類の値を設定することができます。
DENY
サイト側の意図に関わらず、ページをフレーム内に表示することはできません。
SAMEORIGIN
自身と生成元が同じフレーム内に限り、ページを表示することができます。
ALLOW-FROM uri
指定された生成元に限り、ページをフレーム内に表示できます。
https://developer.mozilla.org/ja/docs/HTTP/X-Frame-Options
Railsでは...
SAMEORIGIN がデフォルトです。 https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actionpack/lib/action_dispatch/railtie.rb#L22
Rails4.0 で導入されました。
一部機能をiframeとして外部に提供したい場合はレスポンスヘッダから削除すればokです。
response.headers.delete 'X-Frame-Options'