* @copyright 2016 Microsoft Corporation * @license https://github.com/azure/azure-storage-php/LICENSE * @link https://github.com/azure/azure-storage-php */ namespace MicrosoftAzure\Storage\Common\Internal; use MicrosoftAzure\Storage\Common\ServiceException; use MicrosoftAzure\Storage\Common\Internal\Resources; use MicrosoftAzure\Storage\Common\Internal\Validate; use MicrosoftAzure\Storage\Common\Internal\Utilities; use MicrosoftAzure\Storage\Common\Internal\RetryMiddlewareFactory; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use GuzzleHttp\Client; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Promise\EachPromise; /** * Base class for all services rest proxies. * * @category Microsoft * @package MicrosoftAzure\Storage\Common\Internal * @author Azure Storage PHP SDK * @copyright 2016 Microsoft Corporation * @license https://github.com/azure/azure-storage-php/LICENSE * @version Release: 0.11.0 * @link https://github.com/azure/azure-storage-php */ class ServiceRestProxy extends RestProxy { /** * @var string */ private $_accountName; /** * * @var \Uri */ private $_psrUri; /** * @var array */ private $_options; /** * Initializes new ServiceRestProxy object. * * @param string $uri The storage account uri. * @param string $accountName The name of the account. * @param ISerializer $dataSerializer The data serializer. * @param array $options Array of options for the service */ public function __construct($uri, $accountName, $dataSerializer, $options = []) { if ($uri[strlen($uri)-1] != '/') { $uri = $uri . '/'; } parent::__construct($dataSerializer, $uri); $this->_accountName = $accountName; $this->_psrUri = new \GuzzleHttp\Psr7\Uri($uri); $this->_options = array_merge(array('http' => array()), $options); } /** * Gets the account name. * * @return string */ public function getAccountName() { return $this->_accountName; } /** * Filter the request using the filters. This is for users to create * request. * @param \GuzzleHttp\Psr7\Request $request The request to be filtered. * * @return \GuzzleHttp\Psr7\Request The filtered request. */ protected function requestWithFilter($request) { // Apply filters to the requests foreach ($this->getFilters() as $filter) { $request = $filter->handleRequest($request); } return $request; } /** * Static helper function to create a usable client for the proxy. * The clientOptions can contain the following keys that will affect * the way retry handler is created and applied. * handler: HandlerStack, if set, this function will not * create a handler stack. It will still construct * a default retry handler if not specified by the * following parameters. * have_retry_middleware: boolean, true if the handler is already specified * in the handler stack. * retry_middleware: Middleware, if specified this method will not create * a default retry middle ware. * @param array $clientOptions Added options for client. * * @return \GuzzleHttp\Client */ protected function createClient($clientOptions) { //If retry handler is not defined by the user, create a default //handler. $stack = null; if (array_key_exists('handler', $this->_options['http'])) { $stack = $this->_options['http']['handler']; } elseif (array_key_exists('handler', $clientOptions)) { $stack = $clientOptions['handler']; } else { $stack = HandlerStack::create(); $clientOptions['handler'] = $stack; } //If retry middle ware is specified, push it to the client. //Otherwise use the default middle ware. if (array_key_exists('have_retry_middleware', $clientOptions) && $clientOptions['have_retry_middleware'] == true) { //do nothing } elseif (array_key_exists('retry_middleware', $clientOptions)) { //push the retry middleware to the handler stack. $stack->push($clientOptions['retry_middleware']); } else { //construct the default retry middleware and push to the handler. $stack->push(RetryMiddlewareFactory::create(), 'retry_middleware'); } return (new \GuzzleHttp\Client( array_merge( $this->_options['http'], array( "defaults" => array( "allow_redirects" => true, "exceptions" => true, "decode_content" => true, ), 'cookies' => true, 'verify' => false, // For testing with Fiddler //'proxy' => "localhost:8888", ), $clientOptions ) )); } /** * Send the requests concurrently. Number of concurrency can be modified * by inserting a new key/value pair with the key 'number_of_concurrency' * into the $clientOptions. * * @param array $requests An array holding all the * initialized requests. If empty, * the first batch will be created * using the generator. * @param callable $generator the generator function to * generate request upon fullfilment * @param int $expectedStatusCode The expected status code for each * of the request. * @param array $clientOptions an array of additional options * for the client. * * @return array */ protected function sendConcurrent( $requests, $generator, $expectedStatusCode, $clientOptions = [] ) { //set the number of concurrency to default value if not defined //in the array. $numberOfConcurrency = Resources::NUMBER_OF_CONCURRENCY; if (array_key_exists('number_of_concurrency', $clientOptions)) { $numberOfConcurrency = $clientOptions['number_of_concurrency']; unset($clientOptions['number_of_concurrency']); } //create the client $client = $this->createClient($clientOptions); $promises = \call_user_func( function () use ($requests, $generator, $client) { $sendAsync = function ($request) use ($client) { $options = $request->getMethod() == 'HEAD'? array('decode_content' => false) : array(); return $client->sendAsync($request, $options); }; foreach ($requests as $request) { yield $sendAsync($request); } while (is_callable($generator) && ($request = $generator())) { yield $sendAsync($request); } } ); $eachPromise = new EachPromise($promises, [ 'concurrency' => $numberOfConcurrency, 'fulfilled' => function ($response, $index) use ($expectedStatusCode) { //the promise is fulfilled, evaluate the response self::throwIfError( $response->getStatusCode(), $response->getReasonPhrase(), $response->getBody(), $expectedStatusCode ); }, 'rejected' => function ($reason, $index) { //Still rejected even if the retry logic has been applied. //Throwing exception. throw $reason; } ]); return $eachPromise->promise()->wait(); } /** * Create the request to be sent. * * @param string $method The method of the HTTP request * @param array $headers The header field of the request * @param array $queryParams The query parameter of the request * @param array $postParameters The HTTP POST parameters * @param string $path URL path * @param string $body Request body * * @return \GuzzleHttp\Psr7\Request */ protected function createRequest( $method, $headers, $queryParams, $postParameters, $path, $body = Resources::EMPTY_STRING ) { // add query parameters into headers $uri = $this->_psrUri; if ($path != null) { $uri = $uri->withPath($path); } if ($queryParams != null) { $queryString = Psr7\build_query($queryParams); $uri = $uri->withQuery($queryString); } // add post parameters into bodys $actualBody = null; if (empty($body)) { if (empty($headers['content-type'])) { $headers['content-type'] = 'application/x-www-form-urlencoded'; $actualBody = Psr7\build_query($postParameters); } } else { $actualBody = $body; } $request = new Request( $method, $uri, $headers, $actualBody ); //add content-length to header $bodySize = $request->getBody()->getSize(); if ($bodySize > 0) { $request = $request->withHeader('content-length', $bodySize); } // Apply filters to the requests return $this->requestWithFilter($request); } /** * Sends HTTP request with the specified parameters. * * @param string $method HTTP method used in the request * @param array $headers HTTP headers. * @param array $queryParams URL query parameters. * @param array $postParameters The HTTP POST parameters. * @param string $path URL path * @param int $statusCode Expected status code received in the response * @param string $body Request body * @param array $clientOptions Guzzle Client options * * @return GuzzleHttp\Psr7\Response */ protected function send( $method, $headers, $queryParams, $postParameters, $path, $statusCode, $body = Resources::EMPTY_STRING, $clientOptions = [] ) { $request = $this->createRequest( $method, $headers, $queryParams, $postParameters, $path, $body ); $client = $this->createClient($clientOptions); try { $options = $request->getMethod() == 'HEAD'? array('decode_content' => false) : array(); $response = $client->send($request, $options); self::throwIfError( $response->getStatusCode(), $response->getReasonPhrase(), $response->getBody(), $statusCode ); return $response; } catch (\GuzzleHttp\Exception\RequestException $e) { if ($e->hasResponse()) { $response = $e->getResponse(); self::throwIfError( $response->getStatusCode(), $response->getReasonPhrase(), $response->getBody(), $statusCode ); return $response; } else { throw $e; } } } protected function sendContext($context) { return $this->send( $context->getMethod(), $context->getHeaders(), $context->getQueryParameters(), $context->getPostParameters(), $context->getPath(), $context->getStatusCodes(), $context->getBody() ); } /** * Throws ServiceException if the received status code is not expected. * * @param string $actual The received status code. * @param string $reason The reason phrase. * @param string $message The detailed message (if any). * @param string $expected The expected status codes. * * @return none * * @static * * @throws ServiceException */ public static function throwIfError($actual, $reason, $message, $expected) { $expectedStatusCodes = is_array($expected) ? $expected : array($expected); if (!in_array($actual, $expectedStatusCodes)) { throw new ServiceException($actual, $reason, $message); } } /** * Adds optional header to headers if set * * @param array $headers The array of request headers. * @param AccessCondition $accessCondition The access condition object. * * @return array */ public function addOptionalAccessConditionHeader($headers, $accessCondition) { if (!is_null($accessCondition)) { $header = $accessCondition->getHeader(); if ($header != Resources::EMPTY_STRING) { $value = $accessCondition->getValue(); if ($value instanceof \DateTime) { $value = gmdate( Resources::AZURE_DATE_FORMAT, $value->getTimestamp() ); } $headers[$header] = $value; } } return $headers; } /** * Adds optional header to headers if set * * @param array $headers The array of request headers. * @param AccessCondition $accessCondition The access condition object. * * @return array */ public function addOptionalSourceAccessConditionHeader( $headers, $accessCondition ) { if (!is_null($accessCondition)) { $header = $accessCondition->getHeader(); $headerName = null; if (!empty($header)) { switch ($header) { case Resources::IF_MATCH: $headerName = Resources::X_MS_SOURCE_IF_MATCH; break; case Resources::IF_UNMODIFIED_SINCE: $headerName = Resources::X_MS_SOURCE_IF_UNMODIFIED_SINCE; break; case Resources::IF_MODIFIED_SINCE: $headerName = Resources::X_MS_SOURCE_IF_MODIFIED_SINCE; break; case Resources::IF_NONE_MATCH: $headerName = Resources::X_MS_SOURCE_IF_NONE_MATCH; break; default: throw new \Exception(Resources::INVALID_ACH_MSG); break; } } $value = $accessCondition->getValue(); if ($value instanceof \DateTime) { $value = gmdate( Resources::AZURE_DATE_FORMAT, $value->getTimestamp() ); } $this->addOptionalHeader($headers, $headerName, $value); } return $headers; } /** * Adds HTTP POST parameter to the specified * * @param array $postParameters An array of HTTP POST parameters. * @param string $key The key of a HTTP POST parameter. * @param string $value the value of a HTTP POST parameter. * * @return array */ public function addPostParameter( $postParameters, $key, $value ) { Validate::isArray($postParameters, 'postParameters'); $postParameters[$key] = $value; return $postParameters; } /** * Groups set of values into one value separated with Resources::SEPARATOR * * @param array $values array of values to be grouped. * * @return string */ public static function groupQueryValues($values) { Validate::isArray($values, 'values'); $joined = Resources::EMPTY_STRING; sort($values); foreach ($values as $value) { if (!is_null($value) && !empty($value)) { $joined .= $value . Resources::SEPARATOR; } } return trim($joined, Resources::SEPARATOR); } /** * Adds metadata elements to headers array * * @param array $headers HTTP request headers * @param array $metadata user specified metadata * * @return array */ protected function addMetadataHeaders($headers, $metadata) { $this->validateMetadata($metadata); $metadata = $this->generateMetadataHeaders($metadata); $headers = array_merge($headers, $metadata); return $headers; } /** * Generates metadata headers by prefixing each element with 'x-ms-meta'. * * @param array $metadata user defined metadata. * * @return array. */ public function generateMetadataHeaders($metadata) { $metadataHeaders = array(); if (is_array($metadata) && !is_null($metadata)) { foreach ($metadata as $key => $value) { $headerName = Resources::X_MS_META_HEADER_PREFIX; if (strpos($value, "\r") !== false || strpos($value, "\n") !== false ) { throw new \InvalidArgumentException(Resources::INVALID_META_MSG); } // Metadata name is case-presrved and case insensitive $headerName .= $key; $metadataHeaders[$headerName] = $value; } } return $metadataHeaders; } /** * Gets metadata array by parsing them from given headers. * * @param array $headers HTTP headers containing metadata elements. * * @return array. */ public function getMetadataArray($headers) { $metadata = array(); foreach ($headers as $key => $value) { $isMetadataHeader = Utilities::startsWith( strtolower($key), Resources::X_MS_META_HEADER_PREFIX ); if ($isMetadataHeader) { // Metadata name is case-presrved and case insensitive $MetadataName = str_ireplace( Resources::X_MS_META_HEADER_PREFIX, Resources::EMPTY_STRING, $key ); $metadata[$MetadataName] = $value; } } return $metadata; } /** * Validates the provided metadata array. * * @param mix $metadata The metadata array. * * @return none */ public function validateMetadata($metadata) { if (!is_null($metadata)) { Validate::isArray($metadata, 'metadata'); } else { $metadata = array(); } foreach ($metadata as $key => $value) { Validate::isString($key, 'metadata key'); Validate::isString($value, 'metadata value'); } } }