<?php
namespace Yurun\Util\YurunHttp\Handler;
use Yurun\Util\YurunHttp;
use Yurun\Util\YurunHttp\Attributes;
use Yurun\Util\YurunHttp\Http\Psr7\Uri;
use Yurun\Util\YurunHttp\Http\Response;
use Swoole\Http2\Request as Http2Request;
use Yurun\Util\YurunHttp\Traits\THandler;
use Yurun\Util\YurunHttp\Traits\TCookieManager;
use Yurun\Util\YurunHttp\Http\Psr7\Consts\MediaType;
use Yurun\Util\YurunHttp\Exception\WebSocketException;
use Yurun\Util\YurunHttp\Handler\Swoole\HttpConnectionManager;
use Yurun\Util\YurunHttp\Handler\Swoole\Http2ConnectionManager;
class Swoole implements IHandler
{
use TCookieManager, THandler;
private $httpConnectionManager;
private $http2ConnectionManager;
private $result;
private static $defaultUA;
public function __construct()
{
if(null === static::$defaultUA)
{
static::$defaultUA = sprintf('Mozilla/5.0 YurunHttp/%s Swoole/%s', YurunHttp::VERSION, defined('SWOOLE_VERSION') ? SWOOLE_VERSION : 'unknown');
}
$this->initCookieManager();
$this->httpConnectionManager = new HttpConnectionManager;
$this->http2ConnectionManager = new Http2ConnectionManager;
}
public function close()
{
$this->httpConnectionManager->close();
$this->http2ConnectionManager->close();
}
public function buildRequest($request, $connection, &$http2Request)
{
if($isHttp2 = '2.0' === $request->getProtocolVersion())
{
$http2Request = new Http2Request;
}
else
{
$http2Request = null;
}
$uri = $request->getUri();
if($isHttp2)
{
$http2Request->method = $request->getMethod();
}
else
{
$connection->setMethod($request->getMethod());
}
$this->parseCookies($request, $connection, $http2Request);
$hasFile = false;
$redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0);
if($redirectCount <= 0)
{
$files = $request->getUploadedFiles();
$body = (string)$request->getBody();
if(!empty($files))
{
if($isHttp2)
{
throw new \RuntimeException('Http2 swoole handler does not support upload file');
}
$hasFile = true;
foreach($files as $name => $file)
{
$connection->addFile($file->getTempFileName(), $name, $file->getClientMediaType(), basename($file->getClientFilename()));
}
parse_str($body, $body);
}
if($isHttp2)
{
$http2Request->data = $body;
}
else
{
$connection->setData($body);
}
}
$this->parseSSL($request);
$this->parseProxy($request);
$this->parseNetwork($request);
$settings = $request->getAttribute(Attributes::OPTIONS, []);
if($settings)
{
$connection->set($settings);
}
$request = $request->withHeader('Host', Uri::getDomain($uri));
if(!$hasFile && !$request->hasHeader('Content-Type'))
{
$request = $request->withHeader('Content-Type', MediaType::APPLICATION_FORM_URLENCODED);
}
if(!$request->hasHeader('User-Agent'))
{
$request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, static::$defaultUA));
}
$headers = [];
foreach($request->getHeaders() as $name => $value)
{
$headers[$name] = implode(',', $value);
}
if($isHttp2)
{
$http2Request->headers = $headers;
$http2Request->pipeline = $request->getAttribute(Attributes::HTTP2_PIPELINE, false);
$path = $uri->getPath();
if('' === $path)
{
$path = '/';
}
$query = $uri->getQuery();
if('' !== $query)
{
$path .= '?' . $query;
}
$http2Request->path = $path;
}
else
{
$connection->setHeaders($headers);
}
}
public function send($request)
{
$deferRequest = $this->sendDefer($request);
if($request->getAttribute(Attributes::PRIVATE_IS_HTTP2) && $request->getAttribute(Attributes::HTTP2_NOT_RECV))
{
return true;
}
return !!$this->recvDefer($deferRequest);
}
public function sendDefer($request)
{
if([] !== ($queryParams = $request->getQueryParams()))
{
$request = $request->withUri($request->getUri()->withQuery(http_build_query($queryParams, '', '&')));
}
$uri = $request->getUri();
$isHttp2 = '2.0' === $request->getProtocolVersion();
if($isHttp2)
{
$connection = $this->http2ConnectionManager->getConnection($uri->getHost(), Uri::getServerPort($uri), 'https' === $uri->getScheme() || 'wss' === $uri->getScheme());
}
else
{
$connection = $this->httpConnectionManager->getConnection($uri->getHost(), Uri::getServerPort($uri), 'https' === $uri->getScheme() || 'wss' === $uri->getScheme());
$connection->setDefer(true);
}
$isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET);
$this->buildRequest($request, $connection, $http2Request);
$path = $uri->getPath();
if('' === $path)
{
$path = '/';
}
$query = $uri->getQuery();
if('' !== $query)
{
$path .= '?' . $query;
}
if($isWebSocket)
{
if($isHttp2)
{
throw new \RuntimeException('Http2 swoole handler does not support websocket');
}
if(!$connection->upgrade($path))
{
throw new WebSocketException(sprintf('WebSocket connect faled, error: %s, errorCode: %s', swoole_strerror($connection->errCode), $connection->errCode), $connection->errCode);
}
}
else if(null === ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH)))
{
if($isHttp2)
{
$result = $connection->send($http2Request);
$request = $request->withAttribute(Attributes::PRIVATE_HTTP2_STREAM_ID, $result);
}
else
{
$connection->execute($path);
}
}
else
{
if($isHttp2)
{
throw new \RuntimeException('Http2 swoole handler does not support download file');
}
$connection->download($path, $saveFilePath);
}
return $request->withAttribute(Attributes::PRIVATE_IS_HTTP2, $isHttp2)
->withAttribute(Attributes::PRIVATE_IS_WEBSOCKET, $isHttp2)
->withAttribute(Attributes::PRIVATE_CONNECTION, $connection);
}
public function recvDefer($request)
{
$connection = $request->getAttribute(Attributes::PRIVATE_CONNECTION);
$retryCount = $request->getAttribute(Attributes::PRIVATE_RETRY_COUNT, 0);
$redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0);
$isHttp2 = '2.0' === $request->getProtocolVersion();
$isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET);
$this->getResponse($request, $connection, $isWebSocket, $isHttp2);
$statusCode = $this->result->getStatusCode();
if((0 === $statusCode || (5 === (int)($statusCode/100))) && $retryCount < $request->getAttribute(Attributes::RETRY, 0))
{
$request = $request->withAttribute(Attributes::RETRY, ++$retryCount);
$deferRequest = $this->sendDefer($request);
return $this->recvDefer($deferRequest);
}
if(!$isWebSocket && $statusCode >= 300 && $statusCode < 400 && $request->getAttribute(Attributes::FOLLOW_LOCATION, true))
{
if(++$redirectCount <= ($maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10)))
{
$uri = $this->parseRedirectLocation($this->result->getHeaderLine('location'), $request->getUri());
if(in_array($statusCode, [301, 302, 303]))
{
$method = 'GET';
}
else
{
$method = $request->getMethod();
}
return $this->send($request->withMethod($method)->withUri($uri)->withAttribute(Attributes::PRIVATE_REDIRECT_COUNT, $redirectCount));
}
else
{
$this->result = $this->result->withErrno(-1)
->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects));
return false;
}
}
return $this->result;
}
public function websocket($request, $websocketClient = null)
{
if(!$websocketClient)
{
$websocketClient = new \Yurun\Util\YurunHttp\WebSocket\Swoole;
}
$this->send($request->withAttribute(Attributes::PRIVATE_WEBSOCKET, true));
$websocketClient->init($this, $request, $this->result);
return $websocketClient;
}
public function recv()
{
return $this->result;
}
private function parseCookies(&$request, $connection, $http2Request)
{
$cookieParams = $request->getCookieParams();
foreach($cookieParams as $name => $value)
{
$this->cookieManager->setCookie($name, $value);
}
$cookies = $this->cookieManager->getRequestCookies($request->getUri());
if($http2Request)
{
$http2Request->cookies = $cookies;
}
else
{
$connection->setCookies($cookies);
}
}
public function buildHttp2Response($request, $connection, $response)
{
$success = false !== $response;
$result = new Response($response->data ?? '', $success ? $response->statusCode : 0);
if($success)
{
$result = $result->withStreamId($response->streamId);
foreach($response->headers as $name => $value)
{
$result = $result->withHeader($name, $value);
}
$cookies = [];
if(isset($response->set_cookie_headers))
{
foreach($response->set_cookie_headers as $value)
{
$cookieItem = $this->cookieManager->addSetCookie($value);
$cookies[$cookieItem->name] = (array)$cookieItem;
}
}
$result = $result->withCookieOriginParams($cookies);
}
if($connection)
{
$result = $result->withError(swoole_strerror($connection->errCode))
->withErrno($connection->errCode);
}
return $result->withRequest($request);
}
private function getResponse($request, $connection, $isWebSocket, $isHttp2)
{
if($isHttp2)
{
$response = $connection->recv();
$this->result = $this->buildHttp2Response($request, $connection, $response);
}
else
{
$success = $isWebSocket ? true : $connection->recv();
$this->result = new Response((string)$connection->body, $connection->statusCode);
if($success)
{
foreach($connection->headers as $name => $value)
{
$this->result = $this->result->withHeader($name, $value);
}
$cookies = [];
if(isset($connection->set_cookie_headers))
{
foreach($connection->set_cookie_headers as $value)
{
$cookieItem = $this->cookieManager->addSetCookie($value);
$cookies[$cookieItem->name] = (array)$cookieItem;
}
}
$this->result = $this->result->withCookieOriginParams($cookies);
}
$this->result = $this->result->withRequest($request)
->withError(swoole_strerror($connection->errCode))
->withErrno($connection->errCode);
}
return $this->result;
}
private function parseSSL(&$request)
{
$settings = $request->getAttribute(Attributes::OPTIONS, []);
if($request->getAttribute(Attributes::IS_VERIFY_CA, false))
{
$settings['ssl_verify_peer'] = true;
$caCert =$request->getAttribute(Attributes::CA_CERT);
if(null !== $caCert)
{
$settings['ssl_cafile'] = $caCert;
}
}
else
{
$settings['ssl_verify_peer'] = false;
}
$certPath = $request->getAttribute(Attributes::CERT_PATH, '');
if('' !== $certPath)
{
$settings['ssl_cert_file'] = $certPath;
}
$keyPath = $request->getAttribute(Attributes::KEY_PATH , '');
if('' !== $keyPath)
{
$settings['ssl_key_file'] = $keyPath;
}
$request = $request->withAttribute(Attributes::OPTIONS, $settings);
}
private function parseProxy(&$request)
{
$settings = $request->getAttribute(Attributes::OPTIONS, []);
if($request->getAttribute(Attributes::USE_PROXY, false))
{
$type = $request->getAttribute(Attributes::PROXY_TYPE);
switch($type)
{
case 'http':
$settings['http_proxy_host'] = $request->getAttribute(Attributes::PROXY_SERVER);
$settings['http_proxy_port'] = $request->getAttribute(Attributes::PROXY_PORT);
$settings['http_proxy_user'] = $request->getAttribute(Attributes::PROXY_USERNAME, '');
$settings['http_proxy_password'] = $request->getAttribute(Attributes::PROXY_PASSWORD, '');
break;
case 'socks5':
$settings['socks5_host'] = $request->getAttribute(Attributes::PROXY_SERVER);
$settings['socks5_port'] = $request->getAttribute(Attributes::PROXY_PORT);
$settings['socks5_username'] = $request->getAttribute(Attributes::PROXY_USERNAME, '');
$settings['socks5_password'] = $request->getAttribute(Attributes::PROXY_PASSWORD, '');
break;
}
}
$request = $request->withAttribute(Attributes::OPTIONS, $settings);
}
private function parseNetwork(&$request)
{
$settings = $request->getAttribute(Attributes::OPTIONS, []);
$username = $request->getAttribute(Attributes::USERNAME);
if(null != $username)
{
$auth = base64_encode($username . ':' . $request->getAttribute(Attributes::PASSWORD, ''));
$request = $request->withHeader('Authorization', 'Basic ' . $auth);
}
$settings['timeout'] = $request->getAttribute(Attributes::TIMEOUT, 30000) / 1000;
if($settings['timeout'] < 0)
{
$settings['timeout'] = -1;
}
$settings['keep_alive'] = $request->getAttribute(Attributes::KEEP_ALIVE, true);
$request = $request->withAttribute(Attributes::OPTIONS, $settings);
}
public function getHandler()
{
return null;
}
public function getHttpConnectionManager()
{
return $this->httpConnectionManager;
}
public function getHttp2ConnectionManager()
{
return $this->http2ConnectionManager;
}
public function coBatch($requests, $timeout = null)
{
$handlers = [];
$results = [];
foreach($requests as $i => &$request)
{
$results[$i] = null;
$handlers[$i] = $handler = new Swoole;
$request = $handler->sendDefer($request);
}
unset($request);
foreach($requests as $i => $request)
{
$results[$i] = $handlers[$i]->recvDefer($request);
}
return $results;
}
}