Edited at

二度と使うか!共用ファーストサーバーの糞仕様

More than 3 years have passed since last update.

ファーストサーバーといえば、ファーストサーバ岡田良介社長、新幹線で全裸になって逮捕とかファーストサーバ障害、深刻化する大規模「データ消失」の事案で有名な会社ですが、この会社の提供する共用サーバーもなかなかぶっ飛んでました。

VPSやクラウドサービスが浸透した現代では驚くほど時代遅れ且つ特殊です。

何も知らない私がいつものノリでLaravelアプリをファーストサーバーにデプロイしたら、全然まともに動かず、すごく苦労させられました。

私と同じような苦労をする方が出ないことを願って、その原因と対策方法を紹介します。

このお話に出てくるサーバーはファーストサーバーの共用サーバーで、PHPのバージョンは5.3.19です。


なんでそうした?PHPのconfigureオプション

./configure --enable-cgi --with-config-file-path=/usr/local/lib/php5/cgi --with-config-file-scan-dir=/usr/local/lib/php5/cgi/additional --libdir=/usr/local/lib --disable-ipv6 --enable-mbstring --enable-mbregex --with-gd --with-zlib=/usr --with-zlib-dir=/usr --with-freetype-dir=/usr/local --with-mcrypt=/usr/local --without-pgsql --enable-libxml --with-libxml-dir=/usr/local --enable-xml --enable-dom --with-xsl --with-sqlite --with-mysql=/usr/local/mysql --with-pdo-mysql=/usr/local/mysql --with-jpeg-dir=/usr --with-png-dir=/usr --with-pcre-regex --with-openssl=/usr/local/ssl --with-curl=/usr/local --with-iconv=/usr/local --enable-json --enable-pdo --with-pdo-sqlite --without-pdo-odbc --enable-exif --disable-fileinfo --disable-filter --disable-posix --disable-xmlreader --disable-xmlwriter --without-pear --without-xmlrpc --with-mysql-sock=/var/run/mysql/mysql.sock

この中で注目していただきたいのは

--disable-filter --disable-xmlreader --disable-xmlwriter --without-xmlrpc

で、けっこう使う頻度の高いモジュールが無効化されています。

おかげで後述する色んな問題が発生するんです。


PHPのHashモジュールが無効化されている

Laravelでは認証機能でhash_hmacというPHP標準関数を使用しています。

これは普通にインストールしたPHPには初めから入っているHashモジュールというモジュールが提供している関数です。

が、なぜかファーストサーバーではHashモジュールを意図的に無効化してコンパイルしたPHPが走っています。

なので、Laravelの認証機能が動かない。。。

動くようにするために自前でhash_hmac関数を用意する必要がありました。

if (!function_exists('hash_hmac')) {

function hash_hmac ($algo, $data, $key) {
return md5($data . $key);
}
}

ご覧のとおり、実装はmd5を使った偽モノです。


Filterモジュールが無効化されている

Laravelがバリデーションで使用しているfilter_varfilter_input関数が使えなくされてます。

おかげで、これもPHPで実装する必要がありました。

ネットで「filter_var implement in php」などでググって見つけたコードをイジってつかいましたよ。。。

if(!defined('FILTER_VALIDATE_BOOLEAN')) define('FILTER_VALIDATE_BOOLEAN', 'FILTER_VALIDATE_BOOLEAN');

if(!defined('FILTER_VALIDATE_EMAIL')) define('FILTER_VALIDATE_EMAIL', 'FILTER_VALIDATE_EMAIL');
if(!defined('FILTER_VALIDATE_FLOAT')) define('FILTER_VALIDATE_FLOAT', 'FILTER_VALIDATE_FLOAT');
if(!defined('FILTER_VALIDATE_INT')) define('FILTER_VALIDATE_INT', 'FILTER_VALIDATE_INT');
if(!defined('FILTER_VALIDATE_REGEXP')) define('FILTER_VALIDATE_REGEXP', 'FILTER_VALIDATE_REGEXP');
if(!defined('FILTER_VALIDATE_URL')) define('FILTER_VALIDATE_URL', 'FILTER_VALIDATE_URL');

if(!defined('FILTER_SANITIZE_EMAIL')) define('FILTER_SANITIZE_EMAIL', 'FILTER_SANITIZE_EMAIL');
if(!defined('FILTER_SANITIZE_NUMBER_FLOAT')) define('FILTER_SANITIZE_NUMBER_FLOAT', 'FILTER_SANITIZE_NUMBER_FLOAT');
if(!defined('FILTER_FLAG_ALLOW_FRACTION')) define('FILTER_FLAG_ALLOW_FRACTION', 'FILTER_FLAG_ALLOW_FRACTION');
if(!defined('FILTER_SANITIZE_NUMBER_INT')) define('FILTER_SANITIZE_NUMBER_INT', 'FILTER_SANITIZE_NUMBER_INT');
if(!defined('FILTER_SANITIZE_STRING')) define('FILTER_SANITIZE_STRING', 'FILTER_SANITIZE_STRING');
if(!defined('FILTER_SANITIZE_SPECIAL_CHARS')) define('FILTER_SANITIZE_SPECIAL_CHARS', 'FILTER_SANITIZE_SPECIAL_CHARS');
if(!defined('FILTER_SANITIZE_URL')) define('FILTER_SANITIZE_URL', 'FILTER_SANITIZE_URL');
if(!defined('FILTER_UNSAFE_RAW')) define('FILTER_UNSAFE_RAW', 'FILTER_UNSAFE_RAW');

if(!defined('FILTER_DEFAULT')) define('FILTER_DEFAULT', FILTER_UNSAFE_RAW);

if(!defined('INPUT_GET')) define('INPUT_GET', 'INPUT_GET');
if(!defined('INPUT_POST')) define('INPUT_POST', 'INPUT_POST');

if(!function_exists('filter_input'))
{
function filter_input($type, $variable_name, $filter = FILTER_DEFAULT, $options = array())
{
return filter_input_wrapper($type, $variable_name, $filter, $options);
}
}

if(!function_exists('filter_var'))
{
function filter_var($variable, $filter = FILTER_DEFAULT, $options = array())
{
return filter_var_wrapper($variable, $filter, $options);
}
}

function filter_input_wrapper($type, $variable_name, $filter = FILTER_DEFAULT, $options = array())
{
$input = ($type === INPUT_GET) ? $_GET : $_POST;

return isset($input[$variable_name]) ? filter_var_wrapper($input[$variable_name], $filter, $options) : null;
}

function filter_var_wrapper($variable, $filter = FILTER_DEFAULT, $options = array())
{
switch($filter)
{
case FILTER_SANITIZE_EMAIL:
return preg_replace("/[^a-z0-9\!\#\$\%\&\'\*\+\-\/\=\?\^\_\`\{\|\}\~\@\.\[\]]/iu", '', $variable);
break;

case FILTER_SANITIZE_NUMBER_FLOAT:
$decimal = ($options == FILTER_FLAG_ALLOW_FRACTION) ? '\.' : '';
return preg_replace('@[^\d\+\-' . $decimal . ']@', '', $variable);
break;

case FILTER_SANITIZE_NUMBER_INT:
return preg_replace('@[^\d\+\-]@', '', $variable);
break;

case FILTER_SANITIZE_SPECIAL_CHARS:
return htmlspecialchars($variable, ENT_QUOTES, 'UTF-8', false);
break;

case FILTER_SANITIZE_STRING:
return strip_tags($variable);
break;

case FILTER_SANITIZE_URL:
return preg_replace("/[^a-z0-9\$\-\_\.\+\!\*\'\(\)\,\{\}\|\\\\^\~\[\]\`\<\>\#\%\"\;\/\?\:\@\&\=\.]/iu", '', $variable);
break;

case FILTER_VALIDATE_BOOLEAN:
$input = (is_string($variable)) ? trim(strtolower($variable)) : $variable;

if($input === 1 || $input === true)
return true;

return (bool)in_array($input, array('1', 'true', 'on', 'yes'));
break;

case FILTER_VALIDATE_EMAIL:
return (bool)preg_match('/\A[a-z0-9._()-]+@([a-z0-9]+\.)+[a-z]+\z/i', $variable);
break;

case FILTER_VALIDATE_FLOAT:
return (bool)is_numeric($variable);
break;

case FILTER_VALIDATE_INT:
if(!is_numeric($variable) || floor($variable) != $variable)
return false;

$min_range = (isset($options['options']['min_range'])) ? $options['options']['min_range'] : null;

if($min_range !== null && $variable < $min_range)
return false;

$max_range = (isset($options['options']['max_range'])) ? $options['options']['max_range'] : null;

if($max_range !== null && $variable > $max_range)
return false;

return true;
break;

case FILTER_VALIDATE_REGEXP:
return (bool)preg_match($options['options']['regexp'], $variable);
break;

case FILTER_VALIDATE_URL:
return (bool)preg_match('@^[a-z][\w-]+:(?:/{1,3})?[^\s()<>]+(\.[^\s()<>]+(/[^\s]*)?)?$@iu', $variable);
break;

case FILTER_UNSAFE_RAW:
case FILTER_DEFAULT:
default:
return $variable;
}
}

メールアドレスの正規表現は今回の案件特有のものですので真似しないように。


php.ini設定に癖がある

なんと言いましょうか、PHP4.3とかの時代によく見た設定です。

ディレクティブ

default_charset
EUC-JP

mbstring.internal_encoding
EUC-JP

mbstring.encoding_translation
On

magic_quotes_gpc
On

今どき、だいたいUTF-8ですし、ユーザーの入力値とかをPHPが余計なお世話で文字コード変換してくれるとトラブルの種になるので、以下の様にして無効にしました。

ini_set('default_charset', 'UTF-8');

ini_set('mbstring.internal_encoding', 'UTF-8');
ini_set('mbstring.encoding_translation', 0);
ini_set('magic_quotes_gpc', 0);

magic_quotes_gpcについてはini_setでは変更できないので、.htaccessでやるしか無いです。

php_flag magic_quotes_gpc off


phpinfo()が使えない

何の目的か、わざわざphpinfo()が動かないようになってます。

代わりにファーストサーバーの管理画面から見ないといけません。

おかげで、ちょっと設定を確認したい時にもいちいち管理画面にログインしないといけないです。


cronが使えない

バックアップバッチなんて走らせられません。DBのバックアップは管理画面から出来るし、FTP使えばファイルもバックアップ出来るでしょ?何が問題なの?というスタンスです。


プロセスが突然殺される

cronが使えないならPHPスクリプトをバックグラウンドで動作させて、その中で定期的にバックアップ処理を行おうとしました。

ところが、数時間もするとプロセスが勝手にkillされてしまいました。

ファーストサーバーのサポートスタッフ曰く、定期的に全プロセスを殺してるそうです。


恐ろしや。PHPのバージョンが勝手に上がる

PHPのバージョン管理についてに書いてあるとおり、あるタイミングになったらPHPが一斉にバージョンアップされます。

その後もしばらくは古いバージョンを使うように設定することは出来るようですが、それでも半年くらい経つと強制的にバージョンアップされます。

しかも、新しいバージョンのPHPもかなり癖のある仕様になっているでしょうから、かなり念入りに事前にテストを行っておく必要があります。


終わりに

この記事のタイトルはかなり感情的ですが、実際にこの案件に関わっていた期間はもっともっともっと汚い言葉を連呼していました。かなり抑え気味の丁寧なタイトルを付けたつもりです。

ファーストサーバと格闘していた期間を思い返せば、他では普通に動くプログラムがファーストサーバでは思い通りに動いてくれず落胆し、その原因の調査に時間を取られて焦りまくり、原因がファーストサーバの独自仕様だとわかった時に湧き上がる憎悪に打ち震えるというのを繰り返した、精神衛生上とてつもなく劣悪な状態に陥っていた時間でした。

もう二度と使いたくないし、使うことは無いと思います。