読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

音楽ファイルや動画ファイルの取得にも認証を導入したときにぶつかった問題

Android HTML5

もう既に2年近く前、Android 2.3がまだ新しいと言われていた頃の話だが、当時は相当苦しんだので、検証記録として残してみる。

はじめに

とあるWebアプリケーションの開発を担当している人から、「Androidの実機だと音が再生されない。どうにも原因が分からないので助けて欲しい」という相談を受け、その辺の知識もほとんどないままにヘルプすることになった。
話を聞いてみると、以下の状況ということが分かった。

  • そのWebアプリケーションは、認証処理時に発行したトークンをクッキーに保存し、それをチェックすることで認証済みかどうかを確認する形のもの。
  • 開発時はPCのブラウザにUAを偽装するような拡張機能を入れて動作を見ながら開発している。
  • いざ実機で確認してみると‥
    • iPhoneでアクセスすると、期待通りに音楽ファイルが再生されて音が出る。
    • Android端末でアクセスすると、音が全く出ない。
      • ちなみに、確かメディア再生のHTMLタグがサポートされたのがAndroid 2.3からで、当時私が持っていた「Galaxy S」がAndroid 2.2からAndroid 2.3へのアップデートが提供された頃だった。

下準備

せっかくだからということで、今回、audioタグとvideoタグ、比較としてimgタグを使った検証をしようとテストプログラムを作っていたら、動画に関しては生半可な対応では再生されなかったので(どうやらHTTP 206(Partial Content)を使用するらしい)、Apache httpdのxsendfileモジュールを使用した。
導入に関しては、ここでの説明は手抜きするが、以下のサイトを参考にした。

ざっくり書くと、以下のような感じ。(CentOS 6.4での実行例)

# yum groupinstall "Development Tools"
# yum install httpd httpd-devel
# curl -o mod_xsendfile.c https://tn123.org/mod_xsendfile/mod_xsendfile.c
# apxs -cia mod_xsendfile-0.12/mod_xsendfile.c 
# vi /etc/httpd/conf.d/xsendfile.conf
<IfModule mod_xsendfile.c>
    XsendFile on
    XsendFilePath /var/www/html
</IfModule>
# service httpd configtest
# service httpd restart
  • ※「yum groupinstall "Development Tools"」は代わりに「yum install gcc」だけやればおそらく大丈夫だと思うが、何が足りないか検証するのも面倒なので(待て)。まぁ今回の主目的ではないからということで。
  • ※今回はXsendFilePathを公開ディレクトリにしているが、これは検証用なので、本来はDocumentRoot以外の直接アクセス不可能なディレクトリにすべきです、念のため。

認証無しのケースでの確認

「そもそも再生されない」っていうことだと困るので、まずは以下のプログラムで試す。

<?php

$user = '-';

$time = time();

$msg = array();
$msg[] = "HTML5Test[{$_SERVER['REQUEST_METHOD']}]";
$msg[] = "\"{$user}\"";
if (isset($_GET['file'], $_GET['type'])) {
    $size = filesize($_GET['file']);
    header("Content-Length: {$size}");
    header("Content-Type: {$_GET['type']}");
    header("X-Sendfile: {$_GET['file']}");

    $msg[] = "\"file={$_GET['file']}&type={$_GET['type']}\"";
} else {
    $msg[] = "\"file=view&type=text/html\"";
?>
<div>
    <img src="?file=image1.png&amp;type=image/png&amp;time=<?php echo $time;?>" />
</div>
<div>
    <audio src="?file=audio1.mp3&amp;type=audio/mpeg&amp;time=<?php echo $time;?>"
            autoplay="autoplay"
            controls="controls">
        audio not supported
    </audio>
</div>
<div>
    <video src="?file=video1.mp4&amp;type=video/mp4&amp;time=<?php echo $time;?>"
            autoplay="autoplay"
            controls="controls"
            width="320" height="240">
        video not supported
    </video>
</div>
<?php
}

$msg[] = "\"{$_SERVER['HTTP_USER_AGENT']}\"";
$msg[] = "\"{$_SERVER['REQUEST_URI']}\"";
trigger_error(implode(' ', $msg), E_USER_NOTICE);
?>

timeパラメータは、一応のキャッシュ対策。
これで、以下のケースを試してみる。

実際にアクセスすると、Apache httpdのエラーログに以下のようなログが記録される。(PCのGoogle Chromeでの出力例)

[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=view&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=image1.png&type=image/png" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=image1.png&type=image/png&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=audio1.mp3&type=audio/mpeg" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=audio1.mp3&type=audio/mpeg&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php

ちなみに、書いてあるIPアドレスはすべてプライベートアドレスなので、アタックしようとしても無駄よん、念のため(謎)。

オレオレ認証処理を追加して検証

今回、認証機構を作るのが目的ではないので、ユーザIDを入力してPOSTすると、それをセッションに保存しておき、そのデータの有無で認証済みかどうかを判定するという手抜きな仕組みで代用する。(PHPのセッションはクッキーに保存されたIDで識別する設定になっていて、今回の要件には合っているので)

<?php

session_start();
if (isset($_POST['logout'])) {
        unset($_SESSION['authorized']);
}
if (isset($_POST['username'])) {
        $_SESSION['authorized'] = $_POST['username'];
}
$user = isset($_SESSION['authorized']) ? $_SESSION['authorized'] : '-';

$time = time();

$msg = array();
$msg[] = "HTML5Test[{$_SERVER['REQUEST_METHOD']}]";
$msg[] = "\"{$user}\"";
if (isset($_SESSION['authorized'])) {
        if (isset($_GET['file'], $_GET['type'])) {
                $size = filesize($_GET['file']);
                header("Content-Length: {$size}");
                header("Content-Type: {$_GET['type']}");
                header("X-Sendfile: {$_GET['file']}");

                $msg[] = "\"file={$_GET['file']}&type={$_GET['type']}\"";
        } else {
                $msg[] = "\"file=view&type=text/html\"";
?>
<div>
        <img src="?file=image1.png&amp;type=image/png&amp;time=<?php echo $time;?>" />
</div>
<div>
        <audio src="?file=audio1.mp3&amp;type=audio/mpeg&amp;time=<?php echo $time;?>"
                        autoplay="autoplay"
                        controls="controls">
                audio not supported
        </audio>
</div>
<div>
        <video src="?file=video1.mp4&amp;type=video/mp4&amp;time=<?php echo $time;?>"
                        autoplay="autoplay"
                        controls="controls"
                        width="320" height="240">
                video not supported
        </video>
</div>
<hr />
<form action="" method="post">
<input type="submit" name="logout" value="Logout" />
</form>
<?php
        }

} else {
        $msg[] = "\"file=login&type=text/html\"";
?>
<form action="" method="post">
<input type="text" name="username" /><br />
<input type="submit" value="Authz" />
</form>
<?php
}

$msg[] = "\"{$_SERVER['HTTP_USER_AGENT']}\"";
$msg[] = "\"{$_SERVER['REQUEST_URI']}\"";
trigger_error(implode(' ', $msg), E_USER_NOTICE);
?>

認証無しのケースから変わったのは以下。

  • 3〜10行目でオレオレ認証情報の取得などをしている。一応、ログアウト機能も実装。
  • 17行目で認証済みかどうかの条件判定
  • 46〜59行目でログアウトのためのフォームと認証のためのフォームを、それぞれの場合分けに従って表示。

これで先ほどと同じケースを試してみる。(今度は、すべてのケースでのログを、見やすいように余分な情報を除去して載せてみる)

HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php" in /var/www/html/test-auth.php on line 65
HTML5Test[POST] "hhelibex" "file=view&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=image1.png&type=image/png" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=image1.png&type=image/png&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=audio1.mp3&type=audio/mpeg" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/
HTML5Test[POST] "hhelibex-2.3" "file=view&type=text/html" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-2.3" "file=image1.png&type=image/png" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php?file=image1.png&type=image/png&time=1386308864" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "-" "file=login&type=text/html" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
  • Android 4.2(Galaxy S4)の標準ブラウザ
HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php" in /var/www/html/test-auth.php on line 65
HTML5Test[POST] "hhelibex-4.2" "file=view&type=text/html" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-4.2" "file=image1.png&type=image/png" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php?file=image1.png&type=image/png&time=1386309043" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-4.2" "file=audio1.mp3&type=audio/mpeg" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=audio1.mp3&type=audio/mpeg" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65

ログだけではちょっと分からないと思うが、以下のような動作をしている。

  • PCのGoogle Chromeでは、音声、動画ともに自動再生される。
  • Android 2.3では、音声は自動再生される(しようとする)が、動画は再生ボタンを押さないと再生されない。
  • Android 4.2では、音声、動画ともに自動再生される。
    • ただ、複数のメディアの同時再生はできないようで、音声再生中に動画の再生が始まると、音声の再生が一時停止するらしい。逆も同じ。

ちなみに、動画へのアクセスのログが多いのは、再生時に複数回に分けて分割して取得していたりするからだと思われる。

で、パッと見ただけでは分かりにくいかもしれないが、Android 2.3の音声ファイル取得のログが以下のようになっている。

HTML5Test[GET] "-" "file=login&type=text/html" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308864" in /var/www/html/test-auth.php on line 65

まず、ユーザ名が取得できていないことから、セッション識別に必要なクッキーが送られていないと推測される。「file=login」と出力されていることからも、ベースとなるHTML文書の取得とセッションを共有できていないことが分かる。
と、それ以前に、UAが「stagefright/1.1」となっていて、HTMLファイルや画像ファイルとは異なるUAによってアクセスされていることも分かる。
Android 4.2では「stagefright/1.2」が使用され、ユーザ名もちゃんと取れていることから、Android 2.3におけるバグなのかな、と推測される(Android 3.xの端末がないのでそこの検証はできないが‥)

おわりに

今回の動作検証では、手元にあった動画ファイルが大きすぎたので、以下のサイトにある動画素材を使わせてもらいました。