Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active July 4, 2025 18:46
Show Gist options
  • Save masakielastic/44f2a6d84f861d2c4e9d3f02f63c3bdc to your computer and use it in GitHub Desktop.
Save masakielastic/44f2a6d84f861d2c4e9d3f02f63c3bdc to your computer and use it in GitHub Desktop.
php-node で HTTP/2 +TLS サーバーを作成する

php-node で HTTP/2 +TLS サーバーを作成する

構成

  • package.json
  • bin/phpnode-server.js
  • public PHP スクリプトの設置場所

インストール

グローバルコマンドとしてインストールするために次のコマンドを実行します。

npm install -g .

実行

サーバーを起動させます。

php-node-server 8443 ./public

curl を実行してみます。

curl -k -v --data-urlencode 'emoji=🐘' https://localhost:8443/json.php

レスポンスは次のようになります。

* Host localhost:8443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8443...
* GnuTLS priority: NORMAL:-ARCFOUR-128:-CTYPE-ALL:+CTYPE-X509:-VERS-SSL3.0
* ALPN: curl offers h2,http/1.1
* SSL connection using TLS1.3 / ECDHE_RSA_AES_256_GCM_SHA384
*   server certificate verification SKIPPED
*   server certificate status verification SKIPPED
* error fetching CN from cert:The requested data were not available.
*   common name:  (matched)
*   server certificate expiration date OK
*   server certificate activation date OK
*   certificate public key: RSA
*   certificate version: #3
*   subject: O=mkcert development certificate,OU=masakielastic@penguin
*   start date: Fri, 04 Jul 2025 17:09:25 GMT
*   expire date: Mon, 04 Oct 2027 17:09:25 GMT
*   issuer: O=mkcert development CA,OU=masakielastic@penguin,CN=mkcert masakielastic@penguin
* ALPN: server accepted h2
* Connected to localhost (::1) port 8443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://localhost:8443/json.php
* [HTTP/2] [1] [:method: POST]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: localhost:8443]
* [HTTP/2] [1] [:path: /json.php]
* [HTTP/2] [1] [user-agent: curl/8.13.0]
* [HTTP/2] [1] [accept: */*]
* [HTTP/2] [1] [content-length: 30]
* [HTTP/2] [1] [content-type: application/x-www-form-urlencoded]
> POST /json.php HTTP/2
> Host: localhost:8443
> User-Agent: curl/8.13.0
> Accept: */*
> Content-Length: 30
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 30 bytes
< HTTP/2 200 
< date: Fri, 04 Jul 2025 18:02:04 GMT
< 
* Connection #0 to host localhost left intact
{"emoji":"\ud83d\udc18"}
{
"name": "phpnode-server",
"version": "1.0.0",
"type": "module",
"bin": {
"phpnode-server": "./bin/phpnode-server.js"
},
"dependencies": {
"@platformatic/php-node": "^1.3.0",
"node-forge": "^1.3.1"
}
}
#!/usr/bin/env node
import { Php, Request } from '@platformatic/php-node';
import http2 from 'node:http2';
import fs from 'fs';
import path from 'path';
import process from 'process';
import forge from 'node-forge';
// コマンド引数解析(最小限: ポート、docroot、証明書パス省略時は自動生成)
const args = process.argv.slice(2);
const port = Number(args[0]) || 8443;
const docroot = args[1] ? path.resolve(args[1]) : process.cwd();
// 証明書・鍵がなければ自動生成
const certPath = args[2] || path.join(process.cwd(), 'cert.pem');
const keyPath = args[3] || path.join(process.cwd(), 'key.pem');
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
console.log('証明書・鍵が見つかりません。自己署名証明書を自動生成します...');
const pki = forge.pki;
const keys = pki.rsa.generateKeyPair(2048);
const cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: 'commonName', value: 'localhost' },
{ name: 'countryName', value: 'JP' },
{ shortName: 'ST', value: 'Tokyo' },
{ name: 'localityName', value: 'Chiyoda' },
{ name: 'organizationName', value: 'MyOrg' },
{ shortName: 'OU', value: 'Dev' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([
{ name: 'basicConstraints', cA: true },
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true },
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
{ name: 'subjectAltName', altNames: [ { type: 2, value: 'localhost' } ] }
]);
cert.sign(keys.privateKey, forge.md.sha256.create());
fs.writeFileSync(certPath, pki.certificateToPem(cert));
fs.writeFileSync(keyPath, pki.privateKeyToPem(keys.privateKey));
console.log('自己署名証明書・秘密鍵を生成しました:', certPath, keyPath);
}
if (!fs.existsSync(docroot)) {
console.error('指定されたドキュメントルートが存在しません:', docroot);
process.exit(1);
}
const key = fs.readFileSync(keyPath);
const cert = fs.readFileSync(certPath);
const php = new Php({ docroot });
const server = http2.createSecureServer({ key, cert });
server.on('stream', (stream, headers) => {
const url = headers[':path'] || '/';
const method = headers[':method'] || 'GET';
let bodyData = Buffer.alloc(0);
// POST等のbody受信
stream.on('data', chunk => {
bodyData = Buffer.concat([bodyData, chunk]);
});
stream.on('end', async () => {
const request = new Request({
url: `https://localhost${url}`,
method,
headers,
body: bodyData, // POSTデータを正しく渡す
});
try {
const response = await php.handleRequest(request);
stream.respond({
':status': response.statusCode,
...response.headers,
});
stream.end(response.body);
} catch (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
console.error(err);
}
});
});
server.listen(port, () => {
console.log(`php-node HTTP/2 + TLS サーバー起動: https://localhost:${port}/`);
console.log('ドキュメントルート:', docroot);
});
// グレースフルシャットダウン対応
process.on('SIGINT', async () => {
console.log('\nサーバー停止中...');
server.close(async () => {
try {
await php.close(); // ← これが重要!
console.log('グレースフルシャットダウン完了。');
process.exit(0);
} catch (err) {
console.error('php.close()失敗:', err);
process.exit(1);
}
});
});
<?php
echo json_encode($_POST);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment