端っこプログラマーの手帳

主にプログラムに関する手記です

httpd.conf と .htaccess でリライト設定が異なる

.htaccess リライト設定をそのままApache設定ファイル(VirtualHostディレクティブ)に書いても動かずハマったのでメモ。

.htaccessに設定

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php [QSA,L] 

RewriteCond でリクエストファイルが存在するか判定します。

判定 実行される処理
存在する RewriteRuleを通らず直接リクエストファイルを表示
存在しない RewriteRuleを通り index.php にリライトする

よく見るリライト設定です。 説明の便宜上 .htaccess は example.jp のドキュメントルート直下に置いてあることにします。 ドキュメントルートは /var/www/html で /var/www/html/.htaccess にファイルがあるということになります。

VirtualHost ディレクティブに設定

同じように内容を VirtualHostディレクティブに設定します。 上と同じ動きになることを期待しますが、 「400 Bad Request」とエラーになってしまいます。

httpd.conf

<VirtualHost *:80>
    ServerName example.jp
    DocumentRoot /var/www/html

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php [QSA,L] 
</VirtualHost>

何が異なるのか?

リライトログを取って詳細の動きを追ってみました。 .htaccess と VirtualHostディレクティブ で異なる点は2点

REQUEST_FILENAME に入る値が違う

http://example.jp/hoge にアクセスすると

.htaccess   → /var/www/html/hoge
VirtualHost → /hoge

VirtualHostの場合はドキュメントルート部分が抜けます。 正確に動かすには、 %{DOCUMENT_ROOT} 部分を追加する必要があります。

RewriteRule のリライト先に / が必要かどうか

.htaccess   → RewriteRule のリライト先 index.php は .htaccess の置いてある階層の index.php と判定される。
VirtualHost → RewriteRule のリライト先 index.php は、ドキュメントルート直下にならずにエラーとなる

VirtualHost の場合は、/index.php と絶対指定に指定する必要があります。

上の2点を直すとこんな感じになります。 どうやら、VirtualHost 下に書くとドキュメントルートが基点にはならないようです。

httpd.conf

<VirtualHost *:80>
    ServerName example.jp
    DocumentRoot /var/www/html

    RewriteEngine On
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ /index.php [QSA,L] 
</VirtualHost>

ええぃ憶えるのめんどくさいぞぉ

こんなのすぐ忘れます。いつもApacheの設定をいじっている訳でもないですし。 曖昧な記憶から「あれっなんだたっけ」と思いだすオーバーヘッドも大きいです。

実は、Directoryディレクティブを使えば、.htaccess と同じ設定で書けます

こんな感じです。

httpd.conf

<VirtualHost *:80>
    ServerName example.jp
    DocumentRoot /var/www/html

    <Directory /var/www/html>
        RewriteEngine On
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule ^(.*)$ index.php [QSA,L] 
    </Directory>

</VirtualHost>

Apacheのドキュメントにも、上の設定での、.htaccess と Directoryディレクティブは等価とあります。

ところで、ディレクティブの書かれた .htaccess を /www/htdocs/example に置くことと、同じディレクティブを 主サーバ設定の Directory セクション > に書くことは 完全に等価です

Apache HTTP Server Tutorial: .htaccess files - Apache HTTP Server Version 2.4

Apache設定ファイルのリライト設定は、Directory [ドキュメントルート]に書く

と覚えておけば良いのではないでしょうか。これでコードを修正する手間などなくなります。

「Raspberry Pi + レンタルサーバー」で円滑に退社時間を伝える~おきがる伝言鳥~

Raspberry Piレンタルサーバーで、自宅に会社を出た時間を伝える仕組みを作りました。

プログラムは「作って楽しい、使って便利」となれば最高ですが、割と前者の自己満足で終わることが多かったりします。 しかし、今回は後者の便利な点が少し実現できたかなと思っているのでまとめてみます。

我が家のお話。会社から帰るとき自宅に「今から帰るよ~」と電話してます。なぜ電話かって、私の親は携帯を持っていないためメールという手段は持ち合わせいません。電話ってかける方はいいですけど、出る方は大変です。常に電話のあるお茶の間にいる訳じゃないですから。トイレにいっていたり、テレビドラマを見ていて盛り上がっているところかもしれません。

そこで、電話を使わずに「今から帰るよ」と伝える手段を考えてみました。思いついたのは、

スマホでWebページのボタンを押すと、自宅ラズベリーパイの7セグLEDに退社時間が点灯

という装置。名付けて「おきがる伝言鳥~msgbird~」
実は伝言じゃない気もするのですが、初めは退社時間だけでなく、文字でメッセージ通知もする予定でそのときの名残です。でなんで鳥なんだというと、そのままでは味気ないため、鳥のぬいぐるみにLEDをひっかけようと思ったからです。

構成図

f:id:kzhishu:20151103124503j:plain:w600

説明

  1. 退社時にスマホでサーバーにアクセスし退社ボタンを押す
  2. サーバーのJSONファイルに退社時間が書き込まれる
  3. ラズパイは、サーバー上のJSONファイルを監視(1分おきにリクエスト)
  4. 更新があったら退社時間を7セグLEDを点灯。圧電ブザーも鳴らす。

JSONファイルの監視は1分毎なので、点灯するまでブランクがある設計です。ただ、今回の目的は退社時間を伝えることなので、1分後でも問題ないと判断しました。あと、レンタルサーバーを経由するため、自宅のラズパイは外からアクセスできる必要はないです。(外からアクセスは自分の今の技術では難しそうなため断念しました。) 退社ボタン画面は、さくらのレンタルサーバー スタンダードに置きました。 自宅のラズパイは、RaspberryPi2です。言語は、Python3系を使いました。

後で思いついたのですが「node.js + websocket」を使えば、即反映とサーバーへの無駄なリクエストを無くすことができそうです。今後改良をしてみたいと思います。

3週間程使っていますが、安定して動作しています。

f:id:kzhishu:20151128004729j:plain:w600

コードはGitHubにおいてます。

おきがる伝言鳥 GitHub
GitHub - megatk/msgbird: 退社・メッセージ通知 「おきがる伝言鳥」のリポジトリ

client/  自宅ラズパイのPG
doc/    回路図
server/  サーバーに置くPG
README.md  

PHP monologをカスタマイズしてみた

PHPでプログラムを書くときにログをファイルに出力したいこともあると思います。
ゼロベースで仕組みを作るのはしんどいので、monologというライブラリを試してみました。しかし、出力できる情報が足りなかったため思い切ってカスタマイズしてみました。主に、GitHubmonologマニュアルを参考にしています。

Composerの導入

$ curl -s -S https://getcomposer.org/installer | php
$ mv composer.phar composer
$ composer --version
Composer version 1.0-dev (5ccaad92c19ac673435dbb2858ae20d14f34950d) …

まずはComposer本体(パッケージ管理ツール)をインストール。
後々楽なので、パスの通っているディレクトリで実行しました。
また、composer でコマンド実行したいため、リネームして.pharの部分を消します。
$ composer --version とコマンドを打ってみて確認。無事インストールできました。

プロジェクトにセットアップ

適当な場所にプロジェクトのディレクトリを作成し、composer.json というファイルを作成。このファイルに必要なライブラリをJSON形式で指定して、インストールします。

$ mkdir cstest
$ cd cstest
$ vim composer.json
{
    "require": {
        "monolog/monolog": "1.17.*"
    }
}
$ composer install  

バージョンは、https://packagist.org/ からmonolog を探して、最新の1.17系を指定しました。(1.17.* で1.17系の最新版になるようです)
※ composer.json の内容を書き換えたときは、$ composer update で更新します。

monologを試してみる

ドキュメントを参考にしてmonologを試してみます。StreamHandler でログをファイルに出力し、LineFormatter でフォーマットを指定しています。

<?php
require_once('vendor/autoload.php');

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;

$logging_path = 'log/error.log';
$log = new Logger('test');

// フォーマット
$output = "[%datetime%] %level_name%: %message% %context% %extra%\n";

// LineFormatterで出力フォーマット指定
$formatter = new LineFormatter($output);
// StreamHandlerでファイルにログを出力
$stream = new StreamHandler($logging_path, Logger::DEBUG);
$stream->setFormatter($formatter);
$log->pushHandler($stream);

// ログ出力 ex: [2015-10-03 18:35:29] INFO: debug message [] []
$log->addInfo('debug message!!');

monolog での不満点

さて、これで無事ファイルにログが出力されるのですが、「めでたしめでたし」という訳にはいきません。全く物足りないです。次のような情報もほしいです。

・配列やオブジェクトの内容の出力
・データ型の表示
・実行ファイルと行番号
・実行開始から経過時間

Processor 自由にフォーマットを作る

いろいろ調べてみたところ、Processor という付加情報を表示する仕組みがあるところに目を付けました。ユーザが自由に作成した関数を登録でき、その関数内で、$record['extra']という項目に値をセットすれば、フォーマットの %extra% の箇所にその値が出力されるようです。

<?php
$logger->pushProcessor(function ($record) {
    $record['extra']['dummy'] = 'Hello world!';

    return $record;
});

そして、$record の値をvar_dumpしてみた結果です。

array(7) {
  ["message"]=>
  string(13) "debug message"
  ["context"]=>
  array(0) {
  }
  ["level"]=>
  int(200)
  ["level_name"]=>
  string(4) "INFO"
  ["channel"]=>
  string(4) "test"
  ["datetime"]=>
  object(DateTime)#7 (3) {
    ["date"]=>
    string(26) "2015-10-03 19:03:59.766603"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(10) "Asia/Tokyo"
  }
  ["extra"]=>
  array(0) {
  }
}

最初の階層のキー名がすべてフォーマットのキー名と合致しているのがわかります。つまり、$record にキーを追加すればフォーマットを自由に増やすことができそうと予測できます。実際にやってみたところこれは当たっていました。

context を使い Processorに情報を渡す

あとは、Processorに登録した関数内に、ログに出力する情報を渡せればよいだけです。 ログ出力メソッドの第2引数を配列で指定すると、$record['context'] にその配列が入ります。

<?php
$context = array(
    'file' => 'ファイル名',
    'line' => '行数'
);
$log->addInfo('debug message!!', $context);

やりました。これで解決です。以下のようにログ出力用のラッパー関数を作ることで、様々な値がログに出力出来そうです。
(若干乱暴な気もしますが....)

<?php
require_once('vendor/autoload.php');

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;

$logging_path = 'log/error.log';
$log = new Logger('test');

// ★フォーマットに、file と line を追加
$output = "[%datetime%] %level_name% %message% %file% %line%\n";

$formatter = new LineFormatter($output);
$stream = new StreamHandler($logging_path, Logger::DEBUG);
$stream->setFormatter($formatter);
$log->pushHandler($stream);

$log->pushProcessor(function ($record) {
    $record['file'] = $record['context']['file'];
    $record['line'] = $record['context']['line'];
    return $record;
});

function debug($message, $depth=''){
    global $log;
    
    // 呼び出し元ファイルと行数
    $backtrace = debug_backtrace();
    // 指定の深さが存在しない場合は呼び出し元に
    $key = isset($backtrace[$depth]) ? $depth : 0;
    $file = $backtrace[$key]['file'];
    $line = $backtrace[$key]['line'];
    $context = array('file' => $file, 'line' => $line);
    // エラーレベルは一旦固定
    $log->addInfo($message, $context);
}

// ログ出力
debug('debug message!!');

他の情報も追加してみる

他の情報も追加してみます。例のごとくクラス化してみました。

PHP monologをカスタマイズ · GitHub

・ 配列やオブジェクトの内容の出力とデータ型の表示
受け取った値を var_dump を使い、ob_start()等を使い変数に格納させます。 var_dumpの値の改行が、ログ出力時有効にならないため焦りました。これは、以下の記事で解決できました。

http://hack.aipo.com/archives/12506/

・実行開始から経過時間
ミリ秒単位で出力します。開始はインスタンス生成のタイミングで、終了はデバックメソッド呼び出しです。

ログってただでさえ目がチカチカしてしまうので、綺麗に出るというのは結構大切なことだと思います。今後の開発で大いに役に立てたいと思います。

PHPでWunderlist API を叩いてみる

こんにちは~。kzhishuです。

早速なのですが、自分はタスク管理に、Wunderlistを使っています。
タスク登録を一部自動化してみたかったので、APIを調べました。
ドキュメントが簡潔に書かれているため、「英語無理~」という自分のような人間でも比較的簡単にできました。 TwitterとかでAPIに触れたことがあるという方はドキュメントをみてパッパッと料理できてしまうと思います。 ですが、日本語の情報が少ないので、まとめてみます。 なお、Wunderlistのアカウントは既に持っていることを前提に話を進めています。
Wunderlist ドキュメント

APIキーの取得

まずはAPIキーを取得のため、Wunderlistへのアプリーケーション登録が必要です。 「アプリーケーション登録」というと大げさに聞こえますが、アプリケーション名とコールバックURLを登録すればOKです。

Wunderlist開発者用ページ の MY APP > Create New App から以下の情報を入力しました。Name は適当に、App Url と Auth Callback URL を実行環境と合わせれば問題ないと思います。ちなみに、この2項目は、後から変更可能です。

Name
 wnuderlist util
Description
 未入力
App Icon
 未入力
App Url
 http://localhost
Auth Callback URL
 http://localhost/wunderlist

SAVEボタンを押した後、「Client Id」「Client Secret」が表示されます。
この2つがAPIでアクセスする際に必要になります。これをメモっておきましょう。

アクセストークンの取得

APIを使用するために、アクセストークンを取得します。 ドキュメントでのリクエスト手順をPHPで記述してみます。 流れは、

1、初回リクエスト(GET)
2、リダイレクト(code値の取得)
3、code値とAPIキー情報をPOSTでリクエスト
4、3のレスポンスでアクセストークンが返ってくる

といった具合。

<?php
// 1 設定の定義
$client_id = '/* Client Id */';
$client_secret = '/* Client Secret */';
$redirect_url = '/* Auth Callback URL */';
$state = '/* STATE */';    

if(!isset($_GET['code'])){
    // 2 初回のアクセス
    header("Location: https://www.wunderlist.com/oauth/authorize?redirect_uri={$redirect_url}&client_id={$client_id}&state={$state}");
    exit();
}
else{
    // 3 リダイレクト後 アクセストークンの取得
    $code = $_GET['code'];
    $rdstate = $_GET['state'];
    
    if($rdstate != $state){
        exit('stateの内容が違います');
    }
    
    $params = array(
        'client_id' => $client_id,
        'client_secret' => $client_secret,
        'code' => $code
    );
    
    $token_url = 'https://www.wunderlist.com/oauth/access_token';
    $context = array(
        "http" => array(
            "method"  => "POST",
            "content" => http_build_query($params)
        )
    );
    $response = file_get_contents($token_url, false, stream_context_create($context));
    $json = json_decode($response);
    $access_token = $json->access_token;
    /* 以下タスク取得などの処理が続きます */
}
?>

1 設定の定義
先ほど取得した「Client Id」「Client Secret」と登録した「Auth Callback URL」を変数で定義します。 state についてはCSRF対策のために必要なようです。 リダイレクト時の実行プログラムが正しいかどうかを保障するためと理解しました。「リクエスト前のstate」と「リダイレクト後のstate」が同一であるか調べることでその判定ができそうです。予測しづらい文字列を書いておけばよいかと思います。

2 初回のアクセス
client_id, redirect_url, stateをパラメータに https://www.wunderlist.com/oauth/authorize へGETリクエストしています。 初回の実行はここで終了なので、exit(); しています。 成功するとredirect_url に「code」と「state」の2つパラメータと共にリクエストが帰ってきます。このとき、redirect_url と登録した「Auth Callback URL」が異なる場合は失敗します。リダイレクトかどうかは、GETパラメータ「code」の有無で判断しました。

3 リダイレクト後 アクセストークンの取得
リダイレクトの「state」が正しいか検証した後、「code」の値を取得します。 その値を、client_id, client_secret と共に今度はPOSTで https://www.wunderlist.com/oauth/access_token にリクエストします。 処理には file_get_contents() を使いました。(file_get_contents() ってヘッダーつけたり、POSTでリクエストできるんですね) レスポンスはJSON形式で返って来るためそれを取得します。これでアクセストークンを取得できました。

APIを叩く(リスト取得とタスク登録)

これでやっと準備が整いました。ここからリストを取得したりタスクを登録したりというAPIを叩いていくのですが、リクエスト時に2つ決まりがあります。

・リクエストヘッダーにアクセストークンとクライアントIDをつける(X-Access-Token、X-Client-ID)
・パラメータはJSON形式で送る
(またリクエストヘッダーにも Content-Type: application/json; をつける)

以上を元に取得してみます。プログラムは、上の /* 以下タスク取得などの処理が続きます */の部分に書いていきます。まずはリストの取得。 $json に自分のWunderlist のリストがしっかりと含まれ無事成功しました。

<?php
    $header = array(
         "X-Client-ID: {$client_id}",
         "X-Access-Token: {$access_token}",
         "Content-Type: application/json; charset=utf-8",
    );
    
    $list_url = 'https://a.wunderlist.com/api/v1/lists';
    $context = array(
         "http" => array(
              "method"  => "GET",
              "header"  => implode("\r\n", $header)
         )
    );
    $response = file_get_contents($list_url, false, stream_context_create($context));
    $json = json_decode($response);
?>

リストが取得できれば、list_id がわかりますので、そのidを指定することで、タスクの登録ができます。一つ注意点として、パラメータを送る際は Content-Length: [文字数] をリクエストヘッダーに含める必要があるようです。これで無事にタスクを登録することができました。

<?php
    $data = json_encode($params);

    $header = array(
         "X-Client-ID: {$client_id}",
         "X-Access-Token: {$access_token}",
         "Content-Type: application/json; charset=utf-8",
         "Content-Length: ".strlen($data)
    );
    
    $task_url = 'https://a.wunderlist.com/api/v1/tasks';
    $params = array(
        'list_id' => '/* list_id */',
        'title' => 'テスト タスク'
    );
    
    $context = array(
         "http" => array(
              "method"  => "POST",
              "header"  => implode("\r\n", $header),
              "content" => $data
         )
    );
    $response = file_get_contents($task_url, false, stream_context_create($context));
    $json = json_decode($response);
?>

クラス化してみる

記述したコードを上から眺めていると、同じような処理が重複しており、頭がクラクラしてきますので、クラス化してリスト名を指定してタスクが登録できるようなサンプルコードを書いてみました。インスタンス生成後、URLとリクエストメソッドとパラメータを指定すればAPIを叩けるようになっています。 詳細は以下のGitHubにコードを載せておきました。これでだいぶスッキリです。
Wunderlist API · GitHub