|
<?php |
|
declare(strict_types=1); |
|
|
|
error_reporting(E_ALL); |
|
ini_set('display_errors', '1'); |
|
|
|
function logln(string $s): void { |
|
fwrite(STDOUT, $s . PHP_EOL); |
|
fflush(STDOUT); |
|
} |
|
|
|
function hexdump(string $bin, int $max = 256): string { |
|
$len = strlen($bin); |
|
$n = min($len, $max); |
|
$out = ''; |
|
for ($i = 0; $i < $n; $i += 16) { |
|
$chunk = substr($bin, $i, 16); |
|
$hex = implode(' ', str_split(bin2hex($chunk), 2)); |
|
$ascii = preg_replace('/[^\x20-\x7e]/', '.', $chunk); |
|
$out .= sprintf("%04x %-47s %s\n", $i, $hex, $ascii); |
|
} |
|
if ($len > $max) { |
|
$out .= sprintf("... (%d bytes total)\n", $len); |
|
} |
|
return $out; |
|
} |
|
|
|
/** |
|
* read exactly $n bytes; returns null on timeout/EOF/error |
|
*/ |
|
function readN($fp, int $n): ?string { |
|
$buf = ''; |
|
while (strlen($buf) < $n) { |
|
$r = fread($fp, $n - strlen($buf)); |
|
if ($r === false) { |
|
return null; |
|
} |
|
if ($r === '') { |
|
$m = stream_get_meta_data($fp); |
|
if (!empty($m['timed_out']) || feof($fp)) { |
|
return null; |
|
} |
|
usleep(10_000); |
|
continue; |
|
} |
|
$buf .= $r; |
|
} |
|
return $buf; |
|
} |
|
|
|
/** |
|
* HTTP/2 frame: |
|
* length(24) type(8) flags(8) r(1)+stream_id(31) payload(length) |
|
*/ |
|
function readFrame($fp): ?array { |
|
$h = readN($fp, 9); |
|
if ($h === null || strlen($h) < 9) return null; |
|
|
|
$len = (ord($h[0]) << 16) | (ord($h[1]) << 8) | ord($h[2]); |
|
$type = ord($h[3]); |
|
$flags = ord($h[4]); |
|
$sid = (unpack('N', substr($h, 5, 4))[1]) & 0x7FFFFFFF; |
|
|
|
$payload = $len ? readN($fp, $len) : ''; |
|
if ($len && ($payload === null || strlen($payload) < $len)) return null; |
|
|
|
return [$len, $type, $flags, $sid, $payload]; |
|
} |
|
|
|
function packFrame(int $type, int $flags, int $sid, string $payload): string { |
|
$len = strlen($payload); |
|
$h = chr(($len >> 16) & 0xff) . chr(($len >> 8) & 0xff) . chr($len & 0xff); |
|
$h .= chr($type & 0xff) . chr($flags & 0xff); |
|
$h .= pack('N', $sid & 0x7FFFFFFF); |
|
return $h . $payload; |
|
} |
|
|
|
function nameFrameType(int $t): string { |
|
return match ($t) { |
|
0x00 => 'DATA', |
|
0x01 => 'HEADERS', |
|
0x02 => 'PRIORITY', |
|
0x03 => 'RST_STREAM', |
|
0x04 => 'SETTINGS', |
|
0x05 => 'PUSH_PROMISE', |
|
0x06 => 'PING', |
|
0x07 => 'GOAWAY', |
|
0x08 => 'WINDOW_UPDATE', |
|
0x09 => 'CONTINUATION', |
|
default => 'UNKNOWN', |
|
}; |
|
} |
|
|
|
function connectTlsWithAlpn(string $host, int $port, string $alpn, int $timeoutSec = 3) { |
|
// IPv6 でハングしがちな環境向け:IPv4を優先(取れないならホスト名のまま) |
|
$ip4 = gethostbyname($host); |
|
$targetHost = ($ip4 !== $host) ? $ip4 : $host; |
|
$target = "tls://{$targetHost}:{$port}"; |
|
|
|
$ctx = stream_context_create([ |
|
'ssl' => [ |
|
'verify_peer' => true, |
|
'verify_peer_name' => true, |
|
'SNI_enabled' => true, |
|
'peer_name' => $host, // SNI はホスト名で |
|
'alpn_protocols' => $alpn, |
|
], |
|
]); |
|
|
|
logln("target: {$target} (SNI/peer_name={$host})"); |
|
logln("alpn_protocols(configured): {$alpn}"); |
|
|
|
$errno = 0; $errstr = ''; |
|
$fp = @stream_socket_client($target, $errno, $errstr, $timeoutSec, STREAM_CLIENT_CONNECT, $ctx); |
|
if (!$fp) { |
|
throw new RuntimeException("connect failed: ($errno) $errstr"); |
|
} |
|
|
|
// 重要:タイムアウト・バッファを明示 |
|
stream_set_timeout($fp, $timeoutSec); |
|
stream_set_read_buffer($fp, 0); |
|
stream_set_write_buffer($fp, 0); |
|
|
|
$meta = stream_get_meta_data($fp); |
|
$neg = null; |
|
if (isset($meta['crypto']) && is_array($meta['crypto'])) { |
|
foreach (['alpn_protocol','alpn_selected','ssl_alpn_protocol','negotiated_protocol','protocol'] as $k) { |
|
if (isset($meta['crypto'][$k]) && is_string($meta['crypto'][$k]) && $meta['crypto'][$k] !== '') { |
|
$neg = $meta['crypto'][$k]; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
logln("ALPN (negotiated): " . ($neg ?? "(not found; check meta dump)")); |
|
|
|
return [$fp, $meta, $neg]; |
|
} |
|
|
|
/* ========= main ========= */ |
|
|
|
$host = $argv[1] ?? 'www.google.com'; |
|
$port = (int)($argv[2] ?? 443); |
|
|
|
// h2 が選ばれるかを見る。順序は h2 優先。 |
|
$alpn = "h2,http/1.1"; |
|
|
|
// ここは用途に応じて。デモなので短め。 |
|
$timeoutSec = 2; |
|
|
|
try { |
|
[$fp, $meta, $neg] = connectTlsWithAlpn($host, $port, $alpn, $timeoutSec); |
|
} catch (Throwable $e) { |
|
logln("[!] " . $e->getMessage()); |
|
exit(1); |
|
} |
|
|
|
if ($neg !== 'h2') { |
|
logln("[!] negotiated ALPN is not h2. Exiting (this demo expects HTTP/2)."); |
|
// 必要なら meta を見たいときだけ出す |
|
// var_export($meta); echo PHP_EOL; |
|
fclose($fp); |
|
exit(2); |
|
} |
|
|
|
// 1) client connection preface(必須) |
|
$preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; |
|
logln(">>> sending client preface (" . strlen($preface) . " bytes)"); |
|
fwrite($fp, $preface); |
|
|
|
// 2) クライアント SETTINGS(最小:空payloadでもOK) |
|
$settings = packFrame(0x04, 0x00, 0, ''); |
|
logln(">>> sending SETTINGS (empty payload)"); |
|
fwrite($fp, $settings); |
|
|
|
// 3) サーバからの SETTINGS 等を読む |
|
$gotServerSettings = false; |
|
$ackedServerSettings = false; |
|
|
|
$maxFrames = 50; |
|
for ($i = 0; $i < $maxFrames; $i++) { |
|
$fr = readFrame($fp); |
|
if ($fr === null) { |
|
$m = stream_get_meta_data($fp); |
|
logln("no more frames / timeout=" . (!empty($m['timed_out']) ? 'yes' : 'no') . " eof=" . (feof($fp) ? 'yes' : 'no')); |
|
break; |
|
} |
|
|
|
[$len, $type, $flags, $sid, $payload] = $fr; |
|
$tname = nameFrameType($type); |
|
logln(sprintf("FRAME #%d len=%d type=0x%02x(%s) flags=0x%02x sid=%d", $i, $len, $type, $tname, $flags, $sid)); |
|
|
|
if ($len > 0) { |
|
// SETTINGSのpayloadは6バイト単位のID/Valueだが、デモではダンプだけ |
|
logln(hexdump($payload, 256)); |
|
} |
|
|
|
// サーバ SETTINGS(ACKではない)を受けたら ACK を返す |
|
if ($type === 0x04 /*SETTINGS*/ && ($flags & 0x01) === 0) { |
|
$gotServerSettings = true; |
|
if (!$ackedServerSettings) { |
|
$ack = packFrame(0x04, 0x01, 0, ''); |
|
logln(">>> sending SETTINGS ACK"); |
|
fwrite($fp, $ack); |
|
$ackedServerSettings = true; |
|
|
|
// 疎通確認としてはここで終わるのが現実的 |
|
logln("SETTINGS exchange done; exiting demo."); |
|
break; |
|
} |
|
} |
|
|
|
// 念のため:GOAWAY が来たら終わり |
|
if ($type === 0x07 /*GOAWAY*/) { |
|
logln("GOAWAY received; exiting."); |
|
break; |
|
} |
|
} |
|
|
|
fclose($fp); |
|
logln("done"); |