2019年7月26日金曜日

Domino の CORS 対応

CORS (Cross-Origin Resource Sharing)とは、ブラウザがHTMLを読み込んだサーバー(オリジン)以外のサーバーのデータへのアクセスを可能にするための仕組みのことです。

たとえば、node.js アプリを実行中のサーバーへブラウザでアクセスすると、アプリが HTML を返します。そのHTMLに書かれた JavaScript が(オリジンが異なる)Domino サーバー上のデータへ REST API でアクセスしようとします。この時 Domino 側で node.js アプリからのアクセスを許可する設定を行なっていない場合、ブラウザ側でブロックする、というものです。

実際このような構成で、何も対策していない Domino を使ってテストしてみたところ 401 Unauthorized となりました。
※上の画像でメソッドが GET でなく OPTIONS になっていますが、これはプリフライト(サーバーから対応するメソッドの一覧を収集すること)によるものと思います

今回テスト環境として用意した node.js 側のアプリ「app.js」と、そこから呼び出す「index.html」の内容は次のとおりです。

「app.js」の内容
const app = require('express')();
const http = require('http').Server(app);

http.listen(3000, () => console.log('Server started'))

app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html')
})


「index.html」の内容
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>10.0.1 FP2 CORS Test</title>
    <script type="text/javascript">
 function firstscript() {
      var url = 'http://localhost/statpub.nsf/api/data/collections';
      var req = new XMLHttpRequest();
      req.open( 'GET', url);
      req.onreadystatechange = function() {
        if(req.readyState === 4 && req.status === 200) {
          var element = document.getElementById('corstest');
          element.innerHTML = '<pre>' + req.responseText + '</pre>';
        }
      };
      var authBasic = window.btoa('testuser:password');
      req.setRequestHeader('Authorization', 'Basic ' + authBasic);
      req.send();
    }
    </script>
  </head>
  <body onload="firstscript();">
    <div id="corstest">test</div>
  </body>
</html>


node.js のアプリは ポート 3000番を指定しています。Domino の HTTP タスクはデフォルトのままですので 80 番が使用されます。
どちらも同じPC上で稼働しているため、ホストは localhost です。
しかしながらオリジンは「スキーム(http://)」、「ホスト(localhost)」、「ポート」の組み合わせですので node.js の http://localhost:3000 と Domino の http://localhost は同一オリジンとはなりません。


そこで、ブラウザが CORS によりブロックしないよう、Domino 側で CORS への対応を実施することが必要になります。

CORS への対応とは、許可する「オリジン」や「メソッド」等を設定することです。

Domino へアクセスした時、Domino はオリジンやメソッド等が許可されたものであるかを確認します。それらが許可しているものであれば、許可していることを示すヘッダー情報をブラウザへ返します。


さて、Domino のオンラインヘルプによれば、Domino 10.0.1 Fix Pack 2 以降で CORS に対応する、とあります。


実はそれまでのバージョンでも、Webサイトルール文書を作成することによって対応可能だったようです。
smaconne の利用マニュアルには「複数の IBM Domino サーバーを利用する場合のサーバー設定」として記載があります。※光安様、情報ありがとうございます。
また smaconne ではありませんが、Webサイトルール文書を使って設定する際の参考となりそうな情報はこちらにもありました。10.0.1 FP2 以前の Domino をご利用の場合は参考になるのではないでしょうか。

では FP2 ではどのようにして許可の設定を行うのでしょうか。

設定方法はこちらにありますが、次の3つの手順を踏みます。

  1. サーバー文書で CORS を有効にする
  2. CORS のルールを記述したファイル(ファイル名: cors-rules.json)を作成し、Domino\Data\Domino\CORS フォルダへ保存する
  3. HTTP タスクを再起動する

サーバー文書の設定は、Internet Protocols タブ - HTTP タブ - DSAPI のくくりにある 「DSAPI filter file name」に ncorsext を追加します。

手順の2番目で作成する json ファイルのサンプルはこちらにあります。

今回テスト環境用に作成した、CORS のルールを記述したファイル「cors-rules.json」の内容は次のとおりです。
{
  "version": "1.0",
  "rules": [
    {
      "resource": {
        "path": "/api/data/collections"
      },
      "allowOrigins": [ "http://localhost:3000" ],
      "allowMethods": [ "GET" ],
      "allowCredentials": true
    }
  ]
}


node.js アプリのオリジン「http://localhost:3000」から Domino の 「/api/data/collections」への GET 要求を許可しています。


CORS のルールが HTTP タスク起動時に正常にロードされると、コンソールに次のように表示されました。
2019/07/26 15:07:04   HTTP Server: DSAPI CORS Filter Loaded successfully

参考までに json ファイルに誤りがあった際にコンソールに表示されたメッセージがこちらです。
2019/07/25 20:45:11.36 cors::InitConfig> Unable to parse configuration C:\IBM\Domino\data\domino\cors\cors-rules.json
2019/07/25 20:45:11   HTTP Server: Failed to load DSAPI module ncorsext
※上で紹介しました「json ファイルのサンプル」のページに記載されている例には、不要なカンマが含まれており、そのままコピーするとロードに失敗します...

以上の設定を行い、http://localhost:3000 へアクセスしたところ、オリジンが異なる Domino (http://localhost)のデータにアクセスすることができ、取得したデータが表示されました。
左側が取得できた json を表示したもの。右側は取得した際のデバッグ情報

CORS に対応できたことが確認できました。


試しに CORS のルールで、allowOrigins のポート番号を変えたり、allowMethods を POST に変えたりしてみたところ、一致するルールが無いため 401 Unauthorized となりました。

なお allowMethods に "OPTIONS" も追加したほうが良いのかもしれませんが、今回の環境ではあってもなくても変化が見られませんでした。

2019年7月21日日曜日

おれおれ APM で Domino の統計情報をグラフ化

以前のエントリでも紹介しましたが、V10 では統計情報を New Relic というSaaSの監視サービスと連携する新機能があります。

実は、New Relic だけでなく、他の監視サービスとも連携できそうだということはオンラインヘルプにも書かれていたのでなんとなく知識としてはありましたが、これまで試したことはありませんでした。

ところで "New Relic" をグーグル先生に聞くと「APM」という単語が頻繁にヒットします。
この APM とは「アプリケーション性能管理」を指す一般的な用語らしいのですが、世の中には APM を謳うサービスが New Relic 以外にも多く存在することを最近知りました。

参考までに、グーグル先生が教えてくれた APM 関連製品/サービスを羅列してみます。
Datadog, Nagios, Stackify, Sensu, Pingdom, LogicMonitor, Splunk, Grafana, Scout, SteelCentral, Applications Manager, AppDynamics, dynaTrace, CA APM, JENNIFER, Hosted Graphite
※これらが Domino と連携できるかどうかは未確認です

この中のいくつかを試してみたくなったのですが、New Relic のようにグラフ化されない統計情報があるのだろうし、その場合にどうすればグラフ化できるのかを調べる手間よりも、自前でグラフ化できるのならそっちの手間のほうが楽しそうだと考えました。


さしずめ「おれおれAPM」です。


まずは、New Relic 等への統計の連携機能が REST API を使って POST していることを確認するため、POST 先として Domino の nsf ファイルを試してみることにしました。

まずは、POST を受けるために HTTP タスクを起動します。

サーバーコンソールから次のコマンドを投入しました。
load http


ドミノディレクトリのサーバー文書で、Domino Access Service (DAS) を有効にします。
また、統計情報を保存する NSF ファイル「statpub.nsf」とビューにおいても DAS を有効にしておきます。

※DASを有効にするための設定については「IBM Domino REST API 利用ガイド」が詳しいです


私の環境では、現在の統計の連携先が New Relic なので、それをやめるために notes.ini から NEWRELIC_LICENSE_KEY= で始まる行を削除しています。

そして Domino の NSF ファイルへ統計情報を POST するよう、notes.ini へ次のように設定しました。
STATPUB_ENABLE=1
STATPUB_URI=http://localhost/statpub.nsf/api/data/documents
STATPUB_DATA_HEAD={"host":"xsp13/v10","stattime":$Timestamp$000,
STATPUB_DATA_TAIL=}
STATPUB_METRIC_FORMAT="metric.$Name$":$Value$
STATPUB_DELTA_METRIC_FORMAT="metric.delta.$Name$":$Value$
STATPUB_HEADERS=Content-Type: application/json$Newline$Accept: application/json$Newline$StatTimeStamp: $Timestamp$Newline$

New Relic 以外へ連携する場合は STATPUB_ で始まるパラメータを必要に応じていくつか指定します。

1行目の STATPUB_ENABLE= は統計の公開を有効にする場合は 1 を指定します。無効は 0 です。

2行目の URI は、REST API を使用して NSF に文書を作成する場合に指定するものと同じです。

3,4行目は、統計情報を JSON 形式とするために最初と最後に{}を付加したり、後から使うであろうサーバー名などを追加しています。stattime にはタイムスタンプとしてUNIXエポックの日時を付加できるのですが、ミリ秒の部分が欠落しており、後述の Chart.js で使う場合に都合が悪いので、3桁のゼロを追加しています。

5,6行目は統計情報のフォーマットを定義します。統計情報にある Update.DefferdList といった名前の前後にダブルコーテーションを加え、名前と値の区切り文字としてコロンを指定しています。なお、6行目の delta で始まる統計情報には、前回の値との差を示す値がセットされるようです。

7行目には POST する際にヘッダとして指定するべき Content-Type といったパラメータなどを定義しています。複数のパラメータを改行で区切るために $Newline$ を挟んでいます。
また、このままでは NSFファイル内に作成される文書の作成者は Anonymous となりますが、もし、作成者を特定のアカウントにしたい場合は「Authorization: Basic <アカウント:パスワード>」を追加すると Basic 認証できます(<>には BASE64でエンコードした文字列を設定します)。


さて、これを設定して数分待っていると statpub.nsf に文書が登録されていました。


こうしてこの連携機能が REST API を使って POST していることが確認できました。


ところで、APM はリアルタイムにグラフ化できることがメリットのひとつだと思うのですが、NSFファイルにあるデータをリアルタイムにグラフ化するには、NSFにデータが作成された瞬間に値をグラフへ反映する仕組みが必要です。また、そもそもグラフをどうやって表現するかということにも関係します。

実は現在参加しているノーツコンソーシアムの「アプリ開発研究会」で、リアルタイムにグラフ化するほうのネタを提供してしまったため、このエントリでは「リアルタイム」をちょっと犠牲にして実現する方法をご紹介します。

POST 先のデータベース「statpub.nsf」に、次のようなビューを追加しています。
1列目)列の値:statpub、最新のデータを最も上に表示するため降順でソートします
2列目)列の値:metric.Update.DeferredList
3列目)列の値:metric.Update2.DeferredList

グラフには2つの統計値を表示したいので、2列目と3列目で値を表示しています。
今回は「Updateタスクのキューに入っている要求の数」を見ることを想定して 「metric.Update.DefferdList」を指定しました。なお、私の環境は2つのUpdateタスクを起動しているので Update2 の統計情報が存在します


さらにページ「index」を追加しています。データベースをWebブラウザで開く際にこのページが開くようデータベースのプロパティを変更しています。

ページには次のコードを記述してあり、このコード全体をパススルーHTMLとしています。
</form>
<canvas id="myChart"></canvas>
<script type="text/javascript">
    var ctx = document.getElementById('myChart').getContext('2d');
    var chart = new Chart(ctx, {
        type: 'line',
        data: {
            datasets: [{
                data: [],
                label: 'Update.DeferredList'
            },{
                data: [],
                label: 'Update2.DeferredList'
            }]
        },
        options: {
            elements: {
                line: {
                    tension: 0
                }
            },
            scales: {
                xAxes: [{
                    type: 'realtime'
                }]
            },
            plugins: {
                streaming: {
                    duration: 600000, //保持する時間幅=画面の横幅(ミリ秒)
                    refresh: 15000, //次にデータを読み込むまでのインターバル(ミリ秒)
                    delay: 0, //最新のデータを表示する間隔(tension 0 の場合、関係なし)
                    frameRate: 30, //数字を大きくするとスクロールがスムーズになる
                    pause: false,

                    onRefresh: function(chart) {
                        var json = '';
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function() {
                            if (xhr.readyState === 4) {
                                if (xhr.status === 200) {
                                    json = JSON.parse(xhr.responseText);
                                    chart.data.datasets[0].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update.DeferredList"]
                                    });
                                    chart.data.datasets[1].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update2.DeferredList"]
                                    });
                                } else {
                                }
                            }
                        }
                        xhr.open( 'GET', 'http://localhost/statpub.nsf/api/data/collections/name/Update.DeferredList?count=1&systemcolumns=0x80a');
                        xhr.send(null);
                    }
                }
            }
        }
    })
</script>
<form>

また、このページの HTML Head Content には以下のように指定しています。
"<meta charset=\"UTF-8\">
<title>リアルタイム統計情報</title>
<script src=\"https://cdn.jsdelivr.net/npm/moment@2.24.0/min/moment.min.js\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@1.8.0\"></script>
<style>* { margin: 0px; padding: 0px; }</style>"


このページに書いたコードを簡単に説明します。

統計情報の折れ線グラフを時系列に表示したかったので、今回は Chart.js に chartjs-plugin-streaming プラグインを組み合わせています。グラフは canvas タグの場所に最新の10分間(単位をミリ秒で指定するためコード上は600000)だけ表示する設定です。

ここでは、15秒間隔(単位をミリ秒で指定するためコード上は15000)でリフレッシュしていますが、そのタイミングで XMLHTTPRequest を使ってビューの1行目(最新の統計値)だけを取得します。取得できた JSON データをパースして、日時値と表示したい統計情報の値をグラフに反映します。

グラフを表示するにはブラウザから http://localhost/statpub.nsf へアクセスします。

こうして表示されたグラフがこちらです。※実際はもたもたと右から左へ動きます。


ひとまず「おれおれAPM」ができました。