k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

Apache2.2.20でBasic認証しまくったら警告を吐いて止まらなくなった話

今時Basic認証かよ、などと思われそうですが、むしろ今時HTTPSでない認証はあり得ないので、明示的なログアウト/再ログインが必要のないケースであればBasic認証で問題ないんじゃないかというわけで。

非ブラウザのHTTPクライアントアプリケーションと連携して、Webサーバ側に用意した静的ファイルをダウンロードさせるという、サーバ側はほぼAPIのみのアプリケーションです。

しかし、Basic認証の仕様でデータベースを利用した認証を実装したところ、Apacheのエラーログに警告が出るわ出るわ…

[Tue Mar 04 08:55:49 2014] [notice] child pid 8141 exit signal Bus error (7)
[Tue Mar 04 08:55:50 2014] [notice] child pid 7778 exit signal Bus error (7)
[Tue Mar 04 08:55:50 2014] [notice] child pid 7861 exit signal Bus error (7)
[Tue Mar 04 08:55:52 2014] [notice] child pid 7794 exit signal Bus error (7)
[Tue Mar 04 08:56:10 2014] [notice] child pid 7981 exit signal Bus error (7)
[Tue Mar 04 08:58:12 2014] [notice] child pid 7986 exit signal Bus error (7)
[Tue Mar 04 08:58:13 2014] [notice] child pid 8172 exit signal Bus error (7)
[Tue Mar 04 08:58:14 2014] [notice] child pid 7884 exit signal Bus error (7)
[Tue Mar 04 08:58:15 2014] [notice] child pid 8158 exit signal Bus error (7)
[Tue Mar 04 08:58:15 2014] [notice] child pid 8602 exit signal Bus error (7)
[Tue Mar 04 08:58:17 2014] [notice] child pid 8176 exit signal Bus error (7)
[Tue Mar 04 08:58:43 2014] [notice] child pid 8219 exit signal Bus error (7)
[Tue Mar 04 08:58:58 2014] [notice] child pid 7857 exit signal Bus error (7)

動作環境は共用のいわゆるマネージドサーバなので、何が起きてるのかこちらで詳細に調査することもできません。

とりあえず、アプリケーション側で負担になりそうな処理を順々に改善(もしくは手抜き)していったところ、どうもBasic認証を高頻度で行っているのが原因だと判断せざるを得ない結果となりました。

やったこと

ファイルの内容自体は秘密性が要求されるものではないため、このような対処で済ませました。

まだ child pid *** exit signal Bus error (7) の警告が無くなったわけではありませんが、結果的にかなり減少しています。

ついでに output_handler=mb_output_handler にされていたので、これも無効化。(http_output=passだったので関係ないかも。でも気持ち悪いし初期設定でこんなの有効にしないで欲しいなぁ…)

上位プランへの移行も薦められたのですが、現状で何とか耐えられそうです。

不確かな情報ですが、Web上に同様の情報が少なかったので公開してみます。間違いだったらどなたか指摘してください。

参考にした記事

ついでにBasic認証部分のコードも適当に抜粋して載せておきます。(カスタマイズ済みSilexの利用コードだけですが…)

※SilexとPimpleのバージョンは1系なので、2系だとまた書き方変わってるかもしれません。

webapp.php

<?php

$app = include __DIR__ . '/../app.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

//-----------------------------------------------
// Basic認証 + 401 XMLレスポンス
//-----------------------------------------------
$app->authentication = $app->protect(function(Request $request, Application $app) {

    $username = $request->server->get('PHP_AUTH_USER');
    $password = $request->server->get('PHP_AUTH_PW');

    if (isset($username) && strlen($username) >= 1 && isset($password) && strlen($password) >= 1) {
        // ユーザー名とパスワードをバリデーションして、データベースからユーザー情報を取得する処理
        $app->user = $user;
        return;
    }

    $data = array(
        'results' => array(
            'status' => 401,
        ),
    );

    $headers = array(
        'WWW-Authenticate' => sprintf('Basic realm="Restricted Area@%s"', $request->getHttpHost()),
    );

    $rootElement = new \SimpleXMLElement('<results />');
    $rootElement->addChild('status', $data['results']['status']);

    return $app->xml($rootElement->asXML(), $data['results']['status'], $headers);

});

//-----------------------------------------------
// XMLの値をエンティティに変換
//-----------------------------------------------
$app->escapeXmlContent = $app->protect(function($value) {
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
});

//-----------------------------------------------
// XMLレスポンスを返す
//-----------------------------------------------
$app->xml = $app->protect(function($xml, $status, $headers) {
    $headers['Content-Type'] = 'text/xml';
    $headers['X-Content-Type-Options'] = 'nosniff';
    return new Response($xml, $status, $headers);
});

レスポンスのXMLを生成するために SimpleXML を使ってますが、SimpleXMLElement->addChild() はエスケープしてくれないので、こういう関数を定義しといた方がいいです。

あと $app->xml() での X-Content-Type-Options: nosniff と同様に $app->json() でも継承したメソッドで $response->headers->set('X-Content-Type-Options', 'nosniff'); ってやってます。

メソッド上書きじゃなくて After Middleware とか使った方がコードが分散しなくて良いかもしれません。

index.php

<?php
$app = include __DIR__ . DIRECTORY_SEPARATOR . 'webapp.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

//-----------------------------------------------
// ユーザー情報 XMLレスポンス
//-----------------------------------------------
$app->get('/user-info.xml',
    function(Application $app, Request $request)
{

    $data = array(
        'results' => array(
            'status' => 200,
        ),
    );

    $headers = array();

    $rootElement = new \SimpleXMLElement('<results />');
    $rootElement->addChild('status', $data['results']['status']);

    if ($app->user instanceof User) {
        $userElement = $rootElement->addChild('user');
        foreach ($app->user as $name => $value) {
            $userElement->addChild($name, $app->escapeXmlContent($value));
        }
    }

    return $app->xml($rootElement->asXML(), $data['results']['status'], $headers);

})
->before($app->authentication)
->bind('user-info');

$app->run();

こっちはBasic認証かますのに Before Middleware を使ってます。

正直なところ、Basic認証を止めてオレオレ認証に切り替えた方が手っ取り早いとは思うのですが、そうなるとクライアントアプリケーションのコードも変更してもらわないといけないので…。