Node.js v12でTLSv1.3のテストサーバを手軽に構築

# 前置き
Node.jsがv12でTLSv1.3に対応しました。（[リリースノート](https://nodejs.org/ja/blog/release/v12.0.0/)）
クライアントアプリケーションの開発テストをしているときにTLSv1.3のサーバが必要になることがあるのですが、単純なリクエスト・レスポンスを確認するだけなら、Node.jsはOSを問わず手軽に用意できて便利です。
この記事では、WindowsおよびMacでNode.jsを使ったTLSv1.3対応のHTTPSサーバを構築する手順をまとめます。

# 環境
## Windows
* OS: Windows 10 Version 1809 (64bit)
* Node.js: v12.2.0
* OpenSSL: OpenSSL 1.1.1b  26 Feb 2019

## Mac
* OS: Mac OS X 10.14.3 Mojave
* Xcode: 10.2.1 (10E1001)
* nodebrew: 1.0.1
* Node.js: v12.2.0
* OpenSSL: LibreSSL 2.6.5

# Node.jsのインストール
## Windows
[Node.jsの公式サイト](https://nodejs.org/ja/)より最新版のインストーラをダウンロードします。
ダウンロードしたインストーラを起動し、画面の指示に従ってインストールを完了させます。

## Mac
バージョン切替やアンインストール[^1]がしやすいように、公式のパッケージではなくバージョン管理ツールを使ってNode.jsをインストールします。
今回は nodebrew というツールを使用します。
[nodebrewのREADME](https://github.com/hokaccha/nodebrew/blob/master/README.md)に従い、ターミナルを起動してセットアップを行います。
ターミナルは、アプリケーションフォルダ内のユーティリティフォルダの中にあります。

[^1]: MacにはWindowsのようなプログラムのアンインストール機能がないため、公式のパッケージでインストールをすると、テストサーバとして使用した後でNode.jsをきれいにアンインストールするのがやや面倒になります。

```bash:ターミナル
# nodebrewのセットアップを行うコマンド
$ curl -L git.io/nodebrew | perl - setup
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
$ source ~/.bash_profile
$ nodebrew help
nodebrew 1.0.1
# 以下略
```

nodebrewで最新版のNode.jsをインストールし、それを使用するように指定します。

```bash:ターミナル
$ nodebrew install latest
$ nodebrew use latest
$ node -v
v12.2.0
```

# OpenSSLのインストール
## Windows
[OpenSSLのWindowsバイナリを配布しているサイト](https://wiki.openssl.org/index.php/Binaries)からダウンロードします。

今回は[ICS Download](http://wiki.overbyte.eu/wiki/index.php/ICS_Download)の[OpenSSL Binaries Win-64 1.1.1b requires ICS V8.57 or later](http://wiki.overbyte.eu/arch/openssl-1.1.1b-win64.zip)を使用します。
ダウンロードしたzipファイルは解凍しておきます。

### OpenSSL設定ファイルの準備
使用するバイナリによっては、OpenSSLの設定ファイルの準備が別途必要になります。
上記ICS Downloadのバイナリの場合、以下のように設定ファイルを準備します。

1. 適当な場所に openssl.cnf という名前のファイルを作成します。
1. 作成した openssl.cnf ファイルをメモ帳などで開き、[OpenSSL公式のソースに含まれているopenssl.cnfのサンプル](https://raw.githubusercontent.com/openssl/openssl/master/apps/openssl.cnf)の内容をまるごとコピー＆ペーストして保存します。
1. C:\Program Files\Common Files フォルダの中に SSL という名前のフォルダを作成します。
1. 作成した SSL フォルダの中に openssl.cnf ファイルを移動させます。

## Mac
OSにあらかじめ入っているOpenSSL（LibreSSL）を使用しますので、インストールは不要です。

# 秘密鍵とサーバ証明書の作成
## Windows
ダウンロードしたWindowsバイナリのフォルダの中にある openssl.exe を起動し、表示されたコマンドプロンプトで、秘密鍵とサーバ証明書を作成する以下のコマンドを実行します。

-subj オプションの CN= の値には、サーバのIPアドレスを指定します。実際のテスト環境に合わせて値を書き換えてください。
今回の説明では 127.0.0.1 のIPアドレスでサーバ証明書を作成します。

```shell:openssl.exe
# 秘密鍵とサーバ証明書を作成するコマンド
OpenSSL> req -subj '/C=JP/ST=TestState/O=TestCompany/CN=127.0.0.1' \
 -x509 -nodes -days 3650 -newkey rsa:2048 \
 -keyout server-key.pem \
 -out server-cert.pem
```

コマンド実行後、openssl.exe があるフォルダの中に秘密鍵（server-key.pem）とサーバ証明書（server-cert.pem）のファイルが作成されていることを確認します。

## Mac
ターミナルで、秘密鍵とサーバ証明書を作成する以下のコマンドを実行します。

-subj オプションの CN= の値には、サーバのIPアドレスを指定します。実際のテスト環境に合わせて値を書き換えてください。
今回の説明では 127.0.0.1 のIPアドレスでサーバ証明書を作成します。

```bash:ターミナル
$ openssl req -subj '/C=JP/ST=TestState/O=TestCompany/CN=127.0.0.1' \
 -x509 -nodes -days 3650 -newkey rsa:2048 \
 -keyout ~/server-key.pem \
 -out ~/server-cert.pem
```

コマンド実行後、ホームフォルダの中に秘密鍵（server-key.pem）とサーバ証明書（server-cert.pem）のファイルが作成されていることを確認します。

# サーバプログラムの作成
## Windows
1. 任意の作業フォルダを作成します。
1. 先ほど作成した秘密鍵（server-key.pem）とサーバ証明書（server-cert.pem）のファイルを作業フォルダの中に移動させます。
1. 作業フォルダの中に test-server.js という名前のファイルを作成します。（ファイル名は任意です）
1. 作成した test-server.js ファイルをメモ帳などで開き、後述する[サーバプログラム](https://qiita.com/marumaru/items/0c73eee045183a440152#サーバプログラム)の内容をコピー＆ペーストして保存します。

## Mac
作業フォルダを作成します。（フォルダ名は任意です）

```bash:ターミナル
$ mkdir ~/test-server
```

先ほど作成した秘密鍵（server-key.pem）とサーバ証明書（server-cert.pem）のファイルを、作業フォルダの中に移動させます。

```bash:ターミナル
$ mv ~/server-key.pem ~/test-server/
$ mv ~/server-cert.pem ~/test-server/
```

test-server.js ファイルを作成し、後述する[サーバプログラム](https://qiita.com/marumaru/items/0c73eee045183a440152#サーバプログラム)の内容をコピー＆ペーストして保存します。（ファイル名は任意です）

```bash:ターミナル
$ vi ~/test-server/test-server.js
```

viコマンドでファイル編集画面が表示された後は、サーバプログラムの内容をコピー＆ペースト→escキーを押す→`:wq`と入力するとファイルを保存して終了できます。

## サーバプログラム
以下のプログラム内で指定しているサーバのポート番号やIPアドレスの値は、実際のテスト環境に合わせて書き換えてください。
今回の説明では、ポート番号に 8443、IPアドレスに 127.0.0.1 を使用します。

```js:test-server.js
const https = require('https')
const fs = require('fs')
const port = 8443        // サーバのポート番号を指定
const host = '127.0.0.1' // サーバのIPアドレスを指定

const options = {
  key: fs.readFileSync('server-key.pem'),   // 秘密鍵のファイルを指定
  cert: fs.readFileSync('server-cert.pem'), // サーバ証明書のファイルを指定
  maxVersion: 'TLSv1.3',
  minVersion: 'TLSv1.3',
}

// サーバインスタンス作成
const server = https.createServer(options)

// リクエストイベントリスナー
server.on('request', (req, res) => {
  // テスト用のリクエストログを適宜出力
  console.log('**********************')
  console.log('*   Client Request   *')
  console.log('**********************')
  console.log('HTTP Method:', req.method)
  console.log('Request URL:', req.url)
  console.log('HTTP Version:', req.httpVersion)
  console.log('HTTP Header:', req.headers)
  req.on('data', (chunk) => {
    console.log('HTTP Body:', chunk.toString('utf8'))
  })
  // レスポンス送信
  res.setHeader('Content-Type', 'text/plain; charset=utf-8')
  res.writeHead(200)
  res.write('Hello World!\n')
  res.end()
})

// サーバ起動
server.listen(port, host, () => {
  console.log(`Starts the server at https://${host}:${port}/`)
})
```

# サーバの起動
## Windows
Windowsメニューから Node.js command prompt を起動し、作業フォルダへ移動します。
（Windows PowerShellやコマンドプロンプトでも同じように実行できます）

```shell:Node.js_command_prompt
# 作業フォルダへ移動するコマンド
> cd 作業フォルダのパス
```

nodeコマンドでサーバプログラムを実行します。

```shell:Node.js_command_prompt
# サーバプログラムを実行するコマンド
> node test-server.js
Starts the server at https://127.0.0.1:8443/
```

## Mac
ターミナルで作業フォルダへ移動し、nodeコマンドでサーバプログラムを実行します。

```bash:ターミナル
$ cd ~/test-server/
$ node test-server.js
Starts the server at https://127.0.0.1:8443/
```

# 接続の確認
TLSv1.3が使用可能なブラウザで https://サーバのIPアドレス:ポート番号/ へアクセスします。
自己署名証明書によるエラーは無視して、アクセスを続けてください。
アクセスに成功すると、ブラウザに「Hello World!」と表示され、サーバを起動しているコマンドプロンプトやターミナルの画面にリクエストのログが出力されます。

```Shell:Node.js_command_prompt
# WindowsのFirefoxでアクセスした場合のログ出力例
**********************
*   Client Request   *
**********************
HTTP Method: GET
Request URL: /
HTTP Version: 1.1
HTTP Header: {
  host: '127.0.0.1:8443',
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:66.0) Gecko/20100101 Firefox/66.0',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'accept-language': 'ja,en-US;q=0.7,en;q=0.3',
  'accept-encoding': 'gzip, deflate, br',
  dnt: '1',
  connection: 'keep-alive',
  'upgrade-insecure-requests': '1'
}
```

ブラウザの開発ツールを開いた状態でアクセスすると、TLSv1.3のプロトコルバージョンで接続していることが確認できます。
![Firefoxでのアクセス確認結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79324/7ae807ba-dde3-70cd-e2c0-ca8436221d37.png)

サーバを停止する際は、サーバを起動しているコマンドプロンプトやターミナルの画面で Ctrl+C を押してください。

以上でサーバ構築は完了です。

# 後書き
ApacheやnginxでHTTPSサーバを立てる場合、慣れていないと設定ファイルの作成などでつまずき構築に時間がかかってしまうことがあります。
詳細なログやカスタマイズは不要でとりあえずテストサーバを用意したいようなとき、Node.jsは便利な選択肢のひとつになるのではないでしょうか。

---
# 参考サイト
本サーバ構築にあたり、以下のサイトの記事を特に参考に致しました。
ありがとうございました。

* [HTTP | Node.js v12.2.0 Documentation](https://nodejs.org/api/http.html#http_class_http_server)
* [HTTPS | Node.js v12.2.0 Documentation](https://nodejs.org/api/https.html)
* [TLS (SSL) | Node.js v12.2.0 Documentation](https://nodejs.org/api/tls.html)
* [理解してるつもりの SSL/TLS でも、もっと理解したら面白かった話 · けんごのお屋敷](http://tkengo.github.io/blog/2015/12/01/https-details/)
* [TLS1.3で使える・使えない暗号アルゴリズム - Qiita](https://qiita.com/s_machida/items/0f6ef0466503c0f00839)
* [opensslコマンドで自己証明書をいっぱつで作成する - Qiita](https://qiita.com/reoring/items/0da3aa9935c68aa95e88)