From 1e9c587b927ff2e5bba2d9444a7cbcaebe9f74e2 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 3 May 2015 13:40:52 +0000 Subject: [PATCH] rewrite the DNS API to permit setting multiple records of the same type on the same domain e.g. multiple TXT records fixes #333 --- CHANGELOG.md | 7 ++- management/daemon.py | 74 ++++++++++++++++++++-------- management/templates/custom-dns.html | 67 +++++++++++++++---------- management/templates/index.html | 5 ++ 4 files changed, 105 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 221dfc9a..c524c2d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,15 @@ ownCloud: * Downloading files you uploaded to ownCloud broke because of a change in ownCloud 8. +DNS: + +* Internationalized Domain Names (IDNs) should now work in email. If you had custom DNS or custom web settings for internationalized domains, check that they are still working. +* It is now possible to set multiple TXT and other types of records on the same domain in the control panel. +* The custom DNS API was completely rewritten to support setting multiple records of the same type on a domain. Any existing client code using the DNS API will have to be rewritten. (Existing code will just get 404s back.) + System: * Backups now use duplicity's built-in gpg symmetric AES256 encryption rather than my home-brewed encryption. Old backups will be incorporated inside the first backup after this update but then deleted from disk (i.e. your backups from the previous few days will be backed up). -* Internationalized Domain Names (IDNs) should now work in email. If you had custom DNS or custom web settings for internationalized domains, check that they are still working. * All Mail-in-a-Box release tags are now signed on github, instructions for verifying the signature are added to the README, and the integrity of all non-Ubuntu packages downloaded during setup is now verified against a SHA1 hash stored in the tag itself. v0.08 (April 1, 2015) diff --git a/management/daemon.py b/management/daemon.py index 8bd7247a..71159672 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -233,36 +233,70 @@ def dns_set_secondary_nameserver(): except ValueError as e: return (str(e), 400) -@app.route('/dns/set') +@app.route('/dns/custom') @authorized_personnel_only -def dns_get_records(): +def dns_get_records(qname=None, rtype=None): from dns_update import get_custom_dns_config - return json_response([{ + return json_response([ + { "qname": r[0], "rtype": r[1], "value": r[2], - } for r in get_custom_dns_config(env) if r[0] != "_secondary_nameserver"]) + } + for r in get_custom_dns_config(env) + if r[0] != "_secondary_nameserver" + and (not qname or r[0] == qname) + and (not rtype or r[1] == rtype) ]) -@app.route('/dns/set/', methods=['POST']) -@app.route('/dns/set//', methods=['POST']) -@app.route('/dns/set///', methods=['POST']) +@app.route('/dns/custom/', methods=['GET', 'POST', 'PUT', 'DELETE']) +@app.route('/dns/custom//', methods=['GET', 'POST', 'PUT', 'DELETE']) @authorized_personnel_only -def dns_set_record(qname, rtype="A", value=None): +def dns_set_record(qname, rtype="A"): from dns_update import do_dns_update, set_custom_dns_record try: - # Get the value from the URL, then the POST parameters, or if it is not set then - # use the remote IP address of the request --- makes dynamic DNS easy. To clear a - # value, '' must be explicitly passed. - if value is None: - value = request.form.get("value") - if value is None: - value = request.environ.get("HTTP_X_FORWARDED_FOR") # normally REMOTE_ADDR but we're behind nginx as a reverse proxy - if value == '' or value == '__delete__': - # request deletion - value = None - if set_custom_dns_record(qname, rtype, value, "set", env): - return do_dns_update(env) or "No Change" + # Normalize. + rtype = rtype.upper() + + # Read the record value from the request BODY, which must be + # ASCII-only. Not used with GET. + value = request.stream.read().decode("ascii", "ignore").strip() + + if request.method == "GET": + # Get the existing records matching the qname and rtype. + return dns_get_records(qname, rtype) + + elif request.method in ("POST", "PUT"): + # There is a default value for A/AAAA records. + if rtype in ("A", "AAAA") and value == "": + value = request.environ.get("HTTP_X_FORWARDED_FOR") # normally REMOTE_ADDR but we're behind nginx as a reverse proxy + + # Cannot add empty records. + if value == '': + return ("No value for the record provided.", 400) + + if request.method == "POST": + # Add a new record (in addition to any existing records + # for this qname-rtype pair). + action = "add" + elif request.method == "PUT": + # In REST, PUT is supposed to be idempotent, so we'll + # make this action set (replace all records for this + # qname-rtype pair) rather than add (add a new record). + action = "set" + + elif request.method == "DELETE": + if value == '': + # Delete all records for this qname-type pair. + value = None + else: + # Delete just the qname-rtype-value record exactly. + pass + action = "remove" + + if set_custom_dns_record(qname, rtype, value, action, env): + return do_dns_update(env) or "Something isn't right." return "OK" + except ValueError as e: return (str(e), 400) diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index 65295faa..711bc384 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -93,44 +93,56 @@

Use your box’s DNS API to set custom DNS records on domains hosted here. For instance, you can create your own dynamic DNS service.

-

Send a POST request like this:

+

Usage:

-
curl -d "" --user {email}:{password} https://{{hostname}}/admin/dns/set/qname[/rtype[/value]]
+
curl -X VERB [-d "value"] --user {email}:{password} https://{{hostname}}/admin/dns/custom[/qname[/rtype]]
-

HTTP POST parameters

+

(Brackets denote an optional argument.)

+ +

Verbs

+ + + + + + + +
Verb Usage
GET Returns matching custom DNS records as a JSON array of objects. Each object has the keys qname, rtype, and value. The optional qname and rtype parameters in the request URL filter the records returned in the response. The request body (-d "...") must be omitted.
PUT Sets a custom DNS record replacing any existing records with the same qname and rtype. Use PUT (instead of POST) when you only have one value for a qname and rtype, such as typical A records (without round-robin).
POST Adds a new custom DNS record. Use POST when you have multiple TXT records or round-robin A records. (PUT would delete previously added records.)
DELETE Deletes custom DNS records. If the request body (-d "...") is empty or omitted, deletes all records matching the qname and rtype. If the request body is present, deletes only the record matching the qname, rtype and value.
+ +

Parameters

- - - + + +
Parameter Value
email The email address of any administrative user here.
password That user’s password.
qname The fully qualified domain name for the record you are trying to set.
rtype The resource type. A if omitted. Possible values: A (an IPv4 address), AAAA (an IPv6 address), TXT (a text string), or CNAME (an alias, which is a fully qualified domain name).
value The new record’s value. If omitted, the IPv4 address of the remote host is used. This is handy for dynamic DNS! To delete a record, use “__delete__”.
qname The fully qualified domain name for the record you are trying to set. It must be one of the domain names or a subdomain of one of the domain names hosted on this box. (Add mail users or aliases to add new domains.)
rtype The resource type. Defaults to A if omitted. Possible values: A (an IPv4 address), AAAA (an IPv6 address), TXT (a text string), CNAME (an alias, which is a fully qualified domain name — don’t forget the final period), MX, or SRV.
value For PUT, POST, and DELETE, the record’s value. If the rtype is A or AAAA and value is empty or omitted, the IPv4 or IPv6 address of the remote host is used (be sure to use the -4 or -6 options to curl). This is handy for dynamic DNS!
-

Note that -d "" is merely to ensure curl sends a POST request. You do not need to put anything inside the quotes. You can also pass the value using typical form encoding in the POST body.

-

Strict SPF and DMARC records will be added to all custom domains unless you override them.

Examples:

+

Try these examples. For simplicity the examples omit the --user me@mydomain.com:yourpassword command line argument which you must fill in with your email address and password.

+
# sets laptop.mydomain.com to point to the IP address of the machine you are executing curl on
-curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/laptop.mydomain.com
+curl -X PUT https://{{hostname}}/admin/dns/custom/laptop.mydomain.com
 
-# sets an alias
-curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/cname/bar.mydomain.com
+# deletes that record and all A records for that domain name
+curl -X DELETE https://{{hostname}}/admin/dns/custom/laptop.mydomain.com
 
-# clears the alias
-curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/bar.mydomain.com/cname/__delete__
+# sets a CNAME alias
+curl -X PUT -d "bar.mydomain.com." https://{{hostname}}/admin/dns/custom/foo.mydomain.com/cname
 
-# sets a TXT record using the alternate value syntax
-curl -d "value=something%20here" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/txt
+# deletes that CNAME and all CNAME records for that domain name
+curl -X DELETE https://{{hostname}}/admin/dns/custom/foo.mydomain.com/cname
 
-# sets a SRV record for the "service" and "protocol" hosted on "target" server
-curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/_service._protocol.{{hostname}}/srv/"priority weight port target"
+# adds a TXT record using POST to preserve any previous TXT records
+curl -X POST -d "some text here" https://{{hostname}}/admin/dns/custom/foo.mydomain.com/txt
 
-# sets a SRV record using the value syntax
-curl -d "value=priority weight port target" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/_service._protocol.host/srv
+# deletes that one TXT record while preserving other TXT records
+curl -X DELETE -d "some text here" https://{{hostname}}/admin/dns/custom/foo.mydomain.com/txt