config = $config; $this->client = $client; $this->certificateManager = $certificateManager; $this->remoteHostValidator = $remoteHostValidator; } private function buildRequestOptions(array $options): array { $proxy = $this->getProxyUri(); $defaults = [ RequestOptions::VERIFY => $this->getCertBundle(), RequestOptions::TIMEOUT => 30, ]; $options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options); if ($options['nextcloud']['allow_local_address'] === false) { $onRedirectFunction = function ( \Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, \Psr\Http\Message\UriInterface $uri ) use ($options) { $this->preventLocalAddress($uri->__toString(), $options); }; $defaults[RequestOptions::ALLOW_REDIRECTS] = [ 'on_redirect' => $onRedirectFunction ]; } // Only add RequestOptions::PROXY if Nextcloud is explicitly // configured to use a proxy. This is needed in order not to override // Guzzle default values. if ($proxy !== null) { $defaults[RequestOptions::PROXY] = $proxy; } $options = array_merge($defaults, $options); if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) { $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler'; } if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) { $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip'; } // Fallback for save_to if (isset($options['save_to'])) { $options['sink'] = $options['save_to']; unset($options['save_to']); } return $options; } private function getCertBundle(): string { // If the instance is not yet setup we need to use the static path as // $this->certificateManager->getAbsoluteBundlePath() tries to instantiate // a view if (!$this->config->getSystemValueBool('installed', false)) { return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; } return $this->certificateManager->getAbsoluteBundlePath(); } /** * Returns a null or an associative array specifying the proxy URI for * 'http' and 'https' schemes, in addition to a 'no' key value pair * providing a list of host names that should not be proxied to. * * @return array|null * * The return array looks like: * [ * 'http' => 'username:password@proxy.example.com', * 'https' => 'username:password@proxy.example.com', * 'no' => ['foo.com', 'bar.com'] * ] * */ private function getProxyUri(): ?array { $proxyHost = $this->config->getSystemValueString('proxy', ''); if ($proxyHost === '') { return null; } $proxyUserPwd = $this->config->getSystemValueString('proxyuserpwd', ''); if ($proxyUserPwd !== '') { $proxyHost = $proxyUserPwd . '@' . $proxyHost; } $proxy = [ 'http' => $proxyHost, 'https' => $proxyHost, ]; $proxyExclude = $this->config->getSystemValue('proxyexclude', []); if ($proxyExclude !== [] && $proxyExclude !== null) { $proxy['no'] = $proxyExclude; } return $proxy; } private function isLocalAddressAllowed(array $options) : bool { if (($options['nextcloud']['allow_local_address'] ?? false) || $this->config->getSystemValueBool('allow_local_remote_servers', false)) { return true; } return false; } protected function preventLocalAddress(string $uri, array $options): void { if ($this->isLocalAddressAllowed($options)) { return; } $host = parse_url($uri, PHP_URL_HOST); if ($host === false || $host === null) { throw new LocalServerException('Could not detect any host'); } if (!$this->remoteHostValidator->isValid($host)) { throw new LocalServerException('Host "'.$host.'" violates local access rules'); } } public function get(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('get', $uri, $this->buildRequestOptions($options)); $isStream = isset($options['stream']) && $options['stream']; return new Response($response, $isStream); } public function head(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('head', $uri, $this->buildRequestOptions($options)); return new Response($response); } public function post(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); if (isset($options['body']) && is_array($options['body'])) { $options['form_params'] = $options['body']; unset($options['body']); } $response = $this->client->request('post', $uri, $this->buildRequestOptions($options)); $isStream = isset($options['stream']) && $options['stream']; return new Response($response, $isStream); } public function put(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('put', $uri, $this->buildRequestOptions($options)); return new Response($response); } public function patch(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('patch', $uri, $this->buildRequestOptions($options)); return new Response($response); } public function delete(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options)); return new Response($response); } public function options(string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request('options', $uri, $this->buildRequestOptions($options)); return new Response($response); } public function getResponseFromThrowable(\Throwable $e): IResponse { if (method_exists($e, 'hasResponse') && method_exists($e, 'getResponse') && $e->hasResponse()) { return new Response($e->getResponse()); } throw $e; } public function request(string $method, string $uri, array $options = []): IResponse { $this->preventLocalAddress($uri, $options); $response = $this->client->request($method, $uri, $this->buildRequestOptions($options)); $isStream = isset($options['stream']) && $options['stream']; return new Response($response, $isStream); } protected function wrapGuzzlePromise(PromiseInterface $promise): IPromise { return new GuzzlePromiseAdapter( $promise, $this->logger ); } public function getAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); $response = $this->client->requestAsync('get', $uri, $this->buildRequestOptions($options)); return $this->wrapGuzzlePromise($response); } public function headAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); $response = $this->client->requestAsync('head', $uri, $this->buildRequestOptions($options)); return $this->wrapGuzzlePromise($response); } public function postAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); if (isset($options['body']) && is_array($options['body'])) { $options['form_params'] = $options['body']; unset($options['body']); } return $this->wrapGuzzlePromise($this->client->requestAsync('post', $uri, $this->buildRequestOptions($options))); } public function putAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); $response = $this->client->requestAsync('put', $uri, $this->buildRequestOptions($options)); return $this->wrapGuzzlePromise($response); } public function deleteAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); $response = $this->client->requestAsync('delete', $uri, $this->buildRequestOptions($options)); return $this->wrapGuzzlePromise($response); } public function optionsAsync(string $uri, array $options = []): IPromise { $this->preventLocalAddress($uri, $options); $response = $this->client->requestAsync('options', $uri, $this->buildRequestOptions($options)); return $this->wrapGuzzlePromise($response); } }