.gitignore000064400000000036144761607220006544 0ustar00/vendor/ /tests/ composer.lockREADME.md000064400000000010144761607220006023 0ustar00# dhttp composer.json000064400000001417144761607220007302 0ustar00{ "name": "boru/dhttp", "type": "library", "autoload": { "psr-4": { "boru\\dhttp\\": "src/" } }, "authors": [ { "name": "Daniel Hayes", "email": "dhayes@boruapps.com" } ], "require": { "boru/dhutils": "*", "clue/mq-react": "^1.5", "react/http": "^1.8", "react/async": "^2.0", "react/child-process": "^0.6.5" }, "suggest": { "boru/dhdb": "dhDB library for database", "boru/dhcli": "CLI toolkit/library for making CLI apps", "boru/dhapi": "Toolkit/library for creating API apps" }, "repositories": [ { "type": "composer", "url": "https://satis.boruapps.com" } ] } instructions-composer.txt000064400000000300144761607220011700 0ustar00{ "require": { "boru/dhttp": "*" }, "repositories": [ { "type": "composer", "url": "https://satis.boruapps.com" } ] }src/Client.php000064400000031436144761607220007302 0ustar00[ 'verify_peer' => static::$verifyPeer, 'verify_peer_name' => static::$verifyPeerName, ] ]); static::$browserClient = new \React\Http\Browser(static::$Connector); static::$browserClient = static::$browserClient->withRejectErrorResponse(false); } } public static function setOptions($options=[]) { static::$options = $options; } public static function verifyPeer($verifyPeer=true,$verifyPeerName=true) { static::$verifyPeer = $verifyPeer; static::$verifyPeerName = $verifyPeerName; } /** * * @param MiddlewareInterface $middleware * @return void */ public static function addRequestMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { static::addRequestMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } static::$requestMiddleware[] = $middleware; } public static function addResponseMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { static::addResponseMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } static::$responseMiddleware[] = $middleware; } public static function setRequestMiddleware($middleware=[]) { if(empty($middleware)) { static::$requestMiddleware = []; return; } static::$requestMiddleware = []; static::addRequestMiddleware($middleware); } public static function setResponseMiddleware($middleware=[]) { if(empty($middleware)) { static::$responseMiddleware = []; return; } static::$responseMiddleware = []; static::addResponseMiddleware($middleware); } public static function maxConcurrency($maxConcurrency) { static::$maxConcurrency = $maxConcurrency; } private static function registerDeferred($id,$deferred) { static::$promises[$id] = $deferred; } private static function registerPromise($id,$promise) { static::$results[$id] = $promise; } /** * @param Request $request * @return \React\Promise\PromiseInterface */ public static function send($request) { static::init(); static::registerDeferred($request->getId(),$request->getDeferred()); static::applyRequestMiddleware($request); $promise = static::$browserClient->request($request->getMethod(),$request->getUrl(),$request->getHeaders(),$request->getBody()); static::registerPromise($request->getId(),$promise); $promise->then(function(\Psr\Http\Message\ResponseInterface $response) use ($request) { static::processSuccess($request,$response); },function(Exception $e) use ($request) { static::processException($request,$e); }); static::checkConcurrency(); return $promise; } private static function processSuccess($request,$response) { if(static::$debugInfo) { dhGlobal::info("dhBrowser response from",$request->getMethod(),$request->getUrl()); } $resp = new Response($response,$request); static::applyResponseMiddleware($response); static::processResponse($request->getId(),$resp); return $resp; } private static function processException($request,$e) { if(static::$debugError) { dhGlobal::error("dhBrowser error from",$request->getMethod(),$request->getUrl(),"-",$e->getMessage()); } static::processResponse($request->getId(),$e); return $e; } private static function processResponse($id,$response) { if(!isset(static::$promises[$id])) { return; } $deferred = static::$promises[$id]; unset(static::$promises[$id]); static::$results[$id] = $response; if($response instanceof \Exception) { $deferred->reject($response); return; } $deferred->resolve($response); return; } private static function applyRequestMiddleware(&$request) { if(empty(static::$requestMiddleware)) { return; } $queue = new \SplQueue(); foreach(static::$requestMiddleware as $middleware) { $queue->enqueue($middleware); } $next = function($request) use ($queue,&$next) { if($queue->isEmpty()) { return $request; } $middleware = $queue->dequeue(); return $middleware($request,$next); }; $request = $next($request); } private static function applyResponseMiddleware(&$response) { if(empty(static::$responseMiddleware)) { return; } $queue = new \SplQueue(); foreach(static::$responseMiddleware as $middleware) { $queue->enqueue($middleware); } $next = function($response) use ($queue,&$next) { if($queue->isEmpty()) { return $response; } $middleware = $queue->dequeue(); return $middleware($response,$next); }; $response = $next($response); } /** * @return Response|PromiseInterface|Exception */ public static function getResponse($id) { return isset(static::$results[$id]) ? static::$results[$id] : false; } private static function checkConcurrency() { if(!static::isOverConcurrency()) { return; } Loop::addPeriodicTimer(0.1,function($timer) { if(!static::isOverConcurrency()) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); } private static function isOverConcurrency() { if(count(static::$promises) < static::$maxConcurrency) { return false; } return true; } public static function awaitAll() { if(!empty(static::$promises)) { Loop::addPeriodicTimer(0.1,function($timer) { if(empty(static::$promises)) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); } } /** * Send the request and if async=true, return a promise. Otherwise await the return and return the Response * @param mixed $method ["GET","POST","PUT","DELETE","PATCH","HEAD"] * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function request($method,$url,$options=[],$async=null) { $options = array_merge(static::$options,$options); if(is_null($async) && isset($options["async"])) { $async = $options["async"]; } else { $async = true; } $options['method'] = $method; $options["url"] = $url; $options["async"] = $async; $request = new Request($options); $result = $request->send(); if($async) { return $result; } $request->await(); return $request->response(); } /** * Send a GET request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function get($url,$options=[],$async=null) { return static::request("GET",$url,$options,$async); } /** * Send a POST request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function post($url,$options=[],$async=null) { return static::request("POST",$url,$options,$async); } /** * Send a PUT request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function put($url,$options=[],$async=null) { return static::request("PUT",$url,$options,$async); } /** * Send a DELETE request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function delete($url,$options=[],$async=null) { return static::request("DELETE",$url,$options,$async); } /** * Send a PATCH request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function patch($url,$options=[],$async=null) { return static::request("PATCH",$url,$options,$async); } /** * Send a HEAD request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function head($url,$options=[],$async=null) { return static::request("HEAD",$url,$options,$async); } /** * Send a OPTIONS request * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function options($url,$options=[],$async=null) { return static::request("OPTIONS",$url,$options,$async); } }src/core/Request.php000064400000022452144761607220010442 0ustar00id = uniqid(); $this->setOptions($options); $this->deferred = new Deferred(); } public function __destruct() { if($this->async && !$this->isDone()) { $this->await(); } } public function setOptions($options=[]) { $this->optionsToValues($options); return $this; } public function setMethod($method) { $this->method = $method; return $this; } public function setUrl($url) { $this->url = $url; return $this; } public function setBaseUrl($url) { $this->baseUrl = $url; return $this; } public function query($keyPath,$value=null,$separator=".") { if(is_array($keyPath)) { $this->query = $keyPath; return $this; } dhGlobal::dotAssign($this->query,$keyPath,$value,$separator); return $this; } public function header($key,$value=null) { $this->headers[$key]=$value; return $this; } /** * @param mixed $type One of "form","json","raw" * @return $this */ public function bodyType($type) { $this->bodyType = $type; return $this; } public function as($type) { return $this->bodyType($type); } public function body($keyPath,$value=null,$separator=".") { if(is_null($value)) { if(!is_array($keyPath)) { $check = json_decode($keyPath,true); if(is_array($check)) { $keyPath = $check; } } if(is_array($keyPath)) { foreach($keyPath as $k=>$v) { $this->body($k,$v,$separator); } } else { $this->body = $keyPath; } } else { dhGlobal::dotAssign($this->body,$keyPath,$value,$separator); } return $this; } public function setBody($key=null,$value=null,$separator=".") { if(is_null($value)) { if(is_array($key)) { foreach($key as $k=>$v) { $this->body($k,$v,$separator); } } else { $check = json_decode($key,true); if(is_array($check)) { foreach($check as $k=>$v) { $this->json($k,$v,$separator); } } else { $this->body = $key; } } } else { if(!is_array($value)) { $check = json_decode($key,true); if(is_array($check)) { $value = $check; } } if(is_array($value)) { foreach($value as $k=>$v) { $this->body($key.$separator.$k,$v,$separator); } } $this->body($key,$value,$separator); } return $this; } public function json($key=null,$value=null,$separator=".") { $this->header('Content-Type','application/json'); $this->bodyType = "json"; $this->setBody($key,$value,$separator); return $this; } public function form($key=null,$value=null,$separator=".") { $this->header('Content-Type','application/x-www-form-urlencoded'); $this->bodyType = "form"; $this->setBody($key,$value,$separator); return $this; } public function raw($data) { $this->bodyType = "raw"; $this->body = $data; return $this; } public function async($async=true) { $this->async = $async; return $this; } public function authBasic($userPass=[]) { $this->headers["Authorization"] = "Basic ".base64_encode(implode(":",$userPass)); return $this; } public function authToken($token) { $this->headers["Authorization"] = $token; return $this; } private function optionsToValues($options=[]) { foreach($options as $think=>$value) { switch($think) { case "body": $this->body($value); break; case "json": $this->json($value); break; case "form": $this->form($value); break; case "raw": $this->raw($value); break; case "query": $this->query($value); break; case "headers": foreach($value as $k=>$v) { $this->header($k,$v); } break; case "auth": if(is_array($value)) { $this->authBasic($value); } else { $this->authToken($value); } break; case "bodyType": $this->bodyType($value); break; case "as": $this->bodyType($value); break; case "method": $this->setMethod($value); break; case "url": $this->setUrl($value); break; case "baseUrl": $this->setBaseUrl($value); break; case "async": $this->async($value); break; } } } public function getMethod() { return $this->method; } public function getUrl() { return $this->url; } public function getFullUrl() { $url = $this->url; if(strtolower(substr($url,0,4)) != "http") { $preurl = $this->baseUrl; if(substr($url,-1) != "/" && substr($preurl,0,1) != "/") { $preurl .= "/"; } $url = $preurl.$url; } if(!empty($this->query)) { if(strpos($url,"?")===false) { $url .= "?"; } else { $url .= "&"; } $url .= http_build_query($this->query); } return $url; } public function getHeaders() { return !is_array($this->headers) ? [] : $this->headers; } public function getRawBody() { return $this->body; } public function getBody() { $body = $this->body; if(is_array($body)) { if($this->bodyType == "form") { $body = http_build_query($body); } else if($this->bodyType == "json") { $body = json_encode($body); } } return $body; } public function getBodyType() { return $this->bodyType; } public function getQuery() { return $this->query; } public function getQueryAsString() { return http_build_query($this->query); } public function getDeferred() { return $this->deferred; } public function getPromise() { return $this->promise; } public function getId() { return $this->id; } public function send($options=[]) { $this->optionsToValues($options); $this->promise = Client::send($this); if($this->async) { return $this->deferred->promise(); } $this->await(); return $this->response(); } public function isDone() { return $this->response() !== false; } /** * Get the response * @return Response|\Exception|false */ public function response() { if(is_null($this->response)) { $response = Client::getResponse($this->id); if($response instanceof PromiseInterface) { return false; } $this->response = $response; } return $this->response; } /** * Await the response and return it * @return bool */ public function await() { if($this->isDone()) { return $this->response(); } Loop::addPeriodicTimer(0.1,function($timer) { if($this->isDone()) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); return true; } }src/core/Response.php000064400000006511144761607220010606 0ustar00response = $response; $this->request = $request; } public function code() { return $this->getStatusCode(); } public function phrase() { return $this->getReasonPhrase(); } public function header($header=null,$default=false) { if(is_null($header)) { return $this->getHeaders(); } if(!$this->hasHeader($header)) { return $default; } return $this->getHeader($header); } public function headerLine($header,$default=false) { if(!$this->hasHeader($header)) { return $default; } else { return $this->getHeaderLine($header); } } public function request() { return $this->request; } /** * * @param bool $json * @param bool $arr * @return mixed */ public function body($json=false,$arr=true) { if($json) { return json_decode($this->asString(),$arr); } else { return $this->asString(); } } public function asString() { if(is_null($this->bodyString)) { $this->bodyString = (string) $this->getBody()->__toString(); } return $this->bodyString; } public function asArray() { return $this->body(true); } public function asObject() { return $this->body(true,false); } //ResponseInterface methods: public function getStatusCode() { return $this->response->getStatusCode(); } public function withStatus($code, $reasonPhrase = '') { return $this->response->getStatusCode($code, $reasonPhrase); } public function getReasonPhrase() { return $this->response->getReasonPhrase(); } public function getProtocolVersion() { return $this->response->getProtocolVersion(); } public function withProtocolVersion($version) { return $this->response->withProtocolVersion($version); } public function getHeaderLine($name) { return $this->response->getHeaderLine($name); } public function withHeader($name, $value) { return $this->response->withHeader($name, $value); } public function withAddedHeader($name, $value) { return $this->response->withAddedHeader($name, $value); } public function withoutHeader($name) { return $this->response->withoutHeader($name); } public function hasHeader($header) { return $this->response->hasHeader($header); } public function getHeader($header) { return $this->response->getHeader($header); } public function getHeaders() { return $this->response->getHeaders(); } public function getBody() { return $this->response->getBody(); } public function withBody(\Psr\Http\Message\StreamInterface $body) { return $this->response->withBody($body); } }src/middleware/CallbackMiddleware.php000064400000000540144761607220013663 0ustar00callback = $callback; } public function __invoke($object,$next) { $callback = $this->callback; return $callback($object,$next); } }src/middleware/MiddlewareInterface.php000064400000000173144761607220014071 0ustar00hwm = $hwm; } public function __toString() { return $this->getContents(); } public function getContents() { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } public function close() { $this->buffer = ''; } public function detach() { $this->close(); return null; } public function getSize() { return strlen($this->buffer); } public function isReadable() { return true; } public function isWritable() { return true; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a BufferStream'); } public function eof() { return strlen($this->buffer) === 0; } public function tell() { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } /** * Reads data from the buffer. */ public function read($length) { $currentLength = strlen($this->buffer); if ($length >= $currentLength) { // No need to slice the buffer because we don't have enough data. $result = $this->buffer; $this->buffer = ''; } else { // Slice up the result to provide a subset of the buffer. $result = substr($this->buffer, 0, $length); $this->buffer = substr($this->buffer, $length); } return $result; } /** * Writes data to the buffer. */ public function write($string) { $this->buffer .= $string; // TODO: What should happen here? if (strlen($this->buffer) >= $this->hwm) { return false; } return strlen($string); } public function getMetadata($key = null) { if ($key == 'hwm') { return $this->hwm; } return $key ? null : []; } } src/psr7/PumpStream.php000064400000010237144761607220011050 0ustar00source = $source; $this->size = isset($options['size']) ? $options['size'] : null; $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; $this->buffer = new BufferStream(); } public function __toString() { try { return Stream::copyToString($this); } catch (\Exception $e) { return ''; } } public function close() { $this->detach(); } public function detach() { $this->tellPos = false; $this->source = null; return null; } public function getSize() { return $this->size; } public function tell() { return $this->tellPos; } public function eof() { return !$this->source; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a PumpStream'); } public function isWritable() { return false; } public function write($string) { throw new \RuntimeException('Cannot write to a PumpStream'); } public function isReadable() { return true; } public function read($length) { $data = $this->buffer->read($length); $readLen = strlen($data); $this->tellPos += $readLen; $remaining = $length - $readLen; if ($remaining) { $this->pump($remaining); $data .= $this->buffer->read($remaining); $this->tellPos += strlen($data) - $readLen; } return $data; } public function getContents() { $result = ''; while (!$this->eof()) { $result .= $this->read(1000000); } return $result; } public function getMetadata($key = null) { if (!$key) { return $this->metadata; } return isset($this->metadata[$key]) ? $this->metadata[$key] : null; } private function pump($length) { if ($this->source) { do { $data = call_user_func($this->source, $length); if ($data === false || $data === null) { $this->source = null; return; } $this->buffer->write($data); $length -= strlen($data); } while ($length > 0); } } } src/psr7/Request.php000064400000007234144761607220010406 0ustar00 * @author Martijn van der Ven * * Copied from Nyholm/Psr7 -- modified for boru\dhttp; */ class Request implements \Psr\Http\Message\RequestInterface { use MessageTrait; /** @var string|null */ private $requestTarget; /** @var string */ private $method; /** @var UriInterface|null */ private $uri; /** * @param string $method HTTP method * @param string|UriInterface $uri URI * @param array $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version */ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1') { if (!($uri instanceof UriInterface)) { $uri = new Uri($uri); } $this->method = $method; $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; if (!$this->hasHeader('Host')) { $this->updateHostFromUri(); } // If we got no body, defer initialization of the stream until Request::getBody() if ('' !== $body && null !== $body) { $this->stream = Stream::create($body); } } public function getRequestTarget() { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); if ($target === '') { $target = '/'; } if ($this->uri->getQuery() != '') { $target .= '?' . $this->uri->getQuery(); } return $target; } public function withRequestTarget(string $requestTarget) { if (preg_match('#\s#', $requestTarget)) { throw new \InvalidArgumentException( 'Invalid request target provided; cannot contain whitespace' ); } $new = clone $this; $new->requestTarget = $requestTarget; return $new; } public function getMethod() { return $this->method; } public function withMethod(string $method) { $this->assertMethod($method); $new = clone $this; $new->method = strtoupper($method); return $new; } public function getUri() { return $this->uri; } public function withUri(UriInterface $uri, bool $preserveHost = false) { if ($uri === $this->uri) { return $this; } $new = clone $this; $new->uri = $uri; if (!$preserveHost || !isset($this->headerNames['host'])) { $new->updateHostFromUri(); } return $new; } private function updateHostFromUri() { $host = $this->uri->getHost(); if ($host == '') { return; } if (($port = $this->uri->getPort()) !== null) { $host .= ':' . $port; } if (isset($this->headerNames['host'])) { $header = $this->headerNames['host']; } else { $header = 'Host'; $this->headerNames['host'] = 'Host'; } // Ensure Host is the first header. // See: http://tools.ietf.org/html/rfc7230#section-5.4 $this->headers = [$header => [$host]] + $this->headers; } }src/psr7/Stream.php000064400000033467144761607220010220 0ustar00size = $options['size']; } $this->customMetadata = isset($options['metadata']) ? $options['metadata'] : []; $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); $this->uri = $this->getMetadata('uri'); } /** * Closes the stream when the destructed */ public function __destruct() { $this->close(); } public function __toString() { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Exception $e) { return ''; } } public function getContents() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $contents = stream_get_contents($this->stream); if ($contents === false) { throw new \RuntimeException('Unable to read stream contents'); } return $contents; } public function close() { if (isset($this->stream)) { if (is_resource($this->stream)) { fclose($this->stream); } $this->detach(); } } public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->size = $this->uri = null; $this->readable = $this->writable = $this->seekable = false; return $result; } public function getSize() { if ($this->size !== null) { return $this->size; } if (!isset($this->stream)) { return null; } // Clear the stat cache if the stream has a URI if ($this->uri) { clearstatcache(true, $this->uri); } $stats = fstat($this->stream); if (isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } return null; } public function isReadable() { return $this->readable; } public function isWritable() { return $this->writable; } public function isSeekable() { return $this->seekable; } public function eof() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } return feof($this->stream); } public function tell() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $result = ftell($this->stream); if ($result === false) { throw new \RuntimeException('Unable to determine stream position'); } return $result; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { $whence = (int) $whence; if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } if (fseek($this->stream, $offset, $whence) === -1) { throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . var_export($whence, true)); } } public function read($length) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } if ($length < 0) { throw new \RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } $string = fread($this->stream, $length); if (false === $string) { throw new \RuntimeException('Unable to read from stream'); } return $string; } public function write($string) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } // We can't know the size after writing anything $this->size = null; $result = fwrite($this->stream, $string); if ($result === false) { throw new \RuntimeException('Unable to write to stream'); } return $result; } public function getMetadata($key = null) { if (!isset($this->stream)) { return $key ? null : []; } elseif (!$key) { return $this->customMetadata + stream_get_meta_data($this->stream); } elseif (isset($this->customMetadata[$key])) { return $this->customMetadata[$key]; } $meta = stream_get_meta_data($this->stream); return isset($meta[$key]) ? $meta[$key] : null; } /** * Create a new stream based on the input type. * * Options is an associative array that can contain the following keys: * - metadata: Array of custom metadata. * - size: Size of the stream. * * This method accepts the following `$resource` types: * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. * - `string`: Creates a stream object that uses the given string as the contents. * - `resource`: Creates a stream object that wraps the given PHP stream resource. * - `Iterator`: If the provided value implements `Iterator`, then a read-only * stream object will be created that wraps the given iterable. Each time the * stream is read from, data from the iterator will fill a buffer and will be * continuously called until the buffer is equal to the requested read size. * Subsequent read calls will first read from the buffer and then call `next` * on the underlying iterator until it is exhausted. * - `object` with `__toString()`: If the object has the `__toString()` method, * the object will be cast to a string and then a stream will be returned that * uses the string value. * - `NULL`: When `null` is passed, an empty stream object is returned. * - `callable` When a callable is passed, a read-only stream object will be * created that invokes the given callable. The callable is invoked with the * number of suggested bytes to read. The callable can return any number of * bytes, but MUST return `false` when there is no more data to return. The * stream object that wraps the callable will invoke the callable until the * number of requested bytes are available. Any additional bytes will be * buffered and used in subsequent reads. * * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data * @param array $options Additional options * * @return StreamInterface * * @throws \InvalidArgumentException if the $resource arg is not valid. */ public static function create($resource = '', array $options = []) { if (is_scalar($resource)) { $stream = self::tryFopen('php://temp', 'r+'); if ($resource !== '') { fwrite($stream, $resource); fseek($stream, 0); } return new Stream($stream, $options); } switch (gettype($resource)) { case 'resource': /* * The 'php://input' is a special stream with quirks and inconsistencies. * We avoid using that stream by reading it into php://temp */ $metaData = \stream_get_meta_data($resource); if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { $stream = self::tryFopen('php://temp', 'w+'); fwrite($stream, stream_get_contents($resource)); fseek($stream, 0); $resource = $stream; } return new Stream($resource, $options); case 'object': if ($resource instanceof StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { return new PumpStream(function () use ($resource) { if (!$resource->valid()) { return false; } $result = $resource->current(); $resource->next(); return $result; }, $options); } elseif (method_exists($resource, '__toString')) { return static::create((string) $resource, $options); } break; case 'NULL': return new Stream(self::tryFopen('php://temp', 'r+'), $options); } if (is_callable($resource)) { return new PumpStream($resource, $options); } throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); } /** * Safely opens a PHP stream resource using a filename. * * When fopen fails, PHP normally raises a warning. This function adds an * error handler that checks for errors and throws an exception instead. * * @param string $filename File to open * @param string $mode Mode used to open the file * * @return resource * * @throws \RuntimeException if the file cannot be opened */ public static function tryFopen($filename, $mode) { $ex = null; set_error_handler(function () use ($filename, $mode, &$ex) { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, func_get_args()[1] )); return true; }); try { $handle = fopen($filename, $mode); } catch (\Exception $e) { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage() ), 0, $e); } restore_error_handler(); if ($ex) { /** @var $ex \RuntimeException */ throw $ex; } return $handle; } /** * Copy the contents of a stream into a string until the given number of * bytes have been read. * * @param StreamInterface $stream Stream to read * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @return string * * @throws \RuntimeException on error. */ public static function copyToString(StreamInterface $stream, $maxLen = -1) { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); // Using a loose equality here to match on '' and false. if ($buf == null) { break; } $buffer .= $buf; } return $buffer; } $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); // Using a loose equality here to match on '' and false. if ($buf == null) { break; } $buffer .= $buf; $len = strlen($buffer); } return $buffer; } } src/psr7/Uri.php000064400000022347144761607220007517 0ustar00 * @author Martijn van der Ven * * Copied from Nyholm/Psr7 -- modified for boru\dhttp; * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Uri implements \Psr\Http\Message\UriInterface { const SCHEMES = ['http' => 80, 'https' => 443]; const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; const CHAR_GEN_DELIMS = ':\/\?#\[\]@'; /** @var string Uri scheme. */ private $scheme = ''; /** @var string Uri user info. */ private $userInfo = ''; /** @var string Uri host. */ private $host = ''; /** @var int|null Uri port. */ private $port; /** @var string Uri path. */ private $path = ''; /** @var string Uri query string. */ private $query = ''; /** @var string Uri fragment. */ private $fragment = ''; public function __construct(string $uri='') { if ('' !== $uri) { if (false === $parts = \parse_url($uri)) { throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); } // Apply parse_url parts to a URI. $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { $this->userInfo .= ':' . $parts['pass']; } } } public function __toString() { return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); } public function getScheme() { return $this->scheme; } public function getAuthority() { if ('' === $this->host) { return ''; } $authority = $this->host; if ('' !== $this->userInfo) { $authority = $this->userInfo . '@' . $authority; } if (null !== $this->port) { $authority .= ':' . $this->port; } return $authority; } public function getUserInfo() { return $this->userInfo; } public function getHost() { return $this->host; } public function getPort() { return $this->port; } public function getPath() { $path = $this->path; if ('' !== $path && '/' !== $path[0]) { if ('' !== $this->host) { // If the path is rootless and an authority is present, the path MUST be prefixed by "/" $path = '/' . $path; } } elseif (isset($path[1]) && '/' === $path[1]) { // If the path is starting with more than one "/", the // starting slashes MUST be reduced to one. $path = '/' . \ltrim($path, '/'); } return $path; } public function getQuery() { return $this->query; } public function getFragment() { return $this->fragment; } public function withScheme($scheme) { if (!\is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); } if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } $new = clone $this; $new->scheme = $scheme; $new->port = $new->filterPort($new->port); return $new; } public function withUserInfo($user, $password = null) { if (!\is_string($user)) { throw new \InvalidArgumentException('User must be a string'); } $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); if (null !== $password && '' !== $password) { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string'); } $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); } if ($this->userInfo === $info) { return $this; } $new = clone $this; $new->userInfo = $info; return $new; } public function withHost($host) { if (!\is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); } if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } $new = clone $this; $new->host = $host; return $new; } public function withPort($port) { if ($this->port === $port = $this->filterPort($port)) { return $this; } $new = clone $this; $new->port = $port; return $new; } public function withPath($path) { if ($this->path === $path = $this->filterPath($path)) { return $this; } $new = clone $this; $new->path = $path; return $new; } public function withQuery($query) { if ($this->query === $query = $this->filterQueryAndFragment($query)) { return $this; } $new = clone $this; $new->query = $query; return $new; } public function withFragment($fragment) { if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { return $this; } $new = clone $this; $new->fragment = $fragment; return $new; } /** * Create a URI string from its various parts. */ private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment) { $uri = ''; if ('' !== $scheme) { $uri .= $scheme . ':'; } if ('' !== $authority) { $uri .= '//' . $authority; } if ('' !== $path) { if ('/' !== $path[0]) { if ('' !== $authority) { // If the path is rootless and an authority is present, the path MUST be prefixed by "/" $path = '/' . $path; } } elseif (isset($path[1]) && '/' === $path[1]) { if ('' === $authority) { // If the path is starting with more than one "/" and no authority is present, the // starting slashes MUST be reduced to one. $path = '/' . \ltrim($path, '/'); } } $uri .= $path; } if ('' !== $query) { $uri .= '?' . $query; } if ('' !== $fragment) { $uri .= '#' . $fragment; } return $uri; } /** * Is a given port non-standard for the current scheme? */ private static function isNonStandardPort(string $scheme, int $port) { return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme]; } private function filterPort($port) { if (null === $port) { return null; } $port = (int) $port; if (0 > $port || 0xFFFF < $port) { throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return self::isNonStandardPort($this->scheme, $port) ? $port : null; } private function filterPath($path) { if (!\is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); } private function filterQueryAndFragment($str) { if (!\is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); } private static function rawurlencodeMatchZero(array $match) { return \rawurlencode($match[0]); } }src/psr7/traits/MessageTrait.php000064400000015130144761607220012646 0ustar00 array of values */ private $headers = []; /** @var array Map of lowercase header name => original name at registration */ private $headerNames = []; /** @var string */ private $protocol = '1.1'; /** @var StreamInterface|null */ private $stream; public function getProtocolVersion() { return $this->protocol; } public function withProtocolVersion(string $version) { if ($this->protocol === $version) { return $this; } $new = clone $this; $new->protocol = $version; return $new; } public function getHeaders() { return $this->headers; } public function hasHeader(string $header) { return isset($this->headerNames[strtolower($header)]); } public function getHeader(string $header) { $header = strtolower($header); if (!isset($this->headerNames[$header])) { return []; } $header = $this->headerNames[$header]; return $this->headers[$header]; } public function getHeaderLine(string $header) { return implode(', ', $this->getHeader($header)); } public function withHeader(string $header, $value) { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { unset($new->headers[$new->headerNames[$normalized]]); } $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } public function withAddedHeader(string $header, $value) { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $new->headers[$header] = array_merge($this->headers[$header], $value); } else { $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } return $new; } public function withoutHeader(string $header) { $normalized = strtolower($header); if (!isset($this->headerNames[$normalized])) { return $this; } $header = $this->headerNames[$normalized]; $new = clone $this; unset($new->headers[$header], $new->headerNames[$normalized]); return $new; } public function getBody() { if (!$this->stream) { $this->stream = Utils::streamFor(''); } return $this->stream; } public function withBody(StreamInterface $body) { if ($body === $this->stream) { return $this; } $new = clone $this; $new->stream = $body; return $new; } private function setHeaders(array $headers) { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { // Numeric array keys are converted to int by PHP. $header = (string) $header; $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; } } } private function normalizeHeaderValue($value) { if (!is_array($value)) { return $this->trimAndValidateHeaderValues([$value]); } if (count($value) === 0) { throw new \InvalidArgumentException('Header value can not be an empty array.'); } return $this->trimAndValidateHeaderValues($value); } private function trimAndValidateHeaderValues(array $values) { return array_map(function ($value) { if (!is_scalar($value) && null !== $value) { throw new \InvalidArgumentException(sprintf( 'Header value must be scalar or null but %s provided.', is_object($value) ? get_class($value) : gettype($value) )); } $trimmed = trim((string) $value, " \t"); $this->assertValue($trimmed); return $trimmed; }, array_values($values)); } private function assertHeader($header) { if (!is_string($header)) { throw new \InvalidArgumentException(sprintf( 'Header name must be a string but %s provided.', is_object($header) ? get_class($header) : gettype($header) )); } if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header name.', $header) ); } } private function assertValue(string $value) { // The regular expression intentionally does not support the obs-fold production, because as // per RFC 7230#3.2.4: // // A sender MUST NOT generate a message that includes // line folding (i.e., that has any field-value that contains a match to // the obs-fold rule) unless the message is intended for packaging // within the message/http media type. // // Clients must not send a request with line folding and a server sending folded headers is // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting // folding is not likely to break any legitimate use case. if (! preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header value.', $value) ); } } private function assertMethod($method) { if (!is_string($method) || $method === '') { throw new \InvalidArgumentException('Method must be a non-empty string.'); } } }