소스 검색

feat(dyndns): allow preserving IP address, fixes #445

Peter Thomassen 2 년 전
부모
커밋
ddf39b1303
4개의 변경된 파일57개의 추가작업 그리고 36개의 파일을 삭제
  1. 14 10
      api/desecapi/tests/base.py
  2. 16 0
      api/desecapi/tests/test_dyndns12update.py
  3. 10 14
      api/desecapi/views/dyndns.py
  4. 17 12
      docs/dyndns/update-api.rst

+ 14 - 10
api/desecapi/tests/base.py

@@ -1311,19 +1311,23 @@ class DynDomainOwnerTestCase(DomainOwnerTestCase):
             else:
             else:
                 return self.client.get(self.reverse("v1:dyndns12update"), kwargs)
                 return self.client.get(self.reverse("v1:dyndns12update"), kwargs)
 
 
-    def assertDynDNS12Update(self, domain_name=None, mock_remote_addr="", **kwargs):
-        pdns_name = self._normalize_name(domain_name).lower() if domain_name else None
-        return self._assertDynDNS12Update(
-            [
+    def assertDynDNS12Update(
+        self, domain_name=None, mock_remote_addr="", expect_update=True, **kwargs
+    ):
+        if expect_update:
+            pdns_name = (
+                self._normalize_name(domain_name).lower() if domain_name else None
+            )
+            requests = [
                 self.request_pdns_zone_update(name=pdns_name),
                 self.request_pdns_zone_update(name=pdns_name),
                 self.request_pdns_zone_axfr(name=pdns_name),
                 self.request_pdns_zone_axfr(name=pdns_name),
-            ],
-            mock_remote_addr,
-            **kwargs,
-        )
+            ]
+        else:
+            requests = []
+        return self._assertDynDNS12Update(requests, mock_remote_addr, **kwargs)
 
 
-    def assertDynDNS12NoUpdate(self, mock_remote_addr="", **kwargs):
-        return self._assertDynDNS12Update([], mock_remote_addr, **kwargs)
+    def assertDynDNS12NoUpdate(self, *args, **kwargs):
+        return self.assertDynDNS12Update(expect_update=False, *args, **kwargs)
 
 
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()

+ 16 - 0
api/desecapi/tests/test_dyndns12update.py

@@ -204,6 +204,22 @@ class DynDNS12UpdateTest(DynDomainOwnerTestCase):
             self.assertEqual(response.data, "good")
             self.assertEqual(response.data, "good")
             self.assertIP(ipv4=v4, ipv6=v6)
             self.assertIP(ipv4=v4, ipv6=v6)
 
 
+    def test_preserve_ip(self):
+        current_v4 = "127.0.0.1"
+        current_v6 = "::1"
+        self.assertDynDNS12Update(self.my_domain.name, ip=current_v4, ipv6=current_v6)
+        for (v4, v6) in [
+            ("preserve", "::3"),
+            ("1.2.3.4", "preserve"),
+            ("preserve", "preserve"),
+        ]:
+            self.assertDynDNS12Update(
+                self.my_domain.name, ip=v4, ipv6=v6, expect_update=v4 != v6
+            )
+            current_v4 = current_v4 if v4 == "preserve" else v4
+            current_v6 = current_v6 if v6 == "preserve" else v6
+            self.assertIP(ipv4=current_v4, ipv6=current_v6)
+
 
 
 class SingleDomainDynDNS12UpdateTest(DynDNS12UpdateTest):
 class SingleDomainDynDNS12UpdateTest(DynDNS12UpdateTest):
     NUM_OWNED_DOMAINS = 1
     NUM_OWNED_DOMAINS = 1

+ 10 - 14
api/desecapi/views/dyndns.py

@@ -43,9 +43,7 @@ class DynDNS12UpdateView(generics.GenericAPIView):
                 param = self.request.query_params[p]
                 param = self.request.query_params[p]
             except KeyError:
             except KeyError:
                 continue
                 continue
-            if not len(param):
-                return None
-            if separator in param:
+            if separator in param or param in ("", "preserve"):
                 return param
                 return param
 
 
         # Check remote IP address
         # Check remote IP address
@@ -136,22 +134,20 @@ class DynDNS12UpdateView(generics.GenericAPIView):
     def get(self, request, *args, **kwargs):
     def get(self, request, *args, **kwargs):
         instances = self.get_queryset().all()
         instances = self.get_queryset().all()
 
 
-        ipv4 = self._find_ip(["myip", "myipv4", "ip"], separator=".")
-        ipv6 = self._find_ip(["myipv6", "ipv6", "myip", "ip"], separator=":")
+        record_params = {
+            "A": self._find_ip(["myip", "myipv4", "ip"], separator="."),
+            "AAAA": self._find_ip(["myipv6", "ipv6", "myip", "ip"], separator=":"),
+        }
 
 
         data = [
         data = [
             {
             {
-                "type": "A",
-                "subname": self.subname,
-                "ttl": 60,
-                "records": [ipv4] if ipv4 else [],
-            },
-            {
-                "type": "AAAA",
+                "type": type_,
                 "subname": self.subname,
                 "subname": self.subname,
                 "ttl": 60,
                 "ttl": 60,
-                "records": [ipv6] if ipv6 else [],
-            },
+                "records": [ip_param] if ip_param else [],
+            }
+            for type_, ip_param in record_params.items()
+            if ip_param != "preserve"
         ]
         ]
 
 
         serializer = self.get_serializer(instances, data=data, many=True, partial=True)
         serializer = self.get_serializer(instances, data=data, many=True, partial=True)

+ 17 - 12
docs/dyndns/update-api.rst

@@ -105,23 +105,28 @@ To update more than one domain name, please see
 
 
 .. _determine-ip-addresses:
 .. _determine-ip-addresses:
 
 
-Determine IP addresses
+Determine IP Addresses
 **********************
 **********************
 The last ingredient we need for a successful update of your DNS records is your
 The last ingredient we need for a successful update of your DNS records is your
 IPv4 and/or IPv6 addresses, for storage in the ``A`` and ``AAAA`` records,
 IPv4 and/or IPv6 addresses, for storage in the ``A`` and ``AAAA`` records,
 respectively.
 respectively.
 
 
-For IPv4, we will use the first IPv4 address it can find in the query string
-parameters ``myip``, ``myipv4``, ``ip`` (in this order). If none of them is
-set, it will use the IP that connected to the API, if a IPv4 connection was
-made. If no address is found or if an empty value was provided instead of an IP
-address, the ``A`` record will be deleted from the DNS.
-
-For IPv6, the procedure is similar. We check ``myipv6``, ``ipv6``, ``myip``,
-``ip`` query string parameters (in this order) and the IP that was used to
-connect to the API for IPv6 addresses and use the first one found. If no
-address is found or an empty value provided instead, the ``AAAA`` record will
-be deleted.
+For IPv4, we check the query string parameters ``myip``, ``myipv4``, ``ip``
+(in this order) for an IPv4 address to record in the database.
+When the special string ``preserve`` is provided instead of an IP address, the
+address on record (if any) will be kept as is.
+If none of the parameters is set, the connection's client IP address will be
+used if it is an IPv4 connection; otherwise the IPv4 address will be deleted
+from the DNS.
+IP deletion can also be forced by providing an empty value (e.g. ``myipv4=``).
+
+For IPv6, the procedure is similar.
+We check the ``myipv6``, ``ipv6``, ``myip``, ``ip`` query string parameters
+(in this order) and the IP that was used to connect to the API for IPv6
+addresses and use the first one found.
+The ``preserve`` rule applies as above.
+If nothing is found or an empty value provided, the ``AAAA`` record will be
+deleted.
 
 
 
 
 Update Response
 Update Response