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

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

タグ:express

Node.js + Express のプログラミング環境で作ったアプリケーションで、「全てのリクエストに対する共通の前処理」を行う方法を紹介します。

Node.js + Express を使って HTTP リクエストを処理するアプリケーションの例として以下のようなコードを想定します(ルーティングを使っていませんが、使っている場合でも同様に可能です):
var express = require( 'express' );
var bodyParser = require( 'body-parser' );
var app = express();

app.use( bodyParser.urlencoded( { extended: true, limit: '10mb' } ) );
app.use( bodyParser.json() );

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'get /' }, 2, null ) );
  res.end();
});

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'get /hello.' }, 2, null ) );
  res.end();
});

app.post( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'post /hello.' }, 2, null ) );
  res.end();
});


var port = process.env.port || 3000;
app.listen( port );
console.log( 'server started on ' + port );

上記例では GET /, GET /hello, POST /hello という3種類の HTTP リクエストに対するハンドラが定義されていて、それぞれに対応する異なる処理が個別に行われるようになっています。HTTP リクエストはもっと多くても少なくても構いません。

このような条件下において「全ての HTTP リクエストに対して、リクエストパラメータに id=XXX が含まれていたら、それを記録する」という処理を追加したい場合にどうするか? というのが今回のお題です。その実装例が以下赤字部分です(以下ではシンプルに id の値を記録ではなく表示しているだけです):
var express = require( 'express' );
var bodyParser = require( 'body-parser' );
var app = express();

app.use( bodyParser.urlencoded( { extended: true, limit: '10mb' } ) );
app.use( bodyParser.json() );

//. 全てのリクエストに対して前処理
app.use( '/*', function( req, res, next ){
  //. HTTP リクエスト URL に id=XXX というパラメータが含まれていたら表示する
  var id = req.query.id;
  if( id ){
    console.log( 'id: ' + id );  
  }
  next();  //. 個別処理へ
});

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'get /' }, 2, null ) );
  res.end();
});

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'get /hello.' }, 2, null ) );
  res.end();
});

app.post( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'post /hello.' }, 2, null ) );
  res.end();
});


var port = process.env.port || 3000;
app.listen( port );
console.log( 'server started on ' + port );


app.use( '/path', function( req, res, next ){ ... } ); で /path への全 HTTP リクエスト(GET, POST, PUT, DELETE, ..)のハンドラを用意します。この /path 部分を '/*' とすることで全てのパスへの全てのリクエストに対するハンドラが定義できます。

このハンドラの中で id=XXXX というパラメータが指定されているかを調べ、指定されていたらその値(req.query.id)を取り出して表示しています。実際にはここで取り出した id 値を表示するだけでなく、記録したり、別の処理に使ったりすることを想定しています。

この処理で最も大事なのは最後に next(); を実行していることです。シングルスレッド処理の Node.js ではこれでハンドリング処理を終了とするのではなく、「次の処理へ」と指示することで個別のハンドラへの処理も行わせています。これによって共通の事前処理をした上で、個別処理も行う、という実装が可能になります。



Node.js + Express で作るウェブアプリケーションを HTTPS 対応し、更に「HTTPS 以外の通信を認めない」ように設定してみます。なお SSL 対応用のドメイン取得や証明書の取得は完了しているものとし、ドメインは mydomain.com 、秘密鍵ファイルと証明書ファイルがそれぞれ、
 ・/etc/letsencrypt/live/mydomain.com/privkey.pem (秘密鍵)
 ・/etc/letsencrypt/live/mydomain.com/cert.pem (証明書)
に存在しているものと仮定して以下を説明します。


【Node.js + Express で HTTPS 通信】
Node.js + Express で HTTPS 通信を行うには https モジュールを利用します:
var express = require( 'express' );
var app = express();

var fs = require( 'fs' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var https_port = 8443;

https_server.listen( https_port );

console.log( "server starging on " + https_port + ' ...' );

https モジュールを使って 8443 番ポートで待ち受ける https サーバーを作ります。その際に秘密鍵や証明書を指定することで SSL 通信を行うことができるようにしています。

このファイルを Node.js で実行して、ウェブブラウザや curl コマンドで https://mydomain.com:8443/hello にアクセスすることで動作を確認することができます(curl コマンドで localhost など、異なるドメインにアクセスする場合は $ curl https://localhost:8443/hello --insecure といった具合に --insecure オプションを付けて実行することで確認できます)。

また、上記の方法だと https のみアクセスできます(http だと接続エラー)が、http / https 両方でのアクセスに対応するには以下のように改良します:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

これを実行すると、http を 8080 番ポートで、https を 8443 番ポートで同時に待受けてレスポンスを返すことができるようになりました。ウェブブラウザや curl コマンドで http://mydomain.com:8080/hello や https://mydomain.com:8443/hello にアクセスして挙動を確認してください。


【HTTP 通信をしない(HTTPS へ転送)】
最後に、上記プログラムを更に改良して HTTP 通信をしない(強制的に HTTPS 通信に転送する)ための設定を加えます。それには Strict-Transport-Security ヘッダを付けてレスポンスを返すことで実現できます:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

//add HSTS
app.use( function( req, res, next ){
  res.setHeader( 'Strict-Transport-Security', 'max-age=15552000' );
  next();
});

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

HSTS(HTTP Strict Transport Security) という仕組みを強制することで HTTP でリクエストを受け取っても HTTPS に強制的に変更(リダイレクト)されて通信を続ける、というものです。リダイレクトさせるには HTTP そのものを閉じるわけにはいかない(リダイレクト直前のリクエストを受け取らないといけない)ので、このようなレスポンスヘッダによって実現しています:

2019112500



【参考】
良い感じにHTTPS対応したexpress(Node.js)サーバを立てる
node.jsによるHTTPSサーバの作り方

某アプリを Slack 対応する経緯で Slack API の中の、特に認証/認可を司る OAuth API を使う機会があったので自己まとめです。

もともとやりたかったのはウェブアプリに Slack アカウントでログインして、そのログインした人の権限でチャネル一覧を取得し(※)、ウェブアプリから指定したチャネルにメッセージを書き込む、ということでした。この中の※部分までを Node.js + Express + EJS で実現したコードを Github に公開しています(後述)。


実際に試してみるにはまず Slack に対象アプリケーションを登録する必要があります。 https://api.slack.com/apps を開いてログインし、"Create New App" ボタンをクリックしてウェブアプリを登録します:
2019052401


登録するアプリの名前と、対象ワークスペースを指定します(つまり同じアプリを複数のワークスペースで使いたい場合は、アプリを複数登録する必要があります)。以下では名前は "Slack OAuth Sample"、ワークスペースは "dotnsf" を指定しています(ワークスペースはログインしたユーザーが利用可能なワークスペース一覧から選択します)。最後に "Create App" ボタンをクリックして作成します:
2019052402


すると指定したアプリケーションの API 設定画面に切り替わります。画面左上に入力したアプリ名がデフォルトアイコンと一緒に表示されていて、"Basic Information" メニューが選択されていることを確認します:
2019052403


この画面を下スクロールすると App Credentials という項目があります。この中の Client IDClient Secret の値を後で使うので、どこかにコピーしておくか、いつでもこの画面を開ける状態にしておきましょう。なお Client ID の値は画面内に表示されていますが、Client Secret の値は初期状態では非表示になっています。"Show" ボタンをクリックして内容を表示し、その表示された値をあとで使うことに注意してください。またこれらの値は他の人には教えないように、自分で管理する必要があります:
2019052404


次に画面左のメニューから "OAuth & Permissions" を選び、少し下にスクロールすると Redirect URLs という項目があります。ここにウェブアプリケーションを動かす際のコールバック URL を登録しておく必要があります。"Add New Redirect URL" ボタンをクリックします:
2019052405


すると Redirect URL を追加する画面になるので、http(s)://サーバー名/slack/callback と入力します。この値は開発時には開発時用のサーバー名とポート番号、本番環境では本番環境用のサーバー名を指定する必要があります。下図では開発時向けに localhost の 6010 番ポートで動かす想定で http://localhost:6010/slack/callback と指定しています。ここの値は実際の環境に合わせて適宜変更してください。入力し終わったら "Add" ボタンをクリックして、その後 "Save URLs" ボタンをクリックします:
2019052406


画面上部に "Success!" というメッセージが表示されればリダイレクト URL の設定は完了です。正しい Redirect URLs が登録されたことを確認します:
2019052407


続けて、このアプリで利用する Slack 機能のスコープを指定します。実は OAuth 認証だけであればここの設定は不要なのですが、今回のデモアプリでは OAuth 認証後にログインユーザーが参照できるチャネルの一覧を取得して表示する、という機能が含まれています。また実際のアプリケーションではそのアプリケーションで実装する機能によって、ここでスコープを追加する必要があります:
2019052408


今回はログインユーザーが利用できるチャネル一覧を取得するため、"channels:read" スコープを追加します。また他に必要なスコープがあればここで追加します。最後に "Save Changes" ボタンをクリックして変更を反映します:
2019052409


これで Slack API 側の設定は完了しました。

では改めて Github からアプリケーションを取得します。Node.js がインストール済みの実行サーバー上で以下の URL を指定して git clone するか、ソースコードをダウンロード&展開してください:
 https://github.com/dotnsf/slack-oauth

2019052410


ソースコード内の settings.js をテキストエディタで開き、exports.slack_client_id の値と exports.slack_client_secret の値を上記で確認した client_id と client_secret の値に(コピー&ペーストなどで)変更して、保存してください。

なお、このサンプルアプリケーションでは以下のリクエスト API(?)が用意されていて、これらを明示的&内部的に使って動きます:
リクエスト API用途
GET /ユーザーがアクセスする唯一のページ。アクセス時に認証情報がセッションに含まれているとチャネル一覧が表示される。認証情報がセッションに含まれていない場合は認証前とみなして「ログイン」ボタンを表示する
GET /slack/loginユーザーページで「ログイン」ボタンをクリックした時に実行される。Slack の OAuth 認証ページにリダイレクトされる
GET /slack/callbackSlack の OAuth 認証ページで Authorize された時のリダイレクトページ。この URL が Slack OAuth 設定時に指定されている必要がある。アクセストークンを暗号化してセッションに保存し、GET / へリダイレクトされる
POST /slack/logoutログアウト(セッション情報を削除)する
GET /channelsチャネル一覧を取得する。認証後にユーザーページが表示されると内部的にこの REST API が AJAX 実行されて、画面にチャネル一覧が表示される。


では実際に起動してみます。起動サーバーにログインし、ソースコードのあるフォルダに移動した状態で、以下を実行します:
$ npm install
$ node app

そしてウェブブラウザで起動中のアプリケーションにアクセスします。以下の例では localhost:6010 でアプリケーションが起動されている想定になっているので、http://localhost:6010/ にアクセスします。上記の GET / が実行され、ログイン前のシンプルなページが表示されます。ここで "Login" を選択します:
2019052411


GET /slack/login が実行され、ブラウザは Slack API の OAuth 認証ページにリダイレクトされます。アプリケーションが利用する scope が表示され、このまま認証処理を許可するかどうかを聞かれます。許可する場合は "Authorize" を選択します:
2019052412


Authorize を選択すると認証と認可が完了し、そのアクセストークンが GET /slack/callback へ渡されます。そこでアクセストークンを暗号化してセッションに含めます。この状態であらためてトップページ(GET /)が表示され、ログイン処理が住んでいるのでログイン後の画面が表示されます。AJAX で GET /channels が実行され、ログインユーザーが参照することのできるチャネルの一覧が表示されれば成功です:
2019052413


以上、Slack API の OAuth を使ってウェブアプリケーションから Slack の認証を行い、認証ユーザーの権限で Slack API を外部から実行する、というアプリケーションのサンプルを作って実行するまでの手順紹介でした。


 

以前に express-ipfilter ライブラリを使って、Node.js アプリの IP アドレスフィルタリングを行うサンプルを紹介しました:
http://dotnsf.blog.jp/archives/1066182158.html

↑ここで紹介したサンプルは一応動くものですが、アプリケーションを IBM Cloud の Cloud Foundry アプリとしてデプロイすると( IP アドレスフィルタリングが)正しく動かないことがわかりました。原因は Cloud Foundry 内のルーティングで x-forwarded-for ヘッダの情報が変わってしまい、正しい IP アドレスを取得できなくなってしまうようでした。

IBM Cloud の Cloud Foundry 環境でもこの IP アドレスフィルタリングを有効にするには、フィルタリングを行う前に Express() の use メソッドを使って、
app.use( 'trust proxy', true );

を呼び出してからフィルタリングを行う必要があります。

(解説)
http://expressjs.com/ja/api.html



 

Node.js を使っていて 404 エラーや 500 エラーなどが発生した場合に表示されるエラーページをカスタマイズできないか、と思って挑戦してみました。

今回挑戦した環境では Node.js にフレームワークの Express と、テンプレートエンジンに EJS を使いました。エラーページを EJS で作ることを想定しています。

まず、JavaScript のコードはこんな感じです:

app.js
// app.js
var express = require( 'express' );
var app = express();

//. EJS テンプレートエンジン
app.set( 'views', __dirname + '/templates' );
app.set( 'view engine', 'ejs' );

//. / へのアクセスは正常にできる
app.get( '/', function( req, res ){
  res.render( 'index', {} );
});

//. /err へのアクセスは 500 エラーとする
app.get( '/err', function( req, res ){
  res.render( 'index', { value: novalue } ); //. novalue 変数が未定義なので 500 エラーが発生する
});

//. 有効なルーティングを上記に記述
//. /, /err 以外のパスは 404 エラー

//. 404 エラーが発生した場合、
app.use( function( req, res, next ){
  res.status( 404 ); //. 404 エラー
  res.render( 'err404', { path: req.path } ); //. 404 エラーが発生したパスをパラメータとして渡す
});

//. 500 エラーが発生した場合、
app.use( function( err, req, res, next ){
  res.status( 500 ); //. 500 エラー
  res.render( 'err500', { error: err } ); //. 500 エラーの内容をパラメータとして渡す
});

var port = 3000;
app.listen( port );
console.log( 'server started on ' + port );

上記を補足すると、Express でルーティングを2つ定義しています。1つめがドキュメントルート(/)へのアクセスで、この場合は正常な処理として(後述の)index.ejs を表示します。

2つめは /err へのアクセスで、こちらの場合はわざと 500 エラーを発生させています(index.ejs でページを表示させるような記述にしていますが、実際にはこの中で使われている novalue という変数が未定義なので、そこで 500 エラーが発生するようにしています)。

ルーティングとしてはこの2つ(/ と /err)だけを定義しているので、これら以外のパス(例えば /home)にアクセスがあった場合は 404 エラーが発生します。404 エラーが発生した場合の処理としては err404.ejs というテンプレートを使って、アクセスしたパス(/home)が存在していない、という旨のエラーページを表示します。

最後に 500 エラーが発生した場合の処理を定義しています。ここでは err500.ejs というテンプレートを使って、エラーの内容をあわせて表示します。上記で /err ページにアクセスすると 500 エラーが発生するので、この err500.ejs のページが表示されることになります。


次に上記で使うことにした3種類のテンプレート(index.ejs, err404.ejs, err500.ejs)を用意します。それぞれ以下のような内容にしました:

index.ejs
<html>
<head>
<title>index</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
↑単に index と表示されるだけのシンプルなページ


err404.ejs
<html>
<head>
<title>err404</title>
</head>
<body>
<h1>err404</h1>
<%= path %> is not defined nor found.
</body>
</html>
↑ "(エラーがあったパス)is not defined nor found." というエラーメッセージが表示されるページ


err500.ejs
<html>
<head>
<title>err500</title>
</head>
<body>
<h1>err500</h1>
Error: <%= error %>
</body>
</html>
↑ "Error: (エラー内容)" というエラーメッセージが表示されるページ


app.js ではこれらのテンプレートファイルが templates/ フォルダに存在していることを前提に参照するようにしています。したがって templates/ フォルダを作って、その中にこれら3つの .ejs ファイルを格納しておきます。これで準備OK。


実際に動かして試してみました。まずは正常系、/ へのアクセスは普通に問題なくできます:
2018070401


次に /home にアクセスして、わざと 404 エラーを発生させてみた時の画面です。err404.ejs で定義されたテンプレートを使って期待通りにエラーページが表示されています:
2018070402


最後に /err にアクセスして、わざと 500 エラーを発生させてみた時の画面です。こちらも err500.ejs で定義されたテンプレートを使って期待通りにエラー内容が表示されています:
2018070403


上記コードのサンプルをこちらに用意しました:
https://github.com/dotnsf/nodejserror


これで Node.js でもカスタムエラーページを作れることがわかりました。

このページのトップヘ