Node.jsを使ったローカルHTTPSサーバーを走らせる

# 目的
OAuth2認可を使ってオンラインサービスと連携させることができるコマンドラインツールを作成中です。ユーザー認証後に認可コードをこのツールで受け取りたいと思っているので、`127.0.0.1`で走るHTTPSサーバーをリダイレクト先として用意したい。
その検証として、まずローカルHTTPSサーバーをNode.jsで走らせてみました。言語は実際の使う予定のTypeScriptとなっています。

## 何を使う?
* Node.js
* httpsモジュール
* TypeScript

# 準備
## まずはHTTPで地ならし
私はNode.jsローカル環境ではHTTPサーバーすらを立てた経験がないので、まずはHTTPから試してみる。

```typescript:HTTPサーバーならばこれでOK。
import http from 'http';

const PORT = 9090;
const HOST = '127.0.0.1';

function startHttpServer(portNumber: number, hostName: string): Promise<http.IncomingMessage> {
  return new Promise((resolve, reject)=>{
    http.createServer((request, response)=>{
      response.writeHead(200, {
        'Content-Type': 'text/plain'
      });
      response.end(`welcome to local page!`);
      resolve(request);
    }).listen(portNumber, hostName);
  });
}

(async (portNumber, hostName)=>{
  const req = await startHttpServer(portNumber, hostName);
})(PORT, HOST);
```
HTTPは楽勝。
<img width="377" alt="スクリーンショット 2019-12-21 18.16.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537237/e98d597d-00e6-32f8-db22-6cc5285e09b0.png">

## 単純にHTTP -> HTTPSで置き換えてやってみる
単純にhttpモジュールをhttpsモジュールに置き換えて見ました。結論から言いますと以下のコードは機能しません。

```typescript:単純な置き換えでは機能しない。
import https from 'https';
import { IncomingMessage } from 'http';

const PORT = 9090;
const HOST = '127.0.0.1';

function startHttpServer(portNumber: number, hostName: string): Promise<IncomingMessage> {
  return new Promise((resolve, reject)=>{
    https.createServer((request, response)=>{
      response.writeHead(200, {
        'Content-Type': 'text/plain'
      });
      response.end(`welcome to local HTTPS page!`);
      resolve(request);
    }).listen(portNumber, hostName);
  });
}

(async (portNumber, hostName)=>{
  const req = await startHttpServer(portNumber, hostName);
})(PORT, HOST);
```
実行すると、当然ですが、エラーになります。
<img width="584" alt="スクリーンショット 2019-12-21 20.09.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537237/353cd9e7-bed4-b0d4-2d4c-7bf87b5c98e1.png">

## 何が必要なのか？
[公式ドキュメント](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener)を読んでみます。目につくのが```options```で指定している。keyとcertです。

```javascript:https.createServer([options][,requestListener])
const options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};
```
keyとcertはそれぞれ何かと言うと、同じく[公式ドキュメントのTLSモジュール](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options)をみると下のように書いてありました。PEMフォーマットのプライベートキーですね。

> key \<string> | \<string[]> | \<Buffer> | \<Buffer[]> | \<Object[]>
> Private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with options.passphrase. Multiple keys using different algorithms can be provided either as an array of unencrypted key strings or buffers, or an array of objects in the form {pem: <string|buffer>[, passphrase: <string>]}. The object form can only occur in an array. object.passphrase is optional. Encrypted keys will be decrypted with object.passphrase if provided, or options.passphrase if it is not.

certについて上記のページに下の説明があります。PEMフォーマットの証明書ですね。

> cert \<string> | \<string[]> | \<Buffer> | \<Buffer[]> 
> Cert chains in PEM format. One cert chain should be provided per private key. Each cert chain should consist of the PEM formatted certificate for a provided private key, followed by the PEM formatted intermediate certificates (if any), in order, and not including the root CA (the root CA must be pre-known to the peer, see ca). When providing multiple cert chains, they do not have to be in the same order as their private keys in key. If the intermediate certificates are not provided, the peer will not be able to validate the certificate, and the handshake will fail.

幸いMacには元々`openssl`が入っていたので、これを使ってプライベートキーと証明書を作成します。
こちらのブログ、「[秘密鍵、公開鍵、証明書、CSR生成のOpenSSLコマンドまとめ](https://blog.freedom-man.com/openssl-command)」 が大変参考になりました。このブログにあった「秘密鍵と自己署名証明書を一括で作成したい」を使います。
対話形式で入力する組織情報はどれが必須で、どれが省略可能なのかまでは調べていません。全部省略したらエラーになってしまいました。


```bash:プライベートキーと証明書の作成
$ openssl req -x509 -sha256 -nodes -days 9999 -newkey rsa:2048 -keyout localhttps_key.pem -out localhttps_cert.pem
Generating a 2048 bit RSA private key
............................................+++
...+++
writing new private key to 'localhttps_key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) []:Tokyo
Organization Name (eg, company) []:FiftyFifty
Organizational Unit Name (eg, section) []:FortyNine
Common Name (eg, fully qualified host name) []:localhost
Email Address []:.
```

このコマンドが成功すると、`localhttps_key.pem`と言うプライベートキーと`localhttps_cert.pem`と言う証明書ファイルが生成されます。
## 機能するコード例
プライベートキーと証明書とをオプションで指定したら、HTTPSでアクセスできるようになりました。
実際にアクセすると、Chromeブラウザは強い警告を出してきますが、許可すればなんとかアクセスはできます。

```typescript:機能するコード
import fs from 'fs';
import https from 'https';
import { IncomingMessage } from 'http';

const PORT = 9090;
const HOST = '127.0.0.1';

const options = {
  key: fs.readFileSync('./localhttps_key.pem'),
  cert: fs.readFileSync('./localhttps_cert.pem')
}

function startHttpServer(portNumber: number, hostName: string): Promise<IncomingMessage> {
  return new Promise((resolve, reject)=>{
    https.createServer(options, (request, response)=>{
      response.writeHead(200, {
        'Content-Type': 'text/plain'
      });
      response.end(`welcome to local HTTPS page!`);
      resolve(request);
    }).listen(portNumber, hostName);
  });
}

(async (portNumber, hostName)=>{
  const req = await startHttpServer(portNumber, hostName);
  console.log(`${JSON.stringify(req.headers)}`);
})(PORT, HOST);
```