2021-03-23 16:41:31 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @copyright Copyright (c) 2021, Lukas Reschke <lukas@statuscode.ch>
|
|
|
|
*
|
|
|
|
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
|
|
*
|
|
|
|
* @license GNU AGPL version 3 or any later version
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
2021-06-04 19:52:51 +00:00
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2021-03-23 16:41:31 +00:00
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
namespace OC\Http\Client;
|
|
|
|
|
2022-10-27 12:33:31 +00:00
|
|
|
use OC\Net\IpAddressClassifier;
|
|
|
|
use OCP\Http\Client\LocalServerException;
|
2021-03-23 16:41:31 +00:00
|
|
|
use Psr\Http\Message\RequestInterface;
|
|
|
|
|
|
|
|
class DnsPinMiddleware {
|
|
|
|
/** @var NegativeDnsCache */
|
|
|
|
private $negativeDnsCache;
|
2022-10-27 12:33:31 +00:00
|
|
|
private IpAddressClassifier $ipAddressClassifier;
|
2021-03-23 16:41:31 +00:00
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
NegativeDnsCache $negativeDnsCache,
|
2022-10-27 12:33:31 +00:00
|
|
|
IpAddressClassifier $ipAddressClassifier
|
2021-04-06 11:39:24 +00:00
|
|
|
) {
|
2021-03-23 16:41:31 +00:00
|
|
|
$this->negativeDnsCache = $negativeDnsCache;
|
2022-10-27 12:33:31 +00:00
|
|
|
$this->ipAddressClassifier = $ipAddressClassifier;
|
2021-03-23 16:41:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 08:52:18 +00:00
|
|
|
/**
|
|
|
|
* Fetch soa record for a target
|
|
|
|
*
|
|
|
|
* @param string $target
|
|
|
|
* @return array|null
|
|
|
|
*/
|
|
|
|
private function soaRecord(string $target): ?array {
|
|
|
|
$labels = explode('.', $target);
|
|
|
|
|
|
|
|
$top = count($labels) >= 2 ? array_pop($labels) : '';
|
|
|
|
$second = array_pop($labels);
|
|
|
|
|
|
|
|
$hostname = $second . '.' . $top;
|
2023-09-12 09:17:37 +00:00
|
|
|
$responses = $this->dnsGetRecord($hostname, DNS_SOA);
|
2021-07-05 08:52:18 +00:00
|
|
|
|
|
|
|
if ($responses === false || count($responses) === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return reset($responses);
|
|
|
|
}
|
|
|
|
|
2021-03-23 16:41:31 +00:00
|
|
|
private function dnsResolve(string $target, int $recursionCount) : array {
|
|
|
|
if ($recursionCount >= 10) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2021-07-07 10:40:27 +00:00
|
|
|
$recursionCount++;
|
2021-03-23 16:41:31 +00:00
|
|
|
$targetIps = [];
|
|
|
|
|
2021-07-05 08:52:18 +00:00
|
|
|
$soaDnsEntry = $this->soaRecord($target);
|
|
|
|
$dnsNegativeTtl = $soaDnsEntry['minimum-ttl'] ?? null;
|
2021-03-23 16:41:31 +00:00
|
|
|
|
|
|
|
$dnsTypes = [DNS_A, DNS_AAAA, DNS_CNAME];
|
2021-07-05 08:52:18 +00:00
|
|
|
foreach ($dnsTypes as $dnsType) {
|
2021-03-23 16:41:31 +00:00
|
|
|
if ($this->negativeDnsCache->isNegativeCached($target, $dnsType)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-09-12 09:17:37 +00:00
|
|
|
$dnsResponses = $this->dnsGetRecord($target, $dnsType);
|
2021-03-23 16:41:31 +00:00
|
|
|
$canHaveCnameRecord = true;
|
2021-07-12 13:06:30 +00:00
|
|
|
if ($dnsResponses !== false && count($dnsResponses) > 0) {
|
2021-07-05 08:52:18 +00:00
|
|
|
foreach ($dnsResponses as $dnsResponse) {
|
2021-03-23 16:41:31 +00:00
|
|
|
if (isset($dnsResponse['ip'])) {
|
|
|
|
$targetIps[] = $dnsResponse['ip'];
|
|
|
|
$canHaveCnameRecord = false;
|
|
|
|
} elseif (isset($dnsResponse['ipv6'])) {
|
|
|
|
$targetIps[] = $dnsResponse['ipv6'];
|
|
|
|
$canHaveCnameRecord = false;
|
|
|
|
} elseif (isset($dnsResponse['target']) && $canHaveCnameRecord) {
|
|
|
|
$targetIps = array_merge($targetIps, $this->dnsResolve($dnsResponse['target'], $recursionCount));
|
|
|
|
$canHaveCnameRecord = true;
|
|
|
|
}
|
|
|
|
}
|
2021-07-05 08:52:18 +00:00
|
|
|
} elseif ($dnsNegativeTtl !== null) {
|
|
|
|
$this->negativeDnsCache->setNegativeCacheForDnsType($target, $dnsType, $dnsNegativeTtl);
|
2021-03-23 16:41:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $targetIps;
|
|
|
|
}
|
|
|
|
|
2023-09-12 09:17:37 +00:00
|
|
|
/**
|
|
|
|
* Wrapper for dns_get_record
|
|
|
|
*/
|
|
|
|
protected function dnsGetRecord(string $hostname, int $type): array|false {
|
|
|
|
return \dns_get_record($hostname, $type);
|
|
|
|
}
|
|
|
|
|
2021-03-23 16:41:31 +00:00
|
|
|
public function addDnsPinning() {
|
|
|
|
return function (callable $handler) {
|
|
|
|
return function (
|
|
|
|
RequestInterface $request,
|
|
|
|
array $options
|
|
|
|
) use ($handler) {
|
|
|
|
if ($options['nextcloud']['allow_local_address'] === true) {
|
|
|
|
return $handler($request, $options);
|
|
|
|
}
|
|
|
|
|
|
|
|
$hostName = (string)$request->getUri()->getHost();
|
|
|
|
$port = $request->getUri()->getPort();
|
|
|
|
|
|
|
|
$ports = [
|
|
|
|
'80',
|
|
|
|
'443',
|
|
|
|
];
|
|
|
|
|
2021-04-06 11:39:24 +00:00
|
|
|
if ($port !== null) {
|
2021-03-23 16:41:31 +00:00
|
|
|
$ports[] = (string)$port;
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:20:35 +00:00
|
|
|
$targetIps = $this->dnsResolve(idn_to_utf8($hostName), 0);
|
2021-03-23 16:41:31 +00:00
|
|
|
|
2023-09-04 13:18:37 +00:00
|
|
|
if (empty($targetIps)) {
|
|
|
|
throw new LocalServerException('No DNS record found for ' . $hostName);
|
|
|
|
}
|
|
|
|
|
Fix DnsPinMiddleware resolve pinning bug
Libcurl expects the value of the CURLOPT_RESOLVE configurations to be an
array of strings, those strings containing a comma delimited list of
resolved IPs for each host:port combination.
The original code here does create that array with the host:port:ip
combination, but multiple ips for a single host:port result in
additional array entries, rather than adding them to the end of the
string with a comma. Per the libcurl docs, the `CURLOPT_RESOLVE` array
entries should match the syntax `host:port:address[,address]`.
This creates a function-scoped associative array which uses `host:port`
as the key (which are supposed to be unique and this ensures that), and
the value is an array containing IP strings (ipv4 or ipv6). Once the
associative array is populated, it is then set to the CURLOPT_RESOLVE
array, imploding the ip arrays using a comma delimiter so the array
syntax matches the expected by libcurl.
Note that this reorders the "foreach ip" and "foreach port" loops.
Rather than looping over ips then ports, we now loop over ports then
ips, since ports are part of the unique host:port map, and multiple ips
can exist therein.
Signed-off-by: Aaron Ball <nullspoon@oper.io>
2021-07-02 02:37:33 +00:00
|
|
|
$curlResolves = [];
|
2021-03-23 16:41:31 +00:00
|
|
|
|
Fix DnsPinMiddleware resolve pinning bug
Libcurl expects the value of the CURLOPT_RESOLVE configurations to be an
array of strings, those strings containing a comma delimited list of
resolved IPs for each host:port combination.
The original code here does create that array with the host:port:ip
combination, but multiple ips for a single host:port result in
additional array entries, rather than adding them to the end of the
string with a comma. Per the libcurl docs, the `CURLOPT_RESOLVE` array
entries should match the syntax `host:port:address[,address]`.
This creates a function-scoped associative array which uses `host:port`
as the key (which are supposed to be unique and this ensures that), and
the value is an array containing IP strings (ipv4 or ipv6). Once the
associative array is populated, it is then set to the CURLOPT_RESOLVE
array, imploding the ip arrays using a comma delimiter so the array
syntax matches the expected by libcurl.
Note that this reorders the "foreach ip" and "foreach port" loops.
Rather than looping over ips then ports, we now loop over ports then
ips, since ports are part of the unique host:port map, and multiple ips
can exist therein.
Signed-off-by: Aaron Ball <nullspoon@oper.io>
2021-07-02 02:37:33 +00:00
|
|
|
foreach ($ports as $port) {
|
|
|
|
$curlResolves["$hostName:$port"] = [];
|
|
|
|
|
|
|
|
foreach ($targetIps as $ip) {
|
2022-11-08 13:18:05 +00:00
|
|
|
if ($this->ipAddressClassifier->isLocalAddress($ip)) {
|
2022-10-27 12:33:31 +00:00
|
|
|
// TODO: continue with all non-local IPs?
|
2024-02-19 10:19:58 +00:00
|
|
|
throw new LocalServerException('Host "'.$ip.'" violates local access rules');
|
2022-10-27 12:33:31 +00:00
|
|
|
}
|
Fix DnsPinMiddleware resolve pinning bug
Libcurl expects the value of the CURLOPT_RESOLVE configurations to be an
array of strings, those strings containing a comma delimited list of
resolved IPs for each host:port combination.
The original code here does create that array with the host:port:ip
combination, but multiple ips for a single host:port result in
additional array entries, rather than adding them to the end of the
string with a comma. Per the libcurl docs, the `CURLOPT_RESOLVE` array
entries should match the syntax `host:port:address[,address]`.
This creates a function-scoped associative array which uses `host:port`
as the key (which are supposed to be unique and this ensures that), and
the value is an array containing IP strings (ipv4 or ipv6). Once the
associative array is populated, it is then set to the CURLOPT_RESOLVE
array, imploding the ip arrays using a comma delimiter so the array
syntax matches the expected by libcurl.
Note that this reorders the "foreach ip" and "foreach port" loops.
Rather than looping over ips then ports, we now loop over ports then
ips, since ports are part of the unique host:port map, and multiple ips
can exist therein.
Signed-off-by: Aaron Ball <nullspoon@oper.io>
2021-07-02 02:37:33 +00:00
|
|
|
$curlResolves["$hostName:$port"][] = $ip;
|
2021-03-23 16:41:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Fix DnsPinMiddleware resolve pinning bug
Libcurl expects the value of the CURLOPT_RESOLVE configurations to be an
array of strings, those strings containing a comma delimited list of
resolved IPs for each host:port combination.
The original code here does create that array with the host:port:ip
combination, but multiple ips for a single host:port result in
additional array entries, rather than adding them to the end of the
string with a comma. Per the libcurl docs, the `CURLOPT_RESOLVE` array
entries should match the syntax `host:port:address[,address]`.
This creates a function-scoped associative array which uses `host:port`
as the key (which are supposed to be unique and this ensures that), and
the value is an array containing IP strings (ipv4 or ipv6). Once the
associative array is populated, it is then set to the CURLOPT_RESOLVE
array, imploding the ip arrays using a comma delimiter so the array
syntax matches the expected by libcurl.
Note that this reorders the "foreach ip" and "foreach port" loops.
Rather than looping over ips then ports, we now loop over ports then
ips, since ports are part of the unique host:port map, and multiple ips
can exist therein.
Signed-off-by: Aaron Ball <nullspoon@oper.io>
2021-07-02 02:37:33 +00:00
|
|
|
// Coalesce the per-host:port ips back into a comma separated list
|
|
|
|
foreach ($curlResolves as $hostport => $ips) {
|
|
|
|
$options['curl'][CURLOPT_RESOLVE][] = "$hostport:" . implode(',', $ips);
|
|
|
|
}
|
|
|
|
|
2021-03-23 16:41:31 +00:00
|
|
|
return $handler($request, $options);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|