From 7f30a04d2a4b7c6e09267e15d2ecb5c211e9a928 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 19:51:42 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E6=94=AF=E6=8C=81=20Windows=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-windows.yml | 49 +++++ tests/bootstrap.php | 274 ++++++++++++++++++++----- tests/server/Http/server.php | 11 +- tests/server/WebSocket/server.php | 44 ++++ tests/server/WebSocket/start-server.sh | 8 +- tests/server/WebSocket/stop-server.sh | 14 +- tests/server/WebSocket/wss-server.php | 38 ++++ 7 files changed, 371 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/ci-windows.yml create mode 100644 tests/server/WebSocket/server.php create mode 100644 tests/server/WebSocket/wss-server.php diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..453f152 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,49 @@ +name: ci-windows + +on: [push, pull_request] + +jobs: + ci-windows: + runs-on: windows-latest + + strategy: + fail-fast: false + matrix: + php: + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, openssl, mbstring, json + coverage: none + tools: composer:v2 + + - name: Show PHP info + run: | + php -v + php -m + php --ri curl + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run tests + run: composer test + + - name: Print server logs (on failure) + if: failure() + run: php .github/print-logs.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 68b37de..3ab1f3b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,7 +2,15 @@ require dirname(__DIR__) . '/vendor/autoload.php'; +// Unset system proxy env vars so curl connects directly to 127.0.0.1 +// instead of going through a local proxy that would forward based on Host header +foreach (['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy'] as $key) +{ + putenv($key); +} + define('SWOOLE_ON', extension_loaded('swoole')); +define('IS_WIN', 'WIN' === strtoupper(substr(PHP_OS, 0, 3))); /** * @param string $name @@ -21,41 +29,225 @@ function testEnv($name, $default = null) return $result; } -// Http Server -$cmd = __DIR__ . '/server/Http/start-server.sh'; -echo 'Starting Http server...', \PHP_EOL; -echo `{$cmd}`, \PHP_EOL; -$serverStarted = false; -for ($i = 0; $i < 10; ++$i) +/** + * Store for background server info. + * Each item: ['proc' => resource|null, 'pid' => int, 'phpFile' => string, 'name' => string] + */ +$GLOBALS['_server_handles'] = []; + +// --- Register cleanup EARLY so it runs on fatal errors too --- +register_shutdown_function(function () { + echo \PHP_EOL, '=== Cleaning up servers ===', \PHP_EOL; + + // Stop WSS server (Swoole) + if (isset($GLOBALS['_wss_pid']) && $GLOBALS['_wss_pid']) + { + try { + echo 'Stopping WSS server...', \PHP_EOL; + if (!IS_WIN) + { + `kill -15 {$GLOBALS['_wss_pid']} 2>/dev/null`; + } + echo 'WSS server stopped!', \PHP_EOL; + } catch (\Throwable $e) { + echo 'WSS server stop failed: ', $e->getMessage(), \PHP_EOL; + } + } + + // Stop Http2 server + if (SWOOLE_ON && !IS_WIN) + { + try { + $cmd = __DIR__ . '/server/Http2/stop-server.sh'; + echo 'Stopping Http2 server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + echo 'Http2 server stopped!', \PHP_EOL; + } catch (\Throwable $e) { + echo 'Http2 server stop failed: ', $e->getMessage(), \PHP_EOL; + } + } + + // Kill Workerman servers + foreach ($GLOBALS['_server_handles'] as $item) + { + try { + $name = $item['name']; + $pid = $item['pid']; + $proc = $item['proc']; + $phpFile = $item['phpFile']; + + echo 'Stopping ', $name, '...', \PHP_EOL; + + if (IS_WIN) + { + // Kill the ENTIRE process tree (Workerman spawns child workers) + if ($pid > 0) + { + `taskkill /F /T /PID {$pid} 2>nul`; + } + // Also close the proc_open handle + if (is_resource($proc)) + { + proc_close($proc); + } + } + else + { + // On Linux/Mac, use Workerman's built-in stop command + if ($phpFile) + { + `php {$phpFile} stop 2>/dev/null`; + } + } + + echo $name, ' stopped!', \PHP_EOL; + } catch (\Throwable $e) { + echo $name, ' stop failed: ', $e->getMessage(), \PHP_EOL; + } + } + + // Final safety net: kill any remaining php.exe on our test ports + if (IS_WIN) + { + try { + foreach ([8898, 8900] as $port) + { + $output = `netstat -ano 2>nul | findstr ":{$port} " | findstr "LISTENING"`; + if ($output) + { + $lines = explode("\n", trim($output)); + foreach ($lines as $line) + { + $line = trim($line); + if (preg_match('/\s+(\d+)$/', $line, $m)) + { + `taskkill /F /T /PID {$m[1]} 2>nul`; + } + } + } + } + } catch (\Throwable $e) { + // ignore + } + } + + echo '=== Cleanup complete ===', \PHP_EOL; +}); + +/** + * Start a PHP server process in background using proc_open. + * + * On Windows: uses proc_open, records PID for taskkill /T cleanup later. + * On Linux/Mac: uses Workerman daemon mode (php ... start -d). + * + * @param string $phpFile Absolute path to the PHP file + * @param string $name Human-readable name for logging + * + * @return void + */ +function startServerProcess($phpFile, $name) { - $context = stream_context_create(['http' => ['timeout' => 1]]); - if ('YurunHttp' === @file_get_contents(testEnv('HTTP_SERVER_HOST', 'http://127.0.0.1:8898/'), false, $context)) + if (IS_WIN) + { + $logFile = dirname($phpFile) . '/log.log'; + $descriptorspec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $logFile, 'w'], + 2 => ['file', $logFile, 'a'], + ]; + $process = proc_open('php "' . $phpFile . '"', $descriptorspec, $pipes); + if (false === $process) + { + throw new \RuntimeException('Failed to start ' . $name . ': ' . $phpFile); + } + // Close stdin + fclose($pipes[0]); + // Get the PID for tree-kill later + $status = proc_get_status($process); + $GLOBALS['_server_handles'][] = [ + 'proc' => $process, + 'pid' => $status['pid'], + 'phpFile' => $phpFile, + 'name' => $name, + ]; + } + else { - $serverStarted = true; - break; + $cmd = 'php ' . escapeshellarg($phpFile) . ' start -d'; + echo `{$cmd}`, \PHP_EOL; + $GLOBALS['_server_handles'][] = [ + 'proc' => null, + 'pid' => 0, + 'phpFile' => $phpFile, + 'name' => $name, + ]; } - sleep(1); } -if ($serverStarted) + +/** + * Wait for HTTP server to be ready. + */ +function waitForServer($port, $checkBody = 'YurunHttp') { - echo 'Http server started!', \PHP_EOL; + $url = 'http://127.0.0.1:' . $port . '/'; + for ($i = 0; $i < 30; ++$i) + { + $context = stream_context_create(['http' => ['timeout' => 1]]); + if ($checkBody === @file_get_contents($url, false, $context)) + { + return; + } + sleep(1); + } + throw new \RuntimeException('Server start failed on port ' . $port); } -else + +/** + * Wait for WebSocket server to be ready. + */ +function waitForWebSocketServer($port) { - throw new \RuntimeException('Http server start failed'); + for ($i = 0; $i < 30; ++$i) + { + $context = stream_context_create(['http' => ['timeout' => 1]]); + @file_get_contents('http://127.0.0.1:' . $port . '/', false, $context); + if (isset($http_response_header[0]) && false !== strpos($http_response_header[0], '400')) + { + return; + } + sleep(1); + } + throw new \RuntimeException('WebSocket server start failed on port ' . $port); } -if (SWOOLE_ON) +// --- Http Server (Workerman) --- +echo 'Starting Http server...', \PHP_EOL; +startServerProcess(__DIR__ . '/server/Http/server.php', 'Http server'); +waitForServer(8898); +echo 'Http server started!', \PHP_EOL; + +// --- WebSocket Server (Workerman) --- +echo 'Starting WebSocket server...', \PHP_EOL; +startServerProcess(__DIR__ . '/server/WebSocket/server.php', 'WebSocket server'); +waitForWebSocketServer(8900); +echo 'WebSocket server started!', \PHP_EOL; + +// --- WSS Server (Swoole-based, only on non-Windows with Swoole) --- +if (SWOOLE_ON && !IS_WIN) { - // WebSocket Server - $cmd = __DIR__ . '/server/WebSocket/start-server.sh'; - echo 'Starting WebSocket server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo 'Starting WSS server (Swoole)...', \PHP_EOL; + $cmd = 'nohup php ' . escapeshellarg(__DIR__ . '/server/WebSocket/wss-server.php') . ' > /dev/null 2>&1 & echo $!'; + $wssPid = trim(`{$cmd}`); + if ($wssPid) + { + $GLOBALS['_wss_pid'] = $wssPid; + } $serverStarted = false; for ($i = 0; $i < 10; ++$i) { - @file_get_contents(str_replace('ws://', 'http://', testEnv('WS_SERVER_HOST', 'ws://127.0.0.1:8900/'))); - if (isset($http_response_header[0]) && 'HTTP/1.1 400 Bad Request' === $http_response_header[0]) + $context = stream_context_create(['http' => ['timeout' => 1]]); + @file_get_contents('http://127.0.0.1:8902/', false, $context); + if (isset($http_response_header[0]) && false !== strpos($http_response_header[0], '400')) { $serverStarted = true; break; @@ -64,14 +256,17 @@ function testEnv($name, $default = null) } if ($serverStarted) { - echo 'WebSocekt server started!', \PHP_EOL; + echo 'WSS server started!', \PHP_EOL; } else { - throw new \RuntimeException('WebSocekt server start failed'); + throw new \RuntimeException('WSS server start failed'); } +} - // Http2 Server +// --- Http2 Server (Swoole-only, not available on Windows) --- +if (SWOOLE_ON && !IS_WIN) +{ $cmd = __DIR__ . '/server/Http2/start-server.sh'; echo 'Starting Http2 server...', \PHP_EOL; echo `{$cmd}`, \PHP_EOL; @@ -95,24 +290,11 @@ function testEnv($name, $default = null) throw new \RuntimeException('Http2 server start failed'); } } - -register_shutdown_function(function () { - // stop server - $cmd = __DIR__ . '/server/Http/stop-server.sh'; - echo 'Stoping http server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; - echo 'Http Server stoped!', \PHP_EOL; - - if (SWOOLE_ON) - { - $cmd = __DIR__ . '/server/WebSocket/stop-server.sh'; - echo 'Stoping WebSocket server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; - echo 'WebSocket Server stoped!', \PHP_EOL; - - $cmd = __DIR__ . '/server/Http2/stop-server.sh'; - echo 'Stoping Http2 server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; - echo 'Http2 Server stoped!', \PHP_EOL; - } -}); +elseif (!SWOOLE_ON) +{ + echo 'Http2 server skipped (Swoole not available)', \PHP_EOL; +} +else +{ + echo 'Http2 server skipped (not supported on Windows)', \PHP_EOL; +} diff --git a/tests/server/Http/server.php b/tests/server/Http/server.php index aeeb806..94ab459 100644 --- a/tests/server/Http/server.php +++ b/tests/server/Http/server.php @@ -76,9 +76,18 @@ break; case 'redirectOther': $connection->send(new Response(302, [ - 'Location' => 'https://www.httpbin.org/get?id=1', + 'Location' => '/?a=redirectTarget&id=1', ])); break; + case 'redirectTarget': + $args = $request->get(); + unset($args['a']); // strip routing param, mimic httpbin behavior + $connection->send(new Response(200, [ + 'Content-Type' => 'application/json', + ], json_encode([ + 'args' => $args, + ]))); + break; case 'redirect': $connection->send(new Response(302, [ 'Location' => $request->get('url', '/'), diff --git a/tests/server/WebSocket/server.php b/tests/server/WebSocket/server.php new file mode 100644 index 0000000..d1895cf --- /dev/null +++ b/tests/server/WebSocket/server.php @@ -0,0 +1,44 @@ +count = 4; +$ws_worker->name = 'YurunHttp WS Test'; + +$ws_worker->onMessage = function (TcpConnection $connection, $data) use (&$userNameStore) { + $data = json_decode($data, true); + if (!is_array($data)) + { + return; + } + switch ($data['action'] ?? null) + { + case 'login': + $userNameStore[$connection->id] = $data['username']; + $connection->send(json_encode(['success' => true])); + break; + case 'send': + if (isset($userNameStore[$connection->id])) + { + $connection->send($userNameStore[$connection->id] . ':' . $data['message']); + } + break; + } +}; + +$ws_worker->onClose = function (TcpConnection $connection) use (&$userNameStore) { + if (isset($userNameStore[$connection->id])) + { + unset($userNameStore[$connection->id]); + } +}; + +// Run all workers +Worker::runAll(); diff --git a/tests/server/WebSocket/start-server.sh b/tests/server/WebSocket/start-server.sh index d8404ce..3bb8efd 100755 --- a/tests/server/WebSocket/start-server.sh +++ b/tests/server/WebSocket/start-server.sh @@ -4,10 +4,4 @@ __DIR__=$(cd `dirname $0`; pwd) ${__DIR__}/stop-server.sh -if [[ $TRAVIS ]]; then -phpPath="/opt/swoole/bin/php" -else -phpPath="/usr/bin/env php" -fi - -nohup $phpPath $__DIR__/ws-server.php > ${__DIR__}/log.log 2>&1 & echo $! > "$__DIR__/server.pid" +/usr/bin/env php $__DIR__/server.php start -d > ${__DIR__}/log.log \ No newline at end of file diff --git a/tests/server/WebSocket/stop-server.sh b/tests/server/WebSocket/stop-server.sh index 95e6885..5c8154e 100755 --- a/tests/server/WebSocket/stop-server.sh +++ b/tests/server/WebSocket/stop-server.sh @@ -2,16 +2,4 @@ __DIR__=$(cd `dirname $0`; pwd) -pidFile="$__DIR__/server.pid" - -if [ -f $pidFile ];then - cat $pidFile | while read LINE - do - ret=$(ps --no-heading ${LINE} | wc -l) - if [[ "$ret" = "1" ]]; then - echo "PID: ${LINE}" - kill -15 ${LINE} - fi - break - done -fi \ No newline at end of file +/usr/bin/env php $__DIR__/server.php stop \ No newline at end of file diff --git a/tests/server/WebSocket/wss-server.php b/tests/server/WebSocket/wss-server.php new file mode 100644 index 0000000..8497bb6 --- /dev/null +++ b/tests/server/WebSocket/wss-server.php @@ -0,0 +1,38 @@ +set([ + 'open_websocket_protocol' => true, + 'worker_num' => 1, + 'ssl_cert_file' => dirname(dirname(__DIR__)) . '/ssl/server.crt', + 'ssl_key_file' => dirname(dirname(__DIR__)) . '/ssl/server.key', +]); + +$server->on('open', function (Swoole\WebSocket\Server $server, $request) { +}); + +$server->on('message', function (Swoole\WebSocket\Server $server, $frame) use (&$userNameStore) { + $data = json_decode($frame->data, true); + switch ($data['action']) + { + case 'login': + $userNameStore[$frame->fd] = $data['username']; + $server->push($frame->fd, json_encode(['success' => true])); + break; + case 'send': + $server->push($frame->fd, $userNameStore[$frame->fd] . ':' . $data['message']); + break; + } +}); + +$server->on('close', function ($ser, $fd) use (&$userNameStore) { + if (isset($userNameStore[$fd])) + { + unset($userNameStore[$fd]); + } +}); + +$server->start(); From 99d60bc4258abb22d01ac455c0c5d68f8f9482d2 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 19:53:37 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Swoole=206.2=20+=20P?= =?UTF-8?q?HP=208.3-8.5=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f0bbbc..5ff657f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: PHP_DOCKER_VERSION: ${{ matrix.php }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: prepare run: | @@ -42,6 +42,9 @@ jobs: 5.0-php8.0, 5.0-php8.1, 5.0-php8.2, + 6.2-php8.3, + 6.2-php8.4, + 6.2-php8.5, 4.8-php7.2, 4.8-php7.3, 4.8-php7.4, @@ -53,7 +56,7 @@ jobs: SWOOLE_DOCKER_VERSION: ${{ matrix.swoole }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: prepare run: | From d422f97065ae84a19a4718a8a40a28c7be592712 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 20:07:24 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=8E=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20PHP=20=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/YurunHttp/Cookie/CookieManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/YurunHttp/Cookie/CookieManager.php b/src/YurunHttp/Cookie/CookieManager.php index 46cbf94..ee0c976 100644 --- a/src/YurunHttp/Cookie/CookieManager.php +++ b/src/YurunHttp/Cookie/CookieManager.php @@ -293,7 +293,7 @@ private function checkPath($uriPath, $cookiePath) $path = \dirname($path); } } - if ('\\' === \DIRECTORY_SEPARATOR && str_contains($path, \DIRECTORY_SEPARATOR)) + if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos($path, \DIRECTORY_SEPARATOR)) { $path = str_replace(\DIRECTORY_SEPARATOR, '/', $path); } From 36069c636a4732e4291dc5ea7e155c34050428f1 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 20:07:36 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ff657f..ac38a1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: ci-only-curl: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false @@ -29,7 +29,7 @@ jobs: run: php .github/print-logs.php ci-curl-swoole: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false From 6dee19d7f51aae7503f49cb3cb19c8fccc67dca5 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 20:12:54 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/prepare-test.sh | 2 +- .github/workflows/phpcs.yml | 4 ++-- .github/workflows/phpstan.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/prepare-test.sh b/.github/prepare-test.sh index 865098f..a6bed81 100755 --- a/.github/prepare-test.sh +++ b/.github/prepare-test.sh @@ -12,7 +12,7 @@ fi containerName=$1 -docker-compose up -d $containerName \ +docker compose up -d $containerName \ && docker exec $containerName php -v \ && docker exec $containerName php -m \ && docker exec $containerName php --ri curl diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index 18f3b69..70c9271 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false @@ -15,7 +15,7 @@ jobs: SWOOLE_DOCKER_VERSION: ${{ matrix.swoole }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Prepare run: | diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 0077f0c..ccfe110 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false @@ -15,7 +15,7 @@ jobs: SWOOLE_DOCKER_VERSION: ${{ matrix.swoole }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Prepare run: | From 79e28accfba3cf62c5d56bfc446dab6f72c40190 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 21:02:31 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/php.dockerfile | 9 +++++++-- composer.json | 5 +++-- phpstan.neon | 1 + src/YurunHttp/Cookie/CookieManager.php | 2 +- tests/bootstrap.php | 28 +++++++++++++++++--------- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.github/php.dockerfile b/.github/php.dockerfile index 9414bb6..ba10801 100644 --- a/.github/php.dockerfile +++ b/.github/php.dockerfile @@ -1,9 +1,14 @@ ARG PHP_DOCKER_VERSION FROM php:${PHP_DOCKER_VERSION}-cli -RUN apt update +# Buster 已归档,将 apt 源指向 archive.debian.org +RUN sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list +RUN sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list +RUN sed -i '/buster-updates/d' /etc/apt/sources.list -RUN apt install -y --force-yes unzip ca-certificates +RUN apt update --allow-unauthenticated + +RUN apt install --allow-unauthenticated -y --force-yes unzip ca-certificates RUN docker-php-ext-install pcntl > /dev/null diff --git a/composer.json b/composer.json index e9e0cbc..1d5a69a 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "require": { "php": ">=7.1", "psr/http-message": "~1.0|~2.0", - "psr/log": "~1.0|~2.0|~3.0" + "psr/log": "~1.0|~2.0|~3.0", + "symfony/polyfill-php80": "~1.30.0" }, "require-dev": { "swoole/ide-helper": "^4.5", @@ -31,4 +32,4 @@ "@composer test" ] } -} \ No newline at end of file +} diff --git a/phpstan.neon b/phpstan.neon index 1be0aa9..a69efa4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,5 @@ parameters: + phpVersion: 70100 level: 6 paths: diff --git a/src/YurunHttp/Cookie/CookieManager.php b/src/YurunHttp/Cookie/CookieManager.php index ee0c976..46cbf94 100644 --- a/src/YurunHttp/Cookie/CookieManager.php +++ b/src/YurunHttp/Cookie/CookieManager.php @@ -293,7 +293,7 @@ private function checkPath($uriPath, $cookiePath) $path = \dirname($path); } } - if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos($path, \DIRECTORY_SEPARATOR)) + if ('\\' === \DIRECTORY_SEPARATOR && str_contains($path, \DIRECTORY_SEPARATOR)) { $path = str_replace(\DIRECTORY_SEPARATOR, '/', $path); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3ab1f3b..a7db156 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,7 +10,7 @@ } define('SWOOLE_ON', extension_loaded('swoole')); -define('IS_WIN', 'WIN' === strtoupper(substr(PHP_OS, 0, 3))); +define('IS_WIN', 'WIN' === strtoupper(substr(\PHP_OS, 0, 3))); /** * @param string $name @@ -49,7 +49,9 @@ function testEnv($name, $default = null) `kill -15 {$GLOBALS['_wss_pid']} 2>/dev/null`; } echo 'WSS server stopped!', \PHP_EOL; - } catch (\Throwable $e) { + } + catch (\Throwable $e) + { echo 'WSS server stop failed: ', $e->getMessage(), \PHP_EOL; } } @@ -62,7 +64,9 @@ function testEnv($name, $default = null) echo 'Stopping Http2 server...', \PHP_EOL; echo `{$cmd}`, \PHP_EOL; echo 'Http2 server stopped!', \PHP_EOL; - } catch (\Throwable $e) { + } + catch (\Throwable $e) + { echo 'Http2 server stop failed: ', $e->getMessage(), \PHP_EOL; } } @@ -101,7 +105,9 @@ function testEnv($name, $default = null) } echo $name, ' stopped!', \PHP_EOL; - } catch (\Throwable $e) { + } + catch (\Throwable $e) + { echo $name, ' stop failed: ', $e->getMessage(), \PHP_EOL; } } @@ -126,7 +132,9 @@ function testEnv($name, $default = null) } } } - } catch (\Throwable $e) { + } + catch (\Throwable $e) + { // ignore } } @@ -135,10 +143,10 @@ function testEnv($name, $default = null) }); /** - * Start a PHP server process in background using proc_open. + * Start a PHP server process in background. * * On Windows: uses proc_open, records PID for taskkill /T cleanup later. - * On Linux/Mac: uses Workerman daemon mode (php ... start -d). + * On Linux/Mac: calls start-server.sh shell script (Workerman daemon mode). * * @param string $phpFile Absolute path to the PHP file * @param string $name Human-readable name for logging @@ -173,7 +181,7 @@ function startServerProcess($phpFile, $name) } else { - $cmd = 'php ' . escapeshellarg($phpFile) . ' start -d'; + $cmd = dirname($phpFile) . '/start-server.sh'; echo `{$cmd}`, \PHP_EOL; $GLOBALS['_server_handles'][] = [ 'proc' => null, @@ -211,7 +219,7 @@ function waitForWebSocketServer($port) { $context = stream_context_create(['http' => ['timeout' => 1]]); @file_get_contents('http://127.0.0.1:' . $port . '/', false, $context); - if (isset($http_response_header[0]) && false !== strpos($http_response_header[0], '400')) + if (isset($http_response_header[0]) && str_contains($http_response_header[0], '400')) { return; } @@ -247,7 +255,7 @@ function waitForWebSocketServer($port) { $context = stream_context_create(['http' => ['timeout' => 1]]); @file_get_contents('http://127.0.0.1:8902/', false, $context); - if (isset($http_response_header[0]) && false !== strpos($http_response_header[0], '400')) + if (isset($http_response_header[0]) && str_contains($http_response_header[0], '400')) { $serverStarted = true; break; From c11388242ebedd8d0d9c12b4b7b976c975584bc1 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 21:31:26 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/php.dockerfile | 2 +- .github/workflows/phpstan.yml | 2 +- tests/bootstrap.php | 309 ++++++++++++++++------------------ 3 files changed, 147 insertions(+), 166 deletions(-) diff --git a/.github/php.dockerfile b/.github/php.dockerfile index ba10801..ab48f76 100644 --- a/.github/php.dockerfile +++ b/.github/php.dockerfile @@ -12,4 +12,4 @@ RUN apt install --allow-unauthenticated -y --force-yes unzip ca-certificates RUN docker-php-ext-install pcntl > /dev/null -RUN curl -o /usr/bin/composer https://getcomposer.org/composer-1.phar && chmod +x /usr/bin/composer +RUN curl -o /usr/bin/composer https://getcomposer.org/composer-2.phar && chmod +x /usr/bin/composer diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index ccfe110..c3370bc 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -20,7 +20,7 @@ jobs: - name: Prepare run: | ./.github/prepare-test.sh swoole - docker exec swoole composer require phpstan/phpstan:~1.10.0 --dev + docker exec swoole composer require phpstan/phpstan:1.10.0 --dev - name: Test run: docker exec swoole ./vendor/bin/phpstan analyse --memory-limit 1G diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a7db156..b03d9b3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -30,7 +30,7 @@ function testEnv($name, $default = null) } /** - * Store for background server info. + * Store for background server info (Windows only). * Each item: ['proc' => resource|null, 'pid' => int, 'phpFile' => string, 'name' => string] */ $GLOBALS['_server_handles'] = []; @@ -39,51 +39,19 @@ function testEnv($name, $default = null) register_shutdown_function(function () { echo \PHP_EOL, '=== Cleaning up servers ===', \PHP_EOL; - // Stop WSS server (Swoole) - if (isset($GLOBALS['_wss_pid']) && $GLOBALS['_wss_pid']) - { - try { - echo 'Stopping WSS server...', \PHP_EOL; - if (!IS_WIN) - { - `kill -15 {$GLOBALS['_wss_pid']} 2>/dev/null`; - } - echo 'WSS server stopped!', \PHP_EOL; - } - catch (\Throwable $e) - { - echo 'WSS server stop failed: ', $e->getMessage(), \PHP_EOL; - } - } - - // Stop Http2 server - if (SWOOLE_ON && !IS_WIN) + if (IS_WIN) { - try { - $cmd = __DIR__ . '/server/Http2/stop-server.sh'; - echo 'Stopping Http2 server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; - echo 'Http2 server stopped!', \PHP_EOL; - } - catch (\Throwable $e) + // Kill Workerman servers started via proc_open + foreach ($GLOBALS['_server_handles'] as $item) { - echo 'Http2 server stop failed: ', $e->getMessage(), \PHP_EOL; - } - } - - // Kill Workerman servers - foreach ($GLOBALS['_server_handles'] as $item) - { - try { - $name = $item['name']; - $pid = $item['pid']; - $proc = $item['proc']; - $phpFile = $item['phpFile']; + try + { + $name = $item['name']; + $pid = $item['pid']; + $proc = $item['proc']; - echo 'Stopping ', $name, '...', \PHP_EOL; + echo 'Stopping ', $name, '...', \PHP_EOL; - if (IS_WIN) - { // Kill the ENTIRE process tree (Workerman spawns child workers) if ($pid > 0) { @@ -94,28 +62,18 @@ function testEnv($name, $default = null) { proc_close($proc); } + + echo $name, ' stopped!', \PHP_EOL; } - else + catch (\Throwable $e) { - // On Linux/Mac, use Workerman's built-in stop command - if ($phpFile) - { - `php {$phpFile} stop 2>/dev/null`; - } + echo $item['name'], ' stop failed: ', $e->getMessage(), \PHP_EOL; } - - echo $name, ' stopped!', \PHP_EOL; - } - catch (\Throwable $e) - { - echo $name, ' stop failed: ', $e->getMessage(), \PHP_EOL; } - } - // Final safety net: kill any remaining php.exe on our test ports - if (IS_WIN) - { - try { + // Final safety net: kill any remaining php.exe on our test ports + try + { foreach ([8898, 8900] as $port) { $output = `netstat -ano 2>nul | findstr ":{$port} " | findstr "LISTENING"`; @@ -138,24 +96,46 @@ function testEnv($name, $default = null) // ignore } } + else + { + // Linux: stop servers via stop-server.sh (previous version's method) + $cmd = __DIR__ . '/server/Http/stop-server.sh'; + echo 'Stoping http server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + echo 'Http Server stoped!', \PHP_EOL; + + if (SWOOLE_ON) + { + $cmd = __DIR__ . '/server/WebSocket/stop-server.sh'; + echo 'Stoping WebSocket server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + echo 'WebSocket Server stoped!', \PHP_EOL; + + $cmd = __DIR__ . '/server/Http2/stop-server.sh'; + echo 'Stoping Http2 server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + echo 'Http2 Server stoped!', \PHP_EOL; + } + } echo '=== Cleanup complete ===', \PHP_EOL; }); -/** - * Start a PHP server process in background. - * - * On Windows: uses proc_open, records PID for taskkill /T cleanup later. - * On Linux/Mac: calls start-server.sh shell script (Workerman daemon mode). - * - * @param string $phpFile Absolute path to the PHP file - * @param string $name Human-readable name for logging - * - * @return void - */ -function startServerProcess($phpFile, $name) +if (IS_WIN) { - if (IS_WIN) + // ===== Windows: start servers via proc_open (current logic) ===== + + /** + * Start a PHP server process in background. + * + * Uses proc_open, records PID for taskkill /T cleanup later. + * + * @param string $phpFile Absolute path to the PHP file + * @param string $name Human-readable name for logging + * + * @return void + */ + function startServerProcess($phpFile, $name) { $logFile = dirname($phpFile) . '/log.log'; $descriptorspec = [ @@ -179,83 +159,68 @@ function startServerProcess($phpFile, $name) 'name' => $name, ]; } - else - { - $cmd = dirname($phpFile) . '/start-server.sh'; - echo `{$cmd}`, \PHP_EOL; - $GLOBALS['_server_handles'][] = [ - 'proc' => null, - 'pid' => 0, - 'phpFile' => $phpFile, - 'name' => $name, - ]; - } -} -/** - * Wait for HTTP server to be ready. - */ -function waitForServer($port, $checkBody = 'YurunHttp') -{ - $url = 'http://127.0.0.1:' . $port . '/'; - for ($i = 0; $i < 30; ++$i) + /** + * Wait for HTTP server to be ready. + */ + function waitForServer($port, $checkBody = 'YurunHttp') { - $context = stream_context_create(['http' => ['timeout' => 1]]); - if ($checkBody === @file_get_contents($url, false, $context)) + $url = 'http://127.0.0.1:' . $port . '/'; + for ($i = 0; $i < 30; ++$i) { - return; + $context = stream_context_create(['http' => ['timeout' => 1]]); + if ($checkBody === @file_get_contents($url, false, $context)) + { + return; + } + sleep(1); } - sleep(1); + throw new \RuntimeException('Server start failed on port ' . $port); } - throw new \RuntimeException('Server start failed on port ' . $port); -} -/** - * Wait for WebSocket server to be ready. - */ -function waitForWebSocketServer($port) -{ - for ($i = 0; $i < 30; ++$i) + /** + * Wait for WebSocket server to be ready. + */ + function waitForWebSocketServer($port) { - $context = stream_context_create(['http' => ['timeout' => 1]]); - @file_get_contents('http://127.0.0.1:' . $port . '/', false, $context); - if (isset($http_response_header[0]) && str_contains($http_response_header[0], '400')) + for ($i = 0; $i < 30; ++$i) { - return; + $context = stream_context_create(['http' => ['timeout' => 1]]); + @file_get_contents('http://127.0.0.1:' . $port . '/', false, $context); + if (isset($http_response_header[0]) && str_contains($http_response_header[0], '400')) + { + return; + } + sleep(1); } - sleep(1); + throw new \RuntimeException('WebSocket server start failed on port ' . $port); } - throw new \RuntimeException('WebSocket server start failed on port ' . $port); -} -// --- Http Server (Workerman) --- -echo 'Starting Http server...', \PHP_EOL; -startServerProcess(__DIR__ . '/server/Http/server.php', 'Http server'); -waitForServer(8898); -echo 'Http server started!', \PHP_EOL; + // --- Http Server (Workerman) --- + echo 'Starting Http server...', \PHP_EOL; + startServerProcess(__DIR__ . '/server/Http/server.php', 'Http server'); + waitForServer(8898); + echo 'Http server started!', \PHP_EOL; -// --- WebSocket Server (Workerman) --- -echo 'Starting WebSocket server...', \PHP_EOL; -startServerProcess(__DIR__ . '/server/WebSocket/server.php', 'WebSocket server'); -waitForWebSocketServer(8900); -echo 'WebSocket server started!', \PHP_EOL; - -// --- WSS Server (Swoole-based, only on non-Windows with Swoole) --- -if (SWOOLE_ON && !IS_WIN) + // --- WebSocket Server (Workerman) --- + echo 'Starting WebSocket server...', \PHP_EOL; + startServerProcess(__DIR__ . '/server/WebSocket/server.php', 'WebSocket server'); + waitForWebSocketServer(8900); + echo 'WebSocket server started!', \PHP_EOL; +} +else { - echo 'Starting WSS server (Swoole)...', \PHP_EOL; - $cmd = 'nohup php ' . escapeshellarg(__DIR__ . '/server/WebSocket/wss-server.php') . ' > /dev/null 2>&1 & echo $!'; - $wssPid = trim(`{$cmd}`); - if ($wssPid) - { - $GLOBALS['_wss_pid'] = $wssPid; - } + // ===== Linux: start servers via start-server.sh (previous version's method) ===== + + // Http Server + $cmd = __DIR__ . '/server/Http/start-server.sh'; + echo 'Starting Http server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; $serverStarted = false; for ($i = 0; $i < 10; ++$i) { $context = stream_context_create(['http' => ['timeout' => 1]]); - @file_get_contents('http://127.0.0.1:8902/', false, $context); - if (isset($http_response_header[0]) && str_contains($http_response_header[0], '400')) + if ('YurunHttp' === @file_get_contents(testEnv('HTTP_SERVER_HOST', 'http://127.0.0.1:8898/'), false, $context)) { $serverStarted = true; break; @@ -264,45 +229,61 @@ function waitForWebSocketServer($port) } if ($serverStarted) { - echo 'WSS server started!', \PHP_EOL; + echo 'Http server started!', \PHP_EOL; } else { - throw new \RuntimeException('WSS server start failed'); + throw new \RuntimeException('Http server start failed'); } -} -// --- Http2 Server (Swoole-only, not available on Windows) --- -if (SWOOLE_ON && !IS_WIN) -{ - $cmd = __DIR__ . '/server/Http2/start-server.sh'; - echo 'Starting Http2 server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; - $serverStarted = false; - for ($i = 0; $i < 10; ++$i) + if (SWOOLE_ON) { - @file_get_contents(testEnv('HTTP2_SERVER_HOST', 'http://127.0.0.1:8901/')); - if (isset($http_response_header[0]) && 'HTTP/1.1 200 OK' === $http_response_header[0]) + // WebSocket Server + $cmd = __DIR__ . '/server/WebSocket/start-server.sh'; + echo 'Starting WebSocket server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + $serverStarted = false; + for ($i = 0; $i < 10; ++$i) { - $serverStarted = true; - break; + @file_get_contents(str_replace('ws://', 'http://', testEnv('WS_SERVER_HOST', 'ws://127.0.0.1:8900/'))); + if (isset($http_response_header[0]) && 'HTTP/1.1 400 Bad Request' === $http_response_header[0]) + { + $serverStarted = true; + break; + } + sleep(1); + } + if ($serverStarted) + { + echo 'WebSocekt server started!', \PHP_EOL; + } + else + { + throw new \RuntimeException('WebSocekt server start failed'); + } + + // Http2 Server + $cmd = __DIR__ . '/server/Http2/start-server.sh'; + echo 'Starting Http2 server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + $serverStarted = false; + for ($i = 0; $i < 10; ++$i) + { + @file_get_contents(testEnv('HTTP2_SERVER_HOST', 'http://127.0.0.1:8901/')); + if (isset($http_response_header[0]) && 'HTTP/1.1 200 OK' === $http_response_header[0]) + { + $serverStarted = true; + break; + } + sleep(1); + } + if ($serverStarted) + { + echo 'Http2 server started!', \PHP_EOL; + } + else + { + throw new \RuntimeException('Http2 server start failed'); } - sleep(1); - } - if ($serverStarted) - { - echo 'Http2 server started!', \PHP_EOL; - } - else - { - throw new \RuntimeException('Http2 server start failed'); } } -elseif (!SWOOLE_ON) -{ - echo 'Http2 server skipped (Swoole not available)', \PHP_EOL; -} -else -{ - echo 'Http2 server skipped (not supported on Windows)', \PHP_EOL; -} From 5469667dc35b772723b90a808469c779472159aa Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 29 Jun 2026 21:37:09 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/php.dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/php.dockerfile b/.github/php.dockerfile index ab48f76..c562c5f 100644 --- a/.github/php.dockerfile +++ b/.github/php.dockerfile @@ -12,4 +12,4 @@ RUN apt install --allow-unauthenticated -y --force-yes unzip ca-certificates RUN docker-php-ext-install pcntl > /dev/null -RUN curl -o /usr/bin/composer https://getcomposer.org/composer-2.phar && chmod +x /usr/bin/composer +RUN curl -o /usr/bin/composer https://getcomposer.org/composer-1.phar && chmod +x /usr/bin/composer && composer self-update --2.2 From b881c8fc6476f8dbb4e82592ae6d59399cf47514 Mon Sep 17 00:00:00 2001 From: Yurun Date: Tue, 30 Jun 2026 08:40:09 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/bootstrap.php | 48 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b03d9b3..25d7773 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -115,6 +115,26 @@ function testEnv($name, $default = null) echo 'Stoping Http2 server...', \PHP_EOL; echo `{$cmd}`, \PHP_EOL; echo 'Http2 Server stoped!', \PHP_EOL; + + $pidFile = __DIR__ . '/server/WebSocket/wss-server.pid'; + if (is_file($pidFile)) + { + $pid = (int) file_get_contents($pidFile); + if ($pid > 0) + { + echo 'Stoping WSS server (PID: ' . $pid . ')...', \PHP_EOL; + if (function_exists('posix_kill')) + { + posix_kill($pid, \SIGTERM); + } + else + { + `kill -15 {$pid} 2>/dev/null`; + } + } + @unlink($pidFile); + echo 'WSS Server stoped!', \PHP_EOL; + } } } @@ -246,7 +266,7 @@ function waitForWebSocketServer($port) for ($i = 0; $i < 10; ++$i) { @file_get_contents(str_replace('ws://', 'http://', testEnv('WS_SERVER_HOST', 'ws://127.0.0.1:8900/'))); - if (isset($http_response_header[0]) && 'HTTP/1.1 400 Bad Request' === $http_response_header[0]) + if (isset($http_response_header[0]) && false !== stripos($http_response_header[0], '400 Bad Request')) { $serverStarted = true; break; @@ -285,5 +305,31 @@ function waitForWebSocketServer($port) { throw new \RuntimeException('Http2 server start failed'); } + + // WSS Server (Swoole SSL WebSocket) + $wssPidFile = __DIR__ . '/server/WebSocket/wss-server.pid'; + $cmd = 'nohup /usr/bin/env php "' . __DIR__ . '/server/WebSocket/wss-server.php" > "' . __DIR__ . '/server/WebSocket/wss-server.log" 2>&1 & echo $! > "' . $wssPidFile . '"'; + echo 'Starting WSS server...', \PHP_EOL; + echo `{$cmd}`, \PHP_EOL; + $serverStarted = false; + for ($i = 0; $i < 10; ++$i) + { + $context = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]); + @file_get_contents('https://127.0.0.1:8902/', false, $context); + if (isset($http_response_header[0]) && false !== stripos($http_response_header[0], '400')) + { + $serverStarted = true; + break; + } + sleep(1); + } + if ($serverStarted) + { + echo 'WSS server started!', \PHP_EOL; + } + else + { + throw new \RuntimeException('WSS server start failed'); + } } } From a593d0ea79ebdc847b759a46b0d4eaa99a9b2614 Mon Sep 17 00:00:00 2001 From: Yurun Date: Tue, 30 Jun 2026 09:36:12 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/php-low.dockerfile | 19 ------------------- .github/php.dockerfile | 2 +- .github/prepare-test.sh | 6 +----- phpstan.neon | 1 - tests/bootstrap.php | 33 +++++++++++++++++++++------------ 5 files changed, 23 insertions(+), 38 deletions(-) delete mode 100644 .github/php-low.dockerfile diff --git a/.github/php-low.dockerfile b/.github/php-low.dockerfile deleted file mode 100644 index 8825717..0000000 --- a/.github/php-low.dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -ARG PHP_DOCKER_VERSION -FROM php:${PHP_DOCKER_VERSION}-cli - -RUN sed -i s/httpredir.debian.org/archive.debian.org/g /etc/apt/sources.list -RUN sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list - -RUN sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list - -RUN sed -i '/stretch-updates/d' /etc/apt/sources.list -RUN sed -i '/jessie-updates/d' /etc/apt/sources.list -RUN sed -i '/jessie\/updates/d' /etc/apt/sources.list - -RUN apt update --allow-unauthenticated - -RUN apt install --allow-unauthenticated -y --force-yes unzip ca-certificates - -RUN docker-php-ext-install pcntl > /dev/null - -RUN curl -o /usr/bin/composer https://getcomposer.org/composer-1.phar && chmod +x /usr/bin/composer diff --git a/.github/php.dockerfile b/.github/php.dockerfile index c562c5f..b86491c 100644 --- a/.github/php.dockerfile +++ b/.github/php.dockerfile @@ -12,4 +12,4 @@ RUN apt install --allow-unauthenticated -y --force-yes unzip ca-certificates RUN docker-php-ext-install pcntl > /dev/null -RUN curl -o /usr/bin/composer https://getcomposer.org/composer-1.phar && chmod +x /usr/bin/composer && composer self-update --2.2 +RUN curl -o /usr/bin/composer https://getcomposer.org/download/latest-2.2.x/composer.phar && chmod +x /usr/bin/composer diff --git a/.github/prepare-test.sh b/.github/prepare-test.sh index a6bed81..579b203 100755 --- a/.github/prepare-test.sh +++ b/.github/prepare-test.sh @@ -4,11 +4,7 @@ __DIR__=$(cd `dirname $0`; pwd) cd $__DIR__ -if [[ `expr $PHP_DOCKER_VERSION \< 7.1` -eq 0 ]]; then - export PHP_DOCKER_FILE="php.dockerfile" -else - export PHP_DOCKER_FILE="php-low.dockerfile" -fi +export PHP_DOCKER_FILE="php.dockerfile" containerName=$1 diff --git a/phpstan.neon b/phpstan.neon index a69efa4..1be0aa9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,4 @@ parameters: - phpVersion: 70100 level: 6 paths: diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 25d7773..f9c0f3e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,7 +29,7 @@ function testEnv($name, $default = null) return $result; } -/** +/* * Store for background server info (Windows only). * Each item: ['proc' => resource|null, 'pid' => int, 'phpFile' => string, 'name' => string] */ @@ -55,7 +55,7 @@ function testEnv($name, $default = null) // Kill the ENTIRE process tree (Workerman spawns child workers) if ($pid > 0) { - `taskkill /F /T /PID {$pid} 2>nul`; + shell_exec("taskkill /F /T /PID {$pid} 2>nul"); } // Also close the proc_open handle if (is_resource($proc)) @@ -76,7 +76,7 @@ function testEnv($name, $default = null) { foreach ([8898, 8900] as $port) { - $output = `netstat -ano 2>nul | findstr ":{$port} " | findstr "LISTENING"`; + $output = shell_exec("netstat -ano 2>nul | findstr \":{$port} \" | findstr \"LISTENING\""); if ($output) { $lines = explode("\n", trim($output)); @@ -85,7 +85,7 @@ function testEnv($name, $default = null) $line = trim($line); if (preg_match('/\s+(\d+)$/', $line, $m)) { - `taskkill /F /T /PID {$m[1]} 2>nul`; + shell_exec("taskkill /F /T /PID {$m[1]} 2>nul"); } } } @@ -101,19 +101,19 @@ function testEnv($name, $default = null) // Linux: stop servers via stop-server.sh (previous version's method) $cmd = __DIR__ . '/server/Http/stop-server.sh'; echo 'Stoping http server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; echo 'Http Server stoped!', \PHP_EOL; if (SWOOLE_ON) { $cmd = __DIR__ . '/server/WebSocket/stop-server.sh'; echo 'Stoping WebSocket server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; echo 'WebSocket Server stoped!', \PHP_EOL; $cmd = __DIR__ . '/server/Http2/stop-server.sh'; echo 'Stoping Http2 server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; echo 'Http2 Server stoped!', \PHP_EOL; $pidFile = __DIR__ . '/server/WebSocket/wss-server.pid'; @@ -129,7 +129,7 @@ function testEnv($name, $default = null) } else { - `kill -15 {$pid} 2>/dev/null`; + shell_exec("kill -15 {$pid} 2>/dev/null"); } } @unlink($pidFile); @@ -182,6 +182,11 @@ function startServerProcess($phpFile, $name) /** * Wait for HTTP server to be ready. + * + * @param int $port + * @param string $checkBody + * + * @return void */ function waitForServer($port, $checkBody = 'YurunHttp') { @@ -200,6 +205,10 @@ function waitForServer($port, $checkBody = 'YurunHttp') /** * Wait for WebSocket server to be ready. + * + * @param int $port + * + * @return void */ function waitForWebSocketServer($port) { @@ -235,7 +244,7 @@ function waitForWebSocketServer($port) // Http Server $cmd = __DIR__ . '/server/Http/start-server.sh'; echo 'Starting Http server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; $serverStarted = false; for ($i = 0; $i < 10; ++$i) { @@ -261,7 +270,7 @@ function waitForWebSocketServer($port) // WebSocket Server $cmd = __DIR__ . '/server/WebSocket/start-server.sh'; echo 'Starting WebSocket server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; $serverStarted = false; for ($i = 0; $i < 10; ++$i) { @@ -285,7 +294,7 @@ function waitForWebSocketServer($port) // Http2 Server $cmd = __DIR__ . '/server/Http2/start-server.sh'; echo 'Starting Http2 server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; $serverStarted = false; for ($i = 0; $i < 10; ++$i) { @@ -310,7 +319,7 @@ function waitForWebSocketServer($port) $wssPidFile = __DIR__ . '/server/WebSocket/wss-server.pid'; $cmd = 'nohup /usr/bin/env php "' . __DIR__ . '/server/WebSocket/wss-server.php" > "' . __DIR__ . '/server/WebSocket/wss-server.log" 2>&1 & echo $! > "' . $wssPidFile . '"'; echo 'Starting WSS server...', \PHP_EOL; - echo `{$cmd}`, \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; $serverStarted = false; for ($i = 0; $i < 10; ++$i) { From 4e34bf36bf94a8b740bcc88f91803859de08bbcc Mon Sep 17 00:00:00 2001 From: Yurun Date: Tue, 30 Jun 2026 19:47:16 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/bootstrap.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f9c0f3e..d4b4571 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -182,10 +182,10 @@ function startServerProcess($phpFile, $name) /** * Wait for HTTP server to be ready. - * - * @param int $port + * + * @param int $port * @param string $checkBody - * + * * @return void */ function waitForServer($port, $checkBody = 'YurunHttp') @@ -205,9 +205,9 @@ function waitForServer($port, $checkBody = 'YurunHttp') /** * Wait for WebSocket server to be ready. - * + * * @param int $port - * + * * @return void */ function waitForWebSocketServer($port) From e567a3962fd2fa711fbaea25a0c8b3351b4de486 Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 8 Jul 2026 14:52:29 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HttpRequest.php | 44 +++---------------- src/YurunHttp/Co/Batch.php | 11 +---- src/YurunHttp/Handler/Curl.php | 11 +++-- .../Handler/Curl/CurlConnectionPool.php | 7 ++- src/YurunHttp/Handler/Swoole.php | 10 ++--- 5 files changed, 25 insertions(+), 58 deletions(-) diff --git a/src/HttpRequest.php b/src/HttpRequest.php index 5129a10..f88b134 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -226,20 +226,6 @@ class HttpRequest */ public $websocketCompression = false; - /** - * 代理认证方式. - * - * @var array - */ - public static $proxyAuths = []; - - /** - * 代理类型. - * - * @var array - */ - public static $proxyType = []; - /** * 自动扩展名标志. */ @@ -912,6 +898,7 @@ protected function parseRequestBody($requestBody, $contentType) } } $body = http_build_query($body, '', '&'); + break; } } else @@ -943,9 +930,8 @@ public function buildRequest($url = null, $requestBody = null, $method = null, $ $method = $this->method; } list($body, $files) = $this->parseRequestBody(null === $requestBody ? $this->content : $requestBody, $contentType); - $request = new Request($url, $this->headers, $body, $method); $saveFileOption = $this->saveFileOption; - $request = $request->withUploadedFiles($files) + $request = (new Request($url, $this->headers, $body, $method))->withUploadedFiles($files) ->withCookieParams($this->cookies) ->withAttribute(Attributes::MAX_REDIRECTS, $this->maxRedirects) ->withAttribute(Attributes::IS_VERIFY_CA, $this->isVerifyCA) @@ -1127,7 +1113,7 @@ public function download($fileName, $url = null, $requestBody = null, $method = $result = $this->saveFile($fileName)->send($url, $requestBody, $method); if ($isAutoExt) { - self::parseDownloadAutoExt($result, $fileName); + $result = self::parseDownloadAutoExt($result, $fileName); } $this->saveFileOption = []; @@ -1174,9 +1160,9 @@ public static function checkDownloadIsAutoExt($fileName, &$tempFileName) * @param \Yurun\Util\YurunHttp\Http\Response $response * @param string $tempFileName * - * @return void + * @return \Yurun\Util\YurunHttp\Http\Response */ - public static function parseDownloadAutoExt(&$response, $tempFileName) + public static function parseDownloadAutoExt($response, $tempFileName) { $ext = MediaType::getExt($response->getHeaderLine('Content-Type')); if (null === $ext) @@ -1185,23 +1171,7 @@ public static function parseDownloadAutoExt(&$response, $tempFileName) } $savedFileName = substr($tempFileName, 0, -\strlen(self::AUTO_EXT_TEMP_EXT)) . '.' . $ext; rename($tempFileName, $savedFileName); - $response = $response->withSavedFileName($savedFileName); - } -} -if (\extension_loaded('curl')) -{ - // 代理认证方式 - HttpRequest::$proxyAuths = [ - 'basic' => \CURLAUTH_BASIC, - 'ntlm' => \CURLAUTH_NTLM, - ]; - - // 代理类型 - HttpRequest::$proxyType = [ - 'http' => \CURLPROXY_HTTP, - 'socks4' => \CURLPROXY_SOCKS4, - 'socks4a' => 6, // CURLPROXY_SOCKS4A - 'socks5' => \CURLPROXY_SOCKS5, - ]; + return $response->withSavedFileName($savedFileName); + } } diff --git a/src/YurunHttp/Co/Batch.php b/src/YurunHttp/Co/Batch.php index 185205d..34f8002 100644 --- a/src/YurunHttp/Co/Batch.php +++ b/src/YurunHttp/Co/Batch.php @@ -53,17 +53,8 @@ public static function run($requests, $timeout = null, $handlerClass = null, $op { if (isset($result[$i])) { - $response = &$result[$i]; + HttpRequest::parseDownloadAutoExt($result[$i], $result[$i]->getRequest()->getAttribute(Attributes::SAVE_FILE_PATH)); } - else - { - $response = null; - } - if ($response) - { - HttpRequest::parseDownloadAutoExt($response, $response->getRequest()->getAttribute(Attributes::SAVE_FILE_PATH)); - } - unset($response); } return $result; diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index e8911fa..5c67a6c 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -119,7 +119,7 @@ public function close() { CurlHttpConnectionManager::getInstance()->release($this->poolKey, $this->handler); } - else + elseif (\PHP_VERSION_ID < 80000) { curl_close($this->handler); } @@ -203,7 +203,6 @@ public function send(&$request) $bodyContent = false; } $this->buildCurlHandlerEx($request, $handler, $uri, $method, $bodyContent); - $result = null; for ($i = 0; $i <= $retry; ++$i) { $receiveHeaders = []; @@ -227,7 +226,7 @@ public function send(&$request) } } } - if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && $result && '' !== ($location = $result->getHeaderLine('location'))) + if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && isset($result) && '' !== ($location = $result->getHeaderLine('location'))) { $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); if (++$redirectCount <= $maxRedirects) @@ -366,6 +365,7 @@ private function buildCurlHandlerEx(&$request, $handler, $uri = null, $method = break; default: $httpVersion = \CURL_HTTP_VERSION_1_1; + break; } $requestOptions = [ \CURLOPT_URL => (string) $uri, @@ -817,7 +817,10 @@ public function coBatch($requests, $timeout = null) foreach ($curlHandlers as $curlHandler) { curl_multi_remove_handle($mh, $curlHandler); - curl_close($curlHandler); + if (\PHP_VERSION_ID < 80000) + { + curl_close($curlHandler); + } } curl_multi_close($mh); } diff --git a/src/YurunHttp/Handler/Curl/CurlConnectionPool.php b/src/YurunHttp/Handler/Curl/CurlConnectionPool.php index 7ca5ad8..b957642 100644 --- a/src/YurunHttp/Handler/Curl/CurlConnectionPool.php +++ b/src/YurunHttp/Handler/Curl/CurlConnectionPool.php @@ -39,9 +39,12 @@ public function close() $connections = $this->connections; $this->connections = []; $this->queue = new \SplQueue(); - foreach ($connections as $connection) + if (\PHP_VERSION_ID < 80000) { - curl_close($connection); + foreach ($connections as $connection) + { + curl_close($connection); + } } } diff --git a/src/YurunHttp/Handler/Swoole.php b/src/YurunHttp/Handler/Swoole.php index 23224d6..61a0db4 100644 --- a/src/YurunHttp/Handler/Swoole.php +++ b/src/YurunHttp/Handler/Swoole.php @@ -803,16 +803,16 @@ public function coBatch($requests, $timeout = null) $handlers = []; $results = []; $beginTime = microtime(true); - foreach ($requests as $i => &$request) + foreach ($requests as $i => &$request1) { $results[$i] = null; $handlers[$i] = $handler = new self(); - $request = $handler->sendDefer($request); + $request1 = $handler->sendDefer($request1); } - unset($request); + unset($request1); $beginTime = microtime(true); $recvTimeout = null; - foreach ($requests as $i => $request) + foreach ($requests as $i => $request2) { if (null !== $timeout) { @@ -822,7 +822,7 @@ public function coBatch($requests, $timeout = null) break; } } - $results[$i] = $handlers[$i]->recvDefer($request, $recvTimeout)->withTotalTime(microtime(true) - $beginTime); + $results[$i] = $handlers[$i]->recvDefer($request2, $recvTimeout)->withTotalTime(microtime(true) - $beginTime); } return $results; From a0eaeda568de287f5837a6a6c48d16e7d8143576 Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 8 Jul 2026 14:56:02 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/YurunHttp/Handler/Curl.php | 37 ++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index 5c67a6c..aaafbde 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -226,29 +226,32 @@ public function send(&$request) } } } - if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && isset($result) && '' !== ($location = $result->getHeaderLine('location'))) + if (isset($result)) { - $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); - if (++$redirectCount <= $maxRedirects) + if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && '' !== ($location = $result->getHeaderLine('location'))) { - // 重定向清除之前下载的文件 - if (null !== $saveFileFp) + $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); + if (++$redirectCount <= $maxRedirects) { - ftruncate($saveFileFp, 0); - fseek($saveFileFp, 0); + // 重定向清除之前下载的文件 + if (null !== $saveFileFp) + { + ftruncate($saveFileFp, 0); + fseek($saveFileFp, 0); + } + $isLocation = true; + $uri = $this->parseRedirectLocation($location, $uri); + continue; + } + else + { + $result = $result->withErrno(-1) + ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); } - $isLocation = true; - $uri = $this->parseRedirectLocation($location, $uri); - continue; - } - else - { - $result = $result->withErrno(-1) - ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); } + $result = $result->withTotalTime(microtime(true) - $beginTime); + $this->logRequest($request, $result); } - $result = $result->withTotalTime(microtime(true) - $beginTime); - $this->logRequest($request, $result); $this->cookieManager->gc(); $this->saveCookieJar(); break;