Add punycode (IDN) support (#879)

This commit is contained in:
R. Daneel Olivaw 2021-03-16 14:37:05 -04:00 committed by GitHub
parent 4c19f95928
commit 46993e08c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 78 additions and 28 deletions

View File

@ -18,6 +18,7 @@ A PowerDNS web interface with advanced features.
- DynDNS 2 protocol support - DynDNS 2 protocol support
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!) - Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
- Limited API for manipulating zones and records - Limited API for manipulating zones and records
- Full IDN/Punycode support
## Running PowerDNS-Admin ## Running PowerDNS-Admin
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker. There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.

View File

@ -102,6 +102,7 @@ def create_app(config=None):
'email_to_gravatar_url'] = utils.email_to_gravatar_url 'email_to_gravatar_url'] = utils.email_to_gravatar_url
app.jinja_env.filters[ app.jinja_env.filters[
'display_setting_state'] = utils.display_setting_state 'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
# Register context proccessors # Register context proccessors
from .models.setting import Setting from .models.setting import Setting

View File

@ -8,6 +8,7 @@ import ipaddress
from collections.abc import Iterable from collections.abc import Iterable
from distutils.version import StrictVersion from distutils.version import StrictVersion
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime, timedelta
def auth_from_url(url): def auth_from_url(url):
@ -228,3 +229,22 @@ class customBoxes:
"inaddrarpa": ("in-addr", "%.in-addr.arpa") "inaddrarpa": ("in-addr", "%.in-addr.arpa")
} }
order = ["reverse", "ip6arpa", "inaddrarpa"] 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")

View File

@ -162,6 +162,15 @@ class Record(object):
for record in submitted_records: for record in submitted_records:
# Format the record name # 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, # If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled,
# We convert ipv6 address back to reverse record format # We convert ipv6 address back to reverse record format
# before submitting to PDNS API. # before submitting to PDNS API.

View File

@ -272,7 +272,8 @@ def api_login_delete_zone(domain_name):
if resp.status_code == 204: if resp.status_code == 204:
current_app.logger.debug("Request to powerdns API successful") 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='', detail='',
created_by=current_user.username) created_by=current_user.username)
history.add() history.add()

View File

@ -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 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 flask_login import login_required, current_user, login_manager
from ..lib.utils import pretty_domain_name
from ..lib.utils import pretty_json from ..lib.utils import pretty_json
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
from ..models.user import User, Anonymous from ..models.user import User, Anonymous
@ -149,6 +150,17 @@ def add():
msg="Please enter a valid domain name"), 400 msg="Please enter a valid domain name"), 400
#TODO: Validate ip addresses input #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 domain_type == 'slave':
if request.form.getlist('domain_master_address'): if request.form.getlist('domain_master_address'):
domain_master_string = request.form.getlist( domain_master_string = request.form.getlist(
@ -168,7 +180,8 @@ def add():
domain_master_ips=domain_master_ips, domain_master_ips=domain_master_ips,
account_name=account_name) account_name=account_name)
if result['status'] == 'ok': 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({ detail=str({
'domain_type': domain_type, 'domain_type': domain_type,
'domain_master_ips': domain_master_ips, 'domain_master_ips': domain_master_ips,
@ -251,7 +264,8 @@ def delete(domain_name):
if result['status'] == 'error': if result['status'] == 'error':
abort(500) 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) created_by=current_user.username)
history.add() history.add()
@ -294,7 +308,8 @@ def setting(domain_name):
d.grant_privileges(new_user_ids) d.grant_privileges(new_user_ids)
history = History( 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}), detail=str({'user_has_access': new_user_list}),
created_by=current_user.username) created_by=current_user.username)
history.add() history.add()
@ -330,7 +345,8 @@ def change_type(domain_name):
kind=domain_type, kind=domain_type,
masters=domain_master_ips) masters=domain_master_ips)
if status['status'] == 'ok': 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({ detail=str({
"domain": domain_name, "domain": domain_name,
"type": domain_type, "type": domain_type,
@ -362,7 +378,8 @@ def change_soa_edit_api(domain_name):
soa_edit_api=new_setting) soa_edit_api=new_setting)
if status['status'] == 'ok': if status['status'] == 'ok':
history = History( 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({ detail=str({
"domain": domain_name, "domain": domain_name,
"soa_edit_api": new_setting "soa_edit_api": new_setting
@ -421,14 +438,14 @@ def record_apply(domain_name):
'status': 'status':
'error', 'error',
'msg': 'msg':
'Domain name {0} does not exist'.format(domain_name) 'Domain name {0} does not exist'.format(pretty_domain_name(domain_name))
}), 404) }), 404)
r = Record() r = Record()
result = r.apply(domain_name, submitted_record) result = r.apply(domain_name, submitted_record)
if result['status'] == 'ok': if result['status'] == 'ok':
history = History( 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( detail=str(
json.dumps({ json.dumps({
"domain": domain_name, "domain": domain_name,
@ -440,7 +457,8 @@ def record_apply(domain_name):
return make_response(jsonify(result), 200) return make_response(jsonify(result), 200)
else: else:
history = History( 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( detail=str(
json.dumps({ json.dumps({
"domain": domain_name, "domain": domain_name,
@ -566,7 +584,7 @@ def admin_setdomainsetting(domain_name):
if setting.set(new_value): if setting.set(new_value):
history = History( history = History(
msg='Setting {0} changed value to {1} for {2}'. 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) created_by=current_user.username)
history.add() history.add()
return make_response( return make_response(
@ -585,7 +603,7 @@ def admin_setdomainsetting(domain_name):
history = History( history = History(
msg= msg=
'New setting {0} with value {1} for {2} has been created' '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) created_by=current_user.username)
history.add() history.add()
return make_response( return make_response(

View File

@ -1,5 +1,5 @@
{% macro name(domain) %} {% macro name(domain) %}
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a> <a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name | pretty_domain_name }}</strong></a>
{% endmacro %} {% endmacro %}
{% macro dnssec(domain) %} {% macro dnssec(domain) %}
@ -15,11 +15,11 @@
{% endmacro %} {% endmacro %}
{% macro serial(domain) %} {% 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 %} {% endmacro %}
{% macro master(domain) %} {% macro master(domain) %}
{% if domain.master == '[]'%}-{% else %}{{ domain.master|display_master_name }}{% endif %} {% if domain.master == '[]'%}-{% else %}{{ domain.master | display_master_name }}{% endif %}
{% endmacro %} {% endmacro %}
{% macro account(domain) %} {% macro account(domain) %}

View File

@ -1,16 +1,16 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %} {% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %} {% block dashboard_stat %}
<section class="content-header"> <section class="content-header">
<h1> <h1>
Manage domain: <b>{{ domain.name }}</b> Manage domain: <b>{{ domain.name | pretty_domain_name }}</b>
</h1> </h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i <li><a href="{{ url_for('dashboard.dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li> class="fa fa-dashboard"></i> Home</a></li>
<li>Domain</li> <li>Domain</li>
<li class="active">{{ domain.name }}</li> <li class="active">{{ domain.name | pretty_domain_name }}</li>
</ol> </ol>
</section> </section>
{% endblock %} {% endblock %}
@ -52,7 +52,7 @@
{% for record in records %} {% for record in records %}
<tr class="odd row_record" id="{{ domain.name }}"> <tr class="odd row_record" id="{{ domain.name }}">
<td> <td>
{{ (record.name,domain.name)|display_record_name }} {{ (record.name,domain.name) | display_record_name | pretty_domain_name }}
</td> </td>
<td> <td>
{{ record.type }} {{ record.type }}
@ -64,7 +64,7 @@
{{ record.ttl }} {{ record.ttl }}
</td> </td>
<td> <td>
{{ record.data }} {{ record.data | pretty_domain_name }}
</td> </td>
<td> <td>
{{ record.comment }} {{ record.comment }}
@ -110,8 +110,8 @@
{% block extrascripts %} {% block extrascripts %}
<script> <script>
// superglobals // superglobals
window.records_allow_edit = {{ editable_records|tojson }}; window.records_allow_edit = {{ editable_records | tojson }};
window.ttl_options = {{ ttl_options|tojson }}; window.ttl_options = {{ ttl_options | tojson }};
window.nEditing = null; window.nEditing = null;
window.nNew = false; window.nNew = false;
@ -123,7 +123,7 @@
"ordering" : true, "ordering" : true,
"info" : true, "info" : true,
"autoWidth" : false, "autoWidth" : false,
{% if SETTING.get('default_record_table_size')|string in ['5','15','20'] %} {% if SETTING.get('default_record_table_size') | string in ['5','15','20'] %}
"lengthMenu": [ [5, 15, 20, -1], "lengthMenu": [ [5, 15, 20, -1],
[5, 15, 20, "All"]], [5, 15, 20, "All"]],
{% else %} {% else %}

View File

@ -19,7 +19,7 @@
{% endif %} {% endif %}
<section class="content-header"> <section class="content-header">
<h1> <h1>
Manage domain <small>{{ domain.name }}</small> Manage domain <small>{{ domain.name | pretty_domain_name }}</small>
</h1> </h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li> <li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
@ -42,7 +42,7 @@
<div class="row"> <div class="row">
<div class="col-xs-2"> <div class="col-xs-2">
<p>Users on the right have access to manage the records in <p>Users on the right have access to manage the records in
the {{ domain.name }} domain.</p> the {{ domain.name | pretty_domain_name }} domain.</p>
<p>Click on users to move from between columns.</p> <p>Click on users to move from between columns.</p>
<p> <p>
Users in <font style="color: red;">red</font> are Administrators Users in <font style="color: red;">red</font> are Administrators
@ -94,7 +94,7 @@
{% endfor %} {% endfor %}
</select><br /> </select><br />
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api"> <button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change account for {{ domain.name }} <i class="fa fa-check"></i>&nbsp;Change account for {{ domain.name | pretty_domain_name }}
</button> </button>
</form> </form>
</div> </div>
@ -173,7 +173,7 @@
placeholder="Enter valid master ip addresses (separated by commas)"> placeholder="Enter valid master ip addresses (separated by commas)">
</div> </div>
<button type="submit" class="btn btn-flat btn-primary" id="change_type"> <button type="submit" class="btn btn-flat btn-primary" id="change_type">
<i class="fa fa-check"></i>&nbsp;Change type for {{ domain.name }} <i class="fa fa-check"></i>&nbsp;Change type for {{ domain.name | pretty_domain_name }}
</button> </button>
</form> </form>
</div> </div>
@ -216,7 +216,7 @@
<option>OFF</option> <option>OFF</option>
</select><br /> </select><br />
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api"> <button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change SOA-EDIT-API setting for {{ domain.name }} <i class="fa fa-check"></i>&nbsp;Change SOA-EDIT-API setting for {{ domain.name | pretty_domain_name }}
</button> </button>
</form> </form>
</div> </div>
@ -235,7 +235,7 @@
reverted.</p> reverted.</p>
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain" <button type="button" class="btn btn-flat btn-danger pull-left delete_domain"
id="{{ domain.name }}"> id="{{ domain.name }}">
<i class="fa fa-trash"></i>&nbsp;DELETE DOMAIN {{ domain.name }} <i class="fa fa-trash"></i>&nbsp;DELETE DOMAIN {{ domain.name | pretty_domain_name }}
</button> </button>
</div> </div>
</div> </div>