Passed
Push — master ( b07a8f...789bb0 )
by Roeland
15:45 queued 10s
created

Client::isLocalAddressAllowed()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Carlos Ferreira <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Mohammed Abdellatif <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Scott Shambarger <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program. If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OC\Http\Client;
36
37
use GuzzleHttp\Client as GuzzleClient;
38
use GuzzleHttp\RequestOptions;
39
use OCP\Http\Client\IClient;
40
use OCP\Http\Client\IResponse;
41
use OCP\ICertificateManager;
42
use OCP\IConfig;
43
use OCP\ILogger;
44
45
/**
46
 * Class Client
47
 *
48
 * @package OC\Http
49
 */
50
class Client implements IClient {
51
	/** @var GuzzleClient */
52
	private $client;
53
	/** @var IConfig */
54
	private $config;
55
	/** @var ILogger */
56
	private $logger;
57
	/** @var ICertificateManager */
58
	private $certificateManager;
59
	/** @var LocalAddressChecker */
60
	private $localAddressChecker;
61
62
	public function __construct(
63
		IConfig $config,
64
		ILogger $logger,
65
		ICertificateManager $certificateManager,
66
		GuzzleClient $client,
67
		LocalAddressChecker $localAddressChecker
68
	) {
69
		$this->config = $config;
70
		$this->logger = $logger;
71
		$this->client = $client;
72
		$this->certificateManager = $certificateManager;
73
		$this->localAddressChecker = $localAddressChecker;
74
	}
75
76
	private function buildRequestOptions(array $options): array {
77
		$proxy = $this->getProxyUri();
78
79
		$defaults = [
80
			RequestOptions::VERIFY => $this->getCertBundle(),
81
			RequestOptions::TIMEOUT => 30,
82
		];
83
84
		$options['nextcloud']['allow_local_address'] = $this->isLocalAddressAllowed($options);
85
		if ($options['nextcloud']['allow_local_address'] === false) {
86
			$onRedirectFunction = function (
87
				\Psr\Http\Message\RequestInterface $request,
88
				\Psr\Http\Message\ResponseInterface $response,
89
				\Psr\Http\Message\UriInterface $uri
90
			) use ($options) {
91
				$this->preventLocalAddress($uri->__toString(), $options);
92
			};
93
94
			$defaults[RequestOptions::ALLOW_REDIRECTS] = [
95
				'on_redirect' => $onRedirectFunction
96
			];
97
		}
98
99
		// Only add RequestOptions::PROXY if Nextcloud is explicitly
100
		// configured to use a proxy. This is needed in order not to override
101
		// Guzzle default values.
102
		if ($proxy !== null) {
103
			$defaults[RequestOptions::PROXY] = $proxy;
104
		}
105
106
		$options = array_merge($defaults, $options);
107
108
		if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
109
			$options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
110
		}
111
112
		if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
113
			$options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
114
		}
115
116
		// Fallback for save_to
117
		if (isset($options['save_to'])) {
118
			$options['sink'] = $options['save_to'];
119
			unset($options['save_to']);
120
		}
121
122
		return $options;
123
	}
124
125
	private function getCertBundle(): string {
126
		// If the instance is not yet setup we need to use the static path as
127
		// $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
128
		// a view
129
		if ($this->config->getSystemValue('installed', false) === false) {
130
			return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
131
		}
132
133
		return $this->certificateManager->getAbsoluteBundlePath();
134
	}
135
136
	/**
137
	 * Returns a null or an associative array specifiying the proxy URI for
138
	 * 'http' and 'https' schemes, in addition to a 'no' key value pair
139
	 * providing a list of host names that should not be proxied to.
140
	 *
141
	 * @return array|null
142
	 *
143
	 * The return array looks like:
144
	 * [
145
	 *   'http' => 'username:[email protected]',
146
	 *   'https' => 'username:[email protected]',
147
	 *   'no' => ['foo.com', 'bar.com']
148
	 * ]
149
	 *
150
	 */
151
	private function getProxyUri(): ?array {
152
		$proxyHost = $this->config->getSystemValue('proxy', '');
153
154
		if ($proxyHost === '' || $proxyHost === null) {
155
			return null;
156
		}
157
158
		$proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
159
		if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
160
			$proxyHost = $proxyUserPwd . '@' . $proxyHost;
161
		}
162
163
		$proxy = [
164
			'http' => $proxyHost,
165
			'https' => $proxyHost,
166
		];
167
168
		$proxyExclude = $this->config->getSystemValue('proxyexclude', []);
169
		if ($proxyExclude !== [] && $proxyExclude !== null) {
170
			$proxy['no'] = $proxyExclude;
171
		}
172
173
		return $proxy;
174
	}
175
176
	private function isLocalAddressAllowed(array $options) : bool {
177
		if (($options['nextcloud']['allow_local_address'] ?? false) ||
178
			$this->config->getSystemValueBool('allow_local_remote_servers', false)) {
179
			return true;
180
		}
181
182
		return false;
183
	}
184
185
	protected function preventLocalAddress(string $uri, array $options): void {
186
		if ($this->isLocalAddressAllowed($options)) {
187
			return;
188
		}
189
190
		$this->localAddressChecker->ThrowIfLocalAddress($uri);
191
	}
192
193
	/**
194
	 * Sends a GET request
195
	 *
196
	 * @param string $uri
197
	 * @param array $options Array such as
198
	 *              'query' => [
199
	 *                  'field' => 'abc',
200
	 *                  'other_field' => '123',
201
	 *                  'file_name' => fopen('/path/to/file', 'r'),
202
	 *              ],
203
	 *              'headers' => [
204
	 *                  'foo' => 'bar',
205
	 *              ],
206
	 *              'cookies' => ['
207
	 *                  'foo' => 'bar',
208
	 *              ],
209
	 *              'allow_redirects' => [
210
	 *                   'max'       => 10,  // allow at most 10 redirects.
211
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
212
	 *                   'referer'   => true,     // add a Referer header
213
	 *                   'protocols' => ['https'] // only allow https URLs
214
	 *              ],
215
	 *              'sink' => '/path/to/file', // save to a file or a stream
216
	 *              'verify' => true, // bool or string to CA file
217
	 *              'debug' => true,
218
	 *              'timeout' => 5,
219
	 * @return IResponse
220
	 * @throws \Exception If the request could not get completed
221
	 */
222
	public function get(string $uri, array $options = []): IResponse {
223
		$this->preventLocalAddress($uri, $options);
224
		$response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
225
		$isStream = isset($options['stream']) && $options['stream'];
226
		return new Response($response, $isStream);
227
	}
228
229
	/**
230
	 * Sends a HEAD request
231
	 *
232
	 * @param string $uri
233
	 * @param array $options Array such as
234
	 *              'headers' => [
235
	 *                  'foo' => 'bar',
236
	 *              ],
237
	 *              'cookies' => ['
238
	 *                  'foo' => 'bar',
239
	 *              ],
240
	 *              'allow_redirects' => [
241
	 *                   'max'       => 10,  // allow at most 10 redirects.
242
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
243
	 *                   'referer'   => true,     // add a Referer header
244
	 *                   'protocols' => ['https'] // only allow https URLs
245
	 *              ],
246
	 *              'sink' => '/path/to/file', // save to a file or a stream
247
	 *              'verify' => true, // bool or string to CA file
248
	 *              'debug' => true,
249
	 *              'timeout' => 5,
250
	 * @return IResponse
251
	 * @throws \Exception If the request could not get completed
252
	 */
253
	public function head(string $uri, array $options = []): IResponse {
254
		$this->preventLocalAddress($uri, $options);
255
		$response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
256
		return new Response($response);
257
	}
258
259
	/**
260
	 * Sends a POST request
261
	 *
262
	 * @param string $uri
263
	 * @param array $options Array such as
264
	 *              'body' => [
265
	 *                  'field' => 'abc',
266
	 *                  'other_field' => '123',
267
	 *                  'file_name' => fopen('/path/to/file', 'r'),
268
	 *              ],
269
	 *              'headers' => [
270
	 *                  'foo' => 'bar',
271
	 *              ],
272
	 *              'cookies' => ['
273
	 *                  'foo' => 'bar',
274
	 *              ],
275
	 *              'allow_redirects' => [
276
	 *                   'max'       => 10,  // allow at most 10 redirects.
277
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
278
	 *                   'referer'   => true,     // add a Referer header
279
	 *                   'protocols' => ['https'] // only allow https URLs
280
	 *              ],
281
	 *              'sink' => '/path/to/file', // save to a file or a stream
282
	 *              'verify' => true, // bool or string to CA file
283
	 *              'debug' => true,
284
	 *              'timeout' => 5,
285
	 * @return IResponse
286
	 * @throws \Exception If the request could not get completed
287
	 */
288
	public function post(string $uri, array $options = []): IResponse {
289
		$this->preventLocalAddress($uri, $options);
290
291
		if (isset($options['body']) && is_array($options['body'])) {
292
			$options['form_params'] = $options['body'];
293
			unset($options['body']);
294
		}
295
		$response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
296
		return new Response($response);
297
	}
298
299
	/**
300
	 * Sends a PUT request
301
	 *
302
	 * @param string $uri
303
	 * @param array $options Array such as
304
	 *              'body' => [
305
	 *                  'field' => 'abc',
306
	 *                  'other_field' => '123',
307
	 *                  'file_name' => fopen('/path/to/file', 'r'),
308
	 *              ],
309
	 *              'headers' => [
310
	 *                  'foo' => 'bar',
311
	 *              ],
312
	 *              'cookies' => ['
313
	 *                  'foo' => 'bar',
314
	 *              ],
315
	 *              'allow_redirects' => [
316
	 *                   'max'       => 10,  // allow at most 10 redirects.
317
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
318
	 *                   'referer'   => true,     // add a Referer header
319
	 *                   'protocols' => ['https'] // only allow https URLs
320
	 *              ],
321
	 *              'sink' => '/path/to/file', // save to a file or a stream
322
	 *              'verify' => true, // bool or string to CA file
323
	 *              'debug' => true,
324
	 *              'timeout' => 5,
325
	 * @return IResponse
326
	 * @throws \Exception If the request could not get completed
327
	 */
328
	public function put(string $uri, array $options = []): IResponse {
329
		$this->preventLocalAddress($uri, $options);
330
		$response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
331
		return new Response($response);
332
	}
333
334
	/**
335
	 * Sends a DELETE request
336
	 *
337
	 * @param string $uri
338
	 * @param array $options Array such as
339
	 *              'body' => [
340
	 *                  'field' => 'abc',
341
	 *                  'other_field' => '123',
342
	 *                  'file_name' => fopen('/path/to/file', 'r'),
343
	 *              ],
344
	 *              'headers' => [
345
	 *                  'foo' => 'bar',
346
	 *              ],
347
	 *              'cookies' => ['
348
	 *                  'foo' => 'bar',
349
	 *              ],
350
	 *              'allow_redirects' => [
351
	 *                   'max'       => 10,  // allow at most 10 redirects.
352
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
353
	 *                   'referer'   => true,     // add a Referer header
354
	 *                   'protocols' => ['https'] // only allow https URLs
355
	 *              ],
356
	 *              'sink' => '/path/to/file', // save to a file or a stream
357
	 *              'verify' => true, // bool or string to CA file
358
	 *              'debug' => true,
359
	 *              'timeout' => 5,
360
	 * @return IResponse
361
	 * @throws \Exception If the request could not get completed
362
	 */
363
	public function delete(string $uri, array $options = []): IResponse {
364
		$this->preventLocalAddress($uri, $options);
365
		$response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
366
		return new Response($response);
367
	}
368
369
	/**
370
	 * Sends a options request
371
	 *
372
	 * @param string $uri
373
	 * @param array $options Array such as
374
	 *              'body' => [
375
	 *                  'field' => 'abc',
376
	 *                  'other_field' => '123',
377
	 *                  'file_name' => fopen('/path/to/file', 'r'),
378
	 *              ],
379
	 *              'headers' => [
380
	 *                  'foo' => 'bar',
381
	 *              ],
382
	 *              'cookies' => ['
383
	 *                  'foo' => 'bar',
384
	 *              ],
385
	 *              'allow_redirects' => [
386
	 *                   'max'       => 10,  // allow at most 10 redirects.
387
	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
388
	 *                   'referer'   => true,     // add a Referer header
389
	 *                   'protocols' => ['https'] // only allow https URLs
390
	 *              ],
391
	 *              'sink' => '/path/to/file', // save to a file or a stream
392
	 *              'verify' => true, // bool or string to CA file
393
	 *              'debug' => true,
394
	 *              'timeout' => 5,
395
	 * @return IResponse
396
	 * @throws \Exception If the request could not get completed
397
	 */
398
	public function options(string $uri, array $options = []): IResponse {
399
		$this->preventLocalAddress($uri, $options);
400
		$response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
401
		return new Response($response);
402
	}
403
}
404