ラベル php の投稿を表示しています。 すべての投稿を表示
ラベル php の投稿を表示しています。 すべての投稿を表示

2024年11月18日月曜日

phpで文字列としての「\\n」以外の「\n」だけをLF文字(0x0A)に変換する方法

 キャリッジリターン(CR)の文字コード(0x0D)やラインフィード(LF)の文字コード(0x0A)から文字列としての"\r", "\n"に変換するのは特に問題はないと思います。

問題は逆方向で、\nという文字列をLFという文字に変換したいとき、str_replace('\\n',"\n",$text)みたいにやるとやっぱ恥ずかしいですよね。

\\nという文字列で、エスケープしたつもりのデータが埋もれているかもしれません。
\\nという文字列のつもりが0x5C(\),0x0A(LF)になってしまっては悲しいです。

そこで、とりあえず以下のようにするのはいかがでしょう。

  $text= preg_replace( '/(?<!\\\\)\\\\n/', "\n", $text );  

\マークだらけでうんざりしますね。phpで正規表現を扱う者への呪いです。

何がしたいのかというと、$text変数内に\\nという文字列でなく、\nという文字列があったらLF文字(0x0a)に変換するという祈りを込めています。

(?<!\\\\) というのは否定後読みといって\\\\というパターンがない場合にマッチします。phpでは\\\\というパターン\という「文字」にマッチします。呪いでしょう?

ついでに、\\\\nは\\\nと同じ意味を持ちます。まさに呪いです。

以上は前記事でデータベースをmysqlからsqlite3に移行した際に得たネタです。

何が恥ずかしいと言いますと、phpをたまに触って正規表現を使用するたびに\\\\のことをすっかり忘れているからです。

そこで、恥ずかしながら記録として残しておきたいと思います。とりあえずはデータベースのデータ内ではエスケープ文字としての\を特別扱いする必要はないので、この程度で申し訳ございません。

くだらない駄文をお読みいただいてありがとうございました。

2019年4月12日金曜日

Webアプリのユーザ認証でsaslauthdを利用してみる

まあ、別にWebアプリじゃなくてもいいんですけど、ユーザー名とパスワードが正しいかを判定する仕組みを実装するのって結構面倒くさいじゃないですか。

あるいは、すでにログインアカウント名に使われているのと同じアカウント名とパスワードを使いまわしている場合に、自分のアプリケーションでは設定を許さずに違ったパスワードじゃないとエラーにしたいとか。こういったケースでも入力されたユーザ名とパスワードが使われているかどうかの判定が必要になります。

ですが、アプリから/etc/passwdやら/etc/shadowを直接参照するなんて背筋が凍ります。それに、これ以外の認証機構にはまた別のコードを書かなくちゃなりません。

そこで、saslauthdにお願いして、ユーザ名とパスワードが正しいかを判定してもらっちゃおうというのが本記事の目論見です。

saslauthdの説明はそれこそweb上で検索すれば山ほど情報が出てきますが、実際にはほとんどpostfix+dovecotの設定の仕方ばっかりで意外に思われるかもしれませんが、postfixやdovecot以外からでも簡単に使えるんです。

使い方は単純。testsaslauthdコマンドだけでOK。
戻り値が0だったら認証が通ったという単純明快さです。
C言語で各種APIを使ったライブラリを作る必要もありません。

・・・と、言いたいところですが、困ったことにtestsaslauthdコマンドはパスワードを引数で指定しなければなりません。
これだと手動で打ったらシェルのhistoryにばっちり残ってしまいますし、psコマンドでパスワードが見えちゃいます。
極めて遺憾かつ慚愧の念に堪えません。

ですので、別の方法を採用したいと思います。
といっても、これまた単純明快です。

実は、saslauthdが用意してくれているunixドメインソケットにユーザ名とパスワードとサービス名とrealm名をのっけた電文を突っ込むだけで、そのユーザ名とパスワードが正当なものか判定した結果を返してくれます。
testsaslauthdコマンドも、実際にはこの方法を使って実装されています。

認証依頼電文そのものも超シンプル。
フォーマットは、
ユーザ名文字列長(ネットワークバイトオーダーでWORD)
ユーザ名文字列
パスワード文字列長(ネットワークバイトオーダーでWORD)
パスワード文字列
サービス名文字列長(ネットワークバイトオーダーでWORD)
サービス名文字列
realm名文字列長(ネットワークバイトオーダーでWORD)
realm名文字列
というシンプルさです。

結果応答電文も素っ気ないほどシンプル。
ユーザ名とパスワードが正しかった場合:
\000\002("OK"の長さ。ネットワークバイトオーダーでWORD)
OK(文字列。2byte)
だけ。正しくない場合はOKの代わりにNOとなり、その後に理由文字列がついてきます。

こんなに簡単なのだから利用しない手はありません。
そこで、webアプリと言えばみんな大好き(私は嫌い)なphpでユーザ認証関数を作ると以下のような感じになります。
関数の使い方は・・・見ればわかりますよね。例によってエラーチェックが長いですが、Saslauthd::auth()からtrueが返ってきたらユーザ名とパスワードが認証を通った、というだけの関数です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php
class Saslauthd
{
  static public function auth( $user, $pass, $service_name = "password-auth", $realm = "", $socket_name = "/run/saslauthd/mux", &$errorMessage = null )
  {
    if( $user === null || $user == "" || strlen( $user ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid user";
      }
      return false;
    }
    if( $pass === null || strlen( $pass ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid password";
      }
      return false;
    }
    if( $socket_name === null || $socket_name === "" ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid socket name";
      }
      return false;
    }
    if( $service_name === null || $service_name == "" || strlen( $service_name ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid service name";
      }
      return false;
    }
     
    if( $realm === null || strlen( $realm ) > 100){
      if( $errorMessage !== null ){
        $errorMessage = "invalid realm";
      }
      return false;
    }
 
    $sock = socket_create( AF_UNIX, SOCK_STREAM, 0 );
    if( $sock === false ){
      if( $errorMessage !== null ){
        $errorMessage = socket_strerror( socket_last_error() );
      }
      return false;
    }
     
    $ret = socket_connect( $sock, $socket_name );
    if( $ret === false ){
      if( $errorMessage !== null ){
        $errorMessage = socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
     
    $sendbuf = "";
     
    $sendbuf .= pack("n", strlen( $user )). $user ;
    $sendbuf .= pack("n", strlen( $pass )). $pass ;
    $sendbuf .= pack("n", strlen( $service_name )).$service_name;
    $sendbuf .= pack("n", strlen( $realm )).$realm;
 
    $ret = self::sockwrite( $sock, $sendbuf );
     
    if( true !== $ret ){
      socket_close( $sock );
      if( $errorMessage !== null ){
        $errorMessage = "socket_write error: ".$ret;
      }
      return false;
    }
    $readlenstr = socket_read( $sock, 2, PHP_BINARY_READ );
    if( $readlenstr === false ){
      if( $errorMessage !== null ){
        $errorMessage = "socket_read error: ".socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
    $readlen = unpack( "n", $readlenstr )[1];
    $ret = socket_read( $sock, $readlen, PHP_BINARY_READ );
    if( $ret === false ){
      if( $errorMessage !== null ){
        $errorMessage = "socket_read error: ".socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
    socket_close( $sock );
     
    if( $ret === "OK" ){
      return true;
    }
    if( $errorMessage !== null ){
      $errorMessage = $ret;
    }
    return false;
  }
  static private function sockwrite( $sock, $buf )
  {
    $spos = 0;
    $buflen = strlen( $buf );
    if( $buflen === 0 ){
      return true;
    }
    while( true ){
      $ret = socket_write( $sock, substr( $buf, $spos ) );
      if( $ret === false ){
        return socket_strerror( socket_last_error() );
      }
      $spos += $ret;
      if( $spos <= $buflen ){
        break;
      }
    }
    return true;
  }
}
?>
パスワードをhttpsを使わないで通信して全世界に公開したり連続飽和攻撃を考慮しないプログラムを作ったりして頭を抱えるようなことがありませんようにご注意ください。

お読みいただいてありがとうございました。

2016年1月10日日曜日

squirrelmailからrainloopへ乗り換えてみた

前置きです。

CentOS5ではimapクライアント用にsquirrelmailを利用させていただいてました。
かなり高機能で日本語対応も割合しっかりしていたし、カレンダープラグインもついていたので簡易グループウェア的な使い方も出来たので、かなり改造しまくって便利に利用させていただいていました。

しかし、悲しいかなphp4用なのでCentOS7では改修するかphp4をインストールしないと使えません。
ですが、php4を入れるのはまっぴらごめんですし、php5向けのバージョンもリリースされてはいるのですが、これがまたなんとしたことか、日本語のメールの文字化けがひどい。

これを改修して、さらに以前のsquirrelmailの改造を反映させるとなると、ちょっと導入をためらいます。

それに、実際には東日本大震災の時の計画停電のため、まともにMXレコードの宛先としては運用できなくなってしまい、それ以来MXだけは宛先をgoogleに委託してしまったので、グループウェアとしての機能も不要になっていました。

加えて、euc-jpであるというのもネックです。
utf8にしておけば今後どんなヘボいブラウザが主流になったとしても切り捨てられることはまずありませんでしょうが、日本国内ではインターネット上で日本語といえばこれだ!!っていう位置を占めていたはずのISO規格のISO-2022-JP(要するにJISコード)も過去にIEにばっさり切り捨てられた実績があります。
したがって、ISO-2022-JP以上にマイナーな文字コードは避けたい。

現在は取引結果などの残しておきたい古いメールやroot宛てのメールなどが確認できればいいので、これを機に長年お世話になったsquirrelmailさんから卒業させていただき、新しいWebメールシステムを探すことにしました(いつgoogleさんがGoogle Appsを放り出すかわかりませんから)。

すると、同じくphpのRainLoopというWebメールシステムを知りました。

スバラシイ。実にスバラシイ。

zipファイルをダウンロードして、展開して、展開したディレクトリをapacheなどで公開するだけでインストール終了。
設定は、初期管理者id/パスワードがadmin/12345と定義されていて、これを用いてブラウザ上から設定するので、管理者idとパスワードを変更するまではHTTPサーバの起動前にallow先をlocalhostに限定するなどしたほうがいいと思います。

そのうえ動作も軽快、デザインもきれい。日本語も化けない・・・規格内のメールなら(*今日の本題。後述します)。

面白いのは、オンラインデモが用意されていて、どういうものなのかインストールせずに実際に触って試せるところ。
あと、FacebookとGoogleとTwitterとDropboxと統合できるんですけど、ソーシャルとか大嫌いだからやらない。やらないけど、興味がある人のほうが多いはずだからこれも一大特徴といっていいと思います。

大いに気に入ったので採用です。
コミュニティエディションはAGPLv3ですが、別ライセンスも用意されていて、商用利用にも配慮されているようです。

さて本題です。

先ほど日本語も化けないと書きましたが、これはウソではありません。

が、あくまで規格に則ったメールが化けません。
これは規格に則った正しい動作です。文句のつけようがありません。

しかし、日本って割と古い時代からインターネットが普及した国の一つ、かつ、漢字というアルファベットじゃ表せない文字を初期から持ち込んだりしたので、混迷状態にありました。
EBCDIC系のCCSIDとかJUNETコード(のちのISO-2022-JPとかJISとか呼ばれるコードの元祖)とかEUC-JPとかSJISとか経路上で最上位ビットが落ちるとか落ちないとか。
技術者の工夫の歴史でもありますが、まあ、今となっては迷走と言われてもしゃあないかなあ。

折角規格ができても誰も実装しないとかザラで、挙句に自分の使っているOSの文字コードで直接メールを送受信してしまうという快挙暴挙にでるメールクライアントがごく普通に使われちゃったりしちゃったりして(最近でもまだ見かけます。問答無用でUTF8とかね)。

で、当時はインターネットといえばJISコードを使え、ということになっていて、そいつを直接MIMEエンコードせずにSubjectやFrom,To欄に使ったりしてるメールが多かったりします。
MIMEが普及し始めたのって記憶では90年代くらいで(なぜかってーとその頃その手のプログラムの改修を受注して書いた覚えがあるから)、その間も直接mailコマンドでドドーンとMIME?ナニソレ?なメールが行き交っていたり。

で、そういうメールでは当然お作法が守られていません。
ヘッダにJISコードが直打ちなのはともかく、そもそもContent-Typeが定義されてない、されててもcharsetが定義されてないとかザラです。それでも日本国内で主に使われているメールクライアントの場合はほぼ間違いなく正しく表示してくれますので顕在化しにくいですが、それはバッドノウハウの賜物です。

で、今回利用させていただくことにしたRainLoopさんでは、こういった不届きものはもれなく化けます。
仕様を守っていませんし、仕方ないですね。

・・・というわけにはまいりませんので、ちょこっと修正します。
現在のコミュニティエディション ver 1.9.3.365を例に修正例をご紹介します。

まずヘッダです。
rainloop/v/1.9.3.365/app/libraries/MailSo/Mime/HeaderCollection.php: 272行目(Parse関数内)に下記を追記します。

if( stripos( $sRawHeaders, "Content-Type" ) == false ){
        if( strpos( $sRawHeaders, 033 ) != 0 ){
                $sRawHeaders = "Content-Type: text/plain; charset=\"ISO-2022-JP\"\r\n" .
 $sRawHeaders;
        }
}

これはContent-Typeがないメールでヘッダ内にESCコードがある奴はISO-2022-JPと決めうちしてしまうものです。乱暴ですねぇ。mb_detect_encodingを使用しないのは、ある程度実験したところ、まるで使い物にならなかったからです。直撃したほうがまだマシな程度に使い物になりませんでした。
いろんな国がありいろんな言語がありますから、本当に大変なんだろうなぁ、としみじみ思いました。

お次にボディです(ボデーといまだに言ってしまうので格好つけてみました)

rainloop/v/1.9.3.365/app/libraries/MailSo/Mail/Messages.php: 783行目(InitByFetchResponse関数内)に下記を追加します。
if( $sTextCharset != "iso-2022-jp" && strchr( $sText, 033 ) !== false ){
   $sTextCharset="iso-2022-jp";
   $sText = mb_convert_encoding( $sText, "utf8", "iso-2022-jp" );
}

これは、iso-2022-jpなcharsetじゃないのになんで本文がエスケープされてんだよオイ、という時に無理やりJISからUTF8に変換してしまう、まことに乱暴なコードです。

これで、前世紀から今世紀初頭のメールもめでたく現代においても読めるようになりました。

こんな記事、誰が必要とするのかわかりませんが。

どうでもいいですが、このrainloopのソースはきれいで読みやすかったです。
phpで書かれたコードに対して私がこんなことを感じるのは、これは大した褒め言葉ですとも。