megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

Ruby on RailsでreCAPTCHA Enterprise を使いたい

無料版と有料版のreCAPTCHAとがありますが、使い方は一緒かと思ったら違ってちょっと大変だったお話。

環境

Service Version
Ruby 2.7.4
Ruby on Rails 6.1
recaptcha 5.8.1

前提

Gemはrecaptchaを使います。

通常のreCAPTCHAなら下記用意しておけば、後はドキュメント通りでOKです。

Recaptcha.configure do |config|
  config.site_key  = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
  config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'

  # Uncomment the following lines if you are using the Enterprise API:
  # config.enterprise = true
  # config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
  # config.enterprise_project_id = 'my-project'
end

でもenterpriseにはsecret_keyがありません。

代わりにenterprise_api_keyを使います。

ドキュメントにはコメントを外しましょうとはありますが、secret_keyは不要ですとは無かったのでsecret_keyを探して右往左往してしまいました。。

というわけでenterpriseでは以下になります。

Recaptcha.configure do |config|
  config.site_key  = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'

  config.enterprise = true
  config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
  config.enterprise_project_id = 'my-project'
end

各キーの場所

ついでなのでキーのそれぞれの場所も記しておきます。

これまた探すのに手間取って右往左往しましたので。。。

site_key

GCPの「セキュリティ」から「reCAPTCHAT Enterprise」を選択し、キーを作成してください。

enterprise_api_key

GCPの「APIとサービス」からAPIキーを発行してください。

キーの制限としては「reCAPTCHA Enterprise API」を選択しておいてください。

enterprise_project_id

ダッシュボードの「プロジェクト情報」に記載のプロジェクトIDです。

所感

折角recaptchaにEnterprise用の機能まで備わっているのに、分かりづらくてもったいない。。

公式は公式で google-cloud-recaptcha_enterprise を押してきていて、やたらと混乱してしまった。。。

Devise OmniAuthでサービス先へリダイレクトする前にごにょごにょしたい

OmniAuth、ほとんど何もしなくてもOAuthを実装出来て便利ですよね。

でもサービス側へリダイレクトする直前にごにょごにょしたいことってありませんか?

ありませんか。そうですよね、普通は。。

まあ今回は普通じゃなかったんです。リダイレクト直前にちょっとSessionに色々小細工をしたかったんですが、OmniauthCallbacksController#passthruをオーバーライドしてもうまくいかなかったんで、その方法を共有します。

環境

Service Version
Ruby 3.0.2
Ruby on Rails 6.1
devise 4.8.0
omniauth 2.0.4
omniauth-google-oauth2 1.0.0
omniauth-rails_csrf_protection 1.0.0

対応

最初に書いた通り、リダイレクト寸前にごにょごにょしようとpassthruをオーバーライドしたんですが上手くいかず、それもそのはずコントローラーには到達していなかったんですね。

でもご安心ください。OmniAuthは大変によく完成されたgemです。そういったときのためのメソッドが用意されていました。

今回はリダイレクト時に追加されたparameterをsessionに格納してみましょう。

link_to :OAuth, user_xxxxxx_omniauth_authorize_path(hoge: :fuga)

config/initializers/omniauth.rb

OmniAuth.configure do |config|
  config.before_request_phase = ->(env) {
    request = ActionDispatch::Request.new(env.dup)
    request.session[:fuga] = request.params[:hoge]
  }
end

所感

所感というか愚痴というか。

なぜこういう方法を調べたかというと、別の方がなんとも頓珍漢な方法でOAuthまでにごにょごにょされておりまして、それはあんまりだろうと調べてみた結果こういうメソッドが用意されていたという。。

ちっとは自分で調べるという事くらいしてほしいですね。

削除したActiveStorageへのアクセスは404にしたい

画像の更新などによりActiveStorageのBlobが削除されたにもかかわらず、URLがキャッシュされていたなどの理由により削除された画像にアクセスが来た場合、下記エラーが報告される。

ActiveRecord::RecordNotFoundactive_storage/blobs#show
Couldn't find ActiveStorage::Blob with 'id'=xxx

別段この動きに不満はないが、bugsnagに大量にこのエラーが飛んできて重要なエラー通知に埋もれてしまいかねないので404にしたい。

環境

Service Version
Ruby 2.7.3
Ruby on Rails 6.0.3

対応

ActiveStorageのcontrollerは app/controllers/active_storage/ 以下の対応するcontrollerに置けばオーバーライドできるので、そちらでrescueする。

app/controllers/active_storage/base_controller.rb

# frozen_string_literal: true

# The base class for all Active Storage controllers.
class ActiveStorage::BaseController < ActionController::Base
  include ActiveStorage::SetCurrent

  protect_from_forgery with: :exception

  rescue_from ActiveRecord::RecordNotFound, with: :render_404

  private

  def render_404
    head :not_found
  end
end

所感

オーバーライドは何かあった場合が怖いので、もっといい方法があればそちらにしたい。。。
けど app/controllers/active_storage/blobs_controller.rb には下記のように書いているので、オーバーライドが推奨方法なのかもしれない。

Note: These URLs are publicly accessible. If you need to enforce access protection beyond the security-through-obscurity factor of the signed blob references, you'll need to implement your own authenticated redirection controller.

ActiveStorageの画像ファイルなどを、アンカー要素のdownload属性でダウンロードさせたい

ActiveStorageのservice_urldispositionオプションがデフォルトで:inlineなため、画像やpdfのアンカーにdownload属性をつけていても、ブラウザ上で開いてしまう。

これをダウンロードさせたい。

環境

Service Version
Ruby 2.7.2
Ruby on Rails 6.0.3.4
S3 -

対応

ダウンロードさせたいcontent_typeを config.active_storage.content_types_to_serve_as_binary に追加してあげればいいらしい。

y-yagi.hatenablog.com

別にどこに書いてもいいが、今回はサクッと config/application.rbに書いた。

config.active_storage.content_types_to_serve_as_binary += %w(image/png image/jpeg image/gif image/gif image/bmp application/pdf)

以上です。

送信サーバーと同一ドメインの受信サーバー宛にメールが送信できない

送信サーバーは Amazon Lightsail 、受信サーバーはムームーメールという環境でlLightsailからメール送信ができなくなった。

元々AWSのメール送信制限に引っかかりメールが送信できなくなっており、その調査で /etc/postfix/main.cf はいじっていたのでその辺が怪しいだろうとネットの海を探してみたらまさしくヒット。

環境

Service Version
Amazon Lightsail -
ムームーメール -
Postfix 2.10.1

前提

サーバードメインexample.com とする。

調査

調査の基本はログからと先輩方に叩き込まれたので脳死でまずはログを見る。

ログを見るとどうやら info@example.com 宛が何故か自身にメールを送ってしまっている。

/var/log/maillog

postfix/smtpd[32266]: connect from localhost[127.0.0.1]
postfix/smtpd[32266]: 8A714407E92: client=localhost[127.0.0.1]
postfix/cleanup[32270]: 8A714407E92: message-id=<7e603f861e7c4de87fdb9596a7b15229@example.com>
opendkim[23140]: 8A714407E92: DKIM-Signature field added (s=example, d=example.com)
postfix/qmgr[22693]: 8A714407E92: from=<info@example.com>, size=1489, nrcpt=1 (queue active)
postfix/smtpd[32266]: disconnect from localhost[127.0.0.1]
postfix/local[32271]: 8A714407E92: to=<root@example.com>, orig_to=<info@example.com>, relay=local, delay=0.18, delays=0.1/0.06/0/0.01, dsn=2.0.0, status=sent (delivered to mailbox)

正しくは以下のように mx01.muumuu-mail.com 宛に送って欲しい。

postfix/smtpd[394]: connect from localhost[127.0.0.1]
postfix/smtpd[394]: A981E400056: client=localhost[127.0.0.1]
postfix/cleanup[399]: A981E400056: message-id=<17a46dbb2781b457c2325a1b2db4bae7@example.com>
opendkim[23140]: A981E400056: DKIM-Signature field added (s=example, d=example.com)
postfix/smtpd[394]: disconnect from localhost[127.0.0.1]
postfix/qmgr[391]: A981E400056: from=<info@example.com>, size=1489, nrcpt=1 (queue active)
postfix/smtp[400]: A981E400056: to=<info@example.com>, relay=mx01.muumuu-mail.com[157.7.107.7]:25, delay=0.4, delays=0.11/0.03/0.04/0.21, dsn=2.0.0, status=sent (250 Queued! <17a46dbb2781b457c2325a1b2db4bae7@example.com> (Queue-Id: 115281A40582))

そして試しに送った他ドメイン宛のテストはしっかり送信される。

そこでネットの海を探索した所以下のサイトに巡り合った。本当にありがとうございます。助かりました。

www.lesstep.jp

つまり、mydestination はローカルで受信するドメイン名を指定するのですが、ここで下記のように $mydomain を指定していると今回のような事象になってしまいます。

あ、やったわ。メール送信制限にたどり着くまでにこの対応やったわ。。。

というわけで mydestination から $mydomain を削除。

/etc/postfix/main.cf

- mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
+ mydestination = $myhostname, localhost.$mydomain, localhost

そしてPostfixを再起動させたら、正常に送信されるようになりました。

めでたしめでたし。

FPDIでPDF1.5以降の圧縮されたファイルを何とかしたい

FPDIで色々していたら This PDF document probably uses a compression technique which is not supported by the free parser shipped with FPDI.というエラーが。

どうやらPDF1.5以降の圧縮されたPDFの回答は無償版のFPDIではできないとのこと。
んじゃライセンス買おうかと思ったけど、当然高い。

ということで無償で何とかする方法が以下。

環境

Service Version
CentOS 7.3
PHP 7.2.2

対応

QPDFインストール

必要なパッケージをインストール

$ yum install libjpeg-turbo libjpeg-turbo-devel

どうやらCentoOS7.3のgcc4.8では9.1.1のビルドが通らないらしいので、9.1.0を使用。

$ cd /usr/local/src
$ wget https://github.com/qpdf/qpdf/releases/download/release-qpdf-9.1.0/qpdf-9.1.1.tar.gz
$ tar xvfz qpdf-9.1.0.tar.gz
$ cd ./qpdf-9.1.0
$ ./configure --prefix=/usr/local/qpdf/v9.1.0
$ make
$ make install

パスを通す

$ cd /usr/local/qpdf/
$ ln -s ./v9.1.0 ./current
$ cd ./current/bin
$ ln -s /usr/local/qpdf/current/bin/qpdf 

PHPから使ってみる

以下適当なサンプル。

<?php

use setasign\Fpdi\TcpdfFpdi;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

/**
 * 圧縮の解除
 *
 * @param string $inputPath
 * @param string $outputPath
 * @return string 圧縮解除したファイルのパス
 */
function uncompress(string $inputPath, string $outputPath): string
{
    if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) {
        throw \Exception('skip setlocale() failed');
    }

    $outputPath = escapeshellcmd($outputPath);
    $inputPath = escapeshellcmd($inputPath);
    $command = "qpdf --stream-data=uncompress --force-version=1.4 {$inputPath} {$outputPath}";

    $process = Process::fromShellCommandline($command);
    $process->run();

    if (! $process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }

    return $outputPath;
}


$pdf = new TcpdfFpdi();
$pdf->setSourceFile(self::uncompress('/your/inputPath', '/your/outputPath'));

こんな感じで良い感じに書いてください。

雑感

Ghostscriptとかでやる方法などもあるみたいだけど、とりあえずこれで動いてるから一旦終わり!

あとPHPのサンプルは適当なので悪しからず。実際はこんな感じのコードをLaravelで書いて確認しました。

PHPでコマンド実行しようとしたら日本語が消失した

PHPでコマンドを実行する時はOSコマンドインジェクション対策としてescapeshellcmdescapeshellargなどでエスケープすることが多いと思うが、その際にマルチバイト文字が含まれているとマルチバイト文字が空文字に変換されてしまって困った。

環境

Service Version
PHP 7.2.2

解決

実行前にロケール情報を設定してあげることで回避できた。

<?php

if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) {
    throw \Exception('Not exist Locale.');
}

$cmd = escapeshellcmd($cmd);

雑感

そもそもマルチバイトがコマンドに入るような設計がどうなの。