Bladeren bron

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

Peter Thomassen 2 jaren geleden
bovenliggende
commit
b55e4849bd
3 gewijzigde bestanden met toevoegingen van 56 en 17 verwijderingen
  1. 25 1
      api/desecapi/tests/test_dyndns12update.py
  2. 21 10
      api/desecapi/views/dyndns.py
  3. 10 6
      docs/dyndns/update-api.rst

+ 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_)
             url = self.reverse("v1:rrset", name=name, subname=subname, type=type_)
             response = self.client_token_authorized.get(url)
             response = self.client_token_authorized.get(url)
             if value:
             if value:
+                if not isinstance(value, set):
+                    value = {value}
                 self.assertStatus(response, status.HTTP_200_OK)
                 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)
                 self.assertEqual(response.data["ttl"], 60)
             else:
             else:
                 self.assertStatus(response, status.HTTP_404_NOT_FOUND)
                 self.assertStatus(response, status.HTTP_404_NOT_FOUND)
@@ -185,6 +187,28 @@ class DynDNS12UpdateTest(DynDomainOwnerTestCase):
         self.assertEqual(response.data, "good")
         self.assertEqual(response.data, "good")
         self.assertIP(ipv4="127.0.0.1", ipv6="::666")
         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):
     def test_fritz_box(self):
         # /
         # /
         response = self.assertDynDNS12Update(self.my_domain.name)
         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):
     def throttle_scope_bucket(self):
         return self.domain.name
         return self.domain.name
 
 
-    def _find_ip(self, params, separator):
+    def _find_ip(self, param_keys, separator):
         # Check URL parameters
         # Check URL parameters
-        for p in params:
+        for param_key in param_keys:
             try:
             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:
             except KeyError:
                 continue
                 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
         # Check remote IP address
         client_ip = self.request.META.get("REMOTE_ADDR")
         client_ip = self.request.META.get("REMOTE_ADDR")
         if separator in client_ip:
         if separator in client_ip:
-            return client_ip
+            return [client_ip]
 
 
         # give up
         # give up
-        return None
+        return []
 
 
     @cached_property
     @cached_property
     def qname(self):
     def qname(self):
@@ -144,10 +155,10 @@ class DynDNS12UpdateView(generics.GenericAPIView):
                 "type": type_,
                 "type": type_,
                 "subname": self.subname,
                 "subname": self.subname,
                 "ttl": 60,
                 "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)
         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 Addresses
-**********************
+Determine IP Address(es)
+************************
 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 check the query string parameters ``myip``, ``myipv4``, ``ip``
 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
 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
 used if it is an IPv4 connection; otherwise the IPv4 address will be deleted
 from the DNS.
 from the DNS.
@@ -124,10 +125,13 @@ For IPv6, the procedure is similar.
 We check the ``myipv6``, ``ipv6``, ``myip``, ``ip`` query string parameters
 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
 (in this order) and the IP that was used to connect to the API for IPv6
 addresses and use the first one found.
 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
 If nothing is found or an empty value provided, the ``AAAA`` record will be
 deleted.
 deleted.
 
 
+When using the ``myip`` parameter, a mixed-type list of both IPv4 and IPv6
+addresses may be given.
+
 
 
 Update Response
 Update Response
 ```````````````
 ```````````````