まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2019/01/20(日)Net::SMTPでSMTP AUTHのDIGEST-MD5に失敗する問題

さくらインターネットのメールサービスを使用して、SMTP AUTHのDIGEST-MD5認証を使うと送信に失敗する問題を調べてみました。

問題の詳細

PerlのNet::SMTPモジュール等を使用して、さくらインターネットのメールサービスを使い、SMTP Authでメールを送信する失敗します。

ネットで検索するとPerlのNet::SMTPの問題と書かれていたりもしますが、実際にはさくらインターネット側の問題のようです

SMTPのログを取ってみるとこんな感じになります。

> 220 www1234.sakura.ne.jp ESMTP Sendmail 8.15.2/8.15.2; Sun, 20 Jan 2019 21:25:22 +0900 (JST)
EHLO localhost.localdomain
> 250-www1234.sakura.ne.jp Hello xxxx [192.168.0.1], pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE 209715200
250-DSN
250-AUTH CRAM-MD5 DIGEST-MD5 LOGIN PLAIN
250-STARTTLS
250-DELIVERBY
250 HELP
> AUTH DIGEST-MD5
(decoded) nonce="UOM8WDtDO5/ZOCm+tpQaxjRbHOCM5Y5CKekiBEVlmU8=",
		realm="www1234.sakura.ne.jp",
		qop="auth,auth-int,auth-conf",
		cipher="c4-40,rc4-56,rc4,des,3des",
		maxbuf=8192,charset=utf-8,algorithm=md5-sess
>(decoded) authzid="smtp@example.jp",charset=utf-8,cipher=rc4,
>		cnonce="eedacf2b08dc6c60a2a799ee59188455",
>		digest-uri="smtp/XXXXXXXX.sakra.ne.jp",
>		nc=00000001,nonce="UOM8WDtDO5/ZOCm+tpQaxjRbHOCM5Y5CKekiBEVlmU8=",
>		qop=auth-conf,realm="www1236.sakura.ne.jp",
>		response=c54d120c415a6e3d5cd5469b36faa7e1
>		username="smtp@example.jp"
(decoded) rspauth=fc45f7aa080db4dfb73d1b65a1f48d50
>
235 2.0.0 OK Authenticated
> MAIL FROM:<test@example.jp>
Net::SMTP: Net::Cmd::getline(): unexpected EOF on command channel:

ソースに手を入れていろいろ調べてみたところ、

235 2.0.0 OK Authenticated

を受け取ったあと、どんなコマンドを送っても切断されているようです。コマンドを送るまでソケットは生きているのですが、改行してコマンドを確定した瞬間にソケットの強制切断を喰らいます。これを回避するために、なにか特殊な暗号をしゃべるべきなのかもしれませんが皆目検討が付きません。

問題の切り分け

問題を切り分けるために、自前でSMTP AUTH / DIGEST-MD5認証対応のpostfixサーバを構築して実験してみたところ、問題なく送信できました。これにより、さくらインターネット側の不具合の可能性がかなり濃厚になってきました。

なぜ問題が放置されているのか?

ほとんどのメーラー(メールクライアント)は、SMTP AUTHの認証方法を細かく指定することができません。内部で良きにはからってくれるので、仮にDIGEST-MD5認証に挑戦して失敗しても、CRAM-MD5等の他の認証方法を試します。

PerlのNet::SMTPモジュールではなぜ問題が起こるのか?

Net::SMTPモジュールは「EHLO」で返された「AUTH」の中で、最初に対応しているものを認証手段として選択します。失敗しても次の認証方法を試したりはしません。結果として、さくらインターネットのメールサービスと組み合わせて使用すると、SMTP Authに失敗します。

ひとこと

さくらインターネットちゃんとしてください(笑)*1

2018/08/24(金)[RFC]WebPush実装まとめ(Chrome/Firefox/Android/Edge)

2017/02/26(日)最近のDebianに、ソースから Perl 5.8.9 のインストール

adiaryの動作テスト用に Perl 5.8 が欲しかったので、ソースからインストールした時のメモ。

手順

cd /usr/local/src
wget http://www.cpan.org/src/perl-5.8.9.tar.gz
./configure.gnu -Dprefix=/usr/local/perl5.8 -Dusethreads -Duselargefiles \
-Dccflags=-DDEBIAN -D_FORTIFY_SOURCE=2-fstack-protector-strong \
-Dldflags=-Wl,-z,relro -Dlddlflags=-shared -Dcccdlflags=-fPIC \
-Darchname=x86_64-linux-gnu -Dusesitecustomize -Duse64bitint \
-Dpager=/usr/bin/sensible-pager \
-Uafs -Ud_csh -Ud_ualarm -Uusesfio -Uusenm -Ui_libutil -Uversiononly \
-DDEBUGGING=-g -Doptimize=-O2 -Duseshrplib -des

このままコンパイルすると crypt() が使えないので config.sh を手動修正。

d_crypt='define'

この後で make する。

make
make test
make install

参考にしたサイト

2015/05/20(水)Perl DBI と UTF8フラグ と 文字化け問題 (2017/05/31追記)

Perl 5.20にしたら、DBIが何やら不可解な動作をするようになりました。

問題の原因

どうやら UTF8 を扱う機能が増えたみたいです。DBIの接続時のパラメーターにこんなものが増えています。

pg_enable_utf8 => 1 , # 結果をUTF8フラグ付きにする(PostgreSQL)

mysql_enable_utf8 => 1 , # 結果をUTF8フラグ付きにする(MySQL)

PerlのDBIモジュールで自動的にUTF8フラグを付ける

この影響で、PostgreSQL運用のシステムを Perl 5.14 から 5.20 にアップグレードしたところ文字化け発生。また厄介なことをしてくれたものです(汗)

保存されいてる文字コードがおかしい

普通に使っているのに「Wide character in print at」の警告がなぜか出まくります。日本語UTF-8として保存した文字列が文字化けしまくるのですが、そのデータだけ単独に取り出して表示しても化けない。

色々調べてみると以下のことがわかりました。

  • 新DBD は(utf8フラグのついていない)文字列を DB に保存するとき「ASCII文字列に特殊な文字が混ざってる」と解釈してエスケープ処理を行う。
  • この文字列は、通常の utf8 文字列ではないので、データベースの中身を直接覗くと、保存した文字列ではなく特殊なエスケープ文字列として表示される。

PostgreSQLではこんな感じです。

  • 文字列「あいう」を保存。バイナリ列で「E3 83 86 E3 82 B9 E3 83 88」
  • psqlコンソールで表示される文字列「a\u0083\u0086a\u00821a\u0083\u0088」
  • DBI経由で取り出し、utf8フラグを取り除いた文字列「C3 A3 C2 83 C2 86 C3 A3 C2 82 C2 B9 C3 A3 C2 83 C2 88」

同じUTF8文字列でありながら、違う内部表現になっています。当然、元の「あいう」と文字列比較を行うと異なると判定されます。

MySQLではこんな感じです。

  • mysqlコンソールで出力される文字列「C3 A3 C2 81 E2 80 9A C3 A3 C2 81 E2 80 9E C3 A3 C2 81 E2 80 A0」
  • DBI経由で取り出した文字列は utf8フラグ がついていない元の文字列。

問題を整理すると2つに集約できます。

  • DBにきちんとしたUTF8文字列として保存されない。
  • DBに一回保存することで、内部表現の異なる UTF-8 文字列が生成されてしまう(PostgreSQL)。

後者も問題と言えば問題ですが、前者はとても気持ち悪い問題です。

解決策

utf8フラグはうまく使えば「Perlによきに計らってもらい」文字コードの問題が楽になるものですが、utf8文字列をutf8として「そのまま扱いたい」時には問題が多すぎる仕組みです。入出力の都度にencode、decodeが発生し処理を重くする原因にもなります。

またutf8フラグありとフラグなしのUTF8文字列が混在すると、それらを連結したときに文字化けする危険があります。

そうなるとutf8フラグなしで扱いたいのですが、そうするためにはどうしたらいいのでしょうか。

DBD::Pg編

  • utf8フラグのない文字列は、必要に応じて特殊エンコードされてDBに保存される。
  • 「pg_enable_utf8」を設定しないとき。
    • 非ASCII文字列や、特殊エンコードされた文字列は utf8 フラグが付いて取り出される。
  • 「pg_enable_utf8 => 1」のとき。
    • 「pg_enable_utf8」を設定しないときと同様。
  • 「pg_enable_utf8 => 0」のとき。
    • いかなる場合も utf8 フラグは付けなくなる。
    • 特殊エンコードされた文字列は文字化けする。

保存時の挙動。

  • DBD::Pg / Version 3.3.0 to 3.5.3
    • utf8フラグを付けた文字列を保存すれば、そのままutf8文字列としてDBに保存される。
    • utf8フラグの付いていな文字列は特殊エンコードされる。
  • DBD::Pg / Version 3.6.0以降
    • 「pg_enable_utf8 => 0」のとき、utf8フラグの付いた文字列を渡すとエラーになる。
    • 「pg_enable_utf8 => 1」のとき、utf8フラグの付いていない文字列を渡すと特殊エンコードされる。

取り出す時用に「pg_enable_utf8 => 0」を設定して、保存時や「検索時」にutf8フラグを付けるのがよさそうです。

追記参照のこと。

DBD::MySQL

  • DB保存時はutf8フラグの有無はすべて無視する(動作は変化しない)。
  • 「mysql_enable_utf8」を設定しないとき。
    • いかなる場合もutf8フラグは付かない。
    • 非ASCII文字列は特殊エンコードされたDBに保存される。
    • 特殊エンコード文字列は、通常の utf8 文字列で取り出される。
    • DBに格納されている通常の utf8 文字列は、エンコード失敗の「?」に置換されDBから取り出される。
  • 「mysql_enable_utf8 => 0」のとき。
    • 「mysql_enable_utf8」を設定しないときと同様。
  • 「mysql_enable_utf8 => 1」のとき。
    • utf8文字列は、通常の utf8 文字列としてDBに保存される。
    • 非ASCII文字列は utf8 フラグが付いて取り出される。
    • 特殊エンコードされた文字列は文字化けする。

DBD::Pgとは実装がまるで違う事がわかります。

ドキュメントを読んだところ「SET NAMES utf8;」というSQL文を発行することで、同じ効果を得て、かつutf8フラグに影響がでないことがわかりました。ただこの状態では、特殊エンコードされた文字列を読み込むときに文字化けするので注意が必要です。

比べると

DBD::Pgの実装のほうが謎かもしれない。DBD::Pgはトランザクション処理がおかしいという素敵な実績があるからなあ……。「pg_enable_utf8 => 0」のとき保存データのutf8フラグも無視しないと矛盾するのはすぐに分かりそうなもんなんだけど。

DBIの仕様

Perl supports two kinds of strings: Unicode (utf8 internally) and non-Unicode (defaults to iso-8859-1 if forced to assume an encoding). Drivers should accept both kinds of strings and, if required, convert them to the character set of the database being used. Similarly, when fetching from the database character data that isn't iso-8859-1 the driver should convert it into utf8.

http://search.cpan.org/~timb/DBI-1.633/DBI.pm

Perl は Unicode(内部表現utf8)と 非Unicode(iso-8859-1)の2つの文字コードをサポートします。DBDドライバはこの2つの文字コードを受け付けるべきで、必要に応じて database で使用可能な文字列に変換する必要があります。

同様に、iso-8859-1ではない文字列は database から取り出す段階でutf8に変換すべきです。

DBD::Pgの仕様変更 2017/05/30

仕様変更というよりBug Fixされ「CHANGES」を読むと、DBD Version 3.6.0あたりから挙動が異なる(正しい挙動に近くなっている)ようです。

しかし、この変更のせいで「pg_enable_utf8 => 0」のときutf8フラグ付の文字列を渡すと「Wide character in subroutine entry」エラーが発生します。

ですので「pg_enable_utf8 => 0」のときは、$DBD::Pg::VERSIONを参照し「3.3.0~3.5.3の間の時だけUTF8フラグをつけてDBIに渡す」必要があります。

クソバグを仕込んだ上に、互換性考えない中途半端なクソ修正をして、そびえ立つクソ状態なDBD::Pgには呆れるばかり……。

まとめ

  • 今どき文字コード問題、しかも UTF8 で悩むとは思わなかった。
  • 1byte文字圏の UTF8 実装は相変わらずクソ。

2015/05/14(木)スマホでドラッグ&ドロップのエミュレーション

jQuery UIdynatree を使用していて、ドラッグアンドドロップ操作が必須であるにも関わらずスマホでは何もできないので、汎用的な実装をjQuery pluginで実現しました。

タッチ操作でDnDをエミュレーションする

タッチパネル系イベントは独特らしく、短くタップしたときのみ mousedown や click 等のマウスイベントが発生してくれますが、長く触っているとマウスイベントは発生しないようです。

ですので、以下のように実装しました。

  • touchstart で mousedown を発火。
  • touchmove で mousemove を発火し、mouseleave と mouseenter をエミュレーション。
  • touchend で mouseup を発火。

mouseover, mouseoutも実装はできますが無視しました。jQuery UIが問題なく動く程度には実装しているつもりです。

エミュレーションがonの状態で短くタップすると、mousedown/mouseupイベントが2重に発生する可能性がありますが、解決策がないので保留です。

ソース

修正BSDライセンスとします。jQuery pluginですので適当に読み込ませて次のように使ってください。

$(dom).dndEmulation();

以下のソースは最新でない可能性があります。最新版は、adiaryのサイトからGitHub経由で「js/adiary.js」を参照して該当部のみ抜粋してください。該当部のみ抜粋する限り修正BSDライセンスで扱って構いません。

var TouchDnDTime  = 700;
$.fn.extend({
//////////////////////////////////////////////////////////////////////////////
// Copyright (C)2015 nabe@abk, New BSD License.
//////////////////////////////////////////////////////////////////////////////
dndEmulation: function(){
	var self = this[0];
	if (!self) return;

	// mouseイベント作成
	function make_mouse_event(name, evt, touch) {
		var e = $.Event(name);
		e.altKey   = evt.altKey;
		e.metaKey  = evt.metaKey;
		e.ctrlKey  = evt.ctrlKey;
		e.shiftKey = evt.shiftKey;
		e.clientX = touch.clientX;
		e.clientY = touch.clientY;
		e.screenX = touch.screenX;
		e.screenY = touch.screenY;
		e.pageX   = touch.pageX;
		e.pageY   = touch.pageY;
		e.which   = 1;
		return e;
	}
	// 自分自身を含めた親要素をすべて取得
	function get_par_elements(dom) {
		var ary  = [];
		while(dom) {
			ary.push( dom );
			if (dom == self) break;
			dom = dom.parentNode;
		}
		return ary;
	}

	// クロージャ変数
	var prev;
	var flag;

	// mousedownエミュレーション
	this.bind('touchstart', function(_evt){
		var evt = _evt.originalEvent;
		prev = evt.target;
		var e = make_mouse_event('mousedown', evt, evt.touches[0]);
		$( prev ).trigger(e);
		
		// ある程度時間が経過しないときは処理を無効化する。
		flag = false;
		setTimeout(function(){
			flag=true;
		}, TouchDnDTime)
	});

	// mouseupエミュレーション
	this.bind('touchend', function(_evt){
		var evt = _evt.originalEvent;
		var e = make_mouse_event('mouseup', evt, evt.changedTouches[0]);
		$( evt.target ).trigger(e);
	});

	// ドラッグエミュレーション
	this.bind('touchmove', function(_evt){
		var evt = _evt.originalEvent;

		// 一定時間立たなければ、処理を開始しない
		if (!flag) return;

		var touch = evt.changedTouches[0];
		var dom   = document.elementFromPoint(touch.clientX, touch.clientY);
		var enter = get_par_elements(dom);

		// マウス移動イベント
		var e = make_mouse_event('mousemove', evt, touch);
		$(enter).trigger(e);

		// 要素移動がなければこれで終了
		evt.preventDefault();
		if (dom == prev) return;

		// 要素移動があれば leave と enter イベント生成
		var leave = get_par_elements(prev);

		// 重複要素を除去
		while(leave.length && enter.length
		   && leave[leave.length -1] == enter[enter.length -1]) {
			leave.pop();
			enter.pop();
		}

		// イベント発火
		var e_leave = make_mouse_event('mouseleave', evt, touch);
		var e_enter = make_mouse_event('mouseenter', evt, touch);
		$(leave).trigger( e_leave );
		$(leave).trigger( e_enter );

		// 新しい要素を保存
		prev=dom;
	});
}
//////////////////////////////////////////////////////////////////////////////
});