セッションもどき

PHPで開発するとき、小さな組織のお客さんだと、
稀にセッションも使えないようなサーバで動作する
プログラムの実装を要求されることがあります。
そんなときに作った苦し紛れのセッションもどきスクリプトを以下に記します。

<?
if (file_exists("Blowfish.php")) {
    include_once "Blowfish.php";
}

////////////////////////////////////////////////////////////////////////////////
// 定数

// セッション ID を格納するクッキーのキー
define("SESS_KEY", "PHPSESSID");

// セッション ID の文字列長
define("SESS_ID_LEN", 32);

// セッションファイル格納ディレクトリパス
// 公開ディレクトリ以下に置く場合は推測されにくい名称を使うこと
define("SESS_DIR", "tC7h4DaIIOGFnOFkICeXD5Mu");

// セッションデータ暗号化用キー
define("SESS_CRYPT_KEY", "OSyzLnxLLk3iWErjaohljZELtQVIcDKT");

// セッションデータにおける文字列中の改行を示す文字列
define("SESS_STR_NL", "___NEW_LINE___");

// セッションデータ読み書き時のブロックサイズ
define("SESS_BLOCK_SIZE", 256);

// セッションタイムアウト(秒)
define("SESS_TIMEOUT", 900);

////////////////////////////////////////////////////////////////////////////////
// 変数

$___Session___errmsg = "";
$___Session___params = array();

////////////////////////////////////////////////////////////////////////////////
// 関数

/**
 * セッションにおけるエラーメッセージを取得、設定します。
 * @param string $msg エラーメッセージです。
 * @return string エラーメッセージです。
 */
function sessionError($msg = null) {
    global $___Session___errmsg;
    
    if ($msg == null) {
        return $___Session___errmsg;
    }
    else {
        $___Session___errmsg = $msg;
    }
}

/** 
 * セッションパラメタを取得、設定します。
 * 引数をひとつも渡さない場合はパラメタを配列で返します。
 * @param string $key 取得、設定対象のパラメタ名です。
 * @param mixed $value 設定する値です。
 * @return mixed 指定したパラメタ、もしくはパラメタを全て返します。
 */
function session($key = null, $value = null) {
    global $___Session___params;

    // キーが指定されなかった場合は配列ごと返す ================================
    if ($key == null) {
        return $___Session___params;
    }
    // キーが指定された場合は対応する値を扱う ==================================
    else {
        // 値が指定されなかった場合は対応する値を返す --------------------------
        if ($value === null) {
            return $___Session___params[$key];
        }
        // 値が指定された場合は設定する ----------------------------------------
        else {
            $___Session___params[$key] = $value;
        }
    }
}

/**
 * セッションを開始します。
 * @return boolean 成功した場合はtrue、失敗した場合はfalseです。
 */
function createSession() {
    $result  = true;
    $sess_id = "";

    // セッションファイルを格納するディレクトリが無ければ作成する。
    // 更にタイムアウトしたセッションのファイルを削除する。
    if (!file_exists(SESS_DIR)) {
        mkdir(SESS_DIR);
        fclose(fopen($path, "w"));
    }
    else {
        if (($dh = opendir(SESS_DIR)) !== false) {
            while (($elem = readdir($dh)) !== false) {
                if ($elem == "." || $elem == "..") {
                    continue;
                }
                $stat = stat(SESS_DIR . "/" . $elem);
                if (time() - $stat[8] > SESS_TIMEOUT) {
                    unlink(SESS_DIR . "/" . $elem);
                }
            }
        }
    }
    
    // セッション ID 取得
    do {
        // クッキーにセッション ID が設定されていない場合は生成し、
        // クッキーに設定
        if (empty($_COOKIE[SESS_KEY])) {
            $sess_id = getRandomString(SESS_ID_LEN);
            $_COOKIE[SESS_KEY] = $sess_id;
            setcookie(SESS_KEY, $sess_id);
            $result = true;
        }
        // クッキーにセッション ID が設定されている場合は取得
        // セッション ID の文字列長が不正な場合はクッキー変数から削除
        else {
            $sess_id = $_COOKIE[SESS_KEY];
            if (strlen($sess_id) != SESS_ID_LEN) {
                $_COOKIE[SESS_KEY] = "";
                $result = false;
            }
        }
    }
    while (!$result);

    // 値読込
    if ($result) {
        $path = SESS_DIR . "/" . $sess_id; // セッションファイルパス
        
        if (!file_exists($path)) {
            fclose(fopen($path, "w"));
        }
        else {
            $fh = fopen($path, "r");
            if ($fh !== false) {
                // 一定サイズずつ読みこんで復号化する。
                $buf  = "";
                if (class_exists("Crypt_Blowfish")) {
                    $bfish = new Crypt_Blowfish(SESS_CRYPT_KEY);
                    while (!feof($fh)) {
                        // パディングのために null 文字が詰められている可能
                        // 性があるため、念のため置換しておく
                        $buf .= str_replace(
                            "\0",
                            "",
                            $bfish->decrypt(fread($fh, SESS_BLOCK_SIZE))
                        );
                    }
                }
                else {
                    while (!feof($fh)) {
                        $buf .= fread($fh, SESS_BLOCK_SIZE);
                    }
                }
                
                fclose($fh);
                
                // 復号化したセッションデータをパラメタごとに分割する。
                $pos = 0;
                while (true) {
                    $nlPos = strpos($buf, "\n", $pos);
                    if ($nlPos === false) {
                        $line = substr($buf, $pos);
                        $pos  = -1;
                    }
                    else {
                        $line = substr($buf, $pos, $nlPos - $pos);
                        $pos  = $nlPos + 1;
                    }
                    
                    list($key, $value) = split("=", $line, 2);
                    session(
                        $key,
                        getSafeSessionParameter(unserialize($value), true)
                    );
                    
                    if ($pos == -1) {
                        break;
                    }
                }
                
                return true;
            }
            else {
                sessionError(
                    "セッションデータファイル[" . $sess_id . "]" .
                    "を読み込むことが出来ませんでした。"
                );
            }
        }
    }

    return false;
}

/**
 * セッションパラメタの内容をファイルに書き込みます。
 * @return boolean 書き込みに成功した場合はtrue、そうでなければfalseです。
 */
function commitSession() {
    $result = false;
    
    if (!empty($_COOKIE[SESS_KEY])) {
        $params = session();
        $buf    = "";

        // セッションパラメタをひとつの変数に突っ込む
        foreach (array_keys($params) as $key) {
            $buf .=
                "$key=" .
                serialize(getSafeSessionParameter($params[$key])) .
                "\n";
        }

        if (($fh = fopen(SESS_DIR . "/" . $_COOKIE[SESS_KEY], "w")) !== false) {
            // 既定バイト数ずつ暗号化して書き込む
            $pos = 0;
            if (class_exists("Crypt_Blowfish")) {
                $bfish = new Crypt_Blowfish(SESS_CRYPT_KEY);
                while (
                    ($block = substr($buf, $pos, SESS_BLOCK_SIZE)) !== false
                ) {
                    fwrite($fh, $bfish->encrypt($block));
                    $pos += SESS_BLOCK_SIZE;
                }
            }
            else {
                while (
                    ($block = substr($buf, $pos, SESS_BLOCK_SIZE)) !== false
                ) {
                    fwrite($fh, $block);
                    $pos += SESS_BLOCK_SIZE;
                }
            }
            
            fclose($fh);
            $result = true;
        }
    }

    return $result;
}

/**
 * セッションを破棄します。
 */
function destroySession() {
    $result = false;
    
    if (!empty($_COOKIE[SESS_KEY])) {
        $result = unlink(SESS_DIR . "/" . $_COOKIE[SESS_KEY]);
        setcookie(SESS_KEY, "", time() - 3600);
    }
    return $result;
}

/**
 * セッションファイルにデータを保存する際、文字列を安全な表現に変換します。
 * @param mixed $value 変換対象です。これが配列の場合、再帰的に処理します。
 * @param boolean $unsafe 安全な表現から元の表現に戻す場合、trueを指定します。
 * @return mixed 変換後の値です。
 */
function getSafeSessionParameter($value, $unsafe = false) {
    if (is_string($value)) {
        if ($unsafe) {
            return str_replace(SESS_STR_NL, "\n", $value);
        }
        else {
            return str_replace("\n", SESS_STR_NL, $value);
        }
    }
    else if (is_array($value)) {
        for ($i = 0; $i < count($value); $i++) {
            $value[$i] = getSafeSessionParameter($value[$i], $unsafe);
        }
        return $value;
    }
    else {
        return $value;
    }
}

/**
 * ランダムな文字列を返します。
 * @param integer $len 文字列長です。
 * @return string ランダムな文字列です。
 */
function getRandomString($len) {
    $str   = "";
    $chars = array(
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
        "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
        "u", "v", "w", "x", "y", "z",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
        "U", "V", "W", "X", "Y", "Z",
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
    );
    $charCount = count($chars);

    for ($i = 0; $i < $len; $i++) {
        $str .= $chars[mt_rand(0, $charCount - 1)];
    }

    return $str;
}
?>

このライブラリはセッションデータの暗号化にCrypt_Blowfishを使用します
(http://pear.php.net/package/Crypt_Blowfish)。
ダウンロードしてPHPが見つけられるところに置いておけばOKです。
ちなみに、Crypt_Blowfishがなくても一応動きますが、
データは平文で格納されてしまいます。
データファイルの格納パスがドキュメントルートより上ならばいいですが、
このライブラリが必要になるようなサーバだと、
ドキュメントルート下にしかファイルを置けないことが多いと思いますので、
Crypt_Blowfishは必要かと思います。



ソースコードの最初の方の行にある、
定数SESS_DIRとSESS_CRYPT_KEYには任意の値を設定して下さい。
SESS_DIRはセッションデータファイルの格納ディレクトリパスで、
ドキュメントルート下であればせめてランダムな文字列にしておくべきです
(「Options Indexes」みたいにファイル一覧が表示されないようになっていることは大前提)。
また、SESS_CRYPT_KEYは暗号化用のキーですので、
32文字のランダムな文字列を指定する必要があります。



セッションIDはクッキー経由のみなので、
携帯には使えません。
使いたい方は改造してみて下さい。

んで、使い方。

<?
// セッションを使い始めるときには必ず実行。
createSession();

// 値を取得。
$name = session("name");

// 値を設定。
if (strlen($name) > 50) {
    session("errmsg", "名前が長すぎです。");
    
    // セッションの処理が終わったらファイルに書き込む。
    commitSession();
    header("Location: http://hoge.com/top.php");
    exit();
}

// 必要がなくなったらファイルを削除。
destroySession();
?>

あんま使い道ないかも知れませんが、
もし使うときは自己責任で!

Darwin Streaming Server5.5.3/Axis211Aでストリーミング

あまり Darwin Streaming Serverの日本語情報がないので構築例を書いてみます。



Darwin Streaming Server 5.5.3(以下DSS)とネットワークカメラである Axis211A を使ってライブカメラの映像をストリーミング配信するための構築例を紹介します。

Axis211A はカメラ自体に動画配信機能を備えています。
しかし、多数の閲覧者がいる場合には DSS のようなストリーミングサーバ経由で配信を行う方が効率的です。
そのため、ここでは DSS がその動画を受信して配信する手順を説明します。

仕組み

DSS にはリレー機能というものがあります。
これは、他のサーバが配信している動画を受信し、他サーバに配信するというものです。
Axis211A は RTSP 経由で MPEG4 動画を配信することが出来るので、受信は RTSP 経由となります。
DSS は RTSP 経由で動画を受信するために必要なメタファイル(SDP ファイル)を Axis211A から取得し、
動画の受信を開始します。
それに成功すると、Axis211A と同じように、
DSS は指定した場所にストリーミング用メタファイル(SDP ファイル)を作成します。
クライアントはそのファイルを取得することにより、
DSS からのストリーミング配信を受信出来るようになります。
クライアントは RTSP 経由で動画の受信が可能であれば DSS サーバ以外でもOKで、
Quick Time Player でも受信が可能です。

インストール

今回は Redhat Enterprise Linux 2.1 / Fedora Core 3 上で構築しました。
ソース一式を取得して、展開・ビルドします。
なぜかソース一式のアーカイブはtarでまとめられているだけで、
圧縮されていません。
以下の手順はrootで行うことが前提です。

# tar xvf DarwinStreamingSrvr5.5.3-Source.tar
# cd DarwinStreamingSrvr5.5.3-Source
# ./Buildit
# ./Install

以上で完了です。

設定

まず DSS 側の設定です。
web 管理画面から「Relay Settings」→「New Relay」を選択し、リレー設定を新規作成します。
リレー設定新規作成のための情報入力画面に遷移したら、以下のとおり情報を入力します。

Relay Name任意の設定名
Statusチェックを入れる
Source Settings
Source Hostname or IP AddressAxis211A のIPアドレス
Mount Pointメタファイルの配置位置(Axis211A の場合は「/mpeg4/media.amp」)
Request incoming streamチェックを入れる
User NameAxis211A の管理ユーザ名
PasswordAxis211A の管理パスワード
Destination Settings
Hostname or IP Addressサーバのアドレス(通常は「127.0.0.1」でOK)
Announced UDPチェックを入れる
Mount Pointメタファイルの配置位置
User NameDSS の管理ユーザ名
PasswordDSS の管理パスワード

設定を保存すると、DSS は Axis211A に対してリレー接続を開始します。
接続に成功したかどうか確認するには「Relay Status」を選択し、リレー状況を表示させます。
その画面で新規作成した設定名のリレーが表示されており、かつ「Bytes Relayed」列の値が増加していればリレー成功です。

ストリーミングの受信

Quick Time Player で受信する例を次に示します。
DSS が稼動しているサーバのアドレスが192.168.20.80、「Destination Settings」の「Mount Point」が/media.sdpの場合、Quick Time Player で「rtsp://192.168.20.80/media.sdp」に接続すると受信できます。

叙情派ニュースクールハードコア

ブログを始めてみたはいいものの、
あまり僕は筆が回る方ではないためになかなか更新出来ずにいます・・・。
しかしせっかくブログを開設したので、
このままではもったいない!
ということで、今回は叙情派ニュースクールハードコアの音源をいくつか紹介したいと思います。

The Underdark/Funeral Diner

Underdark

Underdark

US/San Franciscoのバンドです。

複雑な展開の上で静寂と轟音が絶叫が交錯し、
哀愁漂うメロディが流れる類稀なる傑作です!
テンポはゆっくりで、2ビートのような速い曲はありません。
全てのパートから繊細さを感じる演奏力の高いバンドですが、
個人的には静かな場所で鳴るドラムのフロアタムの音から、
壮大な世界感を感じました。
例えるならオーケストラにおけるティンパニのような。
最初聴いたときにはあまりピンとくる音ではなかったのですが、
聴けば聴くほど自分の中の評価が高くなってくる音源です!
ちなみにendzweckのDr.宇宙氏が運営しているレーベル、
Cosmic Noteからも音源をリリースしています。
また、もうすぐ来日するようなので、非常に楽しみです!

[来日ツアー日程]
http://www.funeraldiner.com/shows.html

Between Two Unseens/Taken

Between Two Unseens (Bonus Dvd)

Between Two Unseens (Bonus Dvd)

惜しまれつつも解散してしまった
叙情派ニュースクールハードコアバンドの雄、
US/Californiaのtakenによる最終音源です。
もうこれは1曲目「arrested impulse」からヤバイ!
曲名の如く抑えつけられた衝動を解放するかのように、
疾走感溢れる音をもってして、
このアルバムは幕を開けます。
叙情派ニュースクール特有のテンションノートを多用したギターのコード、
メロディに加え、感情を爆発させたようなボーカルの絶叫!
4曲目「eternity was on our lips」の長いシンガロングパートも感動モノです!
しかもこの音源にはライブの様子を収めたDVDもついています。
この手の音が好きな方は購入必須ですね。

From Here To Eternity/Envy

FROM HERE TO ETERNITY

FROM HERE TO ETERNITY

言わずと知れた日本を代表するハードコアバンド、
envyの1stフルアルバムです。
最近の音源はゆっくりな曲ばかりになりましたが、
この音源は速い、エモい、力強いの三拍子が揃っています。
もうこの時点でenvyの世界感の土台っていうのは築かれていたのかな、
という感じがしますね。
僕個人としては、ニュースクールというよりはオールドスクールな感じの
音質だと感じました(あくまで「僕個人」です)。

今回はこんなところでしょうか。

2007/01/20 駅弁@京王百貨店新宿店

土曜日に駅弁買いに新宿の京王百貨店行ってきました。
なぜかというと、23日まで全国の駅弁を売るイベントをやっていたためです。
結構すぐに売り切れてしまうという話だったので、 開店20分前に行きました。
が、既に行列が出来ていてびっくり!
駅弁という商品の性質故か、並んでいる人々の平均年齢は高めでした。
後述する「磯の鮑の片想い」と「ふくめし」が目玉だったらしく、
購入者の列がとんでもないことになっていました。


以下、駅弁毎の感想です。

1, 磯の鮑の片想い/\2,000(画像左)
我が地元、岩手県宮古市の駅弁です。
鮑まるごとひとつに、数の子、いくらがのっていました。
それにしても鮑、数の子がやわらかい!
東京で久々にうまい鮑を食べることができましたね。
味付けは別に工夫してる様子もなく、 素材勝負な感じでした。
開店後30分足らず売り切れているのには驚きました。

2, ふくめし/\1,800
福岡県・小倉駅の駅弁です。
ふくめしの「ふく」は「ふぐ」のことで、ふぐを使った駅弁ですね。
これはご飯にダシがきいており、さらに湯引きしたふぐも弾力があり、
総合的にとてもおいしかったです。
少し量が少なかったように思えますが、女性は十分かも。
これも開店して一時間もたたずに売り切れたんじゃないでしょうか。

3, 牛串弁当/\1,000
山形県米沢駅の駅弁です。
名前の通り、牛串が二本のっている弁当です。
牛肉なのでボリュームがあるのですが、
やっぱりこういう肉を使った弁当だと「あたたかい方がうまいかも」
と特に思いました。
それを差し引いても肉が柔らかく安価であることから、
満足度の高い弁当だと思いました。
この駅弁は容易に買うことが出来ました。

4, 峠の釜めし/\900
群馬県・横川駅の駅弁です。
これは写真をとる前に食べてしまったので、画像がありません・・・。
鶏肉を使った釜めしですね。
この駅弁は11時から販売開始だったので、
他三つを買ってから販売開始待ちの列に並びました。
名物駅弁だけあり、長蛇の列を築いていました。
味はなんというか、
インパクトのある味ではないのですがほっとする味です。
「あーこれ駅弁だわ」みたいな。
んー、語彙が未熟なのでうまく説明できません。
とにかくうまいです。