diff --git a/.env b/.env index bb21aa50f2..b9e5bc9732 100644 --- a/.env +++ b/.env @@ -21,13 +21,13 @@ _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key -_APP_DNS=8.8.8.8,1.1.1.1 +_APP_DNS=172.16.238.100 # CoreDNS _APP_DOMAIN=traefik _APP_CONSOLE_DOMAIN=localhost _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost -_APP_DOMAIN_TARGET_CNAME=test.l -_APP_DOMAIN_TARGET_A=127.0.0.1 +_APP_DOMAIN_TARGET_CNAME=cname.localhost +_APP_DOMAIN_TARGET_A=203.0.0.1 _APP_DOMAIN_TARGET_AAAA=::1 _APP_DOMAIN_TARGET_CAA=digicert.com _APP_RULES_FORMAT=md5 diff --git a/docker-compose.yml b/docker-compose.yml index 5e65122256..804435ab36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,9 +89,12 @@ services: - ./public:/usr/src/code/public - ./src:/usr/src/code/src - ./dev:/usr/src/code/dev + dns: + - 172.16.238.100 depends_on: - mariadb - redis + - coredns # - clamav entrypoint: - php @@ -524,9 +527,12 @@ services: image: appwrite-dev networks: - appwrite + dns: + - 172.16.238.100 depends_on: - redis - mariadb + - coredns volumes: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw @@ -1068,6 +1074,20 @@ services: - appwrite volumes: - appwrite-redis:/data:rw + + coredns: # DNS server for testing purposes (Proxy APIs) + image: coredns/coredns:1.12.4 + container_name: appwrite-coredns + restart: unless-stopped + <<: *x-logging + command: ["-conf", "/mnt/resources/Corefile"] + ports: + - "1053:1053" + networks: + appwrite: + ipv4_address: 172.16.238.100 + volumes: + - ./tests/resources/coredns:/mnt/resources:ro # Dev Tools Start ------------------------------------------------------------------------------------------ # @@ -1146,6 +1166,9 @@ networks: name: gateway appwrite: name: appwrite + ipam: + config: + - subnet: 172.16.238.0/24 runtimes: name: runtimes diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php index 70d27328a7..97b668a943 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php @@ -68,8 +68,17 @@ class Get extends Action $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); - if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { - $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + + $certificateHasUpdatedAt = $certificate->getUpdatedAt() !== null; + $ruleHasUpdatedAt = $rule->getUpdatedAt() !== null; + if ($certificateHasUpdatedAt) { + if ($ruleHasUpdatedAt) { + if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } + } else { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } } $response->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php index 1024f870a7..55138b49d7 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php @@ -110,8 +110,17 @@ class Update extends Action $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); - if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { - $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + + $certificateHasUpdatedAt = $certificate->getUpdatedAt() !== null; + $ruleHasUpdatedAt = $rule->getUpdatedAt() !== null; + if ($certificateHasUpdatedAt) { + if ($ruleHasUpdatedAt) { + if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } + } else { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } } $response->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index bef7b31881..065c5a0b95 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -110,8 +110,17 @@ class XList extends Action $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); - if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { - $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + + $certificateHasUpdatedAt = $certificate->getUpdatedAt() !== null; + $ruleHasUpdatedAt = $rule->getUpdatedAt() !== null; + if ($certificateHasUpdatedAt) { + if ($ruleHasUpdatedAt) { + if (new \DateTime($certificate->getUpdatedAt()) > new \DateTime($rule->getUpdatedAt())) { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } + } else { + $rule->setAttribute('$updatedAt', $certificate->getUpdatedAt()); + } } } diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php index 5a1cd1dea6..50d978902f 100644 --- a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -15,6 +15,36 @@ class ProxyCustomServerTest extends Scope use ProjectCustom; use SideServer; + protected function tearDown(): void + { + // Cleanup for testRuleVerification test + // Required as it uses static domain name + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1000)->toString(), + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $ruleId = $rule['$id']; + $response = $this->deleteRule($ruleId); + $this->assertEquals(204, $response['headers']['status-code']); + } + + if ($rules['body']['total'] > 0) { + $rules = $this->listRules([ + 'queries' => [ + Query::endsWith('domain', 'webapp.com')->toString(), + Query::limit(1) + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, count($rules['body']['rules'])); + $this->assertEquals(0, $rules['body']['total']); + } + } + public function testCreateRule(): void { $domain = \uniqid() . '-api.myapp.com'; @@ -540,4 +570,130 @@ class ProxyCustomServerTest extends Scope $this->assertEquals(0, $rules['body']['total']); $this->assertCount(0, $rules['body']['rules']); } + + public function testRuleVerification(): void + { + + // 1. Site rule can verify + $site = $this->setupSite(); + $siteId = $site['siteId']; + + $rule = $this->createSiteRule('stage-site.webapp.com', $siteId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['verificationLogs']); + + $this->cleanupRule($rule['body']['$id']); + $this->cleanupSite($siteId); + + // 2. Function rule can verify + $function = $this->setupFunction(); + $functionId = $function['functionId']; + + $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['verificationLogs']); + $this->cleanupRule($rule['body']['$id']); + + $rule = $this->createAPIRule('stage-site.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['verificationLogs']); + $this->cleanupRule($rule['body']['$id']); + + $this->cleanupFunction($functionId); + + // 3. Wrong A record fails to verify + $rule = $this->createAPIRule('wrong-a-webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['verificationLogs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 4. Correct A record can verify + $rule = $this->createAPIRule('webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['verificationLogs']); + + $this->cleanupRule($rule['body']['$id']); + + // 5. Correct CNAME record can verify (no CAA record) + $rule = $this->createAPIRule('stage.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['verificationLogs']); + + $this->cleanupRule($rule['body']['$id']); + + // 6. Missing CNAME record fails to verify + $rule = $this->createAPIRule('stage-missing-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['verificationLogs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 7. Wrong CNAME record fails to verify + $rule = $this->createAPIRule('stage-wrong-cname.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['verificationLogs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 8. Wrong CAA record fails to verify + $rule = $this->createAPIRule('stage-wrong-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['verificationLogs']); + + $ruleId = $rule['body']['$id']; + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(400, $rule['headers']['status-code']); + $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $this->cleanupRule($ruleId); + + // 9. Correct CAA record can verify + $rule = $this->createAPIRule('stage-correct-caa.webapp.com'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verifying', $rule['body']['status']); + $this->assertEmpty($rule['body']['verificationLogs']); + + $this->cleanupRule($rule['body']['$id']); + } } diff --git a/tests/resources/coredns/Corefile b/tests/resources/coredns/Corefile new file mode 100644 index 0000000000..591291aed3 --- /dev/null +++ b/tests/resources/coredns/Corefile @@ -0,0 +1,15 @@ +# Re-use public resolver to answer unknown queries +. { + forward . 1.1.1.1 +} + +# Zones configuration +webapp.com { + file /mnt/resources/webapp.com.zone +} + +# Zones configuration +wrong-a-webapp.com { + file /mnt/resources/wrong-a-webapp.com.zone +} + diff --git a/tests/resources/coredns/webapp.com.zone b/tests/resources/coredns/webapp.com.zone new file mode 100644 index 0000000000..55026ca94b --- /dev/null +++ b/tests/resources/coredns/webapp.com.zone @@ -0,0 +1,23 @@ +; Nessessary setup +$ORIGIN webapp.com. +@ IN SOA ns1.webapp.com. hostmaster.webapp.com. (2025091201 7200 3600 1209600 3600) +@ IN NS ns1.webapp.com. +ns1 IN A 127.0.0.1 + +; Custom DNS records +@ IN A 203.0.0.1 + +stage-site IN CNAME sites.localhost. + +stage-function IN CNAME functions.localhost. + +stage IN CNAME cname.localhost. +; No CAA record intentionally + +stage-wrong-cname IN CNAME cname-wrong.tests.appwrite.io. + +stage-wrong-caa IN A 203.0.0.1 +stage-wrong-caa IN CAA 0 issue "unknown-issuer.org" + +stage-correct-caa IN CNAME cname.localhost. +stage-correct-caa IN CAA 0 issue "digicert.com" diff --git a/tests/resources/coredns/wrong-a-webapp.com.zone b/tests/resources/coredns/wrong-a-webapp.com.zone new file mode 100644 index 0000000000..d4997419b4 --- /dev/null +++ b/tests/resources/coredns/wrong-a-webapp.com.zone @@ -0,0 +1,8 @@ +; Nessessary setup +$ORIGIN wrong-a-webapp.com.zone. +@ IN SOA ns1.wrong-a-webapp.com.zone. hostmaster.wrong-a-webapp.com.zone. (2025091201 7200 3600 1209600 3600) +@ IN NS ns1.wrong-a-webapp.com.zone. +ns1 IN A 127.0.0.1 + +; Custom DNS records +@ IN A 203.0.0.5