PH社独自仕様Webサーバー(PD Web)

HTTPを用いた双方向通信を実現するために弊社が独自に仕様を定義したWebサーバーです。

PD Webの概要

  • PD Web はHTTPにおいて双方向通信を提供します。
    • 下流方向へのペイロード転送は、上流方向へのPOSTに対する応答メッセージとして提供されます。

    • 上流方向へ送るべきペイロードが存在しない場合、PD Repeaterは、 「受信ポーリング間隔」に指定される間隔で空接続を行います。

    • 下流方向へ送るべきペイロードが存在しない場合、Webサーバーは応答ヘッダーのみを返します。 PD Repeaterは応答メッセージが空でない場合、 push_toキーに指定される下流モジュールのUNIXドメインソケットにペイロードを転送します。

  • PD Web の認証方式は双方向のトークン認証です。
    • PD RepeaterとWebサーバーには、デバイス毎に指定されたIDと鍵(Key)の情報を持ちます。

    • PD Repeaterは、リクエストヘッダに記載するバージョン番号(PD Web仕様のバージョン番号)・ID・タイムスタンプ・ ペイロードのハッシュ値(MD5)から構成される署名対象文字列(リクエスト用)を鍵(Key)で著名した ハッシュ値(Shignature)をリクエスト用のトークンとします。

    • Webサーバーは、バージョン番号・ID・タイムスタンプ・ペイロードのハッシュ値(MD5)にリクエストヘッダの トークンを加えた署名対象文字列(応答用)を鍵(Key)で著名したハッシュ値(Shignature)を応答用のトークンとします。

    • 応答ヘッダーのShignatureもしくはペイロードのMD5が期待される値と一致しない場合、 以降、PD Repeaterは応答ヘッダーに期待されるShignatureが返されるまでペイロードを空とします。

    • Webサーバー一括のBASIC認証を併用することも可能です。

  • エラーハンドリング
    • PD Repeaterとwebサーバー間のエラーハンドリングはHTTPステータスコードのみで行われます。 ステータスが200~299以外の場合は、応答ヘッダーの内容に関わらず下流モジュールへのペイロードの転送は行われません。

  • ペイロードのフォーマットとトランザクション管理
    • 上流方向のペイロードのフォーマットはJSON文字列であり、 複数のメッセージをまとめて転送できるようメッセージの数に限らず最上位階層は配列となります。

    • PD Repeaterの下流方向のメッセージフォーマットは第2-5-2章に示す通りですが、 ペイロードのフォーマットは規定されていません。下流方向のペイロードのフォーマットは PD Repeaterからペイロードを受け取る下流のモジュールの仕様に依存します。

    • ペイロードのトランザクション(到達や再送処理)管理は、Webサーバーと下流のモジュール間で行われることを前提とし、 PD Repeaterはトランザクションの管理を行いません。

    • 弊社が提供するPD Handler ModbusとPD Agentはトランザクション管理用途に下流方向のペイロードの ハッシュ値(MD5)をJSON文字列 "reply_to" キーの値として返す仕様となっています。

PD WebのHTTPヘッダー

PD Webで規定されているHTTPヘッダーは次の通りです。

HTTPヘッダー

内容

X-Pd-Web-Version

PD Web の仕様のバージョン番号

X-Pd-Web-Id

クライアントのID

X-Pd-Web-Time

RFC3339準拠のタイムスタンプ

X-Pd-Web-Md5

ハッシュ値

X-Pd-Web-Signature

ヘッダ情報と鍵(Key)から作成されたハッシュ値

Content-Type

application/json;charset=UTF-8

PD Repeaterからの要求ヘッダーの例を示します。

Host: 127.0.01
Accept: */*
X-Pd-Web-Version: 1.0
X-Pd-Web-Id: pd_web_02
X-Pd-Web-Time: 2017-09-01T18:11:01.101+09:00
X-Pd-Web-Md5: 26b32b7bc6b4587c2ded48128e809b08
X-Pd-Web-Signature: c91279bc0d9896745e4e12e73917aafd56c017966a89375273ba4b9be8b02096
Content-Length: 190
Content-Type: application/json;charset=UTF-8

PD Repeaterへの応答ヘッダーの例を示します。

HTTP/1.1 200 OK
Date: Fri, 01 Sep 2017 08:34:35 GMT
Server Apache/2.4.27 (Unix)
X-Powered-By: PHP/5.6.31
X-Pd-Web-Version: 1.0
X-Pd-Web-Id: pd_web_03
X-Pd-Web-Time: 2017-09-01T17:34:35.000+09:00
X-Pd-Web-Md5: d41d8cd98f00b204e9800998ecf8427e
X-Pd-Web-Signature: b4e23876e866e9225e2f83cbd1df8b6387fe5da9e160b222953464b1ae87a9d3
Content-Length: 0
Content-Type: application/json;charset=UTF-8

PD Webのトークン

PD Repeaterで作成されるリクエストヘッダのトークン(X-Pd-Web-Sinature)は、 Webサーバーにおいて次のPHPスクリプトにより再生できます。

$hash_hmac_data =
    $_SERVER['HTTP_X_PD_WEB_VERSION'] .
    $_SERVER['HTTP_X_PD_WEB_ID'] .
    $_SERVER['HTTP_X_PD_WEB_TIME'] .
    $_SERVER['HTTP_X_PD_WEB_MD5'];
$signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);

ここで$keyは、は $_SERVER['HTTP X PD WEB ID'] と対をなす予めWebサーバーに保存された鍵(Key)となります。 再生した $signature と $_SERVER['HTTP X PD WEB SIGNATURE'] を比較することで認証します。

応答ヘッダーのトークン(X-Pd-Web-Sinature)は、Webサーバーにおいて次のPHPスクリプトにより作成します。

$tm = localtime();
$timestamp = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000+09:00",
    $tm[5]+1900,$tm[4]+1,$tm[3],$tm[2],$tm[1],$tm[0]);
$hash_hmac_data = '1.0' . $_SERVER['HTTP_X_PD_WEB_ID'] . $timestamp .
     md5($payload) . $_SERVER['HTTP_X_PD_WEB_SIGNATURE'];
$signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);

ここで、$payload は、ペイロードの文字列、送信すべき文字列(下流方向の制御メッセージ)が無い場合は、$payload=""; とします。 リクエストヘッダのトークンとは異なり、被署名文字列に PD Repeater から送られて来たリクエストヘッダのトークン($_SERVER['HTTP X PD WEB SIGNATURE'])が、含まれる点に注意して下さい。

Webサーバー(PHPスクリプト)の実装例

Webサーバー(PHPスクリプト)の実装例を示します。

<?php

/*
 * 本 PHP スクリプトは PD Repeater の PD Web を利用するための、
 * サーバ側 PHP スクリプトのサンプルです.
 *
 * 本スクリプトを動作させるためには、次の SQL構文で作成された SQLite3 データベース
 * (スクリプト中の $db_file)が必要となります.
 *
 *     CREATE TABLE client (id TEXT, key TEXT, flags INTEGER, payload BLOB, md5 TEXT);
 *     CREATE INDEX index_client ON client (id);
 *
 * ここで、id は、各センサーデバイス毎に設定する PD Web の ID, key はトークンを  
 * 作成するための鍵、contens は PD Repeater を介して各センサーデバイスへ送信する
 * JSON 文字列(ペイロード)、
 * md5 は JSON文字列のMD5ハッシュ値、flags はペイロードの送信状態を示すコードで
 * 0:送信済、1:処理中、2:未送信 を意味します.
 *
 * 以下に初期値の設定例を示します.
 *
 *     INSERT INTO client(id, key, flags, payload, md5)
 *                VALUES('id00', 'key00', 0, '{"any_key":"any_value"}',
 *                       'd29e8a13452e5bc5218d9df7e6ea991f');"
 *     INSERT INTO client(id, key, flags, payload, md5)
 *                VALUES('id01', 'key10', 0, '{"any_key":"any_value"}', 
 *                       'd29e8a13452e5bc5218d9df7e6ea991f');"
 *
 * ここで、d29e8a13452e5bc5218d9df7e6ea991 は、'{"any_key":"any_value"}' の MD5値です.
 *  Linux OS では、次のコマンドで取得することができます.
 *
 *     echo -n '{"any_key":"any_value"}' | md5sum
 *
 * ペイロードを送信するには SQLの UPDATEを用いて flags を 2:未送信 に変更します.
 * 
 *     UPDATE client SET flags = 2 WHERE id = 'id00';
 * 
 * 勿論ペイロードとMD5値を合わせてセットすることも可能です.
 *
 *     UPDATE client
 *                SET flags = 2,
 *                    payload = '{"any_key":"new_value"}',
 *                    md5 = '94f030ebd7bed4a5ee08fc6fa75ae64e'
 *                WHERE id = 'id00';
 *
 * 送信を終えるを PHP スクリプトは flag を 1 に更新し、'reply_to' キーを含む
 * 応答ペイロードを待ちます.
 *
 * 応答ペイロードの例
 *
 *      {"reply_to":"94f030ebd7bed4a5ee08fc6fa75ae64e","result":"done"}
 *
 * 応答ペイロードは PD Repeater を介してペイロードを受け取る各センサーデバイス
 * (のハンドラ)がPD Repeater 介して返すものです.
 * PD Agent や PD Handler Modbus は 'reply_to' キーで payload の MD5値をハンドリング
 * しますが、独自のハンドラを用いる場合は、独自の応答ペイロードとすることも可能です.
 *
 * 応答ペイロード受け取ると PHP スクリプトは、データベース上の md5 と reply_to の値を比較し、
 * flag を 0 に更新します.
 * 受信ペイロードに応答ペイロードが含まれていない場合、PHP スクリプトは flag を 2 に戻し、
 * 再送します.
 *
 */
    $dump_file = '/tmp/dump.txt';
    $db_file = '/tmp/pd_web.db';

    /* HTTP ヘッダーの確認 */
    if (!(isset($_SERVER['HTTP_X_PD_WEB_VERSION']) &&
          isset($_SERVER['HTTP_X_PD_WEB_ID']) &&
          isset($_SERVER['HTTP_X_PD_WEB_TIME']) &&
          isset($_SERVER['HTTP_X_PD_WEB_MD5']) &&
          isset($_SERVER['HTTP_X_PD_WEB_SIGNATURE']))) {
        /* HTTPヘッダーが不正な場合 Bad Request 400 を返す. */
        http_response_code (400);
        exit;
    }

    /* SQLite3 データーベースの読み込み */

    /* 本コードはサンプルであるため、SQL インジェクション対策を省略しています.
       実運用に利用するためには $_SERVER['HTTP_X_PD_WEB_ID'] のバリデーション
       を十分行って下さい. */

    $db = new SQLite3($db_file);
    $query = sprintf("SELECT key, flags, payload, md5 FROM client WHERE id = '%s';",
        $_SERVER['HTTP_X_PD_WEB_ID']);
    $results = $db->query($query);

    if(! $results) {
        /* HTTP_X_PD_WEB_ID が存在しない場合は Unauthorized 401 を返す. */
        http_response_code (401);
        $db->close();
        exit;
    }
    else {
        $row = $results->fetchArray();
        $key = $row['key'];
        $flags = $row['flags'];
        $payload_tx = $row['payload'];
        $md5_tx = $row['md5'];
    }

    /* HTTP リクエストヘッダ内の所定の文字列とデータベース上の key を用い
       signature を作成する */

    $hash_hmac_data = 
        $_SERVER['HTTP_X_PD_WEB_VERSION'] .
        $_SERVER['HTTP_X_PD_WEB_ID'] .
        $_SERVER['HTTP_X_PD_WEB_TIME'] .
        $_SERVER['HTTP_X_PD_WEB_MD5'];

    $signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);

    /* HTTP 応答ヘッダ共通部の設定 */
    date_default_timezone_set('Asia/Tokyo');
    $tm = localtime();
    $timestamp = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000+09:00",
        $tm[5]+1900,$tm[4]+1,$tm[3],$tm[2],$tm[1],$tm[0]);

    header('Pd_Web_Version: 1.0'); 
    header('Pd_Web_Id: ' . $_SERVER['HTTP_X_PD_WEB_ID']); 
    header('Pd_Web_Time: ' . $timestamp); 
    header('Content-Type: application/json;charset=UTF-8'); 

    /* リクエストヘッダの被署名文字列が VERSION.ID.TIME.MD5 で構成されているのに対し、
           応答ヘッダの被署名文字列は、VERSION.ID.TIME.MD5.SIGNATURE 
       (SIGNATUREは、リクエストヘッダに含まれる文字列) で構成されている点に注意 */

    if ($signature != $_SERVER['HTTP_X_PD_WEB_SIGNATURE']) {
        /* HTTP_X_PD_WEB_SIGNATURE と signature が一致しない場合は、
           401 Unauthorized を返す. */
        header('Pd_Web_Md5: ' . md5('')); 
        $hash_hmac_data = '1.0' . $_SERVER['HTTP_X_PD_WEB_ID'] . $timestamp .
            md5('') . $_SERVER['HTTP_X_PD_WEB_SIGNATURE'];
        $signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);
        header('Pd_Web_Signature: ' . $signature); 
        http_response_code (401);
        $db->close();
        exit;
    }

    $payload = file_get_contents("php://input");

    if(md5($payload) != $_SERVER['HTTP_X_PD_WEB_MD5']) {
        /* payload の MD5値 と HTTP_X_PD_WEB_MD5 が一致しない場合は、
           406 Not Acceptable を返す. */
        header('Pd_Web_Md5: ' . md5('')); 
        $hash_hmac_data = '1.0' . $_SERVER['HTTP_X_PD_WEB_ID'] . $timestamp .
            md5('') . $_SERVER['HTTP_X_PD_WEB_SIGNATURE'];
        $signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);
        header('Pd_Web_Signature: ' . $signature); 
        http_response_code (406);
        $db->close();
        exit;
    }

    /* 受信ペイロードを HTTP リクエストヘッダと共に dump_file に格納する */
    $fp = fopen($dump_file, 'a+');
    fputs($fp, $_SERVER['HTTP_X_PD_WEB_VERSION']);
    fputs($fp, "\n");
    fputs($fp, $_SERVER['HTTP_X_PD_WEB_ID']);
    fputs($fp, "\n");
    fputs($fp, $_SERVER['HTTP_X_PD_WEB_TIME']);
    fputs($fp, "\n");
    fputs($fp, $_SERVER['HTTP_X_PD_WEB_MD5']);
    fputs($fp, "\n");
    fputs($fp, $_SERVER['HTTP_X_PD_WEB_SIGNATURE']);
    fputs($fp, "\n");
    fputs($fp, $_SERVER['CONTENT_LENGTH']);
    fputs($fp, "\n");
    fputs($fp, $payload);
    fputs($fp, "\n");
    fputs($fp, "\n");
    fclose( $fp );

    /* flags が 1 (処理中ペイロードあり) の場合、受信ペイロードから、
       'reply_to' キーの値を取得 */
    if($flags === 1) {
        $json = mb_convert_encoding(
            $payload, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
        $array = json_decode($json,true);
        if ($array !== NULL) {
            /* 受信ペイロードは、JSONオブジェクトの配列です. */
            $payload_count = count($array);
            for($i=0;$i<$payload_count;$i++) {
                if (isset($array[$i]['reply_to'])) {
                    /* 'reply_to' キーの値と md5_tx を比較 */
                    if($array[$i]['reply_to'] === $md5_tx) {
                           /* 一致している場合は flags を 0 にする */
                        $flags = 0;
                    }
                    else {
                           /* 一致していない場合は flags を 2 にする */
                        $flags = 2;
                    }
                }
                break;
            }
            if($i == $payload_count) {
                /* 'reply_to' キーが存在しない場合は flags を 2 にする */
                $flags = 2;
            }
        }
        else {
              /* JSON文字列で無い場合、flags を 2 にする */
            $flags = 2;
        }

        /* データベースの flags を更新する. */
        $query = sprintf("UPDATE client SET flags = %d WHERE id = '%s';" ,
            $flags, $_SERVER['HTTP_X_PD_WEB_ID']);
        $results = $db->query($query);
    }

    if($flags == 2) {
        /* flags が 2 (送信ペイロードあり) の場合は、payload_tx を送り、200 OK を返す. */
        header('Pd_Web_Md5: ' . md5($payload_tx));
        $hash_hmac_data = '1.0' . $_SERVER['HTTP_X_PD_WEB_ID'] . $timestamp .
            md5($payload_tx) . $_SERVER['HTTP_X_PD_WEB_SIGNATURE'];
        $signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);
        header('Pd_Web_Signature: ' . $signature); 
        echo $payload_tx;
        http_response_code (200);

        /* flags を 1 (処理中ペイロードあり) に変更し、データベースの flags を更新する. */
        $flags = 1;
        $query = sprintf("UPDATE client SET flags = %d WHERE id = '%s';" ,
            $flags, $_SERVER['HTTP_X_PD_WEB_ID']);
        $results = $db->query($query);
    }
    else {
        /* 200 OK を返す. */
        header('Pd_Web_Md5: ' . md5('')); 
        $hash_hmac_data = '1.0' . $_SERVER['HTTP_X_PD_WEB_ID'] . $timestamp .
            md5('') . $_SERVER['HTTP_X_PD_WEB_SIGNATURE'];
        $signature = hash_hmac ('sha256', $hash_hmac_data, $key, false);
        header('Pd_Web_Signature: ' . $signature); 
        http_response_code (200);
    }

    $db->close();
    exit;
?>