Express 4 のログ出力とフォームの処理

Express 4 のログ出力とフォームの処理について調べてみました。

コードはこちら。
https://github.com/hoshi-takanori/express-sample

## ログ出力

Express 4 ではログ出力のパッケージは独立して morgan という名前になったようなので、まずこれをインストールします。

```package.json
{
  ...
  "dependencies": {
    "express": "*",
    "morgan": "*",
    "jade": "*"
  }
}
```

```shell-session
$ npm install
```

使い方はこんな感じ。

```app.js
var express = require('express');
var morgan = require('morgan');
var app = express();

...

app.use(morgan({ format: 'dev', immediate: true }));
app.use(express.static(__dirname + '/public'));

...
```

ブラウザからアクセスすると、ログが標準出力に（format: 'dev' を指定したので、色付きで）出力されます。

```shell-session
$ node app
GET / - - ms - -
GET /css/style.css - - ms - -
GET /favicon.ico - - ms - -
```

app.use(morgan()); は複数書くこともでき、書いたものはすべて実行されます。
また、書く場所も重要で、app.use(express.static()); の後ろに書くと、static が実行された場合にはログは出力されません。

## ログのフォーマット

morgan() の引数に文字列を渡すか、format: を指定することで、ログのフォーマットを指定することができます。
まず、デフォルトの表示。

```app.js
app.use(morgan());
//app.use(morgan('default'));
//app.use(morgan({ format: 'default' }));
```

```shell-session
127.0.0.1 - - [Tue, 24 Jun 2014 12:34:56 GMT] "GET / HTTP/1.1" 200 205 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0"
127.0.0.1 - - [Tue, 24 Jun 2014 12:34:56 GMT] "GET /css/style.css HTTP/1.1" 200 111 "http://localhost:3000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0"
```

次に、短いやつ。

```app.js
app.use(morgan('short'));
//app.use(morgan({ format: 'short' }));
```

```shell-session
127.0.0.1 - GET / HTTP/1.0 200 205 - 33.145 ms
127.0.0.1 - GET /css/style.css HTTP/1.0 200 111 - 1.900 ms
```

他にもいろいろな表示方法が可能で、自分でフォーマットを定義することもできます。

また、immediate: に true を指定するかどうかで、表示の内容やタイミングが変わります。
immediate: に true を指定すると、リクエストに基づいて（処理の前に）ログが出力されるのに対して、immediate: に true を指定しない場合は、レスポンスに基づいて（リクエストに対する処理が完了した後に）ログが出力されます。
つまり、immediate: true の場合は処理の結果（ステータスコードや、実行時間など）を反映できなかったり、immediate: true ならリクエストがあった場合には必ず出力されるけど、immediate: true でなければ処理中に何かあったら表示されない可能性がある（？）といった違いがあります。

```app.js
...

app.use(morgan('short'));
app.use(morgan({ format: 'dev', immediate: true }));

...

app.get('/', function (req, res) {
  console.log('processing...');
  res.send('Hello, World!');
});

...
```

```shell-session
GET / - - ms - -
processing...
127.0.0.1 - GET / HTTP/1.0 200 13 - 6.657 ms
```

## 動作の切り替えと出力先の変更

開発中とデプロイ時（production モード）で動作を切り替えるには次のようにします。

```app.js
...

if (app.get('env') == 'production') {
  app.use(morgan());
} else {
  //app.use(morgan('short'));
  app.use(morgan({ format: 'dev', immediate: true }));
}

...
```

```run.sh
#!/bin/sh

export NODE_ENV=production

node app
```

また、開発時は標準出力、デプロイ時にはファイルにログを出力、なんてこともできます。

```app.js
...

if (app.get('env') == 'production') {
  var fs = require('fs');
  var stream = fs.createWriteStream(__dirname + '/log.txt', { flags: 'a' });
  app.use(morgan({ stream: stream }));
} else {
  ...
}

...
```

さらに、フィルター関数を指定することなどもできるようです。

## フォームの処理 (GET)

適当なフォームを作ってみます。まずは GET。
app.get() の関数の中で req.param() を使うとパラメータが取れます。簡単。

```app.js
...

app.get('/', function (req, res) {
  var username = req.param('username');
  res.render('index', { title: 'Express Sample', username: username });
});

...
```

```view/index.jade
  ...
    h1= title
    if username
      p Welcome, #{username}.
    else
      p What's your name?
      form(method='get', action='/')
        input(type='text', name='username')
        input(type='submit', value='OK')
```

## フォームの処理 (POST)

次に、POST。POST のパラメータを取得するには body-parser が必要です。

```package.json
{
  ...
  "dependencies": {
    "express": "*",
    "morgan": "*",
    "body-parser": "*",
    "jade": "*"
  }
}
```

```app.js
var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var app = express();

...

app.use(bodyParser.urlencoded({ extended: false }));

...
```

そして、app.post() で POST 処理用の関数を登録します。

```app.js
...

app.post('/login', function (req, res) {
  var username = req.param('username');
  var password = req.param('password');
  if (username == password) {
    res.render('index', { title: 'Express Sample', username: username });
  } else {
    res.render('index', { title: 'Express Sample', error: 'Unknown username or password.' });
  }
});

...
```

```view/index.jade
  ...
    h1= title
    if username
      p Welcome, #{username}.
    else
      if error
        p= error
      else
        p What's your name?
      form(method='post', action='/login')
        input(type='text', name='username')
        input(type='password', name='password')
        input(type='submit', value='OK')
```

## リダイレクト

ログイン成功時には、app.post() で直接結果を返すのではなく、いったん app.get() にリダイレクトすると良いかもしれません。

```app.js
...

app.post('/login', function (req, res) {
  var username = req.param('username');
  var password = req.param('password');
  if (username == password) {
    res.redirect('/?username=' + encodeURIComponent(username));
  } else {
    res.render('index', { title: 'Express Sample', error: 'Unknown username or password.' });
  }
});

...
```

もっとも、username のような情報は URL に埋め込むよりも、クッキーに保存（そのまま保存するのはセキュリティ的に問題がありますが）したり、セッションを使ったりすべきでしょう。

## クッキー

クッキーの値を取得するには cookie-parser が必要です。

```package.json
{
  ...
  "dependencies": {
    "express": "*",
    "morgan": "*",
    "body-parser": "*",
    "cookie-parser": "*",
    "jade": "*"
  }
}
```

```app.js
var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var app = express();

...

app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
//app.use(cookieParser('cookie secret'));

...
```

'cookie secret' は signed cookie というものを使う場合に、任意の文字列を指定します。（signed cookie はクッキーの値の改ざんを防ぐもので、クッキーの値を隠すものではありません。）

そして、app.get() と app.post() でクッキーの処理を行います。

```app.js
...

app.get('/', function (req, res) {
  var username = req.cookies.username;
  //var username = req.signedCookies.username;
  res.render('index', { title: 'Express Sample', username: username });
});

app.post('/login', function (req, res) {
  var username = req.param('username');
  var password = req.param('password');
  if (username == password) {
    res.cookie('username', username);
    //res.cookie('username', username, { signed: true });
    res.redirect('/');
  } else {
    res.render('index', { title: 'Express Sample', error: 'Unknown username or password.' });
  }
});

app.post('/logout', function (req, res) {
  res.clearCookie('username');
  res.redirect('/');
});

...
```

```view/index.jade
  ...
    h1= title
    if username
      p Welcome, #{username}.
      form(method='post', action='/logout')
        input(type='submit', value='Logout')
    else
      if error
        p= error
      else
        p Please login.
      form(method='post', action='/login')
        input(type='text', name='username', placeholder='Username')
        br
        input(type='password', name='password', placeholder='Password')
        br
        input(type='submit', value='Login')
```

セキュリティやプライバシーを考えると、クッキーに生の値を埋め込むのではなく、セッションを使うべきでしょう。

## セッション

セッションを利用するには express-session が必要です。

```package.json
{
  ...
  "dependencies": {
    "express": "*",
    "morgan": "*",
    "body-parser": "*",
    "cookie-parser": "*",
    "express-session": "*",
    "jade": "*"
  }
}
```

```app.js
var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var app = express();

...

app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
  secret: process.env.SESSION_SECRET || 'session secret',
  resave: false,
  saveUninitialized: false
}));

...
```

'session secret' はセッション用のクッキーの値の改ざんを防ぐものです。デプロイ時には秘密の値にできるように、環境変数を参照しています。

そして、app.get() と app.post() のクッキーの処理を、セッションを使って書き換えます。

```app.js
...

app.get('/', function (req, res) {
  var username = req.session.username;
  res.render('index', { title: 'Express Sample', username: username });
});

app.post('/login', function (req, res) {
  var username = req.param('username');
  var password = req.param('password');
  if (username == password) {
    req.session.username = username;
    res.redirect('/');
  } else {
    res.render('index', { title: 'Express Sample', error: 'Unknown username or password.' });
  }
});

app.post('/logout', function (req, res) {
  req.session.destroy();
  res.clearCookie('connect.sid', { path: '/' });
  res.redirect('/');
});

...
```

セッションの永続化とログインの処理もやりたいんだけど、何が何やら。

## リンク
https://github.com/expressjs/morgan
https://github.com/expressjs/body-parser
https://github.com/expressjs/cookie-parser
https://github.com/expressjs/session