主にmod_deflateモジュールによる圧縮転送が利用できないサーバで圧縮転送を行なうためのスクリプトです。
対象ファイルがリクエストされた際にPHPスクリプトでGZIP圧縮して送信しますが、一度圧縮したファイルはサーバ上にキャッシュファイルとして保存し2度目以降のリクエスト時にはキャッシュを利用しますので、mod_deflateでの圧縮負荷を抑えたい場合にも利用できるかと思います。
本スクリプトで圧縮可能なデータは静的ファイルのみで、CMS等で動的に出力されるHTMLデータは対象外となります。
設置方法
gzip圧縮転送したいファイルのあるディレクトリにgzenc.phpを置き、同階層の .htaccess に以下を追記してください。
.htaccessへの追記内容
#BEGIN gzenc.php
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} ^.*\.(html?|css|js|xml|txt)$ [NC]
RewriteRule ^.*$ gzenc.php?f=$0 [L]
</IfModule>
#END gzenc.php
mod_rewriteが利用できることが前提になります。
指定した拡張子のファイルに対してアクセスがあった際、URLを変えずそのファイル名をgzenc.phpへ渡すよう設定しています。
gzenc.php
<?php
// 有効な拡張子とcontent-typeのリスト
// .htaccessへのRewriteCond %{REQUEST_URI}設定とペアになるよう指定
$enabledExtensions = [
'html' => 'text/html',
'htm' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'xml' => 'application/xml',
'txt' => 'text/plain',
];
// レスポンスへのETag出力及びリクエストのIf-None-Matchとの照合
define('USE_ETAG', false); // 使用:true / 非使用:false
// Accept-Encodingを確認しての圧縮可否判断(falseの場合はAccept-Encodingに関わらず常に圧縮)
define('CHECK_ACCEPT_ENCODING', true); // 使用:true / 非使用:false
// GZIP圧縮レベル
define('COMPRESS_LEVEL', -1); // -1(デフォルト), 0(非圧縮), 1(低圧縮低負荷) ~ 9(高圧縮高負荷)
/**
*
*/
// キャッシュ保存ディレクトリ
define('CACHE_DIR', __DIR__. '/gzenc_cache');
// キャッシュ保存ディレクトリが無ければ作成してダミーhtml作成
if(!file_exists(CACHE_DIR)) {
mkdir(CACHE_DIR, 0700);
touch(CACHE_DIR. '/index.html');
}
// mod_rewriteでのリダイレクトではなくスクリプト名のURLで直にリクエストされた場合は終了
if(isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) {
if(strpos($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']) === 0) notFoundExit();
}
// ファイル名取得
if(isset($_GET['f'])) {
$filePath = realpath($_GET['f']);
// ディレクトリトラバーサル対策
// 指定ファイルパスが本スクリプトのカレント配下でない場合は終了
if(strpos($filePath, __DIR__) !== 0) notFoundExit();
} else {
// ファイル名未指定時は終了
notFoundExit();
}
// 指定ファイルが存在しなければ終了
if(!file_exists($filePath)) notFoundExit();
// 指定ファイルの拡張子取得
$ext = strtolower(preg_replace('/.*\.(\w+)$/', "$1", $filePath));
// 取得した拡張子が有効リストに無ければ終了
if(!array_key_exists($ext, $enabledExtensions)) notFoundExit();
// リクエストヘッダAccept-Encodingにgzipが含まれていなければ元ファイルをそのまま渡して終了
if(CHECK_ACCEPT_ENCODING &&
in_array('gzip', preg_split('/,\s+/', filter_input(INPUT_SERVER, 'HTTP_ACCEPT_ENCODING'))) === false) {
header(
sprintf('Content-type: %s; charset=%s',
$enabledExtensions[$ext],
mb_detect_encoding(file_get_contents($filePath, false, NULL, 0, 2048), 'utf-8, sjis, euc, jis')
)
);
readfile($filePath);
exit;
}
// キャッシュファイルパス
$md5FilePath = md5($filePath);
$cacheFilePath = CACHE_DIR. '/'. $md5FilePath. '.gz';
$jsonFilePath = CACHE_DIR. '/'. $md5FilePath. '.json';
// キャッシュファイルの有無確認
if(file_exists($cacheFilePath)) {
// キャッシュより実体または本スクリプトのほうが新しい場合
// 実体または設定が更新されているとみなしキャッシュを削除
if(filemtime($cacheFilePath) < filemtime($filePath) ||
filemtime($cacheFilePath) < filemtime(__FILE__)
) {
unlink($cacheFilePath);
unlink($jsonFilePath);
}
}
// クライアントからのIf-None-Matchを取得
$ifNoneMatch = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');
// キャッシュファイルあり
if(file_exists($cacheFilePath)) {
// キャッシュに付随するプロパティファイルを取得
$properties = file_exists($jsonFilePath) ?
json_decode(file_get_contents($jsonFilePath), true) : [];
// ETagチェック
if(isset($properties['etag']))
checkEtag($ifNoneMatch, isset($properties['etag']) ? $properties['etag'] : '');
// レスポンスヘッダ出力
header('Content-type: '. $enabledExtensions[$ext].
(isset($properties['charset']) && $properties['charset'] ?
"; charset={$properties['charset']}" : '') );
header('Content-Encoding: gzip');
if(isset($properties['content_length']))
header('Content-Length: '. $properties['content_length']);
if(USE_ETAG && isset($properties['etag']))
header('Etag: '. $properties['etag']);
if(!USE_ETAG && isset($properties['last_modified']))
header('Last-Modified: '. $properties['last_modified']);
// データ出力
readfile($cacheFilePath);
}
// キャッシュファイル無し
else {
// ファイル読込み
$file = file_get_contents($filePath);
// エンコーディング取得
$enc = mb_detect_encoding($file, 'utf-8, sjis, euc, jis');
// gzencode
$file = gzencode($file, COMPRESS_LEVEL);
// キャッシュファイル保存
file_put_contents($cacheFilePath, $file);
$eTag = sprintf('"%s"', md5($file). '-gzip');
$lastModified = gmdate('D, d M Y H:i:s T', filemtime($filePath));
$contentLength = strlen($file);
// プロパティファイル保存
file_put_contents($jsonFilePath,
json_encode([
'charset' => $enc,
'etag' => $eTag,
'content_length' => $contentLength,
'last_modified' => $lastModified,
])
);
// ETagチェック
checkEtag($ifNoneMatch, $eTag);
// レスポンスヘッダ出力
header('Content-type: '. $enabledExtensions[$ext]. ($enc ? "; charset={$enc}" : '') );
header('Content-Encoding: gzip');
header('Content-Length: '. $contentLength);
if(USE_ETAG) header('Etag: '. $eTag);
if(!USE_ETAG) header('Last-Modified: '. $lastModified);
// データ出力
echo $file;
}
exit;
// ETagチェック
function checkEtag($ifNoneMatch, $eTag) {
if(USE_ETAG && $ifNoneMatch !== '' && $eTag !== '' && $ifNoneMatch === $eTag) {
// If-None-MatchとETagが共に空ではなく双方が同一なら304を返して終了
header('HTTP/1.1 304 Not Modified');
exit;
}
}
// NotFound
function notFoundExit() {
header('HTTP/1.1 404 Not Found');
print '<html><body>Not Found</body></html>';
exit;
}
キャッシュディレクトリ
実行するとスクリプトと同階層にキャッシュファイル保存用にgzenc_cacheディレクトリを作成します。
一度圧縮したコンテンツデータはその中へ保存して二度目以降のリクエスト時にはキャッシュファイルを利用します。
スクリプトの設定項目など
// レスポンスへのETag出力及びリクエストのIf-None-Matchとの照合
define('USE_ETAG', false); // 使用:true / 非使用:false
trueに設定するとETagを出力するようになります。
ETag込みのブラウザキャッシュは内容の変化に対しては追従させやすくなりますが、ブラウザは次回表示時にブラウザキャッシュの内容が更新されていないかサーバに対して問い合わせるプロセスが発生しますので、何度も呼ばれる割に更新頻度は低いといったファイルが多い場合はかえってもたつきを感じることもあるかもしれません。
// Accept-Encodingを確認しての圧縮可否判断(falseの場合はAccept-Encodingに関わらず常に圧縮)
define('CHECK_ACCEPT_ENCODING', true); // 使用:true / 非使用:false
通常はtrueのままで大丈夫です。
trueの場合はブラウザから受け取るAccept-Encodingの内容によって圧縮データと非圧縮データどちらを送信するかを振り分けます。
サーバ側でWAFを使用している場合など稀にAccept-Encodingが削除されている場合があり、そういった場合でも圧縮転送を行いたい場合はfalseにします。
falseにすることで強制的に圧縮したデータを送るようになりますが、非対応のごく古いブラウザなどでは表示できなくなります。
使用中止する場合
設置の際に .htaccessに追記した部分、及びgzenc.phpとキャッシュディレクトリgzenc_cacheを削除して下さい。
参考
mod_rewrite | Apache
Media Types | IANA
HTTPヘッダー | MDN
HTTPキャッシュ | MDN
ETag | MDN