diff --git a/README.md b/README.md index 3895aca..6e97af1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A PowerDNS web interface with advanced features. - DynDNS 2 protocol support - Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!) - Limited API for manipulating zones and records +- Full IDN/Punycode support ## Running PowerDNS-Admin There are several ways to run PowerDNS-Admin. The easiest way is to use Docker. diff --git a/powerdnsadmin/__init__.py b/powerdnsadmin/__init__.py index a8270b0..98690c2 100755 --- a/powerdnsadmin/__init__.py +++ b/powerdnsadmin/__init__.py @@ -102,6 +102,7 @@ def create_app(config=None): 'email_to_gravatar_url'] = utils.email_to_gravatar_url app.jinja_env.filters[ 'display_setting_state'] = utils.display_setting_state + app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name # Register context proccessors from .models.setting import Setting diff --git a/powerdnsadmin/lib/utils.py b/powerdnsadmin/lib/utils.py index 2322e17..d7f20a4 100644 --- a/powerdnsadmin/lib/utils.py +++ b/powerdnsadmin/lib/utils.py @@ -8,6 +8,7 @@ import ipaddress from collections.abc import Iterable from distutils.version import StrictVersion from urllib.parse import urlparse +from datetime import datetime, timedelta def auth_from_url(url): @@ -228,3 +229,22 @@ class customBoxes: "inaddrarpa": ("in-addr", "%.in-addr.arpa") } order = ["reverse", "ip6arpa", "inaddrarpa"] + +def pretty_domain_name(value): + """ + Display domain name in original format. + If it is IDN domain (Punycode starts with xn--), do the + idna decoding. + Note that any part of the domain name can be individually punycoded + """ + if isinstance(value, str): + if value.startswith('xn--') \ + or value.find('.xn--'): + try: + return value.encode().decode('idna') + except: + raise Exception("Cannot decode IDN domain") + else: + return value + else: + raise Exception("Require the Punycode in string format") diff --git a/powerdnsadmin/models/record.py b/powerdnsadmin/models/record.py index bfd1918..536aa70 100644 --- a/powerdnsadmin/models/record.py +++ b/powerdnsadmin/models/record.py @@ -162,6 +162,15 @@ class Record(object): for record in submitted_records: # Format the record name # + # Translate record name into punycode (IDN) as that's the only way + # to convey non-ascii records to the dns server + record['record_name'] = record['record_name'].encode('idna').decode() + #TODO: error handling + # If the record is an alias (CNAME), we will also make sure that + # the target domain is properly converted to punycode (IDN) + if record["record_type"] == 'CNAME': + record['record_data'] = record['record_data'].encode('idna').decode() + #TODO: error handling # If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled, # We convert ipv6 address back to reverse record format # before submitting to PDNS API. diff --git a/powerdnsadmin/routes/api.py b/powerdnsadmin/routes/api.py index e33c946..d0df5d9 100644 --- a/powerdnsadmin/routes/api.py +++ b/powerdnsadmin/routes/api.py @@ -272,7 +272,8 @@ def api_login_delete_zone(domain_name): if resp.status_code == 204: current_app.logger.debug("Request to powerdns API successful") - history = History(msg='Delete domain {0}'.format(domain_name), + history = History(msg='Delete domain {0}'.format( + pretty_domain_name(domain_name)), detail='', created_by=current_user.username) history.add() diff --git a/powerdnsadmin/routes/domain.py b/powerdnsadmin/routes/domain.py index 05db78d..02e8204 100644 --- a/powerdnsadmin/routes/domain.py +++ b/powerdnsadmin/routes/domain.py @@ -8,6 +8,7 @@ from distutils.version import StrictVersion from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify, g, session from flask_login import login_required, current_user, login_manager +from ..lib.utils import pretty_domain_name from ..lib.utils import pretty_json from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec from ..models.user import User, Anonymous @@ -149,6 +150,17 @@ def add(): msg="Please enter a valid domain name"), 400 #TODO: Validate ip addresses input + + # Encode domain name into punycode (IDN) + try: + domain_name = domain_name.encode('idna').decode() + except: + current_app.logger.error("Cannot encode the domain name {}".format(domain_name)) + current_app.logger.debug(traceback.format_exc()) + return render_template( + 'errors/400.html', + msg="Please enter a valid domain name"), 400 + if domain_type == 'slave': if request.form.getlist('domain_master_address'): domain_master_string = request.form.getlist( @@ -168,7 +180,8 @@ def add(): domain_master_ips=domain_master_ips, account_name=account_name) if result['status'] == 'ok': - history = History(msg='Add domain {0}'.format(domain_name), + history = History(msg='Add domain {0}'.format( + pretty_domain_name(domain_name)), detail=str({ 'domain_type': domain_type, 'domain_master_ips': domain_master_ips, @@ -251,7 +264,8 @@ def delete(domain_name): if result['status'] == 'error': abort(500) - history = History(msg='Delete domain {0}'.format(domain_name), + history = History(msg='Delete domain {0}'.format( + pretty_domain_name(domain_name)), created_by=current_user.username) history.add() @@ -294,7 +308,8 @@ def setting(domain_name): d.grant_privileges(new_user_ids) history = History( - msg='Change domain {0} access control'.format(domain_name), + msg='Change domain {0} access control'.format( + pretty_domain_name(domain_name)), detail=str({'user_has_access': new_user_list}), created_by=current_user.username) history.add() @@ -330,7 +345,8 @@ def change_type(domain_name): kind=domain_type, masters=domain_master_ips) if status['status'] == 'ok': - history = History(msg='Update type for domain {0}'.format(domain_name), + history = History(msg='Update type for domain {0}'.format( + pretty_domain_name(domain_name)), detail=str({ "domain": domain_name, "type": domain_type, @@ -362,7 +378,8 @@ def change_soa_edit_api(domain_name): soa_edit_api=new_setting) if status['status'] == 'ok': history = History( - msg='Update soa_edit_api for domain {0}'.format(domain_name), + msg='Update soa_edit_api for domain {0}'.format( + pretty_domain_name(domain_name)), detail=str({ "domain": domain_name, "soa_edit_api": new_setting @@ -421,14 +438,14 @@ def record_apply(domain_name): 'status': 'error', 'msg': - 'Domain name {0} does not exist'.format(domain_name) + 'Domain name {0} does not exist'.format(pretty_domain_name(domain_name)) }), 404) r = Record() result = r.apply(domain_name, submitted_record) if result['status'] == 'ok': history = History( - msg='Apply record changes to domain {0}'.format(domain_name), + msg='Apply record changes to domain {0}'.format(pretty_domain_name(domain_name)), detail=str( json.dumps({ "domain": domain_name, @@ -440,7 +457,8 @@ def record_apply(domain_name): return make_response(jsonify(result), 200) else: history = History( - msg='Failed to apply record changes to domain {0}'.format(domain_name), + msg='Failed to apply record changes to domain {0}'.format( + pretty_domain_name(domain_name)), detail=str( json.dumps({ "domain": domain_name, @@ -566,7 +584,7 @@ def admin_setdomainsetting(domain_name): if setting.set(new_value): history = History( msg='Setting {0} changed value to {1} for {2}'. - format(new_setting, new_value, domain.name), + format(new_setting, new_value, pretty_domain_name(domain_name)), created_by=current_user.username) history.add() return make_response( @@ -585,7 +603,7 @@ def admin_setdomainsetting(domain_name): history = History( msg= 'New setting {0} with value {1} for {2} has been created' - .format(new_setting, new_value, domain.name), + .format(new_setting, new_value, pretty_domain_name(domain_name)), created_by=current_user.username) history.add() return make_response( diff --git a/powerdnsadmin/templates/dashboard_domain.html b/powerdnsadmin/templates/dashboard_domain.html index 9fcc4de..4ff566f 100644 --- a/powerdnsadmin/templates/dashboard_domain.html +++ b/powerdnsadmin/templates/dashboard_domain.html @@ -1,5 +1,5 @@ {% macro name(domain) %} - {{ domain.name }} + {{ domain.name | pretty_domain_name }} {% endmacro %} {% macro dnssec(domain) %} @@ -15,11 +15,11 @@ {% endmacro %} {% macro serial(domain) %} - {% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %} + {% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{ domain.serial }}{% endif %} {% endmacro %} {% macro master(domain) %} - {% if domain.master == '[]'%}-{% else %}{{ domain.master|display_master_name }}{% endif %} + {% if domain.master == '[]'%}-{% else %}{{ domain.master | display_master_name }}{% endif %} {% endmacro %} {% macro account(domain) %} diff --git a/powerdnsadmin/templates/domain.html b/powerdnsadmin/templates/domain.html index 23d684a..01f9f93 100644 --- a/powerdnsadmin/templates/domain.html +++ b/powerdnsadmin/templates/domain.html @@ -1,16 +1,16 @@ {% extends "base.html" %} -{% block title %}