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
- 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.

View File

@ -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

View File

@ -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")

View File

@ -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.

View File

@ -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()

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_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(

View File

@ -1,5 +1,5 @@
{% 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 %}
{% 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) %}

View File

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

View File

@ -19,7 +19,7 @@
{% endif %}
<section class="content-header">
<h1>
Manage domain <small>{{ domain.name }}</small>
Manage domain <small>{{ domain.name | pretty_domain_name }}</small>
</h1>
<ol class="breadcrumb">
<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="col-xs-2">
<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>
Users in <font style="color: red;">red</font> are Administrators
@ -94,7 +94,7 @@
{% endfor %}
</select><br />
<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>
</form>
</div>
@ -173,7 +173,7 @@
placeholder="Enter valid master ip addresses (separated by commas)">
</div>
<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>
</form>
</div>
@ -216,7 +216,7 @@
<option>OFF</option>
</select><br />
<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>
</form>
</div>
@ -235,7 +235,7 @@
reverted.</p>
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain"
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>
</div>
</div>