まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:node

最近、ボット関連の API や SDK をよく見るわりに、自分では使う機会が少なかったので、今更ながら1つ作ってみました。

今回作ってみたのは「シェルボット」です。名前の通り、ボットでシェルっぽいインターフェースを実現しました。何言ってるかよくわからない人はこちらの画面を参照ください:
2017060901



ボットっぽい対話型インターフェースを使って、対話しているかのようにシェルコマンドを実行します。今回はインターフェースに独自の HTML ページを用意しましたが、ぶっちゃけ LINE などでも(Messaging API を使うなどすれば)応用できると思っています。

#「チャットボット」とは違うけど「ボット」ですっ!


ソースコードを github で公開しておきました。セキュリティ的な面はまだまだ改良の余地があると思っています(rm コマンド他は実行不可にしていますが、本当は他にも実行を制御するコマンドを考慮したほうがいいと思う):
https://github.com/dotnsf/shellbot


↑このソースコードはビジュアルフローエディタである Node-RED で使う前提のものです。(IBM Bluemix などを使って)Node-RED 環境を用意し、func-exec ノードを追加した上でキャンバスに shellbot_nodes.txt の内容をインポートして、ブラウザで /shell にアクセスするだけです(詳しくは README.md 参照)。

#あー、あと今のところ Linux/Unix 系 OS に導入する前提です。


実際に Node-RED 環境上にインポートした様子のスクリーンショットがこちらです。2本のシンプルな HTTP リクエスト処理が定義されているだけのもの(うち1つは HTML 画面定義)ですが、本当にこれだけでこのシェルボットが動きます:
2017060902


実は今まで「ボット」=「チャットボット」=「コンシェルジュ」的なイメージを持っていてハードルが高かったのですが、今回のアプリを作った結果、難しく考えずに「既存アプリケーションのインターフェースを対話型にする」という実装もアリかな、と思うようになりました。


IBM Bluemix(Cloud Foundry) のプラットフォームが現在持っている制約の1つが「IPアドレスによるアクセス制限」に関するものです。残念ながら現時点ではベースとなっている Cloud Foundry にこの機能がなく、IBM Bluemix でも実装されていません。

というわけで、現状この機能を実現するにはプラットフォーム側ではなくアプリケーション側で用意する必要があります。Node.js アプリケーションでこれを実現する方法の1つとして、Express-IpFilter があります:
https://www.npmjs.com/package/express-ipfilter

2017060601



名前の通りの機能です。Node.js の Express フレームワークの中で IP アドレス制限(許可/拒絶)を簡単に実現することができます。

Express-IpFIlter をインストールするには npm で以下を実行します:
$ npm install express-ipfilter
(実際には express のインストールも必要です)

例えば、以下のような Node.js + Express のシンプルなアプリケーションを例に IP アドレス制御をかける例を紹介します。まずアプリケーション(app.js)は以下のような内容のものを使います:
//. app.js

var express = require( 'express' );
var app = express();

app.use( express.static( __dirname + '/public/' ) );

app.listen( 3000 );

アプリケーションの中でスタティックディレクトリを /public/ に指定しています。そこで /public/index.html というファイルを用意し、中身を以下のようなものにします:
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>はろーわーるど</h1>
</body>
</html>

これを普通に Node.js で実行します:
$ node app

3000 番ポートを listen するように指定しているので、このポートを指定して同一マシンからウェブブラウザでアクセスすると、スタティックディレクトリに用意した index.html を見ることができます:
2017060602


ではこのアプリにアクセス制御をかけてみます。まずは '127.0.0.1' からのアクセスを拒絶するようなフィルターをかけてみます。app.js の内容を以下のように変更します(赤字部分を追加):
//. app.js

var express = require( 'express' );
var ipfilter = require( 'express-ipfilter' ).IpFilter;
var app = express();

var ips = [ '127.0.0.1' ];
app.use( ipfilter( ips ) );

app.use( express.static( __dirname + '/public/' ) );

app.listen( 3000 );

この例では ips という配列変数を用意し、その中に対象とする IP アドレスを文字列配列の形式で代入します。そして ipfilter を有効にしています。

この状態で app.js を起動し、先程と同じように同一マシンからウェブブラウザでアクセスすると以下のようになります:
2017060603


IP アドレス制御が有効になり、アクセスは拒否されました。

では今後は逆に '127.0.0.1' からのアクセスのみ許可するようなフィルタをかけてみます。app.js の内容を以下のように変更します(青字部分を追加):

//. app.js

var express = require( 'express' );
var ipfilter = require( 'express-ipfilter' ).IpFilter;
var app = express();

var ips = [ '127.0.0.1' ];
app.use( ipfilter( ips, { mode: 'allow' } ) );

app.use( express.static( __dirname + '/public/' ) );

app.listen( 3000 );

この状態で再度アプリケーションを起動し、同様にウェブブラウザでアクセスすると、今度は元のように表示されます(つまり IP Filter はデフォルトだと拒絶、'allow' モードを指定すると許可のフィルタをそれぞれ有効にします):
2017060604


これでアプリケーションレベルでの IP アドレス制限が実現できます。

話題(?)のブロックチェーンを小型コンピュータであるラズベリーパイ(以下、「ラズパイ」)から操作する、ということに挑戦してみました。


まずは操作の対象となるブロックチェーン環境を用意します。今回は IBM のクラウドプラットフォームである IBM Bluemix から提供されているマネージドハイパーレジャー環境である IBM Blockchain サービスを使うことにします(2017/Jun/04 現在では HyperLedger Fabric v0.6 と v1.0 がベータ提供されています)。IBM Bluemix アカウントが必要になりますが、30日間無料試用も用意されているので、必要に応じてアカウントを取得してください。

IBM Bluemix にログイン後、米国南部データセンターになっていることを確認し、「サービスの作成」をクリックします:
2017060404


そして「アプリケーション・サービス」カテゴリ内にある "Blockchain" を探してクリック:
2017060405


利用する Blockchain の種類をプランから選択します。今回は無料の "Starter Developer plan(beta)" を選択します。ちなみにこのプランの場合、Hyperledger Fabric は v0.6 を使うことになり、4つのブロックチェーンピアが作成されます。最後に「作成」ボタンをクリック:
2017060406


しばらく待つと IBM Blockchain サービスが起動します。ここでダッシュボードに移動することもできますが、この後で利用する情報を先に取得しておきます。左ペインの「サービス資格情報」を選択:
2017060407


サービス資格情報から「資格情報の表示」をクリックして、表示される JSON テキストを全てコピーしておきます(この後、ラズパイ内で使います):
2017060408


改めて「管理」タブを選択し、「ダッシュボードを起動」ボタンをクリックして、ダッシュボード画面に切り替えておきましょう:
2017060409


IBM Blockchain のダッシュボード画面が表示されます。4つのピアが動いていますが、この時点ではまだチェーンコードは作られていないことが確認できます。これでブロックチェーン側の準備は完了です:
2017060401


では続けてラズパイ側の準備に取り掛かります。以下はラズパイ上での作業です(僕の検証では3Bを使いました)。ラズパイにはネットワークのセットアップが済んだ Raspbian Jessie を用意し、そこに Hyperledger Fabric Node SDK ごとサンプルコードを導入してデモアプリケーションを動かします。というわけでまずは Node.js と npm を用意します。まずはこのドキュメント内にあるように、Node.js v6.2.x ~ v6.10.x のバージョンの環境を用意します(以下は v6.10.3 の例です。v7.x は未対応):
$ sudo apt-get install -y nodejs npm
$ sudo npm cache clean
$ sudo npm install n -g
$ sudo n v6.10.3
$ node -v
v6.10.3  <- v6.10.3 の導入が完了

続けて npm を最新版(以下の例では v5.0.2)に更新します:
$ sudo npm update -g npm
$ sudo npm outdated -g
$ npm -v
v5.0.2  <- v5.0.2 の導入が完了

git でデモアプリをダウンロードし、必要なライブラリをインストールします:
$ git clone https://github.com/IBM-Blockchain/SDK-Demo.git
$ cd SDK-Demo
$ npm install

ダウンロードしたデモアプリの中(直下のディレクトリ)にある ServiceCredentials.json ファイルの中身を、上記でコピーしたサービス資格情報の JSON テキストの中身で全て書き換えます:
{
  "peers": [
    {
      "discovery_host": ******

      :
      :
"cert_path": "/certs/peer/cert.pem" }

これでラズパイ側の準備も完了しました。後は以下のコマンドを実行して、デモアプリを実行します:
$ node helloblockchain.js

以下のようにコマンドが次々を流れていきます。実際にはこの中でチェーンコードの初期化や、初期データ入力、データ変更などが行われています。最後に "Successfully ****" というメッセージがいくつか表示されていればコマンドは成功です:

2017060403


この最後のメッセージについて補足します。この helloblockchain.js ではチェーンコード初期化などの作業を行った後に「"a" という入れ物に 100 、"b" という入れ物に 200 のデータを保存」します(ここまでが初期化)。そして「"a" から "b" へ 10 移動」するようなトランザクションが実行されます(なのでここまでの処理が完了すると "a" に 90 、"b" に 210 入っている状態になるはずです)。最後に「"a" の値を取り出す」処理が実行されます。その辺りの様子が以下のメッセージから分かります("a" が 90 になっています):
  :
  :
Chaincode ID : XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Successfully deployed chaincode: request={"fcn":"init","args":["a","100","b","200"],"chaincodePath":"chaincode","certificatePath":"/certs/peer/cert.pem"}, response={"uuid":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}

Successfully submitted chaincode invoke transaction: request={"chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","fcn":"invoke","args":["a","b","10"]}, response={"uuid":"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"}

Successfully completed chaincode invoke transaction: request={"chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","fcn":"invoke","args":["a","b","10"]}, response={"result":"Tx zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz complete"}
Custom event received, payload: "Event Counter is 1"


Successfully queried  chaincode function: request={"chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","fcn":"query","args":["a"]}, value=90


※↑数値の移動は Submit して Complete しているのが別行で表示されているので、全部で4行表示されています。


なお初期化部分は最初の1回しか実行されないため、この helloblockchain.js を何度か実行すると、"a" の値が 10 ずつ減っていく様子がわかります:
$ node helloblockchain.js

  :
  :
Successfully queried  chaincode function: request={"chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","fcn":"query","args":["a"]}, value=80

$ node helloblockchain.js

  :
  :
Successfully queried  chaincode function: request={"chaincodeID":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","fcn":"query","args":["a"]}, value=70


また、これらの処理を実行すると、ダッシュボード上からもチェーンコードが生成されて、実行されている様子が確認できます(初期状態だと画面更新が3分ごとなので、画面に反映されるまで少し時間がかかるかもしれません):
2017060402


まだ環境の一部がベータ版だったりする状態ではありますが、今やラズパイでもブロックチェーンが動くようになったんですねー。

ファイルのアップロードを伴うアプリケーションを作っていて悩ましいことの1つに「中身の変わらない(全く同じ)ファイルが別のファイル名でアップロードされることがある」ことです。

アプリケーション側の実装としては、アップロードされたファイルをストレージ等に保存する処理を用意しているのですが、その際のファイル名をどうするか? という問題があります。

例えば "a.jpg" という画像ファイルと、"b.jpg" という異なる画像ファイルがあり、これら2つがアップロードされるケースだけを考えるのであれば、ストレージに保存する際にも元のファイル名をそのまま使えることになります:
2017052201
↑異なる2つのファイルを元のファイル名のまま保存する場合(このケースは問題なし)


ところが更に "a.jpg" というファイル名で、既に保存済みの "a.jpg" とは異なるファイルがアップロードされることもあります。この3つ目のアップロードファイルは "a.jpg" というファイル名で(上書き)保存するわけにはいきません(元のファイルが消えてしまう)。ということは元のファイル名をそのまま使うことは正しい処理ではなくなります:
2017052202
↑同じファイル名で中身の違うファイル保存しようとすると上書きすることになってしまう


また別のケースとして、"c.jpg" というファイル名で、"a.jpg" と中身が全く同じ画像ファイルがアップロードされるケースを考えてみます。この場合、ファイル名そのものは元のもの(c.jpg)を使っても被ることがなくいいのですが、全く同じ画像ファイルを2つ保存することになり、無駄にストレージを消費することになります。画像ファイルであればそのサイズもたかが知れているのかもしれませんが、これが仮想イメージとかだったりすると1ファイルで数10Gバイト消費することもあるため、中身の全く同じファイルであれば複数保存せずにすませたいものです:
2017052203
↑異なる名前で中身の同じファイルを元のファイル名のまま保存すると、無駄な保存領域を使うことになる


上記の問題点を実現する方法として、「同じ中身(バイナリ)のファイルは同じファイル名で、異なる中身のファイルは異なるファイル名を用意して保存する」仕組みを用意する方法があります。で、これを比較的簡単に実現する方法がファイルのハッシュ値を使うことが考えられます:
2017052204
↑ファイルのバイナリデータのハッシュ値をファイル名に使えば、中身が異なるファイルは異なるファイル名になる


試しに Node.js で実装したものを Github に公開しました:
https://github.com/dotnsf/node-upload-sample


画像のバイナリデータ(バイト配列)からハッシュ値を生成しているのは app.js 内の以下の箇所です。Node.js 標準の crypto ライブラリを使って、SHA512 アルゴリズムで path のファイルストリームからハッシュ値を生成している箇所です:
  :
var crypto = require( 'crypto' );

  :
  var path = req.file.path;
  var destination = req.file.destination;

  //. Name after Hash value
  var hash = crypto.createHash( 'sha512' );
  var fstream = fs.createReadStream( path );
  hash.setEncoding( 'hex' );
  fstream.on( 'end', function(){
    hash.end();
    var result = hash.read();

      :

ファイル(画像)をストリーム化してハッシュ値を求め、後ろに元の拡張子を付けたものを保存時のファイル名にしています。


このアプリを実際に動かしてみた様子を以下に紹介します。まず動作確認するにあたって、3種類4つのファイルを用意しました。01.png と 02.png はファイル名は異なりますが、全く同じファイルです。03.png は名前も中身も異なります:
2017052301


これらとは別に、中身の異なる 01.png というファイルを用意しました(ファイル名だけ前述のものと被ります)。これら3種類4つのファイルを全て順にアップロードした時の様子が以下です:
2017052302


まずアプリケーションを起動するとこのような画面になります。登録した画像ファイルが一覧表示されますが、まだ何も登録していない状態では何も表示されません。ここで 01.png (03.png と同じ画像の方)を選択して登録してみます:
2017052301


01.png のハッシュ値が生成され、その値をファイル名として保存されました:
2017052302


このリンクをクリックすると、元の 01.png が登録されていることが確認できます:
2017052303


同様にして、今度は 03.png を登録してみます。すると 01.png と 03.png は異なる画像ファイルであるため、ハッシュ値も異なります。そのため別画像として新たにされます:
2017052304


続けて今度は 02.png (01.png と同じ画像ファイル)を登録します。このファイルはファイル名こそ別ですが実体が 01.png と同じものであるためハッシュ値は 01.png と同じものになります。そのため「既存のデータ」と判断され、新たに画像は登録されません(正確には同じ名前のファイル名で、同じ内容を上書きすることになるので、ファイルは増えず、中身が変わることもありません):
2017052305


更にもう1つの 01.png(元の 01.png とはファイル名は同じだが、中身の異なるもの)を登録してみると、今度は中身の違うファイルなのでハッシュ値も別のものになり、新しいファイルとして登録されます:
2017052306


結果的には3種類4つのファイルを登録しましたが、内容の異なる3つのファイルがアップロードされました。中身の同じファイルは二重登録しない、という当初の目的が達成できました:
2017052307


これでストレージの無駄な空間を使わずに済ませられそうです。

サーバーサイドで動的に画像を作りたい、という要求を実現する方法はいくつかありますが、今回は Node.js から使えるライブラリ "node-canvas" を紹介します:
https://github.com/Automattic/node-canvas/


まず、このライブラリを使う上ではいくつかのネイティブライブラリが必要です。詳しくは上記オフィシャルページを参照いただきたいのですが、例えば Ubuntu 環境であれば以下のコマンドを最初に実行して、必要なネイティブライブラリをあらかじめ用意しておきます:
$ sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++

ネイティブライブラリの導入後に、以下のコマンドで node-canvas をインストールします:
$ npm install canvas

ちなみに、Bluemix の SDK for Node.js ランタイムを使う場合には上記のネイティブライブラリが導入されたビルドパックを利用するので、実行環境でのネイティブライブラリの有無に関しては意識する必要はありません。


さて、node-canvas ではサーバーサイドで HTML5 の Canvas を操作するイメージで動的に画像を作ったり、変更したりすることができます。

ブラウザ上でも Canvas は JavaScript で操作することが多いと思いますが、以下のようにほぼ同じような操作で扱うことができます:
var fs = require( 'fs' );

var Canvas = require( 'canvas' ),  //. ここでライブラリ読み込み
    Image = Canvas.Image;          //. 画像生成用オブジェクト

  :

//. public という空ディレクトリをあらかじめ用意しておく(そこに画像を作る)
app.use( express.static( __dirname + '/public' ) );

app.get( '/xxx', function( req, res ){
  //. /xxx に GET アクセスがあったら、その場で画像ファイル xxx.png を生成して、画像にリダイレクトする
  var img = new Image;
  var canvas = new Canvas( 300, 300 );
  var ctx = canvas.getContext( '2d' );

  //. 斜めに赤い線が1本引いてあるだけの画像を作る
  ctx.beginPath();
  ctx.moveTo( 100, 100 );
  ctx.lineTo( 200, 200 );
  ctx.strokeStyle = 'red';
  ctx.stroke();

  //. 画像を Base64 エンコードで取り出して、デコードして、xxx.png という名前で保存する
  var b64 = canvas.toDataURL().split( ',' )[1];
  var buf = new Buffer( b64, 'base64' );
  fs.writeFile( __dirname + '/public/xxx.png', buf, function(){
    res.redirect( '/xxx.png' ); //. 作った画像にリダイレクト
  });
});

上記は僕が作ったサンプルコードの一部を多少変更したものです。Node.js で実行して、/xxx というパスに GET アクセスがあると動的に /xxx.png という画像ファイルを作って(そこにリダイレクトして)表示する、というものです。実際に画像を描いたり作ったりしているのは赤字の部分なのですが、ほぼそのままブラウザ内の JavaScript でも動く記法になっています。

普段から HTML5 の Canvas を使っている人であれば、そのスキルをそのままサーバーサイドで動的に画像を作る技術に応用できると思います。とても便利。

 

このページのトップヘ