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

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

タグ:express

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 アドレス制限が実現できます。

Node.js + Express の環境で SSL を使う(https でアクセスできるようにする)方法を調べたのでまとめました。


まず SSL を使うための鍵ファイルと証明書ファイルを用意します。公式なドメインを所有していて、本物の鍵/証明書ファイルを持っているのであればそれを使っても構いません。試験的に試すのであれば、いわゆる「オレオレ証明書」を作成します。Linux 環境であれば openssl コマンドを使って、以下のように入力します:
$ openssl genrsa -out server_key.pem 2048
Generating RSA private key, 2048 bit long modulus
...............+++
..........................+++
e is 65537 (0x10001)

$ openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me"

$ openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Signature ok
subject=/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me
Getting Private key

$

↑この例では juge.me というホスト名で運用する前提での、サーバーの鍵ファイル(server_key.pem)と証明書ファイル(server_crt.pem)を作成しています。

この2つのファイルと同じディレクトリに app.js というファイル名で、Node.js のソースコードを以下の内容で作成しました。アプリケーションそのものはドキュメントルート(/)にアクセスがあった場合に「ハローワールド」と表示するだけの単純なものです:
//. app.js

var express = require( 'express' ),
    http = require( 'http' ),
    https = require( 'https' ),
    cfenv = require( 'cfenv' ),
    fs = require( 'fs' ),
    app = express();
var appEnv = cfenv.getAppEnv();

//. 鍵ファイルと証明書ファイル var options = { key: fs.readFileSync( './server_key.pem' ), cert: fs.readFileSync( './server_crt.pem' ) };
//. 鍵ファイルと証明書ファイルを指定して、https で待受け var server = https.createServer( options, app ).listen( appEnv.port, function(){ console.log( "server stating on " + appEnv.port + " ..." ); });
//. ドキュメントルートにリクエストがあった場合の処理 app.get( '/', function( req, res ){ res.write( 'ハローワールド' ); res.end(); });

このコードを実行すると待受ポート番号が動的に決定して表示されます(↓の例だと 6015 番ポートで https が待ち受けていることが分かります):
$ node app.js
server stating on 6015 ...

ではウェブブラウザで実際にアクセスしてみます。本当に使っている本物のドメイン/ホスト名であればそのままアクセスできると思いますが、試験的に実行している場合はこのホストに "juge.me" という名前でアクセスできる必要があります。必要に応じて hosts ファイルを編集するなどして、目的のホスト(node app.js を実行したホスト)に "juge.me" というホスト名でアクセスできるような準備をしておいてください。

そしてウェブブラウザで "https://juge.me:(ポート番号)" にアクセスします:
2017041301


オレオレ証明書名物「安全な接続ではない」という警告画面になると思います。ここからの手順はウェブブラウザの種類にもよりますが、FireFox の場合であれば「エラー内容」ボタンをクリックしてから「例外を追加」をクリックして、このサイトの警告を無視するための設定を行います。

そして「セキュリティ例外の追加」ダイアログにて、この URL の「セキュリティ例外を承認」します:
2017041302


すると警告が消えて、プログラムで用意したコードが実行され、「ハローワールド」というメッセージが表示されることが確認できます:
2017041303


IBM Bluemix のような PaaS 環境だと、このあたりも含めてアプリケーションサーバー環境が用意されるので楽ですが、素で Node.js 環境を構築する場合はアプリケーション側でも https 対応を実装する必要があり、意外と面倒ね。

2016 年の年初に「マンホライザー」というサービスを作って動かしていました:
http://dotnsf.blog.jp/archives/1048853473.html

マンホライザーはわかりやすく言うと「マンホール顔ハメ画像作成サービス」です(わかりやすいか??)。先日のマンホールサミット 2017 埼玉に参加した際にも、こんなアナログな顔ハメ写真サービスを提供させていただいたのですが、意外と人気があったのでした。それをウェブ上からもできるようにしよう、というわけで生まれたサービスでした。IBM Watson の画像認識機能を使って実装しており、「テクノロジーの無駄使い」な点も自己採点ポイントが高いものになっています(笑):
2017020701


上記リンク先の、2016 年に公開した当初は PHP で実装していました。その時のソースコードはこちらです:
https://github.com/dotnsf/Manholizer


このコードはこれはこれで現在も動く(注 Visual Recognition API の古いものを使っているので、顔ハメはうまく動きません)ものですが、いくつかの課題もありました。その1つが「スマホからの利用を想定していなかった」という致命的な点です。見た目でのスマホのウェブブラウザ最適化という意味では実現したつもりでしたが、想定外だったのは「スマホのカメラで撮影した画像の解像度が予想以上に高かった&今後は更に高くなることが想定される」ということです。

ある程度詳しい人には常識かもしれませんが、iPhone をはじめとする最近のスマートフォンのカメラはかなり高い解像度の写真を撮影することができます(特に設定を変更しない場合は、高解像度撮影が標準機能になっていることが多いです)。撮影した写真を低解像度にしてメールで送る、といったことは可能ですが、内部的には高解像度のまま保存されていることになります。これは1枚の写真画像のファイルサイズが非常に大きいということを意味しています。これは2つの点で問題になります。

1つはアプリケーションサーバー側の制約を受ける可能性があるという点です。例えば PHP を普通にインストールしたアプリケーションサーバーの場合、アップロードサイズは 2MB に設定されることが多いです。もちろんこの設定を変更できればいい話ですが、最近は PaaS などでミドルウェア設定を変更できないケースもあるので、そうなるとこの制約を受けてしまいます。

もう1つは API 側の仕様上の制約を受ける可能性です。このマンホライザーでは画像から顔の位置を認識・特定する必要があるのですが、その部分に IBM Watson の Visual Recognition API を使っています。この API の制約として現在は「画像は 2MB まで」という制約があるのでした。つまり上記のアプリケーションサーバー側の制約を取り除いただけでは解決にならないことも出てきてしまうのです。

上記で紹介した、以前作った PHP 版のマンホライザーにはこれら2つの課題があり、スマホからの利用を想定すると期待通りに動かないケースが出てきてしまいました。というわけで、ミドルウェアや設計段階から見直したマンホライザーを作り直すことにしたのでした。


上記2点を解決するため、まず PHP のアップロードファイルサイズ制約をうけないよう、アプリケーションサーバーは Node.js を使うことにしました。PHP から Node.js への移植を行いました。これによって 2MB を超える画像もアップロードできるようにしました。

また API 側のサイズ制約については、アップロードした画像を一旦内部的にリサイズし、API が実行できるレベルにまでサイズを減らしてから実行する、というロジックに変更することで対処しました。細かい点ですが、画像を小さくしてから実行するため、API からのレスポンスは「小さくなった画像に対する顔の位置」になります。アプリケーション側ではアップロードした画像のプレビューが表示されており、その画像にマンホールの位置とサイズを調整して「ハメる」わけですが、元の画像サイズに対するレスポンスにはなっていないので、その辺りも考慮する必要が生じます。

そんなこんなの変更を加えてできあがったのがこちらです:
https://manholizer.mybluemix.net/

2017020701

↑ちとズレてるが・・・


使い方は PC またはスマホのウェブブラウザでアプリケーションサーバー(https://manholizer.mybluemix.net/ とか)にアクセスしてください。で、「参照」ボタンをクリック:
2017020702

 

手元のローカルファイルシステムまたはフォトライブラリ等から画像ファイルを選択します。今回は「フリー素材アイドル」の Mika x Rika さんの画像を使わせていただきました:
顔4


この画像を指定して、しばらく待つと・・・
2017020703


こんな感じになりました:
2017020704


合成用マンホールのデザインとして使っているのは(マンホーラーであればおなじみの)東京都マンホールです:
2017020706


(2つの)顔の位置を識別し、うまく顔ハメになるようマンホールの位置と大きさが調整されて合成されています。またマンホールの色がピンクなのは「女性」であると認識されている様子です(男性の場合は青いマンホール画像を合成します)。また2人の上部に "-17" と表示されていますが、これは推定年齢でふたりとも「17才以下」であると推測されている、ということを意味しています(お二人の年齢は存じ上げませんが、全体的に日本人の顔は若く判定される傾向があるようです):
2017020705



なお、このサービスのソースコードはこちらで公開しています。興味ある方はご自身の環境でもどうぞ:
https://github.com/dotnsf/manholizerDemo


自分の環境に導入して使いたい場合の方法は README.md にも記述していますが、IBM Bluemix の Node.js ランタイムで使う場合であれば、まず Bluemix にログインして、SDK for Node.js ランタイムを1つ作成します(この時の名前を後で使います)。また Visual Recognition サービスインスタンスを生成し、その API KEY を取得しておきます(この値も後で使います)。

次に上記サイトからソースコードをダウンロード&展開するか git clone して、以下の2ファイルを編集します。

1つ目は settings.js です。この中の exports.vr_apikey の値に、上記で取得した自分の Watson Visual Recognition サービスの API Key の値を設定します(以下は API Key が abcdabcdabcdabcdabcdabcd であった場合の例です):
(settings.js)

: exports.vr_apykey = 'abcdabcdabcdabcdabcdabcd'; :

もう1つは manifest.yml です。この中を実際に運用するランタイムの情報に設定します。例えば eu-gb リージョンを使って、my_manholizer という名前のランタイムで運用する場合は以下のようになります(ng リージョンの場合、domain は変更せずにそのままで構いません):
(manifest.yml)

: domain: eu-gb.mybluemix.net name: my_manholizer host: my_manholizer :

この状態で cf コマンドを使ってアプリケーションを push すると、作成した Node.js ランタイム上にマンホライザーがデプロイされます:
$ cf push

IBM Bluemix を使わずに運用する場合は・・・ まあ普通に Node.js をインストールして npm install して使ってください(適当)。


IBM が9月に買収した StrongLoop は、Node.jsExpress フレームワークへのコントリビューションの多く、IBM の Node.js による API 開発の中核的な役割になっていくものと思われます。例えば、以下の Watson API リファレンスのサイトは StrongLoop を使って提供されています:
http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/apis/

この StrongLoop (と、前提としての Node.js)そのものは無料で提供されているので、誰でも自分の環境にインストールして使うことができます。 というわけで、CentOS に(Node.js と)StrongLoop をインストールして使ってみました。


StrongLoop は npm で提供されています。なので、まずは Node.js と npm を yum でインストールします:
# yum install epel-release
# yum install nodejs npm --enablerepo=epel

Node.js と npm がインストールできたら、npm を使って LoopBack と StrongLoop をインストールします:
# npm install -g loopback
# npm install -g strongloop

このインストールにはそれなりに時間がかかりますが、作業としてはこれだけで StrongLoop がインストールできます!


では実際に StrongLoop を使ってデータベースのモデルを作り、その CRUD の REST API を公開してみましょう。 まずは空のディレクトリ(下の例では ~/tmp)を作ってそこに移動します:
# cd
# mkdir tmp
# cd tmp

このディレクトリに loopback 環境を作成します:
# slc loopback
2015111001


アプリケーションの名前と、そのアプリケーションの作成ディレクトリを聞かれます。ここでは両方とも myapp と指定しました:
2015111002


すると必要なモジュールのインストールを含めた作業が始まり、完了すると myapp というディレクトリが作られます:
2015111003


出来上がった myapp ディレクトリに移動して、モデルの定義を行います。以下のコマンドを入力します:
# cd myapp
# slc loopback:model

ここからは API で CRUD を行うモデルとして、以下の様な item モデルを定義することにしましょう:
列名列型必須条件
namestringYES
codestringYES
pricenumber 


実際にはこの定義に加え、自動的に id という列が定義されます。では最初に myapp アプリ内に item モデルを作成し、これらの3つの列を順に定義していきます。まず model name は "item" を入力します。次に data-source は、今回はデフォルトのメモリ DB を使うので "db (memory)" を選びます。またこのモデルは永続性(persistant)を有効にしたいので、その Base class には "PersistedModel" を指定してします。またこの item は REST API で公開したいので Expose するかどうかの質問には Yes を選択。この item モデルの複数形名にカスタムな名称を使うわけではない(普通に items とする)ので、Custom plural form の質問は空のままで Enter を押します。そしてこのモデルは common モデルにするので "common" を選択します:
2015111004


次に各列を順に定義します。最初は name 列を定義します。名前には "name" を指定し、その type には "string" を、そして必須かどうかの属性は必須なので "Y" を入力します:
2015111005


続けて "code" 列を定義します。同様にして名前には "code"、type には "string"、そして必須属性を "Y" で指定します:
2015111006


最後に "price" 列を定義します。名前には "price"、type には "number" 、必須属性を "N" で指定します:
2015111007


これで全ての列を指定し終わったので、最後に名前欄を空欄にして Enter を押します。これでモデルの定義が完了です:
2015111008


ここまでの作業ができたら、カレントディレクトリで node.js を起動します:
# node .

すると、デフォルトでは 3000 番ポートで node.js が待受る形で起動します:
2015111001


この状態で表示されているアドレス(http://(strongloopをインストールしたホスト名):3000/explorer)にブラウザでアクセスしてみると、作成した myapp アプリケーション内に定義されている API の一覧が表示され、その中には item モデルについても含まれているはずです:
2015111002


item と書かれた箇所をクリックすると展開され、この item モデル向けに用意されている API の一覧が表示されます。特に何もしなくても item モデルの定義をしただけで一通りの CRUD 操作ができるような API が準備されていることがわかります:
2015111003


用意された API はこの画面から実際に動かして試してみることもできます。例えば一番上にある /items への GET を実行してみましょう。このエントリの /items を書かれた箇所をクリックして開くと、この GET リクエスト API に関する情報が表示されます:
2015111101


この画面内の "Try it out" と書かれたボタンをクリックすると実際に API を発行することができます。実際にクリックすると curl で実行されたコマンドや実際にリクエストされた API の URL などが確認できます。また Response Body には実行結果が表示されます。この例では実行結果は [] と、空の配列になっているので、実行しても何も得られなかった、ということになります(今はまだ中身がないのでこれで正しい実行結果です):
2015111102


では実際に item レコードを作ってみましょう。レコードを作成するには POST リクエストを実行することになるので、/items への POST エントリを開き、Parameters 欄の data の部分に以下のようなフォーマットの JSON データを入力して "Try it out!" ボタンをクリックします( "id" は自動割り振りされるので、指定してあってもいなくても構いません。また実際に指定するデータは適当で構いません):
{
  "name": "アディゼロ匠 B22875",
  "code": "4055339759613",
  "price": 12050,
  "id": 0
}

2015111103


問題なく処理が成功すると、以下の様な結果が得られます。Response Code が 200 になっていれば成功を意味しており、Response Body には以下の様な JSON が返されます("id" の値として 0 を指定していましたが、自動割り振りされた結果の 1 が付与されています)。このデータが正しく追加された、ということになります:
{
  "name": "アディゼロ匠 B22875",
  "code": "4055339759613",
  "price": 12050,
  "id": 1
}

2015111104


データが正しく追加されたので、改めて再度 /items の GET を実行すると、今度は Response Body の結果は [] ではなく、先程入力したデータが含まれた配列になっているはずです。モデルを定義しただけで自動生成された CRUD の API が( /items の GET と POST だけですが)正しく動いていることが確認できました:
2015111105


RDB のテーブルに相当するレコードを定義すれば、(コードを書くこともなく)即 CRUD API が用意される、という意味では便利です。


いずれはモニタリングやトレーシングなどもできる StrongLoop Arc についても勉強して、ここで紹介するつもりです。



今回のエントリ内容は、このエントリの続編的な内容です。実際に作業する準備の手順も含まれているので、実際に試す前に一度内容をご確認ください:
Bluemix 上の Node.js を使う


Express は Node.js 上で使う MVC フレームワークです。いわゆる "MEAN"(mongoDB + Express + AngularJS + Node.js) スタックを構成する要素の1つです。

また EJS は Node.js 上で利用可能なテンプレートエンジンです。HTML ベースのテンプレートにルック&フィールの UI をデザインし、その中身を変数化して動的に埋めていくことで Web アプリケーションの見栄えを作っていくことができます。

設計とデザイン(見た目)とを別チームに分けて開発するようなスタイルでは、こういったテンプレートエンジンと MVC フレームワークを活用して、デザインチームにテンプレートを、設計チームに MVC を含むプログラミング部分を担当してもらう、といった体制で開発することは一般的です。


Express も EJS も、Node.js ではそれなりにメジャーなコンポーネントで、通常はパッケージマネージャーである npm を使ってインストールして利用するものです。では npm 環境が使えない Bluemix では、これらをどのようにして導入して使えばいいのか? という疑問が出てきます。

その答は、上記の前エントリ内でも述べていますが、package.json ファイルを使って必要なモジュールを動的に指定して用意する、ということになります。Express も EJS もこの方法で Bluemix 内の Node.js ランタイム内に導入可能です。以下にその具体的な手順を紹介します。


今回作成するファイルはこの3つです:
|- public/
|    |- hello.ejs
|- app.js
|- package.json

まずはテンプレートを用意します。 public というフォルダを作り、その下に hello.ejs というテキストファイルを以下の内容で(UTF-8 で)作ります。これが見栄えの部分です:
<html>
<h1><%= title %></h1>
<div><%- content %></div>
<div><%= n %> views</div>
</html>

title, content, n という3つの変数が定義されていて、それらの値を使った HTML が記述されています。EJS ではテンプレート内での変数の書き方は2種類あって、<%= ~ %> で括られた部分はエスケープされて出力され、<%- ~ %> で括られた箇所はエスケープされません(HTML タグが HTML タグとして有効になります)。今回の例では content 変数に HTML タグが含まれている場合は HTML タグが有効になります。


次はシステム設計です。 前回の server.js を元に、app.js を以下の内容で作成します:
var express = require( 'express' );
var app = express();

var fs = require( 'fs' );
var ejs = require( 'ejs' );
var template = fs.readFileSync( __dirname + '/public/hello.ejs', 'utf-8' );

var cfenv = require( 'cfenv' );
var appEnv = cfenv.getAppEnv();

var n = 0;

app.get( '/', function( req, res ){
  n ++;
  var data = ejs.render( template, {
    title: "ハローワールド!",
    content: "ようこそ <strong>Express</strong> + <i>EJS</i> の世界へ",
    n: n
  });
  
  res.writeHead( 200, { 'Content-Type': 'text/html' } );
  res.write( data );
  res.end();
});

app.listen( appEnv.port );



Express を使って http アプリケーションが作られ、ドキュメントルート('/')へのリクエスト時に表示する内容は EJS のテンプレートをベースに、その変数部分だけが動的に指定されて表示されるようになっています(EJS のテンプレートパスを指定する際に fs モジュールも使っています)。変数 title, content, n は変数 data 内にまとめて指定しています。なお変数 content の内容は HTML タグを含むテキストです。また変数 n はイクンリメント変数なので、リロードする度に1つずつ値が増えるようにしています。 cfenv モジュールに関しては上述の前回の内容を参照してください。

最後に package.json を以下の内容で用意します:
{
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "cfenv": "1.0.x",
    "express": "4.12.x",
    "ejs": "2.3.x",
    "fs": "0.0.x"
  }
}

"scripts" の中で app.js を起動するよう指定している箇所は前回と同様です。加えて、今回は Express, EJS(とファイルシステム利用のための FS )のモジュールを利用したいので、"dependencies" の中でこれらのモジュールを動的にロードするよう指定しています。


これら3つのファイルが用意できたら、cf ツールで Bluemix 上の Node.js ランタイムにプッシュして、ブラウザでドキュメントルートにアクセスしてみます:
2015100705


hello.ejs テンプレートをベースとしたページが表示されており、また変数 content に含まれている HTML タグが有効になっていることがわかると思います。また同じページをリロードすると変数 n の値がインクリメントされて、1つずつ増えていくのがわかります:
2015100706


npm でインストールしていたモジュールを package.json で指定して動的にインストールする、という違いが分かってしまえば、Bluemix 上でもそんなに違和感なく Node.js 環境が使えるようになります。







 

このページのトップヘ