mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-09 23:20:27 +00:00
Add punycode (IDN) support (#879)
This commit is contained in:
parent
4c19f95928
commit
46993e08c0
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
|
@ -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) %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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> Change account for {{ domain.name }}
|
<i class="fa fa-check"></i> 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> Change type for {{ domain.name }}
|
<i class="fa fa-check"></i> 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> Change SOA-EDIT-API setting for {{ domain.name }}
|
<i class="fa fa-check"></i> 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> DELETE DOMAIN {{ domain.name }}
|
<i class="fa fa-trash"></i> DELETE DOMAIN {{ domain.name | pretty_domain_name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user