20190526のRubyに関する記事は14件です。

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)}
end

FlammarionでGUI化してみると面白そうだ。

参考資料

Python で音楽を作って楽しもう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby SeleniumでElementNotInteractableError

Ruby + Seleniumでgoogle検索した時にハマった。

詳細

以下のスクリプトを実行

ruby
require "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でもいけたワ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby で GoogleSpreadSheet の CSV をダウンロードする

なんか結構面倒で疲れたのでまとめておく。

実装

authorize まわりの処理は、ほぼ Ruby QuickStart ママなので説明省略。

sample.rb
require '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

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.tf
variable "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で作ろうとすると

  1. aws_kms_keyの作成(terraofmr apply①)
  2. キーを利用してwebhook urlを暗号化
  3. moduleを利用した通知設置(terraofrm apply②)

terraform applyで一発でいけないのが気持ち悪いところですが、よろしければ使ってみてください!

中身の簡単な説明

やっていることとしては

  • CodeBuildのビルド状態が変化に関するCloudWatch EventでLambdaを起動
  • Lambdaでイベント情報を整形してSlack通知

です。LambdaにRuby Runtimeも入ったことですし、僕はRubyが好きなのでLambdaはRubyで書きました。

それほど面白いところがあるわけではないですが、Lambdaのデプロイをシンプルに済ますために、標準ライブラリ、Lambdaの環境に元々入っているaws-sdkだけで済ますようにしています。

notify_slack.rb
require "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作成をお願いします。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

~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の仕組みを理解していないため、わからん

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
class 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と記述するだけ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

~エラーとの格闘~ ヘルスケアwebサービスを自分で作る医者の日記

rails チュートリアルで途中わからなくなったため、
再度1.1から始める。

gitの仕組みがわからない、
sshというものが理解できない

pushできない
git remote addを何回か繰り返したが
push できない

格闘したのち
remote rm〜で
一旦消して、githubを使うことにし、pushできた
朝からやって23時ごろここまで10時間以上一つのエラーと格闘

長かった

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cloud9でのRuby開発

Cloud9でRuby始める際にやったこと
・Cloud9のアカウント作成
・Qiitaのアカウント作成
・Figmaのアカウント作成
・teratailのアカウント作成
・GitHubのアカウント作成
・Evernoteのアカウント作成

いま時点で学んだこと
・Markdownという言語があることが分かった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】v 5.2ではprotect_from_forgeryがapplication_controller内にない?

はじめに

先日参加した勉強会にて、
application_controller.rb に記述されているprotect_from_forgery with: :exceptioncsrfを対策 している」(裏を返すと、ここを削除するとcsrf攻撃を受けるリスクが生まれる)
という説明を受けたのだが、
そもそも protect_from_forgery with: :exception
なるコードを見た事が無い気がしたので調べてみた。

比較

以下、Rails versionが5.1.75.2.3rails newした直後のapplication_controller.rbの比較です。

v5.1.7

$ rails -v
Rails 5.1.7
application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

protect_from_forgery with: :exceptionがある!

v5.2.3

$ rails -v
Rails 5.2.3
application_controller.rb
class 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

間違いなどありましたらご指摘ください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsなしでRSpecのspec(テスト)を実行する方法

RSpecは、Rubyのテストフレームワークです。Ruby on Railsと組み合わせて使用されることが多いため、「Railsのテストフレームワーク」と紹介されることも多いですが、ピュアRubyのみ(Rails未導入)でも使用することができます。

[準備] Gemのインストール

RSpecのgemをインストールします。※Railsのインストールは不要です。

$ gem install rspec

rspec+ファイル名 で実行する

たとえば、以下のRubyファイルがあったとします。

sample.rb
def greeting
    "Hello,World!"
end
sample_spec.rb
require "./sample.rb"

describe "greeting" do
    it "returns Hello World!" do
        expect(greeting).to eq "Hello,World!"
    end
end

sample_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 failures

1つのファイルにまとまっていても実行できる

実処理と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でテストする機会は多くないかもしれませんが、ご参考までに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsアプリのRSpecセットアップ方法

はじめに

Rails初心者です。
RSpecを初めて触るので、初めに行うRSpecのインストール〜アプリケーション設定について、備忘録としてまとめます。

既存アプリに追加する前提です。
1から作る場合は、こちらが参考になります。
Railsアプリ作成手順まとめ

バージョン管理

ruby 2.7.0
rails 5.1.6

RSpecセットアップ

①RSpecインストール

1-1 Gemfileにrspec-railsを追加

Gemfile
group :development, :test do
  # 元からあるコードは省略

  gem 'rspec-rails', '~> 3.6.0'

end

1-2 bundle実行

terminal
$ bundle install

②テストデータベース確認

2-1 テスト用データベース確認

・SQLiteの場合

config/database.yml
test:
 <<: *default
database: db/test.sqlite3

・MySQL・PostgreSQLの場合

config/database.yml
test:
 <<: *default
database: アプリ名

があるはずなので確認。

もし書いてなければ、
config/database.ymlに上記のようなコードを追加

terminal
$ bin/rails db:create:all

でテストデータベース作成。

※余談ですが、 railsbin/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 ./test

3-3 デフォルトの形式→ドキュメント形式へ変更

必須ではないですが、RSpecの出力結果を見やすくします。

.rspecファイルに下記を追加。

.rspec
--require spec_helper
--format documentation

3-4 binstubインストール

アプリケーションの起動時間を早くするSpringを追加します。

Gemfile
group :development do

  # 元からあるコードは省略

  gem 'spring-commands-rspec'

end
terminal
$ 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.rb
require_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速習実践ガイド

とても分かりやすいです。
引き続き勉強していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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をやりとりします。
diagram.png

1.2. 問題点

少し長目の記事なので最初にどこが問題であったのか書いておきます。
今回はNginxをReverseProxyとした際に、アップストリームサーバに必要なHTTPヘッダーを渡せていなかったことが問題でした。

2. エラー発生

2.1. 422エラーが返ってきた

ブログを作成し、デプロイも完了。早速ログインしブログを投稿しよう、と思った矢先…
error_cap.png
なんかエラーが出てる…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.rb
require "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.rbincludeしているRack::Request::Helpersに定義されています。

3.3. Rackを見てみる

request.rbを一部添付します。

rack/lib/rack/request.rb
require '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_headerenv3という変数が保持しているハッシュから、引数に指定されたKeyが持つValueを取得します。 get_headerの引数となるKeyにはHTTPヘッダ4が指定されているようなので、Nginxの設定でヘッダを付与してみることにします。

3.4. Nginxの設定を直す

/etc/nginx/conf.d/default.conf
upstream 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の言語にも挑戦しようかと思っています。
オススメがあれば教えてください。
最後まで読んでくださりありがとうございました。


  1. Udemyでチュートリアルを見ながらブログを作成しました。成果物はこちら 

  2. Rackについては以下2つの記事によくまとまっています
    Rails on Rack
    What is Rack in Ruby? 

  3. EnvについてはStack over flowの質問を参照しました。「env is just a hash. Rack itself and various middlewares add values into it.」「envはハッシュで、Rackや様々なミドルウェアがこれにValueを加えていきます」 

  4. HTTPヘッダーの種類についてはMDN web docsを参照し、適切な値を探しました。 

  5. 久保達彦, 道井俊介(2016)『nginx 実践入門』p.139-140, 技術評論社. 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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をやりとりします。
diagram.png

1.2. 問題点

少し長目の記事なので最初にどこが問題であったのか書いておきます。
今回はNginxをReverseProxyとした際に、アップストリームサーバに必要なHTTPヘッダーを渡せていなかったことが問題でした。

2. エラー発生

2.1. 422エラーが返ってきた

ブログを作成し、デプロイも完了。早速ログインしブログを投稿しよう、と思った矢先…
error_cap.png
なんかエラーが出てる…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.rb
require "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.rbincludeしているRack::Request::Helpersに定義されています。

3.3. Rackを見てみる

request.rbを一部添付します。

rack/lib/rack/request.rb
require '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_headerenv3という変数が保持しているハッシュから、引数に指定されたKeyが持つValueを取得します。 get_headerの引数となるKeyにはHTTPヘッダ4が指定されているようなので、Nginxの設定でヘッダを付与してみることにします。

3.4. Nginxの設定を直す

/etc/nginx/conf.d/default.conf
upstream 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の言語にも挑戦しようかと思っています。
オススメがあれば教えてください。
最後まで読んでくださりありがとうございました。


  1. Udemyでチュートリアルを見ながらブログを作成しました。成果物はこちら 

  2. Rackについては以下2つの記事によくまとまっています
    Rails on Rack
    What is Rack in Ruby? 

  3. EnvについてはStack over flowの質問を参照しました。「env is just a hash. Rack itself and various middlewares add values into it.」「envはハッシュで、Rackや様々なミドルウェアがこれにValueを加えていきます」 

  4. HTTPヘッダーの種類についてはMDN web docsを参照し、適切な値を探しました。 

  5. 久保達彦, 道井俊介(2016)『nginx 実践入門』p.139-140, 技術評論社. 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]リスト操作関数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

の簡単な例になります。

数値のリストを作る

ちなみに、業務では固定でリストを初期化することはあまりないと思われます。
普通はリスト系のデータはファクトリーメソットにて生成されて参照すると思います。
ただし、私はテストなどでテストデータとして以下のような初期化をすることはあります。

Java
    List<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())
Ruby
    xs = [1, 2, 3, 4, 5]
    # 実際には上はあまりよくない例であるらしく、
    # @scivolaさんより教えていただいた以下の方が初期化としてはよいです。
    xs = [*1..5]
Python
    xs = [1, 2, 3, 4, 5]
Haskell
    xs = [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関数は省略します。

Java
    xs.stream().map(i -> i * 2).collect(toList());
Ruby
    xs.map {|i| i * 2}
Python
    list(map(lambda i: i * 2, xs))
    # 多分この答えを出す場合、Pythonではリスト内包表記を使うのが普通と思われる。
    [i * 2 for i in xs]
Haskell
    map (*2) xs
Clojure
    (map (fn [n] (* n 2)) xs)
    ; または@lagenorhynqueさんにおしえていただいたもの。こっちのが簡潔でいいですね!
    (map #(* % 2) xs)

filter関数

偶数だけ選んで返す。

結果は[2, 4]になります。

Java
    xs.stream().filter(n -> n % 2 == 0).collect(toList());
Ruby
    xs.select(&:even?)
Python
    list(filter(lambda n: n % 2 == 0, xs))
    #多分普通上のような書き方はせずリスト内包表記になると思います。
    [n for n in xs if n % 2 == 0]
Haskell
    filter even xs
Clojure
    (filter even? xs)

reduce関数

リストの数字を加算して集約して返す。

結果は15になります。

Java
    xs.stream().reduce(Integer::sum).orElse(0);
Ruby
    xs.reduce(:+) 
    #または l.inject(:+)
    #reduce使わない場合
    xs.sum

Pythonは@shiracamusさんよりimportがないとのことで不親切でしたので記載しました。

Python
    from functools import reduce
    from operator import add

    reduce(lambda a, b: a + b, xs)
    # reduce使わない場合
    sum(xs)
    # または@shiracamusさんより教えていただきました、
    reduce(add, xs)
Haskell
    foldl (+) 0 xs
    -- fold使わない場合
    sum xs
Clojure
    (reduce + xs)
    ;@lagenorhynqueさんより教えていただいた
    (apply + xs)
    ;という書き方でも同じ結果です。内部で行われるのは (+ 1 2 3 4 5)
    ; reduceは(+ (+ (+ (+ 1 2) 3) 4) 5)

以上です。

いままで勉強したことのある言語に対して書きましたが、個人的に書いていて気持ちがいいと思ったのはRuby,Clojureです。
なんとなく感じが似ている気がします。(作者がどちらもlispが好きだから?)
Haskellはすごすぎて気持ち悪!(いい意味で)と思うところがあるのですが、楽しい!と思えるのはRuby,Clojureでしょうか?なぜなのかわかりませんが。またHaskellは考え方が他の言語とまるで違うのでなかなか勉強が進まないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む