Node.js server.addContext() の解説とプログラミング例【日本語】
主な役割
- セキュリティコンテキストの管理
各オリジンに対して、使用する TLS バージョンや暗号スイートなどを個別に設定できます。 - TLS 設定の分離
例えば、異なるサブドメインに対して異なる SSL/TLS 証明書を設定する場合などに使用します。 - 複数のオリジンのサポート
一つの HTTP/2 サーバーで、異なるオリジンに対して個別の設定を提供できます。
メソッドの構文
server.addContext(origin, options);
- origin (文字列)
設定を適用するオリジンを指定します。これは、スキーム(https
など)、オーソリティ(ホスト名とポート番号)、およびオプションでパスを含む文字列です。例:'https://example.com:443'
使用例
例えば、https://example.com
と https://sub.example.com
の2つのオリジンに対して異なる TLS 証明書を設定したい場合、以下のようなコードになります。
const http2 = require('http2');
const fs = require('node:fs');
const server = http2.createServer();
// example.com 用の証明書とキー
const optionsForExample = {
key: fs.readFileSync('./example.com-key.pem'),
cert: fs.readFileSync('./example.com-cert.pem')
};
// sub.example.com 用の証明書とキー
const optionsForSubExample = {
key: fs.readFileSync('./sub.example.com-key.pem'),
cert: fs.readFileSync('./sub.example.com-cert.pem')
};
server.addContext('https://example.com:443', optionsForExample);
server.addContext('https://sub.example.com:443', optionsForSubExample);
server.listen(8443, () => {
console.log('HTTP/2 server listening on port 8443');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end('Hello from the HTTP/2 server!');
});
この例では、server.addContext()
を2回呼び出し、それぞれのオリジンに対して異なる TLS 証明書 (key
と cert
) を設定しています。クライアントが https://example.com:443
にアクセスした場合は optionsForExample
の証明書が、https://sub.example.com:443
にアクセスした場合は optionsForSubExample
の証明書が使用されます。
無効なオリジン (Invalid Origin)
- トラブルシューティング
origin
引数が文字列であることを確認してください。- スキーム (
http
またはhttps
)、ホスト名、必要であればポート番号が正しく記述されているか確認してください。 - 大文字・小文字も区別される場合があるので注意してください。
- 原因
origin
引数が文字列型でない場合や、HTTP/2 のオリジンの形式 (scheme://authority[:port]
) に合致しない場合に発生します。 - エラー
TypeError [ERR_INVALID_ARG_TYPE]: The "origin" argument must be of type string
のようなエラーが発生することがあります。
無効なオプション (Invalid Options)
- 原因
options
引数がオブジェクト型でない場合や、必要なプロパティ(例えば TLS 設定に必要なkey
やcert
など)が不足している、または値の型が間違っている場合に発生します。 - エラー
TypeError [ERR_INVALID_ARG_TYPE]: The "options" argument must be of type object
や、オプションオブジェクト内の特定のプロパティに関するエラーが発生することがあります。
ファイルパスのエラー (File Path Errors)
- トラブルシューティング
- ファイルパスが正しいことを確認してください。相対パスを使用している場合は、実行時のカレントディレクトリからの相対位置が正しいか確認してください。
- 指定したファイルが存在し、Node.js プロセスがそのファイルにアクセスできる権限を持っているか確認してください。
- 絶対パスを使用することを検討してください。
- 原因
options
オブジェクトで指定した証明書や秘密鍵のファイルパスが間違っているか、ファイルが存在しない場合に発生します。 - エラー
Error: ENOENT: no such file or directory...
のようなファイルが見つからないエラーが発生することがあります。
ポートの競合 (Port Conflict)
- トラブルシューティング
- 別のポート番号を使用するように変更してください。
- 現在どのプロセスがそのポートを使用しているかを確認し、必要であればそのプロセスを停止してください (
netstat -ano
(Windows),netstat -tuln
またはss -tuln
(Linux/macOS) などで確認できます)。
- 原因
server.listen()
で指定したポート番号が、すでに別のアプリケーションやサービスによって使用されている場合に発生します。 - エラー
Error: listen EADDRINUSE: address already in use :::8443
のように、指定したポートがすでに他のプロセスで使用されているというエラーが発生することがあります。
TLS/SSL 設定の誤り (TLS/SSL Configuration Errors)
- トラブルシューティング
- 使用している証明書と秘密鍵が正しいペアであることを確認してください。
- 自己署名証明書を使用している場合は、クライアント側でその証明書を信頼するように設定する必要があります。
- 信頼された認証局 (CA) によって署名された証明書を使用することを推奨します。
- 中間証明書が必要な場合は、
ca
オプションにそれらを含めるようにしてください。
- 原因
証明書と秘密鍵のペアが一致しない、証明書が信頼されていない(自己署名証明書の場合など)、必要な中間証明書が提供されていない、などの TLS/SSL 設定の誤りに起因します。 - エラー
クライアントからの接続時にエラーが発生したり、証明書に関する警告が表示されたりすることがあります。
HTTP/2 プロトコルの不一致 (HTTP/2 Protocol Mismatch)
- トラブルシューティング
- クライアントが HTTP/2 で接続を試みているか確認してください。
- ブラウザを使用している場合は、HTTP/2 をサポートしている最新のバージョンを使用しているか確認してください。
- 原因
http2.createServer()
で作成したサーバーは HTTP/2 プロトコルで動作するため、クライアントも HTTP/2 をサポートしている必要があります。 - エラー
クライアントが HTTP/1.1 で接続しようとした場合など、プロトコルの不一致によって接続が確立できないことがあります。
- Node.js のドキュメントを参照する
http2
モジュールの公式ドキュメントには、各メソッドの詳細な説明や注意点が記載されています。 - シンプルな構成でテストする
まずは最小限の設定でサーバーを起動し、徐々に複雑な設定を追加していくことで、問題の切り分けがしやすくなります。 - ログ出力を活用する
サーバー側のログやクライアント側のログを出力するように設定し、エラー発生時の状況を把握するのに役立ててください。 - エラーメッセージを注意深く読む
エラーメッセージには、問題の原因や場所に関する重要な情報が含まれています。
例1: 単一のオリジンに対する基本的な TLS 設定
この例では、単一の HTTPS オリジン (https://localhost:8443
) に対して、TLS 証明書と秘密鍵を設定します。
const http2 = require('http2');
const fs = require('node:fs');
const server = http2.createServer({
key: fs.readFileSync('./server.key'), // 秘密鍵のパス
cert: fs.readFileSync('./server.crt') // 証明書のパス
});
server.listen(8443, () => {
console.log('HTTPS/2 server listening on port 8443');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end('Hello from the single origin server!');
});
解説
server.addContext()
は明示的には使用していませんが、createServer
のオプションとして渡された設定は、サーバーのデフォルトコンテキストとして内部的に扱われます。http2.createServer()
のオプションとして直接key
とcert
を指定することで、デフォルトのオリジン (https://<server_address>:<port>
) に対する TLS 設定を行っています。
例2: 異なるサブドメインに対する異なる TLS 設定
この例では、example.com
と sub.example.com
という2つの異なるサブドメインに対して、それぞれ異なる TLS 証明書と秘密鍵を設定します。
const http2 = require('http2');
const fs = require('node:fs');
const server = http2.createServer();
// example.com 用の証明書とキー
const optionsForExample = {
key: fs.readFileSync('./example.com.key'),
cert: fs.readFileSync('./example.com.crt')
};
// sub.example.com 用の証明書とキー
const optionsForSubExample = {
key: fs.readFileSync('./sub.example.com.key'),
cert: fs.readFileSync('./sub.example.com.crt')
};
server.addContext('https://example.com:8443', optionsForExample);
server.addContext('https://sub.example.com:8443', optionsForSubExample);
server.listen(8443, () => {
console.log('HTTP/2 server listening on port 8443');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end(`Hello from ${headers[':authority']}!`);
});
解説
headers[':authority']
を使用して、リクエストされたホスト名を取得し、レスポンスに含めています。これにより、どちらのオリジンにアクセスしたかがわかります。server.addContext()
を2回呼び出し、それぞれのオリジン (https://example.com:8443
とhttps://sub.example.com:8443
) に対して、対応する TLS 設定オブジェクト (optionsForExample
とoptionsForSubExample
) を関連付けています。http2.createServer()
は引数なしで作成されます。
例3: ポートが異なる場合の TLS 設定
この例では、同じホスト名 (example.com
) で、異なるポート (8443
と 8444
) に対して異なる TLS 設定を行います。
const http2 = require('http2');
const fs = require('node:fs');
const server = http2.createServer();
// ポート 8443 用の証明書とキー
const optionsForPort8443 = {
key: fs.readFileSync('./example.com-port8443.key'),
cert: fs.readFileSync('./example.com-port8443.crt')
};
// ポート 8444 用の証明書とキー
const optionsForPort8444 = {
key: fs.readFileSync('./example.com-port8444.key'),
cert: fs.readFileSync('./example.com-port8444.crt')
};
server.addContext('https://example.com:8443', optionsForPort8443);
server.addContext('https://example.com:8444', optionsForPort8444);
server.listen(8443, 'localhost', () => {
console.log('HTTP/2 server listening on localhost:8443 and localhost:8444 (different contexts)');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end(`Hello from ${headers[':authority']}!`);
});
server.listen()
は、どちらかのポートでリッスンを開始すれば、設定されたコンテキストに基づいてリクエストが処理されます。server.addContext()
を使用して、https://example.com:8443
とhttps://example.com:8444
に対して、それぞれ異なる TLS 設定を適用しています。- 同じホスト名 (
example.com
) でも、ポート番号が異なる場合は別のオリジンとして扱われます。
- これらの例は基本的な動作を示すためのものであり、実際にはエラーハンドリングやより複雑なリクエスト処理が必要になる場合があります。
- ファイルパスは、実際のファイルの場所に合わせて修正してください。
- これらの例を実行するには、対応する秘密鍵 (
.key
) ファイルと証明書 (.crt
) ファイルが必要です。自己署名証明書を生成することもできますが、本番環境では信頼された認証局 (CA) の証明書を使用することを推奨します。
http2.createServer() のオプションでデフォルトの TLS 設定を行う
最も基本的な方法は、http2.createServer()
関数のオプションとして、サーバー全体のデフォルトの TLS 設定(秘密鍵、証明書など)を直接指定する方法です。
const http2 = require('http2');
const fs = require('node:fs');
const server = http2.createServer({
key: fs.readFileSync('./server.key'),
cert: fs.readFileSync('./server.crt')
});
server.listen(8443, () => {
console.log('HTTPS/2 server listening on port 8443');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end('Hello from the server!');
});
解説
- 単一の HTTPS サイトをホストする場合や、複数のサブドメインで同じ証明書を使用する場合に適しています。
http2.createServer()
に渡すオブジェクトにkey
、cert
などの TLS 関連のオプションを含めることで、サーバーがリッスンするすべてのオリジンに対して同じ TLS 設定が適用されます。- この方法では、
server.addContext()
は使用しません。
SNI (Server Name Indication) を利用した TLS 設定の動的選択
SNI は TLS プロトコル拡張の一つで、クライアントが接続時にどのホスト名に接続しようとしているかをサーバーに伝えます。これを利用して、サーバー側でリクエストされたホスト名に基づいて適切な TLS 証明書を選択することができます。
Node.js の tls
モジュールと http2
モジュールを組み合わせることで、SNI に基づいた動的な TLS 設定が可能です。
const http2 = require('http2');
const tls = require('node:tls');
const fs = require('node:fs');
const server = tls.createServer({
SNICallback: (servername, cb) => {
let contextOptions = null;
if (servername === 'example.com') {
contextOptions = {
key: fs.readFileSync('./example.com.key'),
cert: fs.readFileSync('./example.com.crt')
};
} else if (servername === 'sub.example.com') {
contextOptions = {
key: fs.readFileSync('./sub.example.com.key'),
cert: fs.readFileSync('./sub.example.com.crt')
};
}
if (contextOptions) {
const secureContext = tls.createSecureContext(contextOptions);
cb(null, secureContext);
} else {
cb(new Error('No matching SNI context'));
}
}
}, (socket) => {
const http2Session = http2.createServer();
http2Session.emit('connection', socket);
});
server.listen(8443, () => {
console.log('HTTPS/2 server listening on port 8443 with SNI');
});
解説
- この方法では、
server.addContext()
は直接使用していませんが、SNI を利用してホスト名に応じた TLS 設定を実現しています。 http2.createServer()
はコールバック内で作成され、TLS ソケット (socket
) をconnection
イベントとして手動で発行することで、HTTP/2 のセッションを確立しています。tls.createServer()
を使用して TLS サーバーを作成し、SNICallback
関数で接続先のホスト名 (servername
) に基づいて異なる TLS コンテキスト (tls.createSecureContext()
) を返しています。
リバースプロキシの利用
Node.js の HTTP/2 サーバーの前に Nginx や Apache などのリバースプロキシを配置し、リバースプロキシ側で TLS の終端処理やオリジンごとのルーティングを行う方法もあります。
[クライアント] --(HTTPS)--> [リバースプロキシ (Nginx/Apache)] --(HTTP/2)--> [Node.js サーバー]
解説
- 複数のドメインや複雑なルーティング要件がある場合に有効なアプローチです。
- この構成では、Node.js サーバー自体は TLS 設定を意識する必要が少なくなり、アプリケーションロジックに集中できます。
- Node.js サーバーは、受信したリクエストのホストヘッダーなどを確認して処理を振り分けます。
- リバースプロキシが TLS 証明書の管理や SNI に基づくルーティングを行い、Node.js サーバーへは HTTP/2 または HTTP/1.1 でリクエストを転送します。
複数の HTTP/2 サーバーを起動する
もし完全に異なる TLS 設定を持つ複数の独立したオリジンを扱う必要がある場合は、それぞれのオリジンに対して個別の HTTP/2 サーバーを異なるポートで起動する方法も考えられます。
// example.com 用サーバー (ポート 8443)
const server1 = http2.createServer({
key: fs.readFileSync('./example.com.key'),
cert: fs.readFileSync('./example.com.crt')
});
server1.listen(8443, () => console.log('example.com server listening on 8443'));
server1.on('stream', (stream, headers) => { /* ... */ });
// sub.example.com 用サーバー (ポート 8444)
const server2 = http2.createServer({
key: fs.readFileSync('./sub.example.com.key'),
cert: fs.readFileSync('./sub.example.com.crt')
});
server2.listen(8444, () => console.log('sub.example.com server listening on 8444'));
server2.on('stream', (stream, headers) => { /* ... */ });
解説
- 管理やデプロイがやや複雑になる可能性がありますが、完全に独立した設定が必要な場合に有効です。
- クライアントはアクセスするオリジンに応じて異なるポートに接続する必要があります。
- 各サーバーはそれぞれの TLS 設定を持ち、異なるポートでリッスンします。
server.addContext()
の代替方法は、主に以下の通りです。
- 複数の HTTP/2 サーバーを起動
完全に独立した設定が必要な場合に有効です。 - リバースプロキシの利用
TLS 終端やルーティングを外部に委譲し、Node.js サーバーを簡略化できます。 - SNI を利用した動的な TLS 設定
複数のドメインで異なる証明書を使用する場合に柔軟性があります。 - http2.createServer() のオプション
単一または共通の TLS 設定で済む場合にシンプルです。