Passed
Push — master ( 526905...0a45f4 )
by Morris
17:36 queued 11s
created
lib/private/App/AppStore/Fetcher/Fetcher.php 1 patch
Indentation   +183 added lines, -183 removed lines patch added patch discarded remove patch
@@ -41,187 +41,187 @@
 block discarded – undo
41 41
 use OCP\ILogger;
42 42
 
43 43
 abstract class Fetcher {
44
-	public const INVALIDATE_AFTER_SECONDS = 3600;
45
-
46
-	/** @var IAppData */
47
-	protected $appData;
48
-	/** @var IClientService */
49
-	protected $clientService;
50
-	/** @var ITimeFactory */
51
-	protected $timeFactory;
52
-	/** @var IConfig */
53
-	protected $config;
54
-	/** @var Ilogger */
55
-	protected $logger;
56
-	/** @var string */
57
-	protected $fileName;
58
-	/** @var string */
59
-	protected $endpointName;
60
-	/** @var string */
61
-	protected $version;
62
-	/** @var string */
63
-	protected $channel;
64
-
65
-	/**
66
-	 * @param Factory $appDataFactory
67
-	 * @param IClientService $clientService
68
-	 * @param ITimeFactory $timeFactory
69
-	 * @param IConfig $config
70
-	 * @param ILogger $logger
71
-	 */
72
-	public function __construct(Factory $appDataFactory,
73
-								IClientService $clientService,
74
-								ITimeFactory $timeFactory,
75
-								IConfig $config,
76
-								ILogger $logger) {
77
-		$this->appData = $appDataFactory->get('appstore');
78
-		$this->clientService = $clientService;
79
-		$this->timeFactory = $timeFactory;
80
-		$this->config = $config;
81
-		$this->logger = $logger;
82
-	}
83
-
84
-	/**
85
-	 * Fetches the response from the server
86
-	 *
87
-	 * @param string $ETag
88
-	 * @param string $content
89
-	 *
90
-	 * @return array
91
-	 */
92
-	protected function fetch($ETag, $content) {
93
-		$appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
94
-
95
-		if (!$appstoreenabled) {
96
-			return [];
97
-		}
98
-
99
-		$options = [
100
-			'timeout' => 10,
101
-		];
102
-
103
-		if ($ETag !== '') {
104
-			$options['headers'] = [
105
-				'If-None-Match' => $ETag,
106
-			];
107
-		}
108
-
109
-		$client = $this->clientService->newClient();
110
-		$response = $client->get($this->getEndpoint(), $options);
111
-
112
-		$responseJson = [];
113
-		if ($response->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
114
-			$responseJson['data'] = json_decode($content, true);
115
-		} else {
116
-			$responseJson['data'] = json_decode($response->getBody(), true);
117
-			$ETag = $response->getHeader('ETag');
118
-		}
119
-
120
-		$responseJson['timestamp'] = $this->timeFactory->getTime();
121
-		$responseJson['ncversion'] = $this->getVersion();
122
-		if ($ETag !== '') {
123
-			$responseJson['ETag'] = $ETag;
124
-		}
125
-
126
-		return $responseJson;
127
-	}
128
-
129
-	/**
130
-	 * Returns the array with the categories on the appstore server
131
-	 *
132
-	 * @return array
133
-	 */
134
-	public function get() {
135
-		$appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
136
-		$internetavailable = $this->config->getSystemValue('has_internet_connection', true);
137
-
138
-		if (!$appstoreenabled || !$internetavailable) {
139
-			return [];
140
-		}
141
-
142
-		$rootFolder = $this->appData->getFolder('/');
143
-
144
-		$ETag = '';
145
-		$content = '';
146
-
147
-		try {
148
-			// File does already exists
149
-			$file = $rootFolder->getFile($this->fileName);
150
-			$jsonBlob = json_decode($file->getContent(), true);
151
-			if (is_array($jsonBlob)) {
152
-
153
-				// No caching when the version has been updated
154
-				if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) {
155
-
156
-					// If the timestamp is older than 3600 seconds request the files new
157
-					if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) {
158
-						return $jsonBlob['data'];
159
-					}
160
-
161
-					if (isset($jsonBlob['ETag'])) {
162
-						$ETag = $jsonBlob['ETag'];
163
-						$content = json_encode($jsonBlob['data']);
164
-					}
165
-				}
166
-			}
167
-		} catch (NotFoundException $e) {
168
-			// File does not already exists
169
-			$file = $rootFolder->newFile($this->fileName);
170
-		}
171
-
172
-		// Refresh the file content
173
-		try {
174
-			$responseJson = $this->fetch($ETag, $content);
175
-			$file->putContent(json_encode($responseJson));
176
-			return json_decode($file->getContent(), true)['data'];
177
-		} catch (ConnectException $e) {
178
-			$this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
179
-			return [];
180
-		} catch (\Exception $e) {
181
-			$this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
182
-			return [];
183
-		}
184
-	}
185
-
186
-	/**
187
-	 * Get the currently Nextcloud version
188
-	 * @return string
189
-	 */
190
-	protected function getVersion() {
191
-		if ($this->version === null) {
192
-			$this->version = $this->config->getSystemValue('version', '0.0.0');
193
-		}
194
-		return $this->version;
195
-	}
196
-
197
-	/**
198
-	 * Set the current Nextcloud version
199
-	 * @param string $version
200
-	 */
201
-	public function setVersion(string $version) {
202
-		$this->version = $version;
203
-	}
204
-
205
-	/**
206
-	 * Get the currently Nextcloud update channel
207
-	 * @return string
208
-	 */
209
-	protected function getChannel() {
210
-		if ($this->channel === null) {
211
-			$this->channel = \OC_Util::getChannel();
212
-		}
213
-		return $this->channel;
214
-	}
215
-
216
-	/**
217
-	 * Set the current Nextcloud update channel
218
-	 * @param string $channel
219
-	 */
220
-	public function setChannel(string $channel) {
221
-		$this->channel = $channel;
222
-	}
223
-
224
-	protected function getEndpoint(): string {
225
-		return $this->config->getSystemValue('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName;
226
-	}
44
+    public const INVALIDATE_AFTER_SECONDS = 3600;
45
+
46
+    /** @var IAppData */
47
+    protected $appData;
48
+    /** @var IClientService */
49
+    protected $clientService;
50
+    /** @var ITimeFactory */
51
+    protected $timeFactory;
52
+    /** @var IConfig */
53
+    protected $config;
54
+    /** @var Ilogger */
55
+    protected $logger;
56
+    /** @var string */
57
+    protected $fileName;
58
+    /** @var string */
59
+    protected $endpointName;
60
+    /** @var string */
61
+    protected $version;
62
+    /** @var string */
63
+    protected $channel;
64
+
65
+    /**
66
+     * @param Factory $appDataFactory
67
+     * @param IClientService $clientService
68
+     * @param ITimeFactory $timeFactory
69
+     * @param IConfig $config
70
+     * @param ILogger $logger
71
+     */
72
+    public function __construct(Factory $appDataFactory,
73
+                                IClientService $clientService,
74
+                                ITimeFactory $timeFactory,
75
+                                IConfig $config,
76
+                                ILogger $logger) {
77
+        $this->appData = $appDataFactory->get('appstore');
78
+        $this->clientService = $clientService;
79
+        $this->timeFactory = $timeFactory;
80
+        $this->config = $config;
81
+        $this->logger = $logger;
82
+    }
83
+
84
+    /**
85
+     * Fetches the response from the server
86
+     *
87
+     * @param string $ETag
88
+     * @param string $content
89
+     *
90
+     * @return array
91
+     */
92
+    protected function fetch($ETag, $content) {
93
+        $appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
94
+
95
+        if (!$appstoreenabled) {
96
+            return [];
97
+        }
98
+
99
+        $options = [
100
+            'timeout' => 10,
101
+        ];
102
+
103
+        if ($ETag !== '') {
104
+            $options['headers'] = [
105
+                'If-None-Match' => $ETag,
106
+            ];
107
+        }
108
+
109
+        $client = $this->clientService->newClient();
110
+        $response = $client->get($this->getEndpoint(), $options);
111
+
112
+        $responseJson = [];
113
+        if ($response->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
114
+            $responseJson['data'] = json_decode($content, true);
115
+        } else {
116
+            $responseJson['data'] = json_decode($response->getBody(), true);
117
+            $ETag = $response->getHeader('ETag');
118
+        }
119
+
120
+        $responseJson['timestamp'] = $this->timeFactory->getTime();
121
+        $responseJson['ncversion'] = $this->getVersion();
122
+        if ($ETag !== '') {
123
+            $responseJson['ETag'] = $ETag;
124
+        }
125
+
126
+        return $responseJson;
127
+    }
128
+
129
+    /**
130
+     * Returns the array with the categories on the appstore server
131
+     *
132
+     * @return array
133
+     */
134
+    public function get() {
135
+        $appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
136
+        $internetavailable = $this->config->getSystemValue('has_internet_connection', true);
137
+
138
+        if (!$appstoreenabled || !$internetavailable) {
139
+            return [];
140
+        }
141
+
142
+        $rootFolder = $this->appData->getFolder('/');
143
+
144
+        $ETag = '';
145
+        $content = '';
146
+
147
+        try {
148
+            // File does already exists
149
+            $file = $rootFolder->getFile($this->fileName);
150
+            $jsonBlob = json_decode($file->getContent(), true);
151
+            if (is_array($jsonBlob)) {
152
+
153
+                // No caching when the version has been updated
154
+                if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) {
155
+
156
+                    // If the timestamp is older than 3600 seconds request the files new
157
+                    if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) {
158
+                        return $jsonBlob['data'];
159
+                    }
160
+
161
+                    if (isset($jsonBlob['ETag'])) {
162
+                        $ETag = $jsonBlob['ETag'];
163
+                        $content = json_encode($jsonBlob['data']);
164
+                    }
165
+                }
166
+            }
167
+        } catch (NotFoundException $e) {
168
+            // File does not already exists
169
+            $file = $rootFolder->newFile($this->fileName);
170
+        }
171
+
172
+        // Refresh the file content
173
+        try {
174
+            $responseJson = $this->fetch($ETag, $content);
175
+            $file->putContent(json_encode($responseJson));
176
+            return json_decode($file->getContent(), true)['data'];
177
+        } catch (ConnectException $e) {
178
+            $this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
179
+            return [];
180
+        } catch (\Exception $e) {
181
+            $this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
182
+            return [];
183
+        }
184
+    }
185
+
186
+    /**
187
+     * Get the currently Nextcloud version
188
+     * @return string
189
+     */
190
+    protected function getVersion() {
191
+        if ($this->version === null) {
192
+            $this->version = $this->config->getSystemValue('version', '0.0.0');
193
+        }
194
+        return $this->version;
195
+    }
196
+
197
+    /**
198
+     * Set the current Nextcloud version
199
+     * @param string $version
200
+     */
201
+    public function setVersion(string $version) {
202
+        $this->version = $version;
203
+    }
204
+
205
+    /**
206
+     * Get the currently Nextcloud update channel
207
+     * @return string
208
+     */
209
+    protected function getChannel() {
210
+        if ($this->channel === null) {
211
+            $this->channel = \OC_Util::getChannel();
212
+        }
213
+        return $this->channel;
214
+    }
215
+
216
+    /**
217
+     * Set the current Nextcloud update channel
218
+     * @param string $channel
219
+     */
220
+    public function setChannel(string $channel) {
221
+        $this->channel = $channel;
222
+    }
223
+
224
+    protected function getEndpoint(): string {
225
+        return $this->config->getSystemValue('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName;
226
+    }
227 227
 }
Please login to merge, or discard this patch.
lib/private/Http/Client/Client.php 1 patch
Indentation   +361 added lines, -361 removed lines patch added patch discarded remove patch
@@ -47,365 +47,365 @@
 block discarded – undo
47 47
  * @package OC\Http
48 48
  */
49 49
 class Client implements IClient {
50
-	/** @var GuzzleClient */
51
-	private $client;
52
-	/** @var IConfig */
53
-	private $config;
54
-	/** @var ILogger */
55
-	private $logger;
56
-	/** @var ICertificateManager */
57
-	private $certificateManager;
58
-
59
-	public function __construct(
60
-		IConfig $config,
61
-		ILogger $logger,
62
-		ICertificateManager $certificateManager,
63
-		GuzzleClient $client
64
-	) {
65
-		$this->config = $config;
66
-		$this->logger = $logger;
67
-		$this->client = $client;
68
-		$this->certificateManager = $certificateManager;
69
-	}
70
-
71
-	private function buildRequestOptions(array $options): array {
72
-		$proxy = $this->getProxyUri();
73
-
74
-		$defaults = [
75
-			RequestOptions::VERIFY => $this->getCertBundle(),
76
-			RequestOptions::TIMEOUT => 30,
77
-		];
78
-
79
-		// Only add RequestOptions::PROXY if Nextcloud is explicitly
80
-		// configured to use a proxy. This is needed in order not to override
81
-		// Guzzle default values.
82
-		if ($proxy !== null) {
83
-			$defaults[RequestOptions::PROXY] = $proxy;
84
-		}
85
-
86
-		$options = array_merge($defaults, $options);
87
-
88
-		if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
89
-			$options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
90
-		}
91
-
92
-		if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
93
-			$options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
94
-		}
95
-
96
-		return $options;
97
-	}
98
-
99
-	private function getCertBundle(): string {
100
-		if ($this->certificateManager->listCertificates() !== []) {
101
-			return $this->certificateManager->getAbsoluteBundlePath();
102
-		}
103
-
104
-		// If the instance is not yet setup we need to use the static path as
105
-		// $this->certificateManager->getAbsoluteBundlePath() tries to instantiiate
106
-		// a view
107
-		if ($this->config->getSystemValue('installed', false)) {
108
-			return $this->certificateManager->getAbsoluteBundlePath(null);
109
-		}
110
-
111
-		return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
112
-	}
113
-
114
-	/**
115
-	 * Returns a null or an associative array specifiying the proxy URI for
116
-	 * 'http' and 'https' schemes, in addition to a 'no' key value pair
117
-	 * providing a list of host names that should not be proxied to.
118
-	 *
119
-	 * @return array|null
120
-	 *
121
-	 * The return array looks like:
122
-	 * [
123
-	 *   'http' => 'username:[email protected]',
124
-	 *   'https' => 'username:[email protected]',
125
-	 *   'no' => ['foo.com', 'bar.com']
126
-	 * ]
127
-	 *
128
-	 */
129
-	private function getProxyUri(): ?array {
130
-		$proxyHost = $this->config->getSystemValue('proxy', '');
131
-
132
-		if ($proxyHost === '' || $proxyHost === null) {
133
-			return null;
134
-		}
135
-
136
-		$proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
137
-		if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
138
-			$proxyHost = $proxyUserPwd . '@' . $proxyHost;
139
-		}
140
-
141
-		$proxy = [
142
-			'http' => $proxyHost,
143
-			'https' => $proxyHost,
144
-		];
145
-
146
-		$proxyExclude = $this->config->getSystemValue('proxyexclude', []);
147
-		if ($proxyExclude !== [] && $proxyExclude !== null) {
148
-			$proxy['no'] = $proxyExclude;
149
-		}
150
-
151
-		return $proxy;
152
-	}
153
-
154
-	protected function preventLocalAddress(string $uri, array $options): void {
155
-		if (($options['nextcloud']['allow_local_address'] ?? false) ||
156
-			$this->config->getSystemValueBool('allow_local_remote_servers', false)) {
157
-			return;
158
-		}
159
-
160
-		$host = parse_url($uri, PHP_URL_HOST);
161
-		if ($host === false) {
162
-			$this->logger->warning("Could not detect any host in $uri");
163
-			throw new LocalServerException('Could not detect any host');
164
-		}
165
-
166
-		$host = strtolower($host);
167
-		// remove brackets from IPv6 addresses
168
-		if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
169
-			$host = substr($host, 1, -1);
170
-		}
171
-
172
-		// Disallow localhost and local network
173
-		if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
174
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
175
-			throw new LocalServerException('Host violates local access rules');
176
-		}
177
-
178
-		// Disallow hostname only
179
-		if (substr_count($host, '.') === 0) {
180
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
181
-			throw new LocalServerException('Host violates local access rules');
182
-		}
183
-
184
-		if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
185
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
186
-			throw new LocalServerException('Host violates local access rules');
187
-		}
188
-
189
-		// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
190
-		if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
191
-			$delimiter = strrpos($host, ':'); // Get last colon
192
-			$ipv4Address = substr($host, $delimiter + 1);
193
-
194
-			if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
195
-				$this->logger->warning("Host $host was not connected to because it violates local access rules");
196
-				throw new LocalServerException('Host violates local access rules');
197
-			}
198
-		}
199
-	}
200
-
201
-	/**
202
-	 * Sends a GET request
203
-	 *
204
-	 * @param string $uri
205
-	 * @param array $options Array such as
206
-	 *              'query' => [
207
-	 *                  'field' => 'abc',
208
-	 *                  'other_field' => '123',
209
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
210
-	 *              ],
211
-	 *              'headers' => [
212
-	 *                  'foo' => 'bar',
213
-	 *              ],
214
-	 *              'cookies' => ['
215
-	 *                  'foo' => 'bar',
216
-	 *              ],
217
-	 *              'allow_redirects' => [
218
-	 *                   'max'       => 10,  // allow at most 10 redirects.
219
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
220
-	 *                   'referer'   => true,     // add a Referer header
221
-	 *                   'protocols' => ['https'] // only allow https URLs
222
-	 *              ],
223
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
224
-	 *              'verify' => true, // bool or string to CA file
225
-	 *              'debug' => true,
226
-	 *              'timeout' => 5,
227
-	 * @return IResponse
228
-	 * @throws \Exception If the request could not get completed
229
-	 */
230
-	public function get(string $uri, array $options = []): IResponse {
231
-		$this->preventLocalAddress($uri, $options);
232
-		$response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
233
-		$isStream = isset($options['stream']) && $options['stream'];
234
-		return new Response($response, $isStream);
235
-	}
236
-
237
-	/**
238
-	 * Sends a HEAD request
239
-	 *
240
-	 * @param string $uri
241
-	 * @param array $options Array such as
242
-	 *              'headers' => [
243
-	 *                  'foo' => 'bar',
244
-	 *              ],
245
-	 *              'cookies' => ['
246
-	 *                  'foo' => 'bar',
247
-	 *              ],
248
-	 *              'allow_redirects' => [
249
-	 *                   'max'       => 10,  // allow at most 10 redirects.
250
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
251
-	 *                   'referer'   => true,     // add a Referer header
252
-	 *                   'protocols' => ['https'] // only allow https URLs
253
-	 *              ],
254
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
255
-	 *              'verify' => true, // bool or string to CA file
256
-	 *              'debug' => true,
257
-	 *              'timeout' => 5,
258
-	 * @return IResponse
259
-	 * @throws \Exception If the request could not get completed
260
-	 */
261
-	public function head(string $uri, array $options = []): IResponse {
262
-		$this->preventLocalAddress($uri, $options);
263
-		$response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
264
-		return new Response($response);
265
-	}
266
-
267
-	/**
268
-	 * Sends a POST request
269
-	 *
270
-	 * @param string $uri
271
-	 * @param array $options Array such as
272
-	 *              'body' => [
273
-	 *                  'field' => 'abc',
274
-	 *                  'other_field' => '123',
275
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
276
-	 *              ],
277
-	 *              'headers' => [
278
-	 *                  'foo' => 'bar',
279
-	 *              ],
280
-	 *              'cookies' => ['
281
-	 *                  'foo' => 'bar',
282
-	 *              ],
283
-	 *              'allow_redirects' => [
284
-	 *                   'max'       => 10,  // allow at most 10 redirects.
285
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
286
-	 *                   'referer'   => true,     // add a Referer header
287
-	 *                   'protocols' => ['https'] // only allow https URLs
288
-	 *              ],
289
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
290
-	 *              'verify' => true, // bool or string to CA file
291
-	 *              'debug' => true,
292
-	 *              'timeout' => 5,
293
-	 * @return IResponse
294
-	 * @throws \Exception If the request could not get completed
295
-	 */
296
-	public function post(string $uri, array $options = []): IResponse {
297
-		$this->preventLocalAddress($uri, $options);
298
-
299
-		if (isset($options['body']) && is_array($options['body'])) {
300
-			$options['form_params'] = $options['body'];
301
-			unset($options['body']);
302
-		}
303
-		$response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
304
-		return new Response($response);
305
-	}
306
-
307
-	/**
308
-	 * Sends a PUT request
309
-	 *
310
-	 * @param string $uri
311
-	 * @param array $options Array such as
312
-	 *              'body' => [
313
-	 *                  'field' => 'abc',
314
-	 *                  'other_field' => '123',
315
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
316
-	 *              ],
317
-	 *              'headers' => [
318
-	 *                  'foo' => 'bar',
319
-	 *              ],
320
-	 *              'cookies' => ['
321
-	 *                  'foo' => 'bar',
322
-	 *              ],
323
-	 *              'allow_redirects' => [
324
-	 *                   'max'       => 10,  // allow at most 10 redirects.
325
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
326
-	 *                   'referer'   => true,     // add a Referer header
327
-	 *                   'protocols' => ['https'] // only allow https URLs
328
-	 *              ],
329
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
330
-	 *              'verify' => true, // bool or string to CA file
331
-	 *              'debug' => true,
332
-	 *              'timeout' => 5,
333
-	 * @return IResponse
334
-	 * @throws \Exception If the request could not get completed
335
-	 */
336
-	public function put(string $uri, array $options = []): IResponse {
337
-		$this->preventLocalAddress($uri, $options);
338
-		$response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
339
-		return new Response($response);
340
-	}
341
-
342
-	/**
343
-	 * Sends a DELETE request
344
-	 *
345
-	 * @param string $uri
346
-	 * @param array $options Array such as
347
-	 *              'body' => [
348
-	 *                  'field' => 'abc',
349
-	 *                  'other_field' => '123',
350
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
351
-	 *              ],
352
-	 *              'headers' => [
353
-	 *                  'foo' => 'bar',
354
-	 *              ],
355
-	 *              'cookies' => ['
356
-	 *                  'foo' => 'bar',
357
-	 *              ],
358
-	 *              'allow_redirects' => [
359
-	 *                   'max'       => 10,  // allow at most 10 redirects.
360
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
361
-	 *                   'referer'   => true,     // add a Referer header
362
-	 *                   'protocols' => ['https'] // only allow https URLs
363
-	 *              ],
364
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
365
-	 *              'verify' => true, // bool or string to CA file
366
-	 *              'debug' => true,
367
-	 *              'timeout' => 5,
368
-	 * @return IResponse
369
-	 * @throws \Exception If the request could not get completed
370
-	 */
371
-	public function delete(string $uri, array $options = []): IResponse {
372
-		$this->preventLocalAddress($uri, $options);
373
-		$response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
374
-		return new Response($response);
375
-	}
376
-
377
-	/**
378
-	 * Sends a options request
379
-	 *
380
-	 * @param string $uri
381
-	 * @param array $options Array such as
382
-	 *              'body' => [
383
-	 *                  'field' => 'abc',
384
-	 *                  'other_field' => '123',
385
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
386
-	 *              ],
387
-	 *              'headers' => [
388
-	 *                  'foo' => 'bar',
389
-	 *              ],
390
-	 *              'cookies' => ['
391
-	 *                  'foo' => 'bar',
392
-	 *              ],
393
-	 *              'allow_redirects' => [
394
-	 *                   'max'       => 10,  // allow at most 10 redirects.
395
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
396
-	 *                   'referer'   => true,     // add a Referer header
397
-	 *                   'protocols' => ['https'] // only allow https URLs
398
-	 *              ],
399
-	 *              'save_to' => '/path/to/file', // save to a file or a stream
400
-	 *              'verify' => true, // bool or string to CA file
401
-	 *              'debug' => true,
402
-	 *              'timeout' => 5,
403
-	 * @return IResponse
404
-	 * @throws \Exception If the request could not get completed
405
-	 */
406
-	public function options(string $uri, array $options = []): IResponse {
407
-		$this->preventLocalAddress($uri, $options);
408
-		$response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
409
-		return new Response($response);
410
-	}
50
+    /** @var GuzzleClient */
51
+    private $client;
52
+    /** @var IConfig */
53
+    private $config;
54
+    /** @var ILogger */
55
+    private $logger;
56
+    /** @var ICertificateManager */
57
+    private $certificateManager;
58
+
59
+    public function __construct(
60
+        IConfig $config,
61
+        ILogger $logger,
62
+        ICertificateManager $certificateManager,
63
+        GuzzleClient $client
64
+    ) {
65
+        $this->config = $config;
66
+        $this->logger = $logger;
67
+        $this->client = $client;
68
+        $this->certificateManager = $certificateManager;
69
+    }
70
+
71
+    private function buildRequestOptions(array $options): array {
72
+        $proxy = $this->getProxyUri();
73
+
74
+        $defaults = [
75
+            RequestOptions::VERIFY => $this->getCertBundle(),
76
+            RequestOptions::TIMEOUT => 30,
77
+        ];
78
+
79
+        // Only add RequestOptions::PROXY if Nextcloud is explicitly
80
+        // configured to use a proxy. This is needed in order not to override
81
+        // Guzzle default values.
82
+        if ($proxy !== null) {
83
+            $defaults[RequestOptions::PROXY] = $proxy;
84
+        }
85
+
86
+        $options = array_merge($defaults, $options);
87
+
88
+        if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
89
+            $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
90
+        }
91
+
92
+        if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
93
+            $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
94
+        }
95
+
96
+        return $options;
97
+    }
98
+
99
+    private function getCertBundle(): string {
100
+        if ($this->certificateManager->listCertificates() !== []) {
101
+            return $this->certificateManager->getAbsoluteBundlePath();
102
+        }
103
+
104
+        // If the instance is not yet setup we need to use the static path as
105
+        // $this->certificateManager->getAbsoluteBundlePath() tries to instantiiate
106
+        // a view
107
+        if ($this->config->getSystemValue('installed', false)) {
108
+            return $this->certificateManager->getAbsoluteBundlePath(null);
109
+        }
110
+
111
+        return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
112
+    }
113
+
114
+    /**
115
+     * Returns a null or an associative array specifiying the proxy URI for
116
+     * 'http' and 'https' schemes, in addition to a 'no' key value pair
117
+     * providing a list of host names that should not be proxied to.
118
+     *
119
+     * @return array|null
120
+     *
121
+     * The return array looks like:
122
+     * [
123
+     *   'http' => 'username:[email protected]',
124
+     *   'https' => 'username:[email protected]',
125
+     *   'no' => ['foo.com', 'bar.com']
126
+     * ]
127
+     *
128
+     */
129
+    private function getProxyUri(): ?array {
130
+        $proxyHost = $this->config->getSystemValue('proxy', '');
131
+
132
+        if ($proxyHost === '' || $proxyHost === null) {
133
+            return null;
134
+        }
135
+
136
+        $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
137
+        if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
138
+            $proxyHost = $proxyUserPwd . '@' . $proxyHost;
139
+        }
140
+
141
+        $proxy = [
142
+            'http' => $proxyHost,
143
+            'https' => $proxyHost,
144
+        ];
145
+
146
+        $proxyExclude = $this->config->getSystemValue('proxyexclude', []);
147
+        if ($proxyExclude !== [] && $proxyExclude !== null) {
148
+            $proxy['no'] = $proxyExclude;
149
+        }
150
+
151
+        return $proxy;
152
+    }
153
+
154
+    protected function preventLocalAddress(string $uri, array $options): void {
155
+        if (($options['nextcloud']['allow_local_address'] ?? false) ||
156
+            $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
157
+            return;
158
+        }
159
+
160
+        $host = parse_url($uri, PHP_URL_HOST);
161
+        if ($host === false) {
162
+            $this->logger->warning("Could not detect any host in $uri");
163
+            throw new LocalServerException('Could not detect any host');
164
+        }
165
+
166
+        $host = strtolower($host);
167
+        // remove brackets from IPv6 addresses
168
+        if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
169
+            $host = substr($host, 1, -1);
170
+        }
171
+
172
+        // Disallow localhost and local network
173
+        if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
174
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
175
+            throw new LocalServerException('Host violates local access rules');
176
+        }
177
+
178
+        // Disallow hostname only
179
+        if (substr_count($host, '.') === 0) {
180
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
181
+            throw new LocalServerException('Host violates local access rules');
182
+        }
183
+
184
+        if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
185
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
186
+            throw new LocalServerException('Host violates local access rules');
187
+        }
188
+
189
+        // Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
190
+        if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
191
+            $delimiter = strrpos($host, ':'); // Get last colon
192
+            $ipv4Address = substr($host, $delimiter + 1);
193
+
194
+            if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
195
+                $this->logger->warning("Host $host was not connected to because it violates local access rules");
196
+                throw new LocalServerException('Host violates local access rules');
197
+            }
198
+        }
199
+    }
200
+
201
+    /**
202
+     * Sends a GET request
203
+     *
204
+     * @param string $uri
205
+     * @param array $options Array such as
206
+     *              'query' => [
207
+     *                  'field' => 'abc',
208
+     *                  'other_field' => '123',
209
+     *                  'file_name' => fopen('/path/to/file', 'r'),
210
+     *              ],
211
+     *              'headers' => [
212
+     *                  'foo' => 'bar',
213
+     *              ],
214
+     *              'cookies' => ['
215
+     *                  'foo' => 'bar',
216
+     *              ],
217
+     *              'allow_redirects' => [
218
+     *                   'max'       => 10,  // allow at most 10 redirects.
219
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
220
+     *                   'referer'   => true,     // add a Referer header
221
+     *                   'protocols' => ['https'] // only allow https URLs
222
+     *              ],
223
+     *              'save_to' => '/path/to/file', // save to a file or a stream
224
+     *              'verify' => true, // bool or string to CA file
225
+     *              'debug' => true,
226
+     *              'timeout' => 5,
227
+     * @return IResponse
228
+     * @throws \Exception If the request could not get completed
229
+     */
230
+    public function get(string $uri, array $options = []): IResponse {
231
+        $this->preventLocalAddress($uri, $options);
232
+        $response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
233
+        $isStream = isset($options['stream']) && $options['stream'];
234
+        return new Response($response, $isStream);
235
+    }
236
+
237
+    /**
238
+     * Sends a HEAD request
239
+     *
240
+     * @param string $uri
241
+     * @param array $options Array such as
242
+     *              'headers' => [
243
+     *                  'foo' => 'bar',
244
+     *              ],
245
+     *              'cookies' => ['
246
+     *                  'foo' => 'bar',
247
+     *              ],
248
+     *              'allow_redirects' => [
249
+     *                   'max'       => 10,  // allow at most 10 redirects.
250
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
251
+     *                   'referer'   => true,     // add a Referer header
252
+     *                   'protocols' => ['https'] // only allow https URLs
253
+     *              ],
254
+     *              'save_to' => '/path/to/file', // save to a file or a stream
255
+     *              'verify' => true, // bool or string to CA file
256
+     *              'debug' => true,
257
+     *              'timeout' => 5,
258
+     * @return IResponse
259
+     * @throws \Exception If the request could not get completed
260
+     */
261
+    public function head(string $uri, array $options = []): IResponse {
262
+        $this->preventLocalAddress($uri, $options);
263
+        $response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
264
+        return new Response($response);
265
+    }
266
+
267
+    /**
268
+     * Sends a POST request
269
+     *
270
+     * @param string $uri
271
+     * @param array $options Array such as
272
+     *              'body' => [
273
+     *                  'field' => 'abc',
274
+     *                  'other_field' => '123',
275
+     *                  'file_name' => fopen('/path/to/file', 'r'),
276
+     *              ],
277
+     *              'headers' => [
278
+     *                  'foo' => 'bar',
279
+     *              ],
280
+     *              'cookies' => ['
281
+     *                  'foo' => 'bar',
282
+     *              ],
283
+     *              'allow_redirects' => [
284
+     *                   'max'       => 10,  // allow at most 10 redirects.
285
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
286
+     *                   'referer'   => true,     // add a Referer header
287
+     *                   'protocols' => ['https'] // only allow https URLs
288
+     *              ],
289
+     *              'save_to' => '/path/to/file', // save to a file or a stream
290
+     *              'verify' => true, // bool or string to CA file
291
+     *              'debug' => true,
292
+     *              'timeout' => 5,
293
+     * @return IResponse
294
+     * @throws \Exception If the request could not get completed
295
+     */
296
+    public function post(string $uri, array $options = []): IResponse {
297
+        $this->preventLocalAddress($uri, $options);
298
+
299
+        if (isset($options['body']) && is_array($options['body'])) {
300
+            $options['form_params'] = $options['body'];
301
+            unset($options['body']);
302
+        }
303
+        $response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
304
+        return new Response($response);
305
+    }
306
+
307
+    /**
308
+     * Sends a PUT request
309
+     *
310
+     * @param string $uri
311
+     * @param array $options Array such as
312
+     *              'body' => [
313
+     *                  'field' => 'abc',
314
+     *                  'other_field' => '123',
315
+     *                  'file_name' => fopen('/path/to/file', 'r'),
316
+     *              ],
317
+     *              'headers' => [
318
+     *                  'foo' => 'bar',
319
+     *              ],
320
+     *              'cookies' => ['
321
+     *                  'foo' => 'bar',
322
+     *              ],
323
+     *              'allow_redirects' => [
324
+     *                   'max'       => 10,  // allow at most 10 redirects.
325
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
326
+     *                   'referer'   => true,     // add a Referer header
327
+     *                   'protocols' => ['https'] // only allow https URLs
328
+     *              ],
329
+     *              'save_to' => '/path/to/file', // save to a file or a stream
330
+     *              'verify' => true, // bool or string to CA file
331
+     *              'debug' => true,
332
+     *              'timeout' => 5,
333
+     * @return IResponse
334
+     * @throws \Exception If the request could not get completed
335
+     */
336
+    public function put(string $uri, array $options = []): IResponse {
337
+        $this->preventLocalAddress($uri, $options);
338
+        $response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
339
+        return new Response($response);
340
+    }
341
+
342
+    /**
343
+     * Sends a DELETE request
344
+     *
345
+     * @param string $uri
346
+     * @param array $options Array such as
347
+     *              'body' => [
348
+     *                  'field' => 'abc',
349
+     *                  'other_field' => '123',
350
+     *                  'file_name' => fopen('/path/to/file', 'r'),
351
+     *              ],
352
+     *              'headers' => [
353
+     *                  'foo' => 'bar',
354
+     *              ],
355
+     *              'cookies' => ['
356
+     *                  'foo' => 'bar',
357
+     *              ],
358
+     *              'allow_redirects' => [
359
+     *                   'max'       => 10,  // allow at most 10 redirects.
360
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
361
+     *                   'referer'   => true,     // add a Referer header
362
+     *                   'protocols' => ['https'] // only allow https URLs
363
+     *              ],
364
+     *              'save_to' => '/path/to/file', // save to a file or a stream
365
+     *              'verify' => true, // bool or string to CA file
366
+     *              'debug' => true,
367
+     *              'timeout' => 5,
368
+     * @return IResponse
369
+     * @throws \Exception If the request could not get completed
370
+     */
371
+    public function delete(string $uri, array $options = []): IResponse {
372
+        $this->preventLocalAddress($uri, $options);
373
+        $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
374
+        return new Response($response);
375
+    }
376
+
377
+    /**
378
+     * Sends a options request
379
+     *
380
+     * @param string $uri
381
+     * @param array $options Array such as
382
+     *              'body' => [
383
+     *                  'field' => 'abc',
384
+     *                  'other_field' => '123',
385
+     *                  'file_name' => fopen('/path/to/file', 'r'),
386
+     *              ],
387
+     *              'headers' => [
388
+     *                  'foo' => 'bar',
389
+     *              ],
390
+     *              'cookies' => ['
391
+     *                  'foo' => 'bar',
392
+     *              ],
393
+     *              'allow_redirects' => [
394
+     *                   'max'       => 10,  // allow at most 10 redirects.
395
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
396
+     *                   'referer'   => true,     // add a Referer header
397
+     *                   'protocols' => ['https'] // only allow https URLs
398
+     *              ],
399
+     *              'save_to' => '/path/to/file', // save to a file or a stream
400
+     *              'verify' => true, // bool or string to CA file
401
+     *              'debug' => true,
402
+     *              'timeout' => 5,
403
+     * @return IResponse
404
+     * @throws \Exception If the request could not get completed
405
+     */
406
+    public function options(string $uri, array $options = []): IResponse {
407
+        $this->preventLocalAddress($uri, $options);
408
+        $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
409
+        return new Response($response);
410
+    }
411 411
 }
Please login to merge, or discard this patch.