- 投稿日:2019-05-26T22:39:47+09:00
Rubyで音(wav)を書き出してみた
小ネタです。
Rubyで音声ファイルって作れないのかなと思ったら、こんな感じでやるとできそうです。gem install numo-narray gem install wavefileサイン波っぽいの
require "wavefile" require "numo/narray" include WaveFile frequency = 440.0 # 生成するサイン波の周波数 seconds = 1.0 # 生成する音の秒数 rate = 44100 # 出力するwavファイルのサンプリング周波数 a = Numo::SFloat.linspace(0, 2 * Math::PI * frequency, rate) # 本当はrate+1として1個削るべき? b = Numo::NMath.sin(a) wave = b.to_a buffer = Buffer.new(b.to_a, Format.new(:mono, :float, rate)) Writer.new("sin.wav", Format.new(:mono, :pcm_16, rate)) do |writer| writer.write(buffer) end三角波はこんな感じかな?
require "wavefile" include WaveFile rate = 44100 triangle = Array.new(100){|i| i < 50 ? i : 100 - i }.map{|i| i / 50.0 - 0.5} buffer = Buffer.new(triangle, Format.new(:mono, :float, rate)) Writer.new("triangle.wav", Format.new(:mono, :pcm_16, rate)) do |writer| 441.times{writer.write(buffer)} end矩形波はこんな感じ?
require "wavefile" include WaveFile rate = 44100 kukei = Array.new(50, -0.5).concat(Array.new(50, 0.5)) buffer = Buffer.new(kukei, Format.new(:mono, :float, rate)) Writer.new("kukei.wav", Format.new(:mono, :pcm_16, rate)) do |writer| 441.times{writer.write(buffer)} endFlammarionでGUI化してみると面白そうだ。
参考資料
- 投稿日:2019-05-26T22:17:25+09:00
Ruby SeleniumでElementNotInteractableError
Ruby + Seleniumでgoogle検索した時にハマった。
詳細
以下のスクリプトを実行
rubyrequire "selenium-webdriver" #driverはchrome driver = Selenium::WebDriver.for :chrome #googleの検索画面 driver.get "https://www.google.co.jp" #検索項目 inputElement = driver.find_element(:name, 'q') inputElement.send_keys "ほげ" #検索ボタン submitElement = driver.find_element(:class, 'gNO89b') submitElement.clickエラーメッセージelement not interactable (Selenium::WebDriver::Error::ElementNotInteractableError)解決策
こちらのterarailで、同じことで困っている方がいた。
ボタンが表示されるまで待たないといけないみたい。
公式ドキュメントを参考に、ボタンが表示されるまで待つ処理を追加してみる。click前に追加#最大10秒待つ wait = Selenium::WebDriver::Wait.new(:timeout => 10) #検索ボタンが表示されるまで wait.until {driver.find_element(:class, 'gNO89b').displayed?結果
いけた。
ちなみに、待つだけならsleep()でも可能だけど、
seleniumで要素を待つ時にsleepを使うのはオススメしないを見てみると、あまりよろしいやり方ではないみたい。
追記:
#submitでもいけたワ
- 投稿日:2019-05-26T20:43:30+09:00
Ruby で GoogleSpreadSheet の CSV をダウンロードする
なんか結構面倒で疲れたのでまとめておく。
実装
authorize まわりの処理は、ほぼ Ruby QuickStart ママなので説明省略。
sample.rbrequire 'csv' require 'active_support' require 'googleauth' require 'googleauth/stores/file_token_store' require 'google/apis/sheets_v4' require 'google/apis/drive_v3' require 'open-uri' SPREADSHEET_ID = 'XXXXXXXXXXXXXXXXX' TOKEN_PATH = "#{__dir__}/token.yml" CREDENTIALS_PATH = "#{__dir__}/credentials.json" OOB_URI = 'urn:ietf:wg:oauth:2.0:oob' SCOPES = [ Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY, Google::Apis::DriveV3::AUTH_DRIVE # NOTE: スプレッドシートをダウンロードするのに必要 ] def authorize client_id = Google::Auth::ClientId.from_file(CREDENTIALS_PATH) token_store = Google::Auth::Stores::FileTokenStore.new(file: TOKEN_PATH) authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPES, token_store) user_id = 'default' credentials = authorizer.get_credentials(user_id) if credentials.nil? url = authorizer.get_authorization_url(base_url: OOB_URI) puts 'Open the following URL in the browser and enter the ' \ "resulting code after authorization:\n" + url code = gets credentials = authorizer.get_and_store_credentials_from_code( user_id: user_id, code: code, base_url: OOB_URI ) end credentials end # ココが今回の話の焦点 def fetch_csv_files_as_hashes sheet_service = Google::Apis::SheetsV4::SheetsService.new sheet_service.client_options.application_name = 'sensuikan1973 sample app' authorization = authorize sheet_service.authorization = authorization spreadsheet = JSON.parse(sheet_service.get_spreadsheet(SPREADSHEET_ID).to_json) # 自前で export 用の URL を組み立てる export_url_list = spreadsheet['sheets'].map do |sheet| "https://docs.google.com/spreadsheets/d/#{SPREADSHEET_ID}/export?format=csv&gid=#{sheet['properties']['sheetId']}" end.compact! export_url_list.map do |export_url| puts "download csv from #{export_url}" # google api を連続で叩くと 429 で怒られちゃうので、間隔を空けておく # See: https://developers.google.com/sheets/api/limits sleep(5) uri = OpenURI.open_uri( export_url, 'rb:utf-8', 'Authorization' => "Bearer #{authorization.access_token}" # NOTE: ここ大事! ) CSV.parse(uri.read, headers: true).map(&:to_hash) # hash にして返す end end参考
- 投稿日:2019-05-26T20:28:45+09:00
CodeBuildのSlack通知をTerraformでサクッと設定する
最近CodeBuildを利用し始めたのですが、CircleCI等のようにはサクッとSlack通知ができないのを少しもどかしく感じました。ビルドの状態がSlack上で楽に把握できないと、コンソールを見に行ってしまったり思考リソースを奪われてしまいますよね。
そこでよし通知をしようと思っても、まさにこれというTerraform Moduleがなかったのでmoduleを作ってみました。
en30/codebuild-to-slack/aws | Terraform Module Registry
これを利用するとCodeBuild上での以下のイベントを簡単にSlack通知できます。
IN_PROGRESS
: 開始SUCCEEDED
: 成功FAILED
: 失敗FAULT
: AWS側が原因での失敗?TIMED_OUT
: タイムアウトSTOPPED
: 中止使い方
main.tfvariable "encrypted_slack_webhook_url" {} resource "aws_kms_key" "slack_webhook_url" { description = "Key for Slack Webhook URL" } module "codebuild_notification" { source = "en30/codebuild-to-slack/aws" version = "0.0.1" encrypted_slack_webhook_url = "${var.encrypted_slack_webhook_url}" slack_channel = "#app" kms_key_arn = "${aws_kms_key.slack_webhook_url.arn}" }
encrypted_slack_webhook_url
は以下のようにAWS CLIを使うことで得ることができます。$ aws kms encrypt --key-id $AWS_KMS_KEY_ID --plaintext $SLACK_WEBHOOK_URL --query CiphertextBlob --output text
$AWS_KMS_KEY_ID
はmoduleへkms_key_arn
として渡しているkeyのid、$SLACK_WEBHOOK_URL
はSlackのIncoming WebhookのURLです。上の例のようにキーもTerraformで作ろうとすると
aws_kms_key
の作成(terraofmr apply
①)- キーを利用してwebhook urlを暗号化
- moduleを利用した通知設置(
terraofrm apply
②)と
terraform apply
で一発でいけないのが気持ち悪いところですが、よろしければ使ってみてください!中身の簡単な説明
やっていることとしては
- CodeBuildのビルド状態が変化に関するCloudWatch EventでLambdaを起動
- Lambdaでイベント情報を整形してSlack通知
です。LambdaにRuby Runtimeも入ったことですし、僕はRubyが好きなのでLambdaはRubyで書きました。
それほど面白いところがあるわけではないですが、Lambdaのデプロイをシンプルに済ますために、標準ライブラリ、Lambdaの環境に元々入っている
aws-sdk
だけで済ますようにしています。notify_slack.rbrequire "uri" require "net/http" require "json" require "base64" require "aws-sdk" COLORS = { "SUCCEEDED" => "good", "FAILED" => "danger", "FAULT" => "danger", "TIMED_OUT" => "danger", "STOPPED" => "warning", }.freeze def decrypt(encrypted_url) client = Aws::KMS::Client.new client.decrypt(ciphertext_blob: Base64.decode64(encrypted_url)).plaintext end def build_url(region, project, slug) region, project, slug = [region, project, slug].map(&URI.method(:encode_www_form_component)) "https://#{region}.console.aws.amazon.com/codesuite/codebuild/projects/#{project}/build/#{slug}/log" end def format(event) # rubocop:disable Metrics/MethodLength project = event["detail"]["project-name"] status = event["detail"]["build-status"] slug = event["detail"]["build-id"].split("/").last { attachments: [ { color: COLORS[status], title: slug, title_link: build_url(event["region"], project, slug), fallback: status, fields: [ { title: "Status", value: status, short: true, }, { title: "Initiator", value: event["detail"]["additional-information"]["initiator"], short: true, }, ], }, ], } end def notify_slack(slack_url, payload) Net::HTTP.post_form(URI.parse(slack_url), payload: JSON.dump(payload)) end def lambda_handler(event:, context:) slack_url = decrypt(ENV["ENCRYPTED_SLACK_WEBHOOK_URL"]) payload = { channel: ENV["SLACK_CHANNEL"], username: ENV["SLACK_USERNAME"], icon_emoji: ENV["SLACK_EMOJI"], }.merge(format(event)) notify_slack(slack_url, payload) end以上です。
AWSもTerraformも使い始めて日が浅いので、何か少しでも気になる部分があれば気軽にコメントやIssue作成をお願いします。
参考
- 投稿日:2019-05-26T18:43:44+09:00
~git~ヘルスケアwebサービスを自分で作る医者の日記
'''
To github.com:shutainer/hello_app.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:shutainer/hello_app.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
'''再びプッシュできない
Qiita
gitの先端 云々、pull やmerge等でなんとかなると思うのだが
gitの仕組みを理解していないため、わからん
- 投稿日:2019-05-26T18:07:32+09:00
Rails Tutorial(2週目)-6-
Railsはリレーショナル・データベース(RDB)を使う
リレーショナルデータベースとは、事前定義された、関連があるデータ項目の集合体です。 この項目は、列と行を持つテーブルのセットとして構成されます。
このデータベースの縦列の事を、カラムという
モデルの作成
$rails generate model User name:string email:string
コントローラ名には複数形を使い、モデル名には単数を使うという慣習がある
(参考)
$rails generate controller Users new
上のコマンドによってマイグレーションファイルが作成されるので、
$rails db:migrate
でマイグレーションの適用を行うあとからカラムの追加などを行いたいときは、
rails generate migration add_カラム名_to_テーブル名(複数) データ型の指定
有効性の検証
モデルファイル内でvalidatesメソッドを記述する。
validatesメソッドは、第1引数に対象となるカラム、第2引数以降にオプションを取る。存在性の検証
app/models/user.rbclass User < ApplicationRecord validates :name, presence: true endオプションを
presence: true
とすることで、存在性を検証できる長さの検証
length: { (maximum or minimum): 長さ}
で長さを検証できるフォーマットの検証
オプションを
format: { with: /<regular expression>/ }
とすることで、フォーマットを検証できる
正規表現を利用する。一意性の検証
オプションを
uniqueness: true
とすることで、検証できる。
さらに、:uniqueness
のオプションとして:case_sensitive
を設定することで、大文字と小文字の区別をなくすことができる。
case_sensitive: true
で大文字小文字の区別をするデータベースのインデックスの生成
検索の高速化のために、データベースのカラムにインデックス(索引)をつける事ができる。
マイグレーションファイルに
add_index モデル名, カラム名, オプション
(例)add_index :users, :email, unique: true
Active Recordのコールバック
Railsアプリケーションを普通に操作すると、その内部でオブジェクトが作成されたり、更新されたりdestroyされたりします。Active Recordはこのオブジェクトライフサイクルへのフックを提供しており、これを用いてアプリケーションやデータを制御できます。
コールバックは、オブジェクトの状態が切り替わる「前」または「後」にロジックをトリガします。コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。コールバックを利用することで、Active Recordオブジェクトが作成/保存/更新/削除/検証/データベースからの読み込み、などのイベント発生時に常に実行されるコードを書くことができます。
(例)
before_save { self.email = email.downcase }
上はコールバックにブロックを渡している。他の方法として、
before_save :(メソッド名)
としておいて、メソッドを他で定義しておく事もできる。セキュアなパスワード
パスワードの安全性を保つために、
ユーザーによって入力されたパスワードはハッシュ化してデータベースに保存される。ユーザーの認証を行う際には、パスワードの送信→パスのハッシュ化→先にデータベースに保存されているハッシュ化されたパスワードの値との比較 という流れ。
セキュアなパスワードの実装
モデルに has_secure_password というメソッドを追加する。
・セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
・2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される18 。
・authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 。has_secure_passwordメソッドを使うための条件は、
データベースにpassword_digest属性を追加すること。よって、
$rails generate migration add_password_digest_to_users password_digest:string
また最先端のハッシュ関数を使うためにGemfileにbcrypt gemを追加する。
あとは、モデル内にhas_secure_passwordと記述するだけ
- 投稿日:2019-05-26T17:25:27+09:00
~エラーとの格闘~ ヘルスケアwebサービスを自分で作る医者の日記
rails チュートリアルで途中わからなくなったため、
再度1.1から始める。gitの仕組みがわからない、
sshというものが理解できないpushできない
git remote addを何回か繰り返したが
push できない格闘したのち
remote rm〜で
一旦消して、githubを使うことにし、pushできた
朝からやって23時ごろここまで10時間以上一つのエラーと格闘長かった
- 投稿日:2019-05-26T17:00:27+09:00
Cloud9でのRuby開発
Cloud9でRuby始める際にやったこと
・Cloud9のアカウント作成
・Qiitaのアカウント作成
・Figmaのアカウント作成
・teratailのアカウント作成
・GitHubのアカウント作成
・Evernoteのアカウント作成いま時点で学んだこと
・Markdownという言語があることが分かった。
- 投稿日:2019-05-26T15:11:24+09:00
【Rails】v 5.2ではprotect_from_forgeryがapplication_controller内にない?
はじめに
先日参加した勉強会にて、
「application_controller.rb
に記述されているprotect_from_forgery with: :exception
が csrfを対策 している」(裏を返すと、ここを削除するとcsrf攻撃を受けるリスクが生まれる)
という説明を受けたのだが、
そもそもprotect_from_forgery with: :exception
なるコードを見た事が無い気がしたので調べてみた。比較
以下、Rails versionが5.1.7と5.2.3で
rails new
した直後のapplication_controller.rb
の比較です。v5.1.7
$ rails -v Rails 5.1.7application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception end↑
protect_from_forgery with: :exception
がある!v5.2.3
$ rails -v Rails 5.2.3application_controller.rbclass ApplicationController < ActionController::Base end↑
protect_from_forgery with: :exception
が無い!結論
確かに、rails5.1台までは
protect_from_forgery with: :exception
がapplication_controller.rbで記述されていることが確認できる。どうやらRails version5.2 以降ではActionController::Base内でprotectしているようです。
https://stackoverflow.com/questions/50905654/rails-5-2-actioncontrollerinvalidauthenticitytoken間違いなどありましたらご指摘ください!
- 投稿日:2019-05-26T13:59:02+09:00
RailsなしでRSpecのspec(テスト)を実行する方法
RSpecは、Rubyのテストフレームワークです。Ruby on Railsと組み合わせて使用されることが多いため、「Railsのテストフレームワーク」と紹介されることも多いですが、ピュアRubyのみ(Rails未導入)でも使用することができます。
[準備] Gemのインストール
RSpecのgemをインストールします。※Railsのインストールは不要です。
$ gem install rspecrspec+ファイル名 で実行する
たとえば、以下のRubyファイルがあったとします。
sample.rbdef greeting "Hello,World!" endsample_spec.rbrequire "./sample.rb" describe "greeting" do it "returns Hello World!" do expect(greeting).to eq "Hello,World!" end endsample_spec.rbのspecを実行するには、以下のコマンドでOKです。(sample_spec.rbが配置されているディレクトリに移動しておくことを忘れずに!)
$ rspec sample_spec.rb #=> 実行結果 . Finished in 0.00628 seconds (files took 0.37104 seconds to load) 1 example, 0 failures1つのファイルにまとまっていても実行できる
実処理とspecが1つのファイルになっていても実行できます。
sample.rb# 実処理 def greeting "Hello,World!" end # テストコード require "./sample.rb" describe "greeting" do it "returns Hello World!" do expect(greeting).to eq "Hello,World!" end end$ rspec sample.rb #=> 実行結果 . Finished in 0.00628 seconds (files took 0.37104 seconds to load) 1 example, 0 failures簡単なプログラムならMinitestを使うのもアリ
RubyにはデフォルトでMinitestというテストフレームワークが備わっています。
MinitestはGemのインストール無しで利用できるので、今回紹介した例のような簡単なRubyプログラムをテストするのであれば、わざわざGemをインストールしないと使えないRSpecを使うメリットはあまりないかもしれません。。。
簡単なRubyプログラムをRSpecでテストする機会は多くないかもしれませんが、ご参考までに。
- 投稿日:2019-05-26T11:57:56+09:00
RailsアプリのRSpecセットアップ方法
はじめに
Rails初心者です。
RSpecを初めて触るので、初めに行うRSpecのインストール〜アプリケーション設定について、備忘録としてまとめます。既存アプリに追加する前提です。
1から作る場合は、こちらが参考になります。
Railsアプリ作成手順まとめバージョン管理
ruby 2.7.0 rails 5.1.6RSpecセットアップ
①RSpecインストール
1-1 Gemfileに
rspec-rails
を追加Gemfilegroup :development, :test do # 元からあるコードは省略 gem 'rspec-rails', '~> 3.6.0' end1-2 bundle実行
terminal$ bundle install②テストデータベース確認
2-1 テスト用データベース確認
・SQLiteの場合
config/database.ymltest: <<: *default database: db/test.sqlite3・MySQL・PostgreSQLの場合
config/database.ymltest: <<: *default database: アプリ名があるはずなので確認。
もし書いてなければ、
config/database.yml
に上記のようなコードを追加
↓terminal$ bin/rails db:create:all
でテストデータベース作成。
※余談ですが、
rails
とbin/rails
の違いについては、
こちらが分かりやすいです。
Rails 4.1以降のコンソールコマンドは必ず bin/ を付けなきゃいけないの?③RSpec設定
3-1 RSpecインストール
terminal$ bin/rails generate rspec:install
これにより、
・RSpec用の設定ファイル(
.rspec
)
・私たちが作成したスペックファイルを格納するディレクトリ(spec
)
・RSpecの動きをカスタマイズするヘルパーファイルの3つが作成されます。
3-2 testディレクトリの削除
RSpecではspecディレクトリのspecファイルに書いてくので、Railsアプリケーション作成時に作られたtestファイルたちを削除します。
terminal$ rm -r ./test3-3 デフォルトの形式→ドキュメント形式へ変更
必須ではないですが、RSpecの出力結果を見やすくします。
.rspec
ファイルに下記を追加。.rspec--require spec_helper --format documentation3-4
binstub
インストールアプリケーションの起動時間を早くする
Spring
を追加します。Gemfilegroup :development do # 元からあるコードは省略 gem 'spring-commands-rspec' endterminal$ bundle install↓
新しいbinstub
を作成terminal$ bundle exec spring binstub rspec実行すると、
bin
ディレクトリ内にrspec
という実行用ファイルが作成されます。④正常にRSpecがインストールできてるか確認
4-1 コマンド実行
terminal$ bin/rspec Running via Spring preloader in process 28279 No examples found. Finished in 0.00074 seconds (files took 0.14443 seconds to load) 0 examples, 0 failuresこのように出力されていれば成功です。
⑤テストファイル自動作成設定
5-1 ファイル作成に応じてテストファイルを作成
rails generate
コマンドのようにジェネレータを使うと、
現時点で既に、
デフォルトのMinitest
ファイルがtest
ディレクトリには作成されず、
RSpec
ファイルがspec
ディレクトリに作成されます。
特に設定は不要です。5-2 不要なテストファイルが作成されない設定
あとは、
不要なファイルが自動で作成されないように
config/application.rb
に設定を加えます。config/application.rbrequire_relative 'boot' require 'rails/all' Bundler.require(*Rails.groups) module Testapps # 自分が作成したアプリ名 class Application < Rails::Application config.load_defaults 5.1 # 元からあるコードは省略 config.generators do |g| g.test_framework :rspec, fixtures: false, # テストDBにレコード作成するファイルの作成をスキップ(初めだけ、のちに削除)。 view_specs: false, # ビューファイル用のスペックを作成しない。 helper_specs: false, # ヘルパーファイル用のスペックを作成しない。 routing_specs: false # routes.rb用のスペックファイル作成しない。 end end end上のコードでは、
モデルスペックとコントローラスペックが
デフォルトで自動作成されるようになっています。他に自動作成してほしいファイルがあれば、
上の該当するコードは書かなくて良いです。参考
Everyday Rails - RSpecによるRailsテスト入門
現場で使える Ruby on Rails 5速習実践ガイドとても分かりやすいです。
引き続き勉強していきます。
- 投稿日:2019-05-26T01:03:22+09:00
NginxをReverseProxyにしてRailsアプリをデプロイしたらHTTPヘッダが原因で422Errorが返ってきた
0. 導入
これがQiita初投稿となります。
Railsでアプリケーション1を作成しデプロイしました。しかし、Nginxの設定が原因でPOST Requestを行ったタイミングでエラーが発生してしまいました。改めて当時の状態を振り返り、どのようにエラーを解消したのかということをまとめてゆきます。1. 概要
1.1. 構成
SakuraのVPSを借りてCentOS7系をインストールしています。
NginxをReverse Proxyとして動作させ、HTTPS通信を終端化し、リクエストをアップストリームサーバ(puma)に転送します。
pumaとRailsアプリはRack2を介してrequestとresponseをやりとりします。
1.2. 問題点
少し長目の記事なので最初にどこが問題であったのか書いておきます。
今回はNginxをReverseProxyとした際に、アップストリームサーバに必要なHTTPヘッダーを渡せていなかったことが問題でした。2. エラー発生
2.1. 422エラーが返ってきた
ブログを作成し、デプロイも完了。早速ログインしブログを投稿しよう、と思った矢先…
なんかエラーが出てる…Status Code 422って何だ?3. トラブルシュート
3.1. Status Code 422 とは?
MDN web docsを参照してみます。
422 Unprocessable Entity:The HyperText Transfer Protocol (HTTP) の 422 Unprocessable Entity 応答状態コードは、サーバーが要求本文のコンテンツ型を理解でき、要求本文の構文が正しいものの、中に含まれている指示が処理できなかったことを表します。
わからない。「中に含まれている指示が処理できなかった」という表現がいささか抽象的です。
If you are the application owner check the logs for more information.
「アプリのオーナーなら詳細についてログを見てください」とのことなのでVPSにログインしRailsのログを確認してみます。
3.2. Logの確認
VPSにログインし下記のログを確認します。
/var/www/app_name/shared/log/production.log(省略) W, [*] WARN -- : [*] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com) I, [*] INFO -- : [*] Completed 422 Unprocessable Entity in 2ms F, [*] FATAL -- : [*] F, [*] FATAL -- : [*] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): F, [*] FATAL -- : [*] F, [*] FATAL -- : [*] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request' (省略)最初のWARNと最後のFATALに、具体的な情報がありそうに思えます。
W, [〜] WARN -- : [〜] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com)
F, [〜] FATAL -- : [〜] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request'WARNでは「HTTPのオリジンヘッダーが
request.base_url
にマッチしていない。」と言われています。FATALではrequest_forgery_protection.rb
がエラーを出しています。まずはエラーを出しているメソッドを確認してゆきます。3.3. request_forgery_protection.rbを見てみる
request_forgery_protection.rb
を一部添付します。rails/actionpack/lib/action_controller/metal/request_forgery_protection.rbrequire "rack/session/abstract/id" require "action_controller/metal/exceptions" require "active_support/security_utils" require "active_support/core_ext/string/strip" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end class InvalidCrossOriginRequest < ActionControllerError #:nodoc: end module RequestForgeryProtection extend ActiveSupport::Concern (省略) def verify_authenticity_token # :doc: mark_for_same_origin_verification! if !verified_request? if logger && log_warning_on_csrf_failure if valid_request_origin? logger.warn "Can't verify CSRF token authenticity." else logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})" end end handle_unverified_request end end (省略) def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true end end (省略) end end
valid_request_origin?
メソッドでrequest.origin == request.base_url
を比較しています。この比較がfalse
を返すとverify_authenticity_token
メソッドが上述のWARNメッセージを出すようです。
base_url
メソッドは、lib/action_dispatch/http/request.rb
がinclude
しているRack::Request::Helpers
に定義されています。3.3. Rackを見てみる
request.rb
を一部添付します。rack/lib/rack/request.rbrequire 'rack/utils' require 'rack/media_type' module Rack class Request class << self attr_accessor :ip_filter end (省略) module Env (省略) def get_header(name) @env[name] end (省略) end module Helpers (省略) DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 } HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME' HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO' (省略) def scheme if get_header(HTTPS) == 'on' 'https' elsif get_header(HTTP_X_FORWARDED_SSL) == 'on' 'https' elsif forwarded_scheme forwarded_scheme else get_header(RACK_URL_SCHEME) end end (省略) def base_url url = "#{scheme}://#{host}" url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme] url end (省略) def forwarded_scheme scheme_headers = [ get_header(HTTP_X_FORWARDED_SCHEME), get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0] ] scheme_headers.each do |header| return header if ALLOWED_SCHEMES.include?(header) end nil end end include Env include Helpers end end
base_url
メソッドを見つけました。
このメソッドの中でscheme
メソッドが呼び出されています。get_header
はenv
3という変数が保持しているハッシュから、引数に指定されたKey
が持つValue
を取得します。get_header
の引数となるKey
にはHTTPヘッダ4が指定されているようなので、Nginxの設定でヘッダを付与してみることにします。3.4. Nginxの設定を直す
/etc/nginx/conf.d/default.confupstream app-name { server unix:/var/www/app-name/shared/tmp/sockets/devcamp-portfolio-puma.sock fail_timeout=0; } server { if ($host = self-ref-penguin.com) { return 301 https://$host$request_uri; } listen 80; server_name self-ref-penguin.com; root /var/www/app-name/current/public; } server { listen 443 ssl http2 default_server; # listen [::]:443 ssl http2 default_server; server_name self-ref-penguin.com; ssl_certificate ***; ssl_certificate_key ***; ssl_session_cache shared:SSL:1m; ssl_session_timeout 10m; ssl_ciphers ***; ssl_protocols ***; location ~ ^/assets/ { root /var/www/app-name/current/public; } try_files $uri/index.html $uri @app-name; location / { proxy_pass http://app-name; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; # この設定が抜けていました proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; } (省略) }X-Forwarded-Protoヘッダーの設定を追記し、「ユーザのリクエストが使用したHTTPスキームを指定する」ように修正。
systemctl restart nginx.service
を実行します。再度ページにアクセスしログインを実行。今度はログイン(POST)が成功したことを確認できました。今回の構成ではNginxがReverse ProxyとなりHTTPSのアクセスを終端化していたのでした。今回のエラーの原因は下記の説明に要約されるかと思います。
アップストリームに伝える必要がある情報にクライアントのリクエスト情報があります。アップストリームサーバーへのリクエストはすべてプロキシを経由するため、そのままではクライアントの送信元アドレスや使用したプロトコルがわからなくなってしまいます。このため、クライアントのリクエスト情報をいくつかのヘッダを付与することでアップストリームへ伝えることができます。これらのヘッダは標準化されていませんが、Squid、Apache HTTPサーバなどでデファクトスタンダードとして使用されており、RubyのRackインタフェースもこれらのヘッダを解釈します。5
4. まとめ
初めて自分でアプリケーションを作成しデプロイを行ってみました。アプリケーションの開発スキルは勿論のこと、まだまだインフラ観点でも至らない点が多々あることを実感しています。特にエラーの原因についてはシステムの構成をしっかりと理解していれば、もっと簡単にあたりをつけることが可能であったと思います。
フロントの開発をしていたつもりが、その副産物としてインフラの知見も得ることができ、中々良い経験になりました。
私について
普段はネットワークエンジニアをしています。
プログラミング未経験で、半年ほど前からRubyを勉強し始めました。
Scriptingを1つ経験したので、次は並行してFunctionalの言語にも挑戦しようかと思っています。
オススメがあれば教えてください。
最後まで読んでくださりありがとうございました。
Rackについては以下2つの記事によくまとまっています
Rails on Rack
What is Rack in Ruby? ↩EnvについてはStack over flowの質問を参照しました。「env is just a hash. Rack itself and various middlewares add values into it.」「envはハッシュで、Rackや様々なミドルウェアがこれにValueを加えていきます」 ↩
HTTPヘッダーの種類についてはMDN web docsを参照し、適切な値を探しました。 ↩
- 投稿日:2019-05-26T01:03:22+09:00
NginxをReverseProxyにしてRailsアプリをデプロイしたらヘッダの扱いにハマった
0. 導入
これがQiita初投稿となります。
Railsでアプリケーション1を作成しデプロイしました。しかし、Nginxの設定が原因でPOST Requestを行ったタイミングでエラーが発生してしまいました。改めて当時の状態を振り返り、どのようにエラーを解消したのかということをまとめてゆきます。1. 概要
1.1. 構成
SakuraのVPSを借りてCentOS7系をインストールしています。
NginxをReverse Proxyとして動作させ、HTTPS通信を終端化し、リクエストをアップストリームサーバ(puma)に転送します。
pumaとRailsアプリはRack2を介してrequestとresponseをやりとりします。
1.2. 問題点
少し長目の記事なので最初にどこが問題であったのか書いておきます。
今回はNginxをReverseProxyとした際に、アップストリームサーバに必要なHTTPヘッダーを渡せていなかったことが問題でした。2. エラー発生
2.1. 422エラーが返ってきた
ブログを作成し、デプロイも完了。早速ログインしブログを投稿しよう、と思った矢先…
なんかエラーが出てる…Status Code 422って何だ?3. トラブルシュート
3.1. Status Code 422 とは?
MDN web docsを参照してみます。
422 Unprocessable Entity:The HyperText Transfer Protocol (HTTP) の 422 Unprocessable Entity 応答状態コードは、サーバーが要求本文のコンテンツ型を理解でき、要求本文の構文が正しいものの、中に含まれている指示が処理できなかったことを表します。
わからない。「中に含まれている指示が処理できなかった」という表現がいささか抽象的です。
If you are the application owner check the logs for more information.
「アプリのオーナーなら詳細についてログを見てください」とのことなのでVPSにログインしRailsのログを確認してみます。
3.2. Logの確認
VPSにログインし下記のログを確認します。
/var/www/app_name/shared/log/production.log(省略) W, [*] WARN -- : [*] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com) I, [*] INFO -- : [*] Completed 422 Unprocessable Entity in 2ms F, [*] FATAL -- : [*] F, [*] FATAL -- : [*] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): F, [*] FATAL -- : [*] F, [*] FATAL -- : [*] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request' (省略)最初のWARNと最後のFATALに、具体的な情報がありそうに思えます。
W, [〜] WARN -- : [〜] HTTP Origin header (https://self-ref-penguin.com) didn't match request.base_url (http://self-ref-penguin.com)
F, [〜] FATAL -- : [〜] actionpack (5.2.3) lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request'WARNでは「HTTPのオリジンヘッダーが
request.base_url
にマッチしていない。」と言われています。FATALではrequest_forgery_protection.rb
がエラーを出しています。まずはエラーを出しているメソッドを確認してゆきます。3.3. request_forgery_protection.rbを見てみる
request_forgery_protection.rb
を一部添付します。rails/actionpack/lib/action_controller/metal/request_forgery_protection.rbrequire "rack/session/abstract/id" require "action_controller/metal/exceptions" require "active_support/security_utils" require "active_support/core_ext/string/strip" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end class InvalidCrossOriginRequest < ActionControllerError #:nodoc: end module RequestForgeryProtection extend ActiveSupport::Concern (省略) def verify_authenticity_token # :doc: mark_for_same_origin_verification! if !verified_request? if logger && log_warning_on_csrf_failure if valid_request_origin? logger.warn "Can't verify CSRF token authenticity." else logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})" end end handle_unverified_request end end (省略) def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true end end (省略) end end
valid_request_origin?
メソッドでrequest.origin == request.base_url
を比較しています。この比較がfalse
を返すとverify_authenticity_token
メソッドが上述のWARNメッセージを出すようです。
base_url
メソッドは、lib/action_dispatch/http/request.rb
がinclude
しているRack::Request::Helpers
に定義されています。3.3. Rackを見てみる
request.rb
を一部添付します。rack/lib/rack/request.rbrequire 'rack/utils' require 'rack/media_type' module Rack class Request class << self attr_accessor :ip_filter end (省略) module Env (省略) def get_header(name) @env[name] end (省略) end module Helpers (省略) DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 } HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME' HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO' (省略) def scheme if get_header(HTTPS) == 'on' 'https' elsif get_header(HTTP_X_FORWARDED_SSL) == 'on' 'https' elsif forwarded_scheme forwarded_scheme else get_header(RACK_URL_SCHEME) end end (省略) def base_url url = "#{scheme}://#{host}" url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme] url end (省略) def forwarded_scheme scheme_headers = [ get_header(HTTP_X_FORWARDED_SCHEME), get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0] ] scheme_headers.each do |header| return header if ALLOWED_SCHEMES.include?(header) end nil end end include Env include Helpers end end
base_url
メソッドを見つけました。
このメソッドの中でscheme
メソッドが呼び出されています。get_header
はenv
3という変数が保持しているハッシュから、引数に指定されたKey
が持つValue
を取得します。get_header
の引数となるKey
にはHTTPヘッダ4が指定されているようなので、Nginxの設定でヘッダを付与してみることにします。3.4. Nginxの設定を直す
/etc/nginx/conf.d/default.confupstream app-name { server unix:/var/www/app-name/shared/tmp/sockets/devcamp-portfolio-puma.sock fail_timeout=0; } server { if ($host = self-ref-penguin.com) { return 301 https://$host$request_uri; } listen 80; server_name self-ref-penguin.com; root /var/www/app-name/current/public; } server { listen 443 ssl http2 default_server; # listen [::]:443 ssl http2 default_server; server_name self-ref-penguin.com; ssl_certificate ***; ssl_certificate_key ***; ssl_session_cache shared:SSL:1m; ssl_session_timeout 10m; ssl_ciphers ***; ssl_protocols ***; location ~ ^/assets/ { root /var/www/app-name/current/public; } try_files $uri/index.html $uri @app-name; location / { proxy_pass http://app-name; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; # この設定が抜けていました proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; } (省略) }X-Forwarded-Protoヘッダーの設定を追記し、「ユーザのリクエストが使用したHTTPスキームを指定する」ように修正。
systemctl restart nginx.service
を実行します。再度ページにアクセスしログインを実行。今度はログイン(POST)が成功したことを確認できました。今回の構成ではNginxがReverse ProxyとなりHTTPSのアクセスを終端化していたのでした。今回のエラーの原因は下記の説明に要約されるかと思います。
アップストリームに伝える必要がある情報にクライアントのリクエスト情報があります。アップストリームサーバーへのリクエストはすべてプロキシを経由するため、そのままではクライアントの送信元アドレスや使用したプロトコルがわからなくなってしまいます。このため、クライアントのリクエスト情報をいくつかのヘッダを付与することでアップストリームへ伝えることができます。これらのヘッダは標準化されていませんが、Squid、Apache HTTPサーバなどでデファクトスタンダードとして使用されており、RubyのRackインタフェースもこれらのヘッダを解釈します。5
4. まとめ
初めて自分でアプリケーションを作成しデプロイを行ってみました。アプリケーションの開発スキルは勿論のこと、まだまだインフラ観点でも至らない点が多々あることを実感しています。特にエラーの原因についてはシステムの構成をしっかりと理解していれば、もっと簡単にあたりをつけることが可能であったと思います。
フロントの開発をしていたつもりが、その副産物としてインフラの知見も得ることができ、中々良い経験になりました。
私について
普段はネットワークエンジニアをしています。
プログラミング未経験で、半年ほど前からRubyを勉強し始めました。
Scriptingを1つ経験したので、次は並行してFunctionalの言語にも挑戦しようかと思っています。
オススメがあれば教えてください。
最後まで読んでくださりありがとうございました。
Rackについては以下2つの記事によくまとまっています
Rails on Rack
What is Rack in Ruby? ↩EnvについてはStack over flowの質問を参照しました。「env is just a hash. Rack itself and various middlewares add values into it.」「envはハッシュで、Rackや様々なミドルウェアがこれにValueを加えていきます」 ↩
HTTPヘッダーの種類についてはMDN web docsを参照し、適切な値を探しました。 ↩
- 投稿日:2019-05-26T00:57:27+09:00
[初心者向け]リスト操作関数map,reduce,filterの各言語の簡単なサンプル
各言語のリスト操作でよく使われる
map
filter
reduce
の簡単な操作を並べてみました。
Javaは冗長ですね。Ruby と Haskellはすごく簡潔です。
ただし、私が普段の業務で使うのがJavaなので、他の言語のことをよく知らずですので、
もっと簡潔に書ける方法があるかもしれません。
- Java 1.8
- Ruby 2.6
- Python 3.7
- Haskell GHC version 8.0.1
- Clojure 1.9.0
の簡単な例になります。
数値のリストを作る
ちなみに、業務では固定でリストを初期化することはあまりないと思われます。
普通はリスト系のデータはファクトリーメソットにて生成されて参照すると思います。
ただし、私はテストなどでテストデータとして以下のような初期化をすることはあります。JavaList<Integer> xs = Arrays.asList(1, 2, 3, 4, 5); //または @saka1029 さんに教えていただいた、Java 10以降なら var xs = List.of(1, 2, 3, 4, 5); //こんな無限リストを使うやり方もあります。業務ではこれもしない? //外からseed値、function,limit値をいれて作れる。 xs = Stream.iterate(1, i -> i + 1).limit(5).collect(toList())Rubyxs = [1, 2, 3, 4, 5] # 実際には上はあまりよくない例であるらしく、 # @scivolaさんより教えていただいた以下の方が初期化としてはよいです。 xs = [*1..5]Pythonxs = [1, 2, 3, 4, 5]Haskellxs = [1, 2, 3, 4, 5] -- または xs = [1..5]Clojure(def xs '(1 2 3 4 5)) ;または @lagenorhynque さんにコメントいただいた方法 (def xs (range 1 (inc 5)))以下このリストをすでに作っているものとします。
map 関数
リストをとって各値を2倍にする。
各言語とも結果は
[2, 4, 6, 8, 10]
になります。
print関数は省略します。Javaxs.stream().map(i -> i * 2).collect(toList());Rubyxs.map {|i| i * 2}Pythonlist(map(lambda i: i * 2, xs)) # 多分この答えを出す場合、Pythonではリスト内包表記を使うのが普通と思われる。 [i * 2 for i in xs]Haskellmap (*2) xsClojure(map (fn [n] (* n 2)) xs) ; または@lagenorhynqueさんにおしえていただいたもの。こっちのが簡潔でいいですね! (map #(* % 2) xs)filter関数
偶数だけ選んで返す。
結果は
[2, 4]
になります。Javaxs.stream().filter(n -> n % 2 == 0).collect(toList());Rubyxs.select(&:even?)Pythonlist(filter(lambda n: n % 2 == 0, xs)) #多分普通上のような書き方はせずリスト内包表記になると思います。 [n for n in xs if n % 2 == 0]Haskellfilter even xsClojure(filter even? xs)reduce関数
リストの数字を加算して集約して返す。
結果は
15
になります。Javaxs.stream().reduce(Integer::sum).orElse(0);Rubyxs.reduce(:+) #または l.inject(:+) #reduce使わない場合 xs.sumPythonは@shiracamusさんよりimportがないとのことで不親切でしたので記載しました。
Pythonfrom functools import reduce from operator import add reduce(lambda a, b: a + b, xs) # reduce使わない場合 sum(xs) # または@shiracamusさんより教えていただきました、 reduce(add, xs)Haskellfoldl (+) 0 xs -- fold使わない場合 sum xsClojure(reduce + xs) ;@lagenorhynqueさんより教えていただいた (apply + xs) ;という書き方でも同じ結果です。内部で行われるのは (+ 1 2 3 4 5) ; reduceは(+ (+ (+ (+ 1 2) 3) 4) 5)以上です。
いままで勉強したことのある言語に対して書きましたが、個人的に書いていて気持ちがいいと思ったのはRuby,Clojureです。
なんとなく感じが似ている気がします。(作者がどちらもlispが好きだから?)
Haskellはすごすぎて気持ち悪!(いい意味で)と思うところがあるのですが、楽しい!と思えるのはRuby,Clojureでしょうか?なぜなのかわかりませんが。またHaskellは考え方が他の言語とまるで違うのでなかなか勉強が進まないです。