全文検索エンジンGroongaの開発に参加している堀本です。
Groongaは、Redmineのプラグインとして組み込むことができます。名前を redmine_full_text_searchといいます。
このプラグインによって、Redmineの全文検索をGroongaを使って実行できます。
今回はこのプラグインへの機能追加した時の添削コミットを紹介します。
redmine_full_text_searchを使うと、検索結果からチケット等に遷移した際に、search_id
, search_n
というURLパラメータが付与された状態でチケット画面が表示されます。
これは、検索精度のチューニングのために付与している情報で、デフォルトで付与されるようになっています。
ただ、ユースケースによっては、これらのパラメーターが付与されたURLでは不都合なことがあります。
そのため、これらのパラメーターを無効にしたいという要望があり、それを実装しました。
実装が完了しPull Requestした当初のソースは以下のようになっていました。
<% @result_set.each_with_index do |e, i| %>
<dt class="<%= e.event_type %> icon icon-<%= e.event_type %>">
<%= content_tag("span", e.project, :class => "project") unless @project == e.project %>
+ <% if fts_add_search_related_parameters_in_url? %>
<%= link_to(e.event_highlighted_title,
e.event_url.merge("search_id" => @search_request.search_id,
"search_n" => i + @search_request.offset),
:data => {:rank => e.rank}) %>
+ <% else %>
+ <%= link_to(e.event_highlighted_title, :data => {:rank => e.rank}) %>
+ <% end %>
</dt>
<dd>
<ol class="search-snippets">
無効にするかどうかをオプションで選択できるようにしています。
<% if fts_add_search_related_parameters_in_url? %>
の部分でパラメーターを付与するオプションが有効かどうかを判定し、有効だったらsearch_id
, search_n
をURLに付与するようにしています。
無効の場合は、<%= link_to(e.event_highlighted_title, :data => {:rank => e.rank}) %>
が実行されてパラメーターが付与されないURLになります。
このコードは、プロジェクトのメンテナーによって以下のように添削されました。
上記のコードは、以下のようにURLパラメーターを付与するかどうかの判定を削除し、 search_result_entry_url(e, i)
というメソッドを呼び出しその結果を表示するだけのコードになっています。
<% @result_set.each_with_index do |e, i| %>
<dt class="<%= e.event_type %> icon icon-<%= e.event_type %>">
<%= content_tag("span", e.project, :class => "project") unless @project == e.project %>
- <% if fts_add_search_related_parameters_in_url? %>
<%= link_to(e.event_highlighted_title,
- e.event_url.merge("search_id" => @search_request.search_id,
- "search_n" => i + @search_request.offset),
+ search_result_entry_url(e, i),
:data => {:rank => e.rank}) %>
- <% else %>
- <%= link_to(e.event_highlighted_title, :data => {:rank => e.rank}) %>
- <% end %>
</dt>
<dd>
<ol class="search-snippets">
URLパラメーターを付与するかどうかの判定は別のファイルに以下のように定義されました。
module Hooks
module SearchHelper
include FullTextSearch::Hooks::SettingsHelper
+
+ def search_result_entry_url(e, i)
+ if fts_enable_tracking?
+ search_parameters = {
+ "search_id" => @search_request.search_id,
+ "search_n" => i + @search_request.offset,
+ }
+ else
+ search_parameters = {}
+ end
+ e.event_url.merge(search_parameters)
+ end
end
end
添削前のコードでは、HTMLにオプションの有無を判定し、生成するURLを切り替える処理を埋め込んでいましたがこの処理を埋め込んだファイルは ERBテンプレートでした。
ERBテンプレートは、HTMLにRubyスクリプトを埋め込めるため、添削前のコミットのように条件を判定して表示を切り替えるというようなこともできます。
ただ、このようにすると、このファイルの見通しは悪くなります。
ERBテンプレートは、データをどうやって表示させるかを書く場所であるため、表示させるデータを選択する処理を埋め込むと表示を担当する処理と、表示するデータを選択する処理が混在してしまい、複雑なコードになりやすくなります。
Ruby on Rails には、こんな時のためにヘルパーという仕組みが用意されています。
添削後のコードでは、オプションの有無の判定はヘルパーに移動され、ERBテンプレートでは、ヘルパーの処理結果を表示するだけになりました。
これにより、ERBテンプレート内の判定処理がなくなり見通しがよくなりました。
見通しの良いコードはバグを埋め込みにくくなりますし、読みやすく理解もしやすくなります。理解しやすいコードはメンテナンスもしやすいです。
ファイルの役割分担を考えて実装することで、より良いコードになるということに改めて気がつく出来事でした。
Fluentdの周辺のIssueチケットを見ていたらProtocol BuffersをFluentdのInputプラグインで扱えると面白そう*1ということで、対応を考えてみた畑ケです。
クリアコードはFluentdの開発に参加しています。
Fluentdにはプラグインというしくみがあり、たくさんのプラグインが開発されています。
Fluentdのプラグインには、Parserプラグインという種類のプラグインがあります。
この種類のプラグインはInputプラグインが受け取ったテキストやバイナリーデータをFluentdで扱いやすくするために使用されます。
Protocol Buffersとは、プログラミング言語間の差異を吸収してデータのやり取りを行うデータ形式です。
データ構造を定義して、それをいくつかの言語のProtocol Buffersを扱える表現にコンパイルすることでProtocol Buffersの形式にシリアライズしたり、デシリアライズできます。
例えば、Goで書いたプログラムでProtocol Buffers形式でシリアライズされたデータをC++で書いたプログラムからProtocol Buffersのデータ定義を用いて元のデータを復元できます。
Fluentdにはin_tcp
やin_http
のような<parse>
ディレクティブを処理できるInputプラグインがあります。InputプラグインはParserプラグインとしてテキストやバイナリーのパースの処理をプラグインとして持ってこれるものがあります。この任意のパース処理を差し替えることができるようにする仕組みがParserプラグインです。
まとめると、FluentdでProtocol Buffersを処理するのに最適な場所はParserです。
そこで、InputプラグインでProtocol Buffersでシリアライズされたバイナリデータを処理できるようにProtocol Buffersを処理するParserプラグインを作成しました。
例えば、このParserプラグインを使ったin_http
プラグインを使ったHTTPエンドポイントを立てると、
FluentdがProtocol BuffersのAPIのHTTPエンドポイントとして振る舞うことができ、色んなシステムと直接つなげることができます。
fluent-plugin-parser-protobufはProtocol Buffersのコンパイラーが必要です。この記事ではProtocol Buffers v3の場合について解説します。
Protocol Buffers v3のコンパイラーや各言語ごとのライブラリーは https://github.com/protocolbuffers/protobuf/releases からダウンロードできます。
Protocol Buffers v3をFluentdで使う手順では、Protocol Buffersのコンパイラーであるprotocがインストール済みであると仮定して解説をします。
Protocol BuffersのIDLの文法はProtocol Buffersの公式ドキュメントの概要を参照してください。
例えば、下記のようなprotobufのIDLを作成します。
syntax = "proto3";
import "google/protobuf/timestamp.proto";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
google.protobuf.Timestamp timestamp = 5;
}
これを、protocでコンパイルします。
$ protoc --proto_path=/path/to/idl --ruby_out=/path/to/output simple.proto
すると、下記のRubyのクラスが生成されます。
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: simple.proto
require 'google/protobuf'
require 'google/protobuf/timestamp_pb'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("simple.proto", :syntax => :proto3) do
add_message "SearchRequest" do
optional :query, :string, 1
optional :page_number, :int32, 2
optional :result_per_page, :int32, 3
optional :corpus, :enum, 4, "SearchRequest.Corpus"
optional :timestamp, :message, 5, "google.protobuf.Timestamp"
end
add_enum "SearchRequest.Corpus" do
value :UNIVERSAL, 0
value :WEB, 1
value :IMAGES, 2
value :LOCAL, 3
value :NEWS, 4
value :PRODUCTS, 5
value :VIDEO, 6
end
end
end
SearchRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("SearchRequest").msgclass
SearchRequest::Corpus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("SearchRequest.Corpus").enummodule
このprotobufクラスがあれば、fluent-plugin-parser-protobufにProtocol Buffersの定義を読み込ませることができます。
<parse>
ディレクティブの中にprotobufパーサーに関連する設定を書きます。
<source>
@type http
port 8080
<parse>
@type protobuf
class_name SearchRequest
class_file "#{File.expand_path(File.join('path', 'to', 'simple_pb.rb'))}"
protobuf_version protobuf3
</parse>
</source>
<match protobuf>
@type stdout
</match>
疎通確認テストとしてHTTPでProtocol Buffersでシリアライズしたリクエストを送るようにします。
そのため、以下のRubyスクリプトを用意します。
require 'google/protobuf'
require "net/http"
require_relative "path/to/simple_pb"
def encoded_simple_binary
request = SearchRequest.new(query: "q=Fluentd",
page_number: 404,
result_per_page: 10,
corpus: :WEB,
timestamp: Time.now)
SearchRequest.encode(request)
end
uri = URI.parse("http://localhost:8080/protobuf")
params = encoded_simple_binary
req = Net::HTTP.new(uri.host, uri.port)
req.post(uri.path, params.to_s)
Fluentdとテストスクリプトはそれぞれ、別の端末で実行します。
$ bundle exec ruby test_http.rb
$ bundle exec fluentd -c fluent.conf -p lib/fluent/plugin
<snip>
2020-06-01 16:45:43 +0900 [info]: #0 fluentd worker is now running worker=0
2020-06-01 16:45:46.377515000 +0900 protobuf: {"query":"q=Fluentd","page_number":404,"result_per_page":10,"corpus":"WEB","timestamp":{"seconds":1590997546,"nanos":371940000}}
最後の2020-06-01 16:45:46.377515000 +0900 protobuf: {"query":"q=Fluentd","page_number":404,"result_per_page":10,"corpus":"WEB","timestamp":{"seconds":1590997546,"nanos":371940000}}
により、Protocol BuffersでシリアライズされていたHTTPリクエストbodyがfluent-plugin-parser-protobufによりパースされ、Hashオブジェクトに分解されているのが確認できます。
FluentdでProtocol Buffersを扱うにはどのようにしたら良いのかの方針を立て、実際にProtocol Buffersをパースするための手順を解説しました。
記事で解説した方法でFluentdがProtocol BuffersのAPIのHTTPエンドポイントとして振る舞うことができればより色んなシステムと直接つなげることができます。
今回のParserプラグインでは取り込むだけですが、FluentdのProtocol BuffersのFormatterプラグインを作成するとProtocol Buffersを用いてデータのやり取りを行うシステムに直接データを送ることができるようになるでしょう。
当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。
*1 https://github.com/fluent/fluentd/issues/3000
来たる6月30日、Firefox ESR78がいよいよ正式にリリースされます(予定)。ESR版としてや1年ぶりのメジャーアップデートです。現時点で既に(ESR版ではなく一般向けの)ベータ版が利用可能になっているため、そろそろ検証を始められている情シス担当の方も増えてきているのではないでしょうか。
Firefox ESR68からの変更点は多岐に渡りますが、その中でも、この記事では法人利用の場面において影響があると考えられる変更点をご紹介します。
最初に、スケジュールについておさらいしましょう。
詳細はMozilla Wikiのリリースカレンダーに記載されていますが、端的には、Firefox ESR78への移行は日本時間で2020年9月23日までに完了させることが強く推奨されます。
Firefox ESR78.0正式版がリリースされた後も、Firefox ESR68のサポートはしばらく継続します。具体的には通常版のFirefoxのリリース3回分*1まではFirefox ESR68のセキュリティアップデートが提供される予定となっています*2。このサポート期限が太平洋標準時で2020年9月22日、日本時間では9月23日ということになります。
Firefoxには以前からトラッキング保護機能が含まれていて、Webサイトによる行動履歴の収集を回避できるようになっています。しかし、この機能はESR68までは初期状態では無効で、有効になるのはプライベートブラウジングウィンドウを使っている時だけでした。
Firefox ESR78では、トラッキング保護機能がプライベートブラウジングウィンドウ以外でも初期状態で有効になっています。また、ブロック対象が拡大され、以下のトラッキング手法もブロックされるようになりました。
訪問したWebページでどのようなトラッキング手法が実際にブロックされているかは、アドレスバーの盾アイコンをクリックすると確認できます。
この機能は法人利用では、プライバシー保護よりはトラフィック削減の観点から有用性が期待できます。というのも、昨今の多くのWebページにはトラッキングスクリプトが含まれており、この事がネットワーク帯域の消費量増につながっているからです。
ただし、トラッキング保護機能の動作はブロックリストに基づいており、機能を使うにはリストが定期的に取得・更新されている必要があります。法人利用では、バックグラウンドで行われる無用な通信の削減を目的として、ブロックリストの取得も禁止している場合がありますが、その状態ではトラッキングブロック機能は有効に機能しないので、運用ルールの見直しが必要になります。
定期的に取得されるブロックリストのデータサイズは、本稿執筆時点では非圧縮状態で250KB程度でした。ブロックリストは1時間に1回の頻度で更新がチェックされ、リストが更新されていた時にだけ実際にダウンロードが行われます。このブロックリストによって実際にどの程度のトラフィック削減効果を得られるかは、状況や閲覧するWebサイトによって変わりますので、できれば試験運用で効果を測定してから判断したい所です。
Firefox ESR78ではWeb Authentication HmacSecret拡張により、Windows 10の2019年5月版以降で、Windows Helloによる指紋認証や顔認証などのパスワード非依存の認証をWebサイトでも使えるようになりました。実際に使うにはWebサイト側の対応が必要なので、パスワード認証のWebサイトが突然顔認証可能になるというわけではないのですが、SaaS形式のWebサービスなどでは今後利用可能になっていくと考えられます。
OSの認証処理との連携強化は、パスワードマネージャの動作にも反映されています。具体的には、パスワードマネージャに保存したパスワードを閲覧する際にOSの認証を求められるようになり、その際には顔認証などを利用できるようになっています。
また、実験的な機能として、security.osclientcerts.autoload
を有効化することによって、WindowsとmacOSでOSの証明書ストアにあるクライアント証明書を利用できるようになりました。これまでFirefoxでクライアント証明書を使うためには、既にWindows自体の証明書データベースにクライアント証明書がある場合でも、それとは別にFirefoxの証明書マネージャを使って手動で証明書を登録する必要がありましたが、このような運用を簡略化することができます。OSの証明書ストアにあるエンタープライズのルート証明書は、Firefox ESR60以前からすでに自動インポート可能になっていましたので、OSの証明書ストアとの連携がさらに強化されたと言えます。
セキュリティに関する変更の中で目立つのは、SSL/TLSで接続しているWebサイトの表示の仕方が変わったという点です。
これは、Googleなどによって進められている「常時SSL化」の流れを承けての、より安全な状態を標準的な状態として取り扱うようにする変更です。Webサイトの安全性そのものが変化したわけではありませんが、「偽サイトに誘導されてしまったのか?」とユーザーが誤解する恐れはりますので、問い合わせが増加しても大丈夫なように備えておきましょう。
逆に、反映が見送られたセキュリティに関わる変更としては、「TLS1.2以上の強制(TLS 1.0/1.1を無効)」があります。これは、医療情報を提供する公のWebサイトにまだTSL 1.0/1.1で提供されている物がある可能性があるためで、新型コロナウィルスの感染拡大とそれに伴う混乱が収束するまでは変更を保留する、という判断に基づいています。
とはいえ、TLS 1.0/1.1には脆弱性があり、暗号化された通信内容が盗聴されてしまう恐れがある、という事実は変わりません。自社のWebサイトでTLS 1.0/1.1を使っていないかを改めて確認し、もしあれば早急にTLS 1.2以上へ移行するようにしましょう。
DNSでの名前解決をより安全にする「DNS over HTTPS」については、米国地域の英語版Firefoxユーザーに対しては設定が既定で有効化されますが、それ以外の地域・言語では無効となっています。この点はFirefox ESR78でも同様です。とはいえ、正式リリースまでの間に判断が変わる可能性も無いとは言い切れませんので、運用上の懸念がまだ残っている場合は、過去記事で紹介した無効化手順に則って機能を無効化するのがお薦めです。
Firefoxのプラグイン対応は段階的に廃止が進んでおり、Firefox ESR68の時点ではAdobe Flash以外のプラグインは機能しなくなっています。この一環としてFirefox ESR78では、従来可能であった「ページを読み込んだ時点で自動的にFlashコンテンツを再生する」動作が廃止されました。今後は、Flashコンテンツの再生には必ずユーザーによる明示的なクリック操作が必要となります。業務でFlashを使用していて、クリック操作無しでの再生を前提に運用している場合は、影響に注意してください。
Firefox ESR78では、音声を伴う動画のみ自動再生を禁止するという既定の動作に加えて、音声を伴わない動画も含めてすべての自動再生を禁止する動作が可能になりました。media.autoplay.default
を 5
に設定すると、すべての動画の自動再生をブロックするようになります。この設定は、ネットワークトラフィックの削減に有効である可能性があります。
動画に関連した変更としては、専用のアプリケーションの導入なしにFirefoxだけでビデオ会議サービスの「Zoom」を利用できるようになった、という改善もあります。ビデオ会議通話の需要が増加した昨今なので、この変更は運用コストの低減に繋がるかもしれません。
Firefox ESR68までのバージョンでは、C:\Program Files\Mozilla Firefox\browser\extensions
にアドオンのファイルを設置したり、Windowsのレジストリに情報を記載したりすることで、ユーザーがアンインストールできない形でアドオンをインストールさせる事ができました*3。Firefox ESR78からは、この形式でのインストールが制限されるようになります。
具体的には、この方法でインストールされたアドオンは、従来であればそのまま読み込まれていましたが、今後は「ユーザーがそのアドオンをインストールしたのと同じ状態」に自動的に変換されるようになります。ユーザーが任意でアドオンをアンインストール可能になるので、アンインストールされては困るアドオンを全社的に使用している場合、過去記事で紹介した代替手法による対策が必要です。
実は、更新チャンネルがESR版に設定されているビルドでは従来通りのサイドローディングが有効な状態になっています。そのため、従来通りの運用を続ける事も不可能ではありません。
しかしながら、サイドローディングを有効にするかどうかはFirefoxのビルド時の指定で決定されるため、Firefox 78のベータ版では動作を確認できません。サイドローディングを使った運用を継続したい場合には、動作の検証はFirefox ESR78.0の正式版がリリースされるのを待って行う必要があります。
以上、Firefox ESR68からFirefox ESR78の間での変更点のうち、法人利用に影響があると考えられるトピックをご紹介しました。
Firefox ESR78より未来のことは現時点では未知数の部分が多いですが、注視する必要がある今後の変更の1つとして、Windowsのタスクスケジューラを使ったバックグラウンドでの自動更新があります。恐らく、来年リリースされるであろう次のESR版からはこの変更が反映されるものと予想されます。法人利用では自動更新を無効化して運用する事例が多いので、タスクスケジューラに基づく自動更新の停止方法は気になる所でしょう。当社のお客さまにも影響があるため、今後何か新しい事が分かり次第、改めて調査してこのブログで解説する予定です。
当社の法人向けFirefoxサポートサービスでは、Firefoxの新しいバージョンがお客さま環境での運用状況にどのように影響を与えるかなど、総合的な調査も必要に応じ承っております。Firefoxを運用していて、先手先手で新バージョンへの対応を進めていきたいとお考えの企業さまがいらっしゃいましたら、お問い合わせフォームよりご相談ください。
全文検索エンジンGroongaの他にPostgreSQLで高速に全文検索できる拡張PGroongaの開発にも参加している堀本です。
今回は、PGroongaの開発中に「注意しないと危ないな」と思ったコードを紹介します。
PGroongaはPostgreSQLの拡張として開発されています。
そのため、当然ですが、PostgreSQLとデータのやり取りを行います。
PostgreSQLには、PostgreSQLの型があり、以下のような型が組み込みの型として用意されています。
https://www.postgresql.jp/document/12/html/datatype.html#DATATYPE-TABLE
この中で文字列を格納する型としてよく使われるのがtext
型(長さ制限のない可変長文字列)です。
今回PGroongaに新しい関数を実装するのに、以下のようにエラーログを出力する処理を書きました。
if (desc->natts <= i)
{
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("pgroonga: an invlid value was specified for column name: %s",
columnNameData)));
}
新しく実装した関数は以下のようなインターフェースを持っていて、PGroongaのインデックスを設定したカラムの名前を与えると、それに対応するPGroongaが内部で管理しているテーブルの名前を返すようになっています。
text pgroonga_index_column_name_string(indexName text, columnName text)
冒頭のコードは、この関数の第二引数に存在しないカラムの名前を指定された場合にエラーログを出力するようにしています。
ここで、ログに出力しているcolumnNameData
は、以下のようにtext
型の変数からVARDATA_ANY
マクロでデータを取り出しています。
const text *columnName = PG_GETARG_TEXT_PP(1);
const char *columnNameData = VARDATA_ANY(columnName);
エラーログで出力しているcolumnNameData
の方はchar *
型なので、%s
を使って出力するので問題ないように見えますが、PostgreSQLのtext
型はNULL終端されていないので、%s
を使って出力すると、意図しない領域まで出力してしまう可能性があります。
このようなバグを防ぐため、PostgreSQLのtext
型を使用する場合は以下のように、必ず長さ指定をする必要があります。
if (desc->natts <= i)
{
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("pgroonga: an invlid value was specified for column name: %s",
- columnNameData)));
+ errmsg("pgroonga: an invlid value was specified for column name: %.*s",
+ (const int)columnNameSize,
+ columnNameData)));
}
C言語ではNULL終端の文字列として文字列を扱う場合(C言語の標準の文字列関数)とデータとデータの長さで文字列を扱う場合(printf
で%.*s
を使う場合など)と文字列の開始位置のポインターと終了位置のポインターで文字列を扱う場合があり、扱いを間違うと思わぬバグを仕込むことになってしまいます。
C言語の文字列に関わる問題は多いですが、改めてC言語の文字列の扱いには注意が必要だと感じたコードの紹介でした。