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

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

タグ:express

「国際化」に対応したウェブアプリケーションを Node.js で作る方法を調べたので、メモ替わりに残しておきます。

ここでの「国際化(internationalization, i18n)」はウェブブラウザで設定した言語によって自動的に英語表記にしたり、日本語にしたり、・・・という切り替えを行えるようなものです。自動翻訳とかそういうものではありません。またブラウザで設定した言語は HTTP リクエスト時に "Accept-Language" ヘッダで送信されることになるので、後述の動作確認は curl コマンドでこのヘッダを指定して行っています。

このような国際化対応アプリケーションを Node.js で、正確には Node.js + Express + EJS の環境で作ってみました。

Node.js で国際化対応アプリケーションを作る場合、i18n というパッケージを使うのが手っ取り早いです:
https://www.npmjs.com/package/i18n

ソースコード(app.js)はこんな感じにしました。余計な部分を削ぎ落として、最小限必要な部分だけを残しています(赤字部分が i18n 関連の箇所です):
//. app.js

var express = require( 'express' ),
    fs = require( 'fs' ),
    ejs = require( 'ejs' ),
    i18n = require( 'i18n' ),
    request = require( 'request' ),
    session = require( 'express-session' ),
    app = express();

var port = 3000;

app.set( 'views', __dirname + '/public' );
app.set( 'view engine', 'ejs' );

i18n.configure({
  locales: ['en', 'ja'],
  directory: __dirname + '/locales'
});
app.use( i18n.init );

app.get( '/', function( req, res ){
  res.render( 'index' );
});

app.listen( port );
console.log( "server starting on " + port + " ..." );

今回は英語(en)と日本語(ja)に対応したアプリケーションにしました。

また '/' にアクセスした時に ejs の index テンプレートを使った画面が表示されるような内容にしています。ちなみに index テンプレート(public/index.ejs)の内容は以下のようになっています:
<html>
<head>
<title><%= __('subject') %></title>
</head>
<body>
<h1><%= __('subject') %></h1>
<hr/>
<%= __('body') %>
</body>
</html>


テンプレート内で subject と body という2つの変数を使った表記を行っています。実際にはこれらの部分に言語設定に合わせた内容が表示されることになります。

そして言語ファイルを以下のように用意します:

(英語用: locales/en.json)
{
  "subject": "subject",
  "body": "body"
}


(日本語用: locales/ja.json)
{
  "subject": "サブジェクト",
  "body": "本文"
}

英語設定で利用した場合、上記の subject 変数部分は "subject", body 変数部分は "body" と表示されます。また日本語設定の場合、それぞれ "サブジェクト" と "本文" となります。


これで準備できました。 npm install して実行(node app)します:
$ npm install
$ node app

確認は別の端末から curl で行いました。まずは Accept-Language を en(英語)にしてアクセス:
$ curl http://localhost:3000/ -H 'Accept-Language: en'

<html>
<head>
<title>subject</title>
</head>
<body>
<h1>subject</h1>
<hr/>
body
</body>
</html>
>

次は日本語設定でアクセスした場合:
$ curl http://localhost:3000/ -H 'Accept-Language: ja'

<html>
<head>
<title>サブジェクト</title>
</head>
<body>
<h1>サブジェクト</h1>
<hr/>
本文
</body>
</html>


期待通りに動いています!


アプリケーションの国際化そのものはこれだけで出来ました。そして問題になるのは「どうやって色んな言語用の JSON リソースファイルを用意するか?」です。1つ1つ翻訳サービスなどを使いながら作る、という方法もありますが、そんな言語リソースファイルの翻訳作業は IBM Cloud の Globalization Pipeline サービスを使うと英語のリソースファイルから各言語に翻訳したリソースファイルをまとめて作ることができてとても便利です。このサービスについては以前のブログで使い方も含めて紹介しているので参照ください:
Globalization Pipeline サービスがリリースされました!


と、最後は宣伝でしたw

Node.js / Express を使って API を作成する場合は、こんな感じの記述で実装することになります(3000 番ポートで listen する POST /mypost を実装する場合の例):
var express = require( 'express' );
var app = express();

  :

app.post( '/mypost', function( req, res ){
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});

これで POST /mypost を 3000 番ポートで待ち受ける API ができました。同一アプリケーション内の HTML などからであれば、こんな感じでリクエストを受けることができるようになります:
(jQuery を使って AJAX でポストする例)
  :
$.ajax({
  type: 'POST',
  url: '/mypost',
  data: { a: 1, b: 2, c: 3 }
}).done( function( result ){
  console.log( result );
});
  :

さて、ではこの POST /mypost を外部アプリケーションの HTML からリクエストするにはどうすればいいでしょうか?深く考えずに URL にサーバー名やポート番号も指定して、
(jQuery を使って AJAX でポストする例)
  :
$.ajax({
  type: 'POST',
  url: 'http://servername:3000/mypost',
  data: { a: 1, b: 2, c: 3 }
}).done( function( result ){
  console.log( result );
});
  :

こんな感じ↑にすればいいかというと、たしかに AJAX 部分はこれでいいのですが、結論としてはまだ不充分です。これを実行すると、ブラウザのコンソールには以下のようなメッセージが表示され、期待通りの挙動にはなりません:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://servername:3000/mypost. (Reason: CORS header 'Access-Control-Allow-Origin' missing).

CORS(Cross Origin Resource Sharing) というメカニズムによって、このリクエストはブロックされている、というエラーメッセージです。つまり「 servername:3000 上で動いている POST /mypost という API を外部から呼ぶことはポリシーに反しているのでできない」ということになります。

この外部リクエストを許可する方法はないでしょうか? CORS の設定の問題なので理論上はできるはずですが、その中でも比較的簡単な方法が cors ライブラリを使う方法です:
2017112401


この cors ライブラリを使うと CORS のデフォルトポリシーを変更して、柔軟に API のクロスオリジン対応が可能になります。以下に例を紹介しますが、まずは cors をインストールしておきます:
$ npm install cors

実際にクロスオリジンリクエストを許可するには以下のようにします。まずは全ての API をクロスオリジンで許可する場合です。API の定義を行う前に app.use() で cors を指定します:
var express = require( 'express' );
var app = express();
var cors = require( 'cors' );

app.use( cors() );   //. ここから下に定義する全ての API にクロスオリジン実行を許可する

  :

app.post( '/mypost', function( req, res ){  //. クロスオリジン実行が許可される
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});

また、特定の API に対して個別にクロスオリジン実行を許可する場合は、以下のように指定します:
var express = require( 'express' );
var app = express();
var cors = require( 'cors' );

  :

app.post( '/mypost', cors(), function( req, res ){  //. POST /mypost のクロスオリジン実行は許可される
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});




 

Node.js でウェブアプリを作って、
$ node app.js

みたいな感じで実行する際に、
listen EACCES 0.0.0.0:443
  :
  :

というエラーが出て実行できないことがあります。この原因と回避方法について紹介します。


この "listen EACCES 0.0.0.0:XXX"(XXX 部分は数字)というエラーは node サーバー起動時の指定ポート番号に1024以下の小さい数字が指定されている場合に発生します。一般的には管理者権限を持っていないユーザー権限で 1024 番以下のポートを listen することはできません。

上記エラーの場合は HTTPS(HTTP+SSL) を使いたくて 443 番ポートを指定して実行したケースでした。このポート番号が小さすぎることに加え、管理者権限を持たないユーザーが実行したことで発生していました。

このエラーを回避するには、管理者権限で node サーバーを起動すればよいので、
$ sudo node app.js

といった感じで、sudo を付けて実行することで回避できます。


(参考)
Node.js + Express で SSL を使う


 

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 対応を実装する必要があり、意外と面倒ね。

このページのトップヘ