Browse Source

feat(dyndns): allow updating with multiple IPs, fixes #575

Peter Thomassen 2 năm trước cách đây
mục cha
commit
b55e4849bd

+ 25 - 1
api/desecapi/tests/test_dyndns12update.py

@@ -12,8 +12,10 @@ class DynDNS12UpdateTest(DynDomainOwnerTestCase):
             url = self.reverse("v1:rrset", name=name, subname=subname, type=type_)
             response = self.client_token_authorized.get(url)
             if value:
+                if not isinstance(value, set):
+                    value = {value}
                 self.assertStatus(response, status.HTTP_200_OK)
-                self.assertEqual(response.data["records"][0], value)
+                self.assertEqual(set(response.data["records"]), value)
                 self.assertEqual(response.data["ttl"], 60)
             else:
                 self.assertStatus(response, status.HTTP_404_NOT_FOUND)
@@ -185,6 +187,28 @@ class DynDNS12UpdateTest(DynDomainOwnerTestCase):
         self.assertEqual(response.data, "good")
         self.assertIP(ipv4="127.0.0.1", ipv6="::666")
 
+    def test_ddclient_dyndns2_mixed_success(self):
+        response = self.assertDynDNS12Update(
+            domain_name=self.my_domain.name,
+            system="dyndns",
+            hostname=self.my_domain.name,
+            myip="10.2.3.4, ::2 , 10.6.5.4 ,::4",
+        )
+        self.assertStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data, "good")
+        self.assertIP(ipv4={"10.2.3.4", "10.6.5.4"}, ipv6={"::2", "::4"})
+
+    def test_ddclient_dyndns2_mixed_invalid(self):
+        for myip in ["10.2.3.4, ", "preserve,::2"]:
+            response = self.assertDynDNS12NoUpdate(
+                domain_name=self.my_domain.name,
+                system="dyndns",
+                hostname=self.my_domain.name,
+                myip=myip,
+            )
+            self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
+            self.assertEqual(response.data["code"], "inconsistent-parameter")
+
     def test_fritz_box(self):
         # /
         response = self.assertDynDNS12Update(self.my_domain.name)

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

@@ -36,23 +36,34 @@ class DynDNS12UpdateView(generics.GenericAPIView):
     def throttle_scope_bucket(self):
         return self.domain.name
 
-    def _find_ip(self, params, separator):
+    def _find_ip(self, param_keys, separator):
         # Check URL parameters
-        for p in params:
+        for param_key in param_keys:
             try:
-                param = self.request.query_params[p]
+                params = {
+                    param.strip()
+                    for param in self.request.query_params[param_key].split(",")
+                    if separator in param or param.strip() in ("", "preserve")
+                }
             except KeyError:
                 continue
-            if separator in param or param in ("", "preserve"):
-                return param
+            if len(params) > 1 and params & {"", "preserve"}:
+                raise ValidationError(
+                    detail={
+                        "detail": f'IP parameter "{param_key}" cannot have addresses and "preserve" at the same time.',
+                        "code": "inconsistent-parameter",
+                    }
+                )
+            if params:
+                return [] if "" in params else list(params)
 
         # Check remote IP address
         client_ip = self.request.META.get("REMOTE_ADDR")
         if separator in client_ip:
-            return client_ip
+            return [client_ip]
 
         # give up
-        return None
+        return []
 
     @cached_property
     def qname(self):
@@ -144,10 +155,10 @@ class DynDNS12UpdateView(generics.GenericAPIView):
                 "type": type_,
                 "subname": self.subname,
                 "ttl": 60,
-                "records": [ip_param] if ip_param else [],
+                "records": ip_params,
             }
-            for type_, ip_param in record_params.items()
-            if ip_param != "preserve"
+            for type_, ip_params in record_params.items()
+            if "preserve" not in ip_params
         ]
 
         serializer = self.get_serializer(instances, data=data, many=True, partial=True)

+ 10 - 6
docs/dyndns/update-api.rst

@@ -105,16 +105,17 @@ To update more than one domain name, please see
 
 .. _determine-ip-addresses:
 
-Determine IP Addresses
-**********************
+Determine IP Address(es)
+************************
 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,
 respectively.
 
 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.
+(in this order) for IPv4 addresses to record in the database.
+Multiple IP addresses may be given as a comma-separated list.
+When the special string ``preserve`` is provided instead, the configuration
+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.
@@ -124,10 +125,13 @@ 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.
+Both the multi-IP syntax and the ``preserve`` rule apply as above.
 If nothing is found or an empty value provided, the ``AAAA`` record will be
 deleted.
 
+When using the ``myip`` parameter, a mixed-type list of both IPv4 and IPv6
+addresses may be given.
+
 
 Update Response
 ```````````````