| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | // テスト用の偽物のHTTPサーバー(MockServerが起動する)。使い方:php http-server.php ポート [証明書 秘密鍵] |
| 39 | // 1回に1つの接続を順番に処理する。証明書と秘密鍵を渡すとHTTPSにも対応する(TLSのClientHelloで判断、Std\Lib\Tls)。 |
| 40 | |
| 41 | require_once __DIR__.'/../../../../autoload.php'; |
| 42 | |
| 43 | use Std\Lib\Tls; |
| 44 | |
| 45 | $port = (int)($argv[1] ?? 0); |
| 46 | $tls = isset($argv[2], $argv[3]) ? ['cert' => $argv[2], 'key' => $argv[3], 'alpn' => ['http/1.1']] : null; |
| 47 | $server = @stream_socket_server("tcp://127.0.0.1:{$port}", $errno, $errstr); |
| 48 | if (!$server) { |
| 49 | fwrite(STDOUT, "error {$errstr}\n"); |
| 50 | exit(1); |
| 51 | } |
| 52 | fwrite(STDOUT, "ready\n"); |
| 53 | fflush(STDOUT); |
| 54 | |
| 55 | $count = []; |
| 56 | // テストが次のレスポンスを決められる(POST /__next)、最後のリクエストを確認出来る(GET /__last) |
| 57 | $next = null; |
| 58 | $last = null; |
| 59 | |
| 60 | /** 生のHTTPレスポンス */ |
| 61 | function response(int $code, string $body, array $headers = []): string { |
| 62 | $reason = [200 => 'OK', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', |
| 63 | 401 => 'Unauthorized', 404 => 'Not Found', 502 => 'Bad Gateway'][$code] ?? 'Status'; |
| 64 | $out = "HTTP/1.1 {$code} {$reason}\r\n"; |
| 65 | $headers += ['Content-Type' => 'application/json', 'Content-Length' => strlen($body), 'Connection' => 'close']; |
| 66 | foreach ($headers as $k => $v) $out .= "{$k}: {$v}\r\n"; |
| 67 | return $out."\r\n".$body; |
| 68 | } |
| 69 | |
| 70 | function json(int $code, array $data): string { |
| 71 | return response($code, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); |
| 72 | } |
| 73 | |
| 74 | while (true) { |
| 75 | $c = @stream_socket_accept($server, 60); |
| 76 | if (!$c) break; // 60秒何も来なければ終了(テストが落ちても残らない様に) |
| 77 | stream_set_timeout($c, 5); |
| 78 | |
| 79 | // HTTPS(最初のバイトがTLSのハンドシェイク) |
| 80 | if ($tls && stream_socket_recvfrom($c, 1, STREAM_PEEK) === "\x16") { |
| 81 | try { |
| 82 | $c = Tls::accept($c, $tls); |
| 83 | } catch (\Throwable $e) { |
| 84 | @fclose($c); |
| 85 | continue; |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // リクエストの読み込み |
| 90 | $head = ''; |
| 91 | while (!str_contains($head, "\r\n\r\n") && ($line = fgets($c)) !== false) $head .= $line; |
| 92 | [$reqLine] = explode("\r\n", $head, 2); |
| 93 | $parts = explode(' ', $reqLine); |
| 94 | $method = $parts[0] ?? ''; |
| 95 | $target = $parts[1] ?? '/'; |
| 96 | $headers = []; |
| 97 | foreach (array_slice(explode("\r\n", trim($head)), 1) as $h) { |
| 98 | if (!str_contains($h, ':')) continue; |
| 99 | [$k, $v] = explode(':', $h, 2); |
| 100 | $headers[strtolower(trim($k))] = trim($v); |
| 101 | } |
| 102 | $body = ''; |
| 103 | $len = (int)($headers['content-length'] ?? 0); |
| 104 | while (strlen($body) < $len && !feof($c)) { |
| 105 | $chunk = fread($c, $len - strlen($body)); |
| 106 | if ($chunk === false || $chunk === '') break; |
| 107 | $body .= $chunk; |
| 108 | } |
| 109 | |
| 110 | $path = parse_url($target, PHP_URL_PATH) ?: '/'; |
| 111 | parse_str((string)parse_url($target, PHP_URL_QUERY), $query); |
| 112 | $count[$path] = ($count[$path] ?? 0) + 1; |
| 113 | $req = ['method' => $method, 'target' => $target, 'headers' => $headers, 'body' => $body]; |
| 114 | |
| 115 | if ($path === '/__quit') { |
| 116 | fclose($c); |
| 117 | break; |
| 118 | } |
| 119 | |
| 120 | $bearer = ($headers['authorization'] ?? '') === 'Bearer tok123'; |
| 121 | |
| 122 | // テスト用の操作(記録しない) |
| 123 | if ($path === '/__next') { |
| 124 | $next = json_decode($body, true); |
| 125 | @fwrite($c, json(200, ['ok' => true])); |
| 126 | fclose($c); |
| 127 | continue; |
| 128 | } |
| 129 | if ($path === '/__last') { |
| 130 | @fwrite($c, json(200, $last ?? [])); |
| 131 | fclose($c); |
| 132 | continue; |
| 133 | } |
| 134 | $last = ['method' => $method, 'path' => $path, 'query' => (string)parse_url($target, PHP_URL_QUERY), 'body' => $body]; |
| 135 | |
| 136 | $out = match (true) { |
| 137 | // Curl用 |
| 138 | $path === '/echo' => json(200, $req), |
| 139 | $path === '/count' => json(200, ['count' => $count[$query['path'] ?? ''] ?? 0]), |
| 140 | $path === '/chunked' => "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n" |
| 141 | ."7\r\nHello, \r\n6\r\nworld!\r\n0\r\n\r\n", |
| 142 | $path === '/redirect' => response(302, '', ['Location' => $query['to'] ?? '/echo']), |
| 143 | $path === '/redirect-303' => response(303, '', ['Location' => '/echo']), |
| 144 | $path === '/redirect-301' => response(301, '', ['Location' => 'echo?from=301']), |
| 145 | $path === '/loop' => response(302, '', ['Location' => '/loop']), |
| 146 | $path === '/big' => response(200, str_repeat('x', 200000), ['Content-Type' => 'text/plain']), |
| 147 | $path === '/slow' => (function() { sleep(3); return response(200, 'late'); })(), |
| 148 | $path === '/missing' => json(404, ['desc' => 'Not found']), |
| 149 | |
| 150 | // Openprovider用(/v1beta) |
| 151 | $path === '/v1beta/auth/login' => (function() use ($body) { |
| 152 | $in = json_decode($body, true); |
| 153 | return is_array($in) && isset($in['username'], $in['password']) |
| 154 | ? json(200, ['code' => 0, 'data' => ['token' => 'tok123', 'reseller_id' => 42]]) |
| 155 | : json(401, ['code' => 196, 'desc' => 'Authentication/Authorization Failed']); |
| 156 | })(), |
| 157 | $path === '/bad/v1beta/auth/login' => json(401, ['code' => 196, 'desc' => 'Authentication/Authorization Failed']), |
| 158 | // /__nextで決めたレスポンス(1回だけ) |
| 159 | $next !== null && str_starts_with($path, '/v1beta/') && $bearer => (function() use (&$next) { |
| 160 | $out = json((int)($next['status'] ?? 200), (array)($next['body'] ?? [])); |
| 161 | $next = null; |
| 162 | return $out; |
| 163 | })(), |
| 164 | $path === '/v1beta/domains/check' && $bearer => (function() use ($body) { |
| 165 | $in = json_decode($body, true); |
| 166 | $results = []; |
| 167 | foreach ($in['domains'] ?? [] as $d) { |
| 168 | $name = "{$d['name']}.{$d['extension']}"; |
| 169 | $results[] = $name === '076.moe' ? ['domain' => $name, 'status' => 'active', 'reason' => 'In use'] |
| 170 | : ['domain' => $name, 'status' => 'free']; |
| 171 | } |
| 172 | return json(200, ['code' => 0, 'data' => ['results' => $results]]); |
| 173 | })(), |
| 174 | $path === '/v1beta/domains' && $method === 'GET' && $bearer |
| 175 | => json(200, ['code' => 0, 'data' => ['results' => [['id' => 1, 'domain' => ['name' => '076', 'extension' => 'moe']]], 'total' => 1]]), |
| 176 | $path === '/v1beta/gateway' => response(502, '<html>Bad Gateway</html>', ['Content-Type' => 'text/html']), |
| 177 | str_starts_with($path, '/v1beta/') && !$bearer => json(401, ['code' => 196, 'desc' => 'Authentication/Authorization Failed']), |
| 178 | |
| 179 | default => json(404, ['desc' => "No route: {$path}"]), |
| 180 | }; |
| 181 | |
| 182 | @fwrite($c, $out); |
| 183 | fclose($c); |
| 184 | } |