へぼいいいわけ このページをアンテナに追加 RSSフィード

2011年03月29日

PHPの出力をキャッシュして、ついでに圧縮してみる

コンストラクタキャッシュの準備を行って、「cacheメソッドが呼ばれたら実際にキャッシュするようにフラグを立てて、デストラクタでキャッシュするクラスを作ったら結構便利だったので公開します。

ソース

<?php
class Output
{
	const CLEVEL_DEFAULT = 5;
	private $cache_path = '';
	private $cache_timeout = 0;
	private $now_time = 0;
	private $compress_level = Output::CLEVEL_DEFAULT;

	public function __construct($match_path, $path = './cache', $clevel = Output::CLEVEL_DEFAULT)
	{
		$this->now_time = time();
		$this->cache_path = $path.'/'.substr(md5($match_path), 0, 10).'.cache';
		if((is_int($clevel) === false) || ($clevel < 0) || ($clevel > 9)){
			throw new Exception('圧縮レベルがおかしい', 1);
		}
		$this->compress_level = $clevel;

		if($this->cache_check() === true){
			// キャッシュ出力
			$this->cache_output();
			// コンストラクタ終了
			throw new Exception('キャッシュを出力したよ', 0);
		}
		// 出力のバッファリング開始
		ob_start();
	}

	public function __destruct()
	{
		if($this->cache_timeout > 0){
			// キャッシュ時間が設定されていた場合
			$this->cache_write();
		}
		// 表示
		ob_end_flush();
	}

	public function cache($timeout = 0)
	{
		$ret = false;
		if($timeout > 0){
			$this->cache_timeout = $timeout;
			$ret = true;
		}
		return $ret;
	}

	private function cache_check()
	{
		$ret = false;
		if(file_exists($this->cache_path)){
			if(filemtime($this->cache_path) > $this->now_time){
				// キャッシュファイルのタイムスタンプが未来だった場合
				$ret = true;
			}
		}
		return $ret;
	}

	private function cache_write()
	{
		$ret = false;
		// バッファに溜まったデータを取得
		$data = ob_get_contents();
		if($data !== false){
			$gzfp = gzopen($this->cache_path, 'wb'.$this->compress_level);
			gzwrite($gzfp, $data);
			gzclose($gzfp);
			$time = $this->now_time + $this->cache_timeout;
			touch($this->cache_path, $time, $time);
			$ret = true;
		}
		return $ret;
	}

	private function cache_output()
	{
		$ret = false;
		if(file_exists($this->cache_path)){
			readgzfile($this->cache_path);
			$ret = true;
		}
		return $ret;
	}
}
?>

適当に標準出力に垂れ流したデータを「Output::cacheメソッドを呼ぶ事で、指定した秒数だけファイルに圧縮してキャッシュすることが出来ます。


使い方

基本的な使い方としては、インスタンスを生成して、キャッシュしたくなったら「Output::cacheメソッドを呼ぶだけ。あとはデストラクタが勝手にキャッシュ処理を実行します。

キャッシュがすでにある場合はコンストラクタキャッシュを出力して例外を投げるので、適当に処理すること。


その1

※Outputクラスは読み込んであるものとします。

<?php
$out = null;
try {
	$out = new Output('hogehoge');
} catch(Exception $e){
	exit(0);
}
for($i = 0; $i < 10000; $i++){
	echo 'hogehogehogehoge', "\n";
}
$out->cache(60);
?>

その2
<?php
function hoge()
{
	$out = null;
	try {
		$out = new Output('hogefunc', '/hoge/huga/web/data', 9);
	} catch(Exception $e){
		return;
	}
	echo 'unkounkounko';
	$data = file_get_contents('http://d.hatena.ne.jp/heiwaboke/');
	if($data !== false){
		echo $data;
		$out->cache(60);
	}
	echo 'unkounkounko';
}

hoge();
hoge();
hoge();
?>

こんなものを作った理由

3週間くらい前の話ですが、Copipe2datというWEBサービスを作りました。その時に、今までフレームワークMFCくらいしか使った事がなかったので、有名どころのフレームワークを覚えてWEBサービスを量産できるようにしようと思い、CodeIgniterというPHPフレームワークを勉強してみました。

そのCodeIgniterですが、びっくりするくらい良く出来ていて複雑な決まりも無く、Copipe2datのようなものはリファレンスを一通り読むだけで簡単に作れてしまいました。コントローラの中で「$this->output->cache(3600);」とか呼ぶだけで全ての出力をキャッシュする機能が標準で付いていたり、フレームワークすげぇと感動してました。

でも、CodeIgniterにはパス中に「/」を含むパスをひとつの引数として受け取れない(必ず「/」で分解されてしまう)という、個人的に気に入らない点がありまして*1、その点を解消するためだけにCodeIgniterのクラス名&メソッド名を丸パクリしたクラス群を作成し、そこにCopipe2datを移植するという面倒な事をしました。

ただCodeIgniterで先に作ってしまったCopipe2datを少しの修正で動くようにするためだけに作ったため、中身スカスカのとてもフレームワークと呼べるようなものではないです。そんな中身スカスカの中でもOutputクラスだけは個人的に気に入ったので、公開してみました。おわり。


参考

*1:設定を弄れば対応できるかもしれないです。

2009年11月06日

PHPで過去にメールを送る

過去にメールを送る装置(スクリプト)を開発しました。

仕様上の問題で、12×3+αバイトまでしか送れず、さらに時間指定も一時間単位となっております。

ゲームのやり過ぎだろ!と思われるかもしれませんが、実際に送れてしまうのです。

とりあえず、ソースを公開しますので見てください。


ソース(PHP)

<?php
class dmail {
	private $version = '0.01';
	private $x_mailer = 'dmail';
	private $check = false;
	private $data;

	public function setData($from = '', $to = '', $date = 0)
	{
		$this->data = array();
		if(1 === preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+([\.][a-z0-9-]+)+$/i", $from)){
			$this->data['From'] = $from;
		} else {
			$this->check = false;
			return false;
		}
		if(1 === preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+([\.][a-z0-9-]+)+$/i", $to)){
			$this->data['To'] = $to;
		} else {
			$this->check = false;
			return false;
		}
		$date = intval($date);
		if($date > 0){
			$this->data['Date'] = date("r", time() - ($date * 3600));
		} else {
			$this->check = false;
			return false;
		}
		$this->data['Reply-To'] = $this->data['From'];
		$this->data['Return-Path'] = $this->data['From'];
		$this->data['MIME-Version'] = '1.0';
		$this->data['Content-Type'] = 'text/plain;charset=ISO-2022-JP';
		$this->check = true;
		return true;
	}

	public function send($data = '')
	{
		if($this->check !== true){
			return false;
		}
		$data = mb_convert_encoding($data, 'JIS', 'SJIS');
		$header = '';
		foreach($this->data as $key => $it){
			$header .= $key.': '.$it."\r\n";
		}
		$header .= 'Content-Transfer-Encoding: base64';
		$data = substr($data, 0, 36);
		$len = floor(strlen($data) / 12);
		for($i = 0; $i < $len; $i++){
			$str = substr($data, $i * 12, 12);
			mail(
				$this->data['To'],
				'=?ISO-2022-JP?B??=',
				base64_encode($str),
				$header,
				'-f '.$this->data['From']
			);
		}
		return true;
	}
}

$dmail = new dmail();
$dmail->setData('差出人@gmail.com', 'あて先@gmail.com', 100);
$dmail->send('てすと');
?>

このスクリプトを実行した場合、本文に「てすと」と書かれたメールが100時間前に「差出人」名義で「あて先」に届きます。

是非、試してみてください。

親しい人に送付して反応を楽しむと良いです。

もしかしたら悪用もできるかもしれません。例えば、待ち合わせ時間に寝坊したときに、1時間前くらいに連絡メールを入れた事にして罪を軽減したり…。


制限事項

現状では、ソフトバンクケータイしか過去に受信してくれません。

さすが、コラボケータイを売り出そうとしている企業だけのことがあります。


しくみ

メールの「Date」ヘッダーにごまかした時刻を設定して送付しています。

それだけです。


最後に

Steins;Gate」を終えて燃え尽きていたときに、いきなり2002年の日付で謎のメール(内容は普通のスパムメール)が届いて運命を感じたため作成しました。

過去にメールを遅れるのはソフトバンクだけ!

2009年09月26日

PHPのstrtotime関数のせいで酷い目にあった

PHPstrtotime関数のせいで酷い目にあいました。

その酷い目というのは、unkarで最近発生していた取得したスレッドのキャッシュが上手く残らない問題の事です。

原因が分かり対策が出来たため、分かったことをメモとして残しておきます。


原因・現象

64bit環境でPHPstrtotime関数が想定している動作をしなかったために起こりました。

64bit環境では、32bit環境のときにFALSEを返した処理でも、タイムスタンプを返してしまうことがあります。タイムスタンプも64bitになり、範囲が大きく広がったためです。

こんなコードがあったとします。

<?php
$t = strtotime($modmod);
if($t !== FALSE){
	$this->mod = $t;
} else {
	$this->mod = $modmod;
}
?>

「$modmod」には日付のような文字列が格納されています。

strtotime関数は日付のような文字列をタイムスタンプに変換します。日付文字列ではない場合「FALSE」が返るため、元々タイムスタンプだったという事にして、そのまま「$this->mod」に格納します。


unkarは9月11日にサーバを移転しました。今まで32bit環境だったのですが、この移転を機に思い切って64bit環境にしてみました。

unkarのメインな処理では、通信する際は全て「Client URL Library」を使用しているため、実は取得したデータの時間をタイムスタンプとして取得できているのですが、何を思ったのか、念のため、strtotime関数を通過させていました。つまり「$modmod」の中には元々タイムスタンプが格納されています。


32bit環境ではこれでも特に問題なく動作しました。渡された値が元々タイムスタンプなら、日付文字列として扱った際に32bitで取りえる範囲のタイムスタンプに変換できないからです。つまり、意味不明な日付が渡されたと判断されます。上のコードで言うと「FALSE」が返っていた為に、「$modmod」の値をそのまま使用していました。

ところが、64bit環境になると話は変わってきます。タイムスタンプの範囲が広がったために、タイムスタンプを日付文字列として変換すると別のタイムスタンプになってしまうことがあるのです。実際には西暦3000年とか9000年とかのイカレたタイムスタンプが返されるようになります。


unkarでは、取得したスレッドの更新時刻(最後に書き込まれた時間)を、保持しているスレッドキャッシュに設定するようにしています。そして、未来のタイムスタンプが設定されている場合、そのスレッドを取得しないように制御しています。こうすることで、dat落ちしているスレッドを何度も読みに行かないようにしています。

今回、スレッドの更新時間に壊れたタイムスタンプを設定してしまったため、実際にはdat落ちしていなくてもdat落ちしたスレッドと同等に扱われてしまい、キャッシュが上手く残らなくなりました。


実験

実は、ここまで実験もせずに脳内妄想のみで書いてしまったので、ここで実際に確認してみました。


実験用コード
<?php
$timestamp = 1245509920;            /* 適当な時間を設定 */
echo $timestamp."\n";
echo date('r', $timestamp)."\n";
$timestamp = strtotime($timestamp); /* 日付文字列として変換 */
echo $timestamp."\n";
echo date('r', $timestamp)."\n";
?>
出力結果
1245509920
Sat, 20 Jun 2009 23:58:40 +0900
250900947950
Sun, 26 Sep 9920 12:45:50 +0900

9920年になってしまっています。

実験は大成功(?)です。脳内妄想は正しかったようです。


いろいろ調べてみたところ、最後の4桁が西暦に変換され、先頭の6桁が2桁ずつ時間、分、秒に変換されるようです。(見れば分かりますね…)

最近のタイムスタンプ(例:1253970432)では「97秒」という、ありえない秒数が指定されたと思われて、64bit環境でも「FALSE」が返されるみたいです。時間帯によっては発生しないなんて酷いバグですね〜。


更新が止まったキャッシュについて

unkarでは、タイムスタンプがイカレたキャッシュが数万単位で存在すると思われます。依頼ごとに対応していたらキリがありません。そこで、西暦2020年を超えるようなキャッシュを強制的に今日の日付に書き換えるプログラムを作り、一気に直そうと思います。失敗したらごめんなさい。

2009年09月19日

クロスサイトスクリプティング(XSS)なんて縁の無い攻撃方法だと思っていました

Googleで「google:2ch/search.php」と検索をかけると出てくる「クロスサイトスクリプティング」という2ちゃんねるのスレッドで、unkarに対して攻撃のデモを行うURLが張られていました。

自分で実際に踏んでみて、面白おかしく動作しやがったので簡易XSS対策を施してみました。


張られていた攻撃デモ用URL

http://unkar.jp/2ch/search.php?q=%22%3E%3Cscript%3Ealert(%2FXSS%2F)%3C%2Fscript%3E


一応、外部からの入力には気を使っていたつもりなのですが、検索して一致するものが無い場合のみ、htmlentities関数を通した文字列を表示せず、生に近い入力を表示していました。

攻撃用の文字列が検索に引っかかるわけも無く、あっさりとXSSできるようでした。

外部入力を表示する必要がある場合は気をつけないといけないですね。

2009年08月22日

PHPで304 Not Modifiedを出力するとapacheのmod_deflateと喧嘩する

unkarで使用しているスクリプト全般を304 Not Modifiedに対応させました。

304 Not Modifiedを対応したことにより、ページが更新されていないにもかかわらず発生していた無駄な転送が今後発生しないようになります。

つまり、ページ表示が速くなるかもしれません。


自分の環境だけかもしれませんが、PHPに304 Not Modifiedを返す機能を実装した際にapacheのmod_deflateと競合して、HTTPヘッダーの前(?)に「0x1F」「0x8B」から始まる数バイトのデータが紛れ込むという事態が発生しました。

PHPに304 Not Modifiedを返す際に、しかも、なぜかfirefoxでしか発生していないようでしたが、PHPが304 Not Modifiedを返す際にmod_deflateを停止させれば発生しなくなるようです。

書いたスクリプト(PHP)

<?php
function getIfModifiedSince()
{
	if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
		$if_mod_sin = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
		if(strpos($if_mod_sin, ' GMT') === false){
			$if_mod_sin .= ' GMT';
		}
		$since = strtotime($if_mod_sin);
		if($since !== false){
			return $since;
		}
	}
	return 0;
}

function checkNotModified($mod = false)
{
	$since = getIfModifiedSince();
	if($mod && is_numeric($mod) && ($since >= $mod)){
		// modが「0」又は「false」ではない
		header('HTTP/1.1 304 Not Modified');
		// mod_deflateを無効化する
		apache_setenv('no-gzip', '1');
		// 出力バッファの削除
		ob_end_clean();
		// 終了
		exit(0);
	}
}

// 出力のバッファリング開始
ob_start();

/**********

なにかいろんな処理

更新時間「$mod」をセットしておく

**********/

checkNotModified($mod);

if($mod && is_numeric($mod)){
	// 最終更新時刻送付
	header('Last-Modified: '.gmdate('D, d M Y H:i:s', $mod).' GMT');
} else {
	header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
}

// 出力
ob_end_flush();

これで、たぶん大丈夫。