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 9414bb6..b86491c 100644 --- a/.github/php.dockerfile +++ b/.github/php.dockerfile @@ -1,10 +1,15 @@ 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 -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/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 865098f..579b203 100755 --- a/.github/prepare-test.sh +++ b/.github/prepare-test.sh @@ -4,15 +4,11 @@ __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 -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/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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f0bbbc..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 @@ -15,7 +15,7 @@ jobs: PHP_DOCKER_VERSION: ${{ matrix.php }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: prepare run: | @@ -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 @@ -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: | 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..c3370bc 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,12 +15,12 @@ jobs: SWOOLE_DOCKER_VERSION: ${{ matrix.swoole }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - 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/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/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..aaafbde 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,29 +226,32 @@ public function send(&$request) } } } - if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && $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; @@ -366,6 +368,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 +820,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; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 68b37de..d4b4571 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,65 +29,227 @@ 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) -{ - $context = stream_context_create(['http' => ['timeout' => 1]]); - if ('YurunHttp' === @file_get_contents(testEnv('HTTP_SERVER_HOST', 'http://127.0.0.1:8898/'), false, $context)) +/* + * Store for background server info (Windows only). + * 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; + + if (IS_WIN) { - $serverStarted = true; - break; + // Kill Workerman servers started via proc_open + foreach ($GLOBALS['_server_handles'] as $item) + { + try + { + $name = $item['name']; + $pid = $item['pid']; + $proc = $item['proc']; + + echo 'Stopping ', $name, '...', \PHP_EOL; + + // Kill the ENTIRE process tree (Workerman spawns child workers) + if ($pid > 0) + { + shell_exec("taskkill /F /T /PID {$pid} 2>nul"); + } + // Also close the proc_open handle + if (is_resource($proc)) + { + proc_close($proc); + } + + echo $name, ' stopped!', \PHP_EOL; + } + catch (\Throwable $e) + { + echo $item['name'], ' stop failed: ', $e->getMessage(), \PHP_EOL; + } + } + + // Final safety net: kill any remaining php.exe on our test ports + try + { + foreach ([8898, 8900] as $port) + { + $output = shell_exec("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)) + { + shell_exec("taskkill /F /T /PID {$m[1]} 2>nul"); + } + } + } + } + } + catch (\Throwable $e) + { + // 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 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 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 shell_exec($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 + { + shell_exec("kill -15 {$pid} 2>/dev/null"); + } + } + @unlink($pidFile); + echo 'WSS Server stoped!', \PHP_EOL; + } + } } - sleep(1); -} -if ($serverStarted) -{ - echo 'Http server started!', \PHP_EOL; -} -else -{ - throw new \RuntimeException('Http server start failed'); -} -if (SWOOLE_ON) + echo '=== Cleanup complete ===', \PHP_EOL; +}); + +if (IS_WIN) { - // 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) + // ===== 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) { - @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]) + $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) { - $serverStarted = true; - break; + throw new \RuntimeException('Failed to start ' . $name . ': ' . $phpFile); } - sleep(1); + // 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, + ]; } - if ($serverStarted) + + /** + * Wait for HTTP server to be ready. + * + * @param int $port + * @param string $checkBody + * + * @return void + */ + function waitForServer($port, $checkBody = 'YurunHttp') { - echo 'WebSocekt 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. + * + * @param int $port + * + * @return void + */ + function waitForWebSocketServer($port) { - throw new \RuntimeException('WebSocekt 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]) && str_contains($http_response_header[0], '400')) + { + return; + } + sleep(1); + } + throw new \RuntimeException('WebSocket server start failed on port ' . $port); } - // Http2 Server - $cmd = __DIR__ . '/server/Http2/start-server.sh'; - echo 'Starting Http2 server...', \PHP_EOL; - echo `{$cmd}`, \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; +} +else +{ + // ===== 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 shell_exec($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]) + $context = stream_context_create(['http' => ['timeout' => 1]]); + if ('YurunHttp' === @file_get_contents(testEnv('HTTP_SERVER_HOST', 'http://127.0.0.1:8898/'), false, $context)) { $serverStarted = true; break; @@ -88,31 +258,87 @@ function testEnv($name, $default = null) } if ($serverStarted) { - echo 'Http2 server started!', \PHP_EOL; + echo 'Http server started!', \PHP_EOL; } else { - throw new \RuntimeException('Http2 server start failed'); + throw new \RuntimeException('Http 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; + // WebSocket Server + $cmd = __DIR__ . '/server/WebSocket/start-server.sh'; + echo 'Starting WebSocket server...', \PHP_EOL; + echo shell_exec($cmd), \PHP_EOL; + $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]) && false !== stripos($http_response_header[0], '400 Bad Request')) + { + $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 shell_exec($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'); + } + + // 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 shell_exec($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'); + } } -}); +} 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();