Skip to content

Commit ae32536

Browse files
authored
Merge pull request #62 from QPhuGit/fix-idn-domain
Handle IDN Cloudflare zones without PHP intl by resolving zone ID from hostname
2 parents f5587ed + 88a4512 commit ae32536

2 files changed

Lines changed: 102 additions & 41 deletions

File tree

cloudflare.php

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ public function getZones()
131131
return $this->call("GET", "client/v4/zones?per_page=" . self::ZONES_PER_PAGE . "&status=active");
132132
}
133133

134+
public function getZoneByName($zoneName)
135+
{
136+
return $this->call(
137+
"GET",
138+
"client/v4/zones?name=" . rawurlencode($zoneName) . "&status=active"
139+
);
140+
}
141+
134142
/**
135143
* @link https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-list-dns-records
136144
* @throws Exception
@@ -341,44 +349,66 @@ function updateDnsRecords()
341349
private function matchHostnameWithZone($hostnameList = [])
342350
{
343351
try {
344-
$zoneList = $this->cloudflareAPI->getZones();
345-
$zoneList = $zoneList['result'];
346-
foreach ($zoneList as $zone) {
347-
$zoneId = $zone['id'];
348-
$zoneName = $zone['name'];
349-
foreach ($hostnameList as $hostname) {
350-
// Check if the hostname ends with the zone name
351-
if ($hostname === $zoneName || substr($hostname, -strlen('.' . $zoneName)) === '.' . $zoneName) {
352-
// Add an IPv4 DNS record for each hostname that matches a zone
353-
$this->dnsRecordList[] = new DnsRecordEntity(
354-
'',
355-
'A',
356-
$hostname,
357-
$this->ipv4,
358-
$zoneId,
359-
'',
360-
''
361-
);
362-
if (isset($this->ipv6)) {
363-
// Add an IPv6 DNS record if an IPv6 address is available
364-
$this->dnsRecordList[] = new DnsRecordEntity(
365-
'',
366-
'AAAA',
367-
$hostname,
368-
$this->ipv6,
369-
$zoneId,
370-
'',
371-
''
372-
);
373-
}
374-
}
352+
foreach ($hostnameList as $hostname) {
353+
$zoneId = $this->findZoneIdByHostname($hostname);
354+
355+
if (!$zoneId) {
356+
continue;
375357
}
358+
359+
$this->dnsRecordList[] = new DnsRecordEntity(
360+
'',
361+
'A',
362+
$hostname,
363+
$this->ipv4,
364+
$zoneId,
365+
'',
366+
''
367+
);
368+
369+
if (isset($this->ipv6)) {
370+
$this->dnsRecordList[] = new DnsRecordEntity(
371+
'',
372+
'AAAA',
373+
$hostname,
374+
$this->ipv6,
375+
$zoneId,
376+
'',
377+
''
378+
);
379+
}
380+
}
381+
382+
if (empty($this->dnsRecordList)) {
383+
$this->exitWithSynologyMsg(SynologyOutput::NO_HOSTNAME);
376384
}
377385
} catch (Exception $e) {
378386
$this->exitWithSynologyMsg(SynologyOutput::NO_HOSTNAME);
379387
}
380388
}
381389

390+
/**
391+
* Summary of findZoneIdByHostname
392+
* @param mixed $hostname
393+
*/
394+
private function findZoneIdByHostname($hostname)
395+
{
396+
$hostname = strtolower(rtrim(trim($hostname), '.'));
397+
$labels = explode('.', $hostname);
398+
$count = count($labels);
399+
400+
for ($i = 0; $i <= $count - 2; $i++) {
401+
$candidateZone = implode('.', array_slice($labels, $i));
402+
$zone = $this->cloudflareAPI->getZoneByName($candidateZone);
403+
404+
if (!empty($zone['result']) && !empty($zone['result'][0]['id'])) {
405+
return $zone['result'][0]['id'];
406+
}
407+
}
408+
409+
return null;
410+
}
411+
382412
/**
383413
* Extracts valid hostnames from a given string of hostnames separated by pipes (|).
384414
*

tests/SynologyCloudflareDDNSAgentTest.php

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ protected function exitWithSynologyMsg($msg = '')
1515
$this->exitMsg = $msg;
1616
throw new ExitException("Exit called with message: $msg");
1717
}
18-
18+
1919
public function callPrivateMethod($methodName, $args = [])
2020
{
2121
$reflection = new ReflectionClass($this);
2222
$method = $reflection->getMethod($methodName);
23+
$method->setAccessible(true);
2324
return $method->invokeArgs($this, $args);
2425
}
2526
}
@@ -30,13 +31,27 @@ public function testIsValidHostname()
3031
{
3132
$mockApi = $this->createMock(CloudflareAPI::class);
3233
$mockIpify = $this->createMock(Ipify::class);
33-
34+
3435
$mockApi->method('verifyToken')->willReturn(['success' => true]);
35-
36-
$mockApi->method('getZones')->willReturn(['result' => []]);
36+
37+
$mockApi->method('getZoneByName')
38+
->willReturnCallback(function ($zoneName) {
39+
if ($zoneName === 'example.com') {
40+
return [
41+
'result' => [
42+
[
43+
'id' => 'test-zone-id',
44+
'name' => 'example.com',
45+
]
46+
]
47+
];
48+
}
49+
50+
return ['result' => []];
51+
});
3752

3853
$agent = new TestableSynologyCloudflareDDNSAgent('apikey', 'example.com', '1.2.3.4', $mockApi, $mockIpify);
39-
54+
4055
$this->assertTrue($agent->callPrivateMethod('isValidHostname', ['example.com']));
4156
$this->assertTrue($agent->callPrivateMethod('isValidHostname', ['sub.example.com']));
4257
$this->assertFalse($agent->callPrivateMethod('isValidHostname', ['-example.com']));
@@ -47,27 +62,43 @@ public function testExtractHostnames()
4762
{
4863
$mockApi = $this->createMock(CloudflareAPI::class);
4964
$mockIpify = $this->createMock(Ipify::class);
65+
5066
$mockApi->method('verifyToken')->willReturn(['success' => true]);
51-
$mockApi->method('getZones')->willReturn(['result' => []]);
67+
68+
$mockApi->method('getZoneByName')
69+
->willReturnCallback(function ($zoneName) {
70+
if ($zoneName === 'example.com') {
71+
return [
72+
'result' => [
73+
[
74+
'id' => 'test-zone-id',
75+
'name' => 'example.com',
76+
]
77+
]
78+
];
79+
}
80+
81+
return ['result' => []];
82+
});
5283

5384
$agent = new TestableSynologyCloudflareDDNSAgent('apikey', 'example.com', '1.2.3.4', $mockApi, $mockIpify);
5485

5586
$input = "example.com|sub.example.com|invalid-";
5687
$expected = ['example.com', 'sub.example.com'];
57-
88+
5889
$this->assertEquals($expected, $agent->callPrivateMethod('extractHostnames', [$input]));
5990
}
60-
91+
6192
public function testConstructorAuthFailure()
6293
{
6394
$mockApi = $this->createMock(CloudflareAPI::class);
6495
$mockIpify = $this->createMock(Ipify::class);
65-
96+
6697
$mockApi->method('verifyToken')->willReturn(['success' => false]);
6798

6899
$this->expectException(ExitException::class);
69100
$this->expectExceptionMessage("Exit called with message: " . SynologyOutput::AUTH_FAILED);
70101

71102
new TestableSynologyCloudflareDDNSAgent('apikey', 'example.com', '1.2.3.4', $mockApi, $mockIpify);
72103
}
73-
}
104+
}

0 commit comments

Comments
 (0)