diff --git a/.gitignore b/.gitignore index d548024..eb7fa03 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ nosetests.xml flask config.py logfile.log +log.txt db_repository/* upload/avatar/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8ee6b7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:latest +MAINTAINER Khanh Ngo "ngokhanhit@gmail.com" +ARG ENVIRONMENT=development +ENV ENVIRONMENT=${ENVIRONMENT} + +WORKDIR /powerdns-admin + +RUN apt-get update -y +RUN apt-get install -y python3-pip python3-dev libmysqlclient-dev supervisor +RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev + +COPY ./requirements.txt /powerdns-admin/requirements.txt +RUN pip3 install -r requirements.txt + +ADD ./supervisord.conf /etc/supervisord.conf +ADD . /powerdns-admin/ +COPY ./configs/${ENVIRONMENT}.py /powerdns-admin/config.py diff --git a/app/lib/utils.py b/app/lib/utils.py index a4a2954..e122043 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -2,20 +2,21 @@ import re import sys import json import requests -import urlparse import hashlib from app import app from distutils.version import StrictVersion +from urllib.parse import urlparse if 'TIMEOUT' in app.config.keys(): TIMEOUT = app.config['TIMEOUT'] else: TIMEOUT = 10 + def auth_from_url(url): auth = None - parsed_url = urlparse.urlparse(url).netloc + parsed_url = urlparse(url).netloc if '@' in parsed_url: auth = parsed_url.split('@')[0].split(':') auth = requests.auth.HTTPBasicAuth(auth[0], auth[1]) @@ -55,7 +56,7 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, if r.status_code not in (200, 400, 422): r.raise_for_status() except Exception as e: - raise RuntimeError("While fetching " + remote_url + ": " + str(e)), None, sys.exc_info()[2] + raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e return r @@ -72,16 +73,16 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None): try: assert('json' in r.headers['content-type']) except Exception as e: - raise Exception("While fetching " + remote_url + ": " + str(e)), None, sys.exc_info()[2] + raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e # don't use r.json here, as it will read from r.text, which will trigger # content encoding auto-detection in almost all cases, WHICH IS EXTREMELY # SLOOOOOOOOOOOOOOOOOOOOOOW. just don't. data = None try: - data = json.loads(r.content) - except UnicodeDecodeError: - data = json.loads(r.content, 'iso-8859-1') + data = json.loads(r.content.decode('utf-8')) + except Exception as e: + raise RuntimeError('Error while loading JSON data from {0}'.format(remote_url)) from e return data @@ -92,6 +93,7 @@ def display_record_name(data): else: return record_name.replace('.'+domain_name, '') + def display_master_name(data): """ input data: "[u'127.0.0.1', u'8.8.8.8']" @@ -99,6 +101,7 @@ def display_master_name(data): matches = re.findall(r'\'(.+?)\'', data) return ", ".join(matches) + def display_time(amount, units='s', remove_seconds=True): """ Convert timestamp to normal time format @@ -139,6 +142,7 @@ def display_time(amount, units='s', remove_seconds=True): return final_string + def pdns_api_extended_uri(version): """ Check the pdns version @@ -148,14 +152,10 @@ def pdns_api_extended_uri(version): else: return "" -def email_to_gravatar_url(email, size=100): + +def email_to_gravatar_url(email="", size=100): """ AD doesn't necessarily have email """ - - if not email: - email="" - - - hash_string = hashlib.md5(email).hexdigest() - return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) + hash_string = hashlib.md5(email.encode('utf-8')).hexdigest() + return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size) diff --git a/app/models.py b/app/models.py index 9aee967..e5d3cb6 100644 --- a/app/models.py +++ b/app/models.py @@ -3,7 +3,6 @@ import ldap import time import base64 import bcrypt -import urlparse import itertools import traceback import pyotp @@ -11,13 +10,14 @@ import re import dns.reversename from datetime import datetime +from urllib.parse import urljoin from distutils.util import strtobool from distutils.version import StrictVersion from flask_login import AnonymousUserMixin from app import app, db -from lib import utils -from lib.log import logger +from app.lib import utils +from app.lib.log import logger logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() if 'LDAP_TYPE' in app.config.keys(): @@ -160,7 +160,7 @@ class User(db.Model): result_set.append(result_data) return result_set - except ldap.LDAPError, e: + except ldap.LDAPError as e: logging.error(e) raise @@ -269,6 +269,7 @@ class User(db.Model): self.role_id = Role.query.filter_by(name='User').first().id if User.query.count() == 0: self.role_id = Role.query.filter_by(name='Administrator').first().id + self.password = self.get_hashed_password(self.plain_text_password) db.session.add(self) @@ -451,8 +452,8 @@ class Domain(db.Model): self.settings.append(DomainSetting(setting=setting, value=value)) db.session.commit() return True - except Exception, e: - logging.error('Can not create setting %s for domain %s. %s' % (setting, self.name, str(e))) + except Exception as e: + logging.error('Can not create setting %s for domain %s. %s' % (setting, self.name, e)) return False def get_domains(self): @@ -476,7 +477,7 @@ class Domain(db.Model): """ headers = {} headers['X-API-Key'] = PDNS_API_KEY - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) return jdata def get_id_by_name(self, name): @@ -500,7 +501,7 @@ class Domain(db.Model): headers = {} headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) list_jdomain = [d['name'].rstrip('.') for d in jdata] try: # domains should remove from db since it doesn't exist in powerdns anymore @@ -564,8 +565,8 @@ class Domain(db.Model): except: db.session.rollback() return {'status': 'ok', 'msg': 'Domain table has been updated successfully'} - except Exception, e: - logging.error('Can not update domain table.' + str(e)) + except Exception as e: + logging.error('Can not update domain table. Error: {0}'.format(e)) return {'status': 'error', 'msg': 'Can not update domain table'} def add(self, domain_name, domain_type, soa_edit_api, domain_ns=[], domain_master_ips=[]): @@ -596,17 +597,17 @@ class Domain(db.Model): } try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data) if 'error' in jdata.keys(): logging.error(jdata['error']) return {'status': 'error', 'msg': jdata['error']} else: logging.info('Added domain %s successfully' % domain_name) return {'status': 'ok', 'msg': 'Added domain successfully'} - except Exception, e: - print traceback.format_exc() + except Exception as e: + traceback.print_exc() logging.error('Cannot add domain %s' % domain_name) - logging.debug(str(e)) + logging.debug(e) return {'status': 'error', 'msg': 'Cannot add this domain.'} def create_reverse_domain(self, domain_name, domain_reverse_name): @@ -671,13 +672,13 @@ class Domain(db.Model): headers = {} headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain_name), headers=headers, method='DELETE') + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain_name), headers=headers, method='DELETE') logging.info('Delete domain %s successfully' % domain_name) return {'status': 'ok', 'msg': 'Delete domain successfully'} - except Exception, e: - print traceback.format_exc() + except Exception as e: + traceback.print_exc() logging.error('Cannot delete domain %s' % domain_name) - logging.debug(str(e)) + logging.debug(e) return {'status': 'error', 'msg': 'Cannot delete domain'} def get_user(self): @@ -730,7 +731,7 @@ class Domain(db.Model): headers = {} headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/axfr-retrieve' % domain), headers=headers, method='PUT') + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/axfr-retrieve' % domain), headers=headers, method='PUT') return {'status': 'ok', 'msg': 'Update from Master successfully'} except: return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} @@ -746,7 +747,7 @@ class Domain(db.Model): headers = {} headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/cryptokeys' % domain.name), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/cryptokeys' % domain.name), headers=headers, method='GET') if 'error' in jdata: return {'status': 'error', 'msg': 'DNSSEC is not enabled for this domain'} else: @@ -791,7 +792,7 @@ class Record(object): headers = {} headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers) except: logging.error("Cannot fetch domain's record data from remote powerdns api") return False @@ -865,11 +866,11 @@ class Record(object): } try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) logging.debug(jdata) return {'status': 'ok', 'msg': 'Record was added successfully'} - except Exception, e: - logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, str(e))) + except Exception as e: + logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, e)) return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} @@ -1043,8 +1044,8 @@ class Record(object): try: headers = {} headers['X-API-Key'] = PDNS_API_KEY - jdata1 = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_delete) - jdata2 = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_new) + jdata1 = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_delete) + jdata2 = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_new) if 'error' in jdata2.keys(): logging.error('Cannot apply record changes.') @@ -1054,8 +1055,8 @@ class Record(object): self.auto_ptr(domain, new_records, deleted_records) logging.info('Record was applied successfully.') return {'status': 'ok', 'msg': 'Record was applied successfully'} - except Exception, e: - logging.error("Cannot apply record changes to domain %s. DETAIL: %s" % (str(e), domain)) + except Exception as e: + logging.error("Cannot apply record changes to domain %s. DETAIL: %s" % (e, domain)) return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} def auto_ptr(self, domain, new_records, deleted_records): @@ -1097,7 +1098,7 @@ class Record(object): self.delete(domain_reverse_name) return {'status': 'ok', 'msg': 'Auto-PTR record was updated successfully'} except Exception as e: - logging.error("Cannot update auto-ptr record changes to domain %s. DETAIL: %s" % (str(e), domain)) + logging.error("Cannot update auto-ptr record changes to domain %s. DETAIL: %s" % (e, domain)) return {'status': 'error', 'msg': 'Auto-PTR creation failed. There was something wrong, please contact administrator.'} def delete(self, domain): @@ -1117,7 +1118,7 @@ class Record(object): ] } try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) logging.debug(jdata) return {'status': 'ok', 'msg': 'Record was removed successfully'} except: @@ -1191,11 +1192,11 @@ class Record(object): ] } try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data) logging.debug("dyndns data: " % data) return {'status': 'ok', 'msg': 'Record was updated successfully'} - except Exception, e: - logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, str(e))) + except Exception as e: + logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, e)) return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} @@ -1217,7 +1218,7 @@ class Server(object): headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/config' % self.server_id), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/config' % self.server_id), headers=headers, method='GET') return jdata except: logging.error("Can not get server configuration.") @@ -1232,7 +1233,7 @@ class Server(object): headers['X-API-Key'] = PDNS_API_KEY try: - jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/statistics' % self.server_id), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/statistics' % self.server_id), headers=headers, method='GET') return jdata except: logging.error("Can not get server statistics.") diff --git a/app/views.py b/app/views.py index 8cc8761..46989ee 100644 --- a/app/views.py +++ b/app/views.py @@ -18,7 +18,7 @@ from werkzeug.security import gen_salt from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting from app import app, login_manager, github -from lib import utils +from app.lib import utils jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name @@ -34,36 +34,43 @@ if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'): else: NEW_SCHEMA = False + @app.context_processor def inject_fullscreen_layout_setting(): fullscreen_layout_setting = Setting.query.filter(Setting.name == 'fullscreen_layout').first() return dict(fullscreen_layout_setting=strtobool(fullscreen_layout_setting.value)) + @app.context_processor def inject_record_helper_setting(): record_helper_setting = Setting.query.filter(Setting.name == 'record_helper').first() return dict(record_helper_setting=strtobool(record_helper_setting.value)) + @app.context_processor def inject_login_ldap_first_setting(): login_ldap_first_setting = Setting.query.filter(Setting.name == 'login_ldap_first').first() return dict(login_ldap_first_setting=strtobool(login_ldap_first_setting.value)) + @app.context_processor def inject_default_record_table_size_setting(): default_record_table_size_setting = Setting.query.filter(Setting.name == 'default_record_table_size').first() return dict(default_record_table_size_setting=default_record_table_size_setting.value) + @app.context_processor def inject_default_domain_table_size_setting(): default_domain_table_size_setting = Setting.query.filter(Setting.name == 'default_domain_table_size').first() return dict(default_domain_table_size_setting=default_domain_table_size_setting.value) + @app.context_processor def inject_auto_ptr_setting(): auto_ptr_setting = Setting.query.filter(Setting.name == 'auto_ptr').first() return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value)) + # START USER AUTHENTICATION HANDLER @app.before_request def before_request(): @@ -92,6 +99,7 @@ def dyndns_login_required(f): return f(*args, **kwargs) return decorated_function + @login_manager.request_loader def login_via_authorization_header(request): auth_header = request.headers.get('Authorization') @@ -100,8 +108,7 @@ def login_via_authorization_header(request): try: auth_header = base64.b64decode(auth_header) username,password = auth_header.split(":") - except TypeError, e: - error = e.message['desc'] if 'desc' in e.message else e + except TypeError as e: return None user = User(username=username, password=password, plain_text_password=password) try: @@ -111,10 +118,9 @@ def login_via_authorization_header(request): else: login_user(user, remember = False) return user - except Exception, e: + except: return None return None - # END USER AUTHENTICATION HANDLER # START CUSTOMIZE DECORATOR @@ -132,18 +138,22 @@ def admin_role_required(f): def http_bad_request(e): return redirect(url_for('error', code=400)) + @app.errorhandler(401) def http_unauthorized(e): return redirect(url_for('error', code=401)) + @app.errorhandler(404) def http_internal_server_error(e): return redirect(url_for('error', code=404)) + @app.errorhandler(500) def http_page_not_found(e): return redirect(url_for('error', code=500)) + @app.route('/error/') def error(code, msg=None): supported_code = ('400', '401', '404', '500') @@ -152,6 +162,7 @@ def error(code, msg=None): else: return render_template('errors/404.html'), 404 + @app.route('/register', methods=['GET']) def register(): SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] @@ -160,12 +171,14 @@ def register(): else: return render_template('errors/404.html'), 404 + @app.route('/github/login') def github_login(): if not app.config.get('GITHUB_OAUTH_ENABLE'): return abort(400) return github.authorize(callback=url_for('authorized', _external=True)) + @app.route('/login', methods=['GET', 'POST']) @login_manager.unauthorized_handler def login(): @@ -224,9 +237,8 @@ def login(): auth = user.is_validate(method=auth_method) if auth == False: return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) - except Exception, e: - error = e.message['desc'] if 'desc' in e.message else e - return render_template('login.html', error=error, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + except Exception as e: + return render_template('login.html', error=e, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) # check if user enabled OPT authentication if user.otp_secret: @@ -255,9 +267,9 @@ def login(): return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) else: return render_template('register.html', error=result) - except Exception, e: - error = e.message['desc'] if 'desc' in e.message else e - return render_template('register.html', error=error) + except Exception as e: + return render_template('register.html', error=e) + @app.route('/logout') def logout(): @@ -284,7 +296,7 @@ def dashboard(): server = Server(server_id='localhost') statistics = server.get_statistic() if statistics: - uptime = filter(lambda uptime: uptime['name'] == 'uptime', statistics)[0]['value'] + uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value'] else: uptime = 0 return render_template('dashboard.html', domains=domains, domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history) @@ -417,8 +429,7 @@ def record_apply(domain_name): """ #TODO: filter removed records / name modified records. try: - pdata = request.data - jdata = json.loads(pdata) + jdata = request.json r = Record() result = r.apply(domain_name, jdata) @@ -429,7 +440,7 @@ def record_apply(domain_name): else: return make_response(jsonify( result ), 400) except: - print traceback.format_exc() + traceback.print_exc() return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500) @@ -441,8 +452,7 @@ def record_update(domain_name): Pulling the records update from its Master """ try: - pdata = request.data - jdata = json.loads(pdata) + jdata = request.json domain_name = jdata['domain'] d = Domain() @@ -452,7 +462,7 @@ def record_update(domain_name): else: return make_response(jsonify( {'status': 'error', 'msg': result['msg']} ), 500) except: - print traceback.format_exc() + traceback.print_exc() return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500) @@ -464,9 +474,9 @@ def record_delete(domain_name, record_name, record_type): r = Record(name=record_name, type=record_type) result = r.delete(domain=domain_name) if result['status'] == 'error': - print result['msg'] + print(result['msg']) except: - print traceback.format_exc() + traceback.print_exc() return redirect(url_for('error', code=500)), 500 return redirect(url_for('domain', domain_name=domain_name)) @@ -478,6 +488,7 @@ def domain_dnssec(domain_name): dnssec = domain.get_domain_dnssec(domain_name) return make_response(jsonify(dnssec), 200) + @app.route('/domain//managesetting', methods=['GET', 'POST']) @login_required @admin_role_required @@ -488,9 +499,9 @@ def admin_setdomainsetting(domain_name): # {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'} # try: - pdata = request.data - jdata = json.loads(pdata) + jdata = request.json data = jdata['data'] + if jdata['action'] == 'set_setting': new_setting = data['setting'] new_value = str(data['value']) @@ -514,7 +525,7 @@ def admin_setdomainsetting(domain_name): else: return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400) except: - print traceback.format_exc() + traceback.print_exc() return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) @@ -531,12 +542,13 @@ def admin(): history_number = History.query.count() if statistics: - uptime = filter(lambda uptime: uptime['name'] == 'uptime', statistics)[0]['value'] + uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value'] else: uptime = 0 return render_template('admin.html', domains=domains, users=users, configs=configs, statistics=statistics, uptime=uptime, history_number=history_number) + @app.route('/admin/user/create', methods=['GET', 'POST']) @login_required @admin_role_required @@ -562,6 +574,7 @@ def admin_createuser(): return redirect(url_for('admin_manageuser')) + @app.route('/admin/manageuser', methods=['GET', 'POST']) @login_required @admin_role_required @@ -576,8 +589,7 @@ def admin_manageuser(): # {'action': 'delete_user', 'data': 'username'} # try: - pdata = request.data - jdata = json.loads(pdata) + jdata = request.json data = jdata['data'] if jdata['action'] == 'delete_user': @@ -614,7 +626,7 @@ def admin_manageuser(): else: return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400) except: - print traceback.format_exc() + traceback.print_exc() return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) @@ -637,6 +649,7 @@ def admin_history(): histories = History.query.all() return render_template('admin_history.html', histories=histories) + @app.route('/admin/settings', methods=['GET']) @login_required @admin_role_required @@ -645,6 +658,7 @@ def admin_settings(): settings = Setting.query.filter(Setting.name != 'maintenance') return render_template('admin_settings.html', settings=settings) + @app.route('/admin/setting//toggle', methods=['POST']) @login_required @admin_role_required @@ -655,19 +669,21 @@ def admin_settings_toggle(setting): else: return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) + @app.route('/admin/setting//edit', methods=['POST']) @login_required @admin_role_required def admin_settings_edit(setting): - pdata = request.data - jdata = json.loads(pdata) + jdata = request.json new_value = jdata['value'] result = Setting().set(setting, new_value) + if (result): return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200) else: return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) + @app.route('/user/profile', methods=['GET', 'POST']) @login_required def user_profile(): @@ -682,7 +698,7 @@ def user_profile(): # json data if request.data: - jdata = json.loads(request.data) + jdata = request.json data = jdata['data'] if jdata['action'] == 'enable_otp': enable_otp = data['enable_otp'] @@ -702,7 +718,6 @@ def user_profile(): save_file_name = current_user.username + '.' + file_extension file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name)) - # update user profile user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False) user.update_profile() @@ -737,6 +752,7 @@ def dyndns_checkip(): # route covers the default ddclient 'web' setting for the checkip service return render_template('dyndns.html', response=request.environ.get('HTTP_X_REAL_IP', request.remote_addr)) + @app.route('/nic/update', methods=['GET', 'POST']) @dyndns_login_required def dyndns_update(): diff --git a/config_template.py b/config_template.py index 288ff47..5970ec3 100644 --- a/config_template.py +++ b/config_template.py @@ -5,7 +5,7 @@ basedir = os.path.abspath(os.path.dirname(__file__)) WTF_CSRF_ENABLED = True SECRET_KEY = 'We are the world' BIND_ADDRESS = '127.0.0.1' -PORT = 9393 +PORT = 9191 LOGIN_TITLE = "PDNS" # TIMEOUT - for large zones diff --git a/configs/development.py b/configs/development.py new file mode 100644 index 0000000..98f7d10 --- /dev/null +++ b/configs/development.py @@ -0,0 +1,59 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +# BASIC APP CONFIG +WTF_CSRF_ENABLED = True +SECRET_KEY = 'changeme' +LOG_LEVEL = 'DEBUG' +LOG_FILE = 'log.txt' + +# TIMEOUT - for large zones +TIMEOUT = 10 + +# UPLOAD DIR +UPLOAD_DIR = os.path.join(basedir, 'upload') + +# DATABASE CONFIG FOR MYSQL +DB_USER = 'powerdnsadmin' +DB_PASSWORD = 'powerdnsadminpassword' +DB_HOST = 'docker.for.mac.localhost' +DB_NAME = 'powerdnsadmin' + +#MySQL +SQLALCHEMY_DATABASE_URI = 'mysql://'+DB_USER+':'+DB_PASSWORD+'@'+DB_HOST+'/'+DB_NAME +SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') +SQLALCHEMY_TRACK_MODIFICATIONS = True + +# AUTHENTICATION CONFIG +BASIC_ENABLED = True +SIGNUP_ENABLED = True + +## LDAP CONFIG +# LDAP_TYPE = 'ldap' +# LDAP_URI = 'ldaps://your-ldap-server:636' +# LDAP_USERNAME = 'cn=dnsuser,ou=users,ou=services,dc=duykhanh,dc=me' +# LDAP_PASSWORD = 'dnsuser' +# LDAP_SEARCH_BASE = 'ou=System Admins,ou=People,dc=duykhanh,dc=me' +## Additional options only if LDAP_TYPE=ldap +# LDAP_USERNAMEFIELD = 'uid' +# LDAP_FILTER = '(objectClass=inetorgperson)' + +## GITHUB AUTHENTICATION +# GITHUB_OAUTH_ENABLE = False +# GITHUB_OAUTH_KEY = 'G0j1Q15aRsn36B3aD6nwKLiYbeirrUPU8nDd1wOC' +# GITHUB_OAUTH_SECRET = '0WYrKWePeBDkxlezzhFbDn1PBnCwEa0vCwVFvy6iLtgePlpT7WfUlAa9sZgm' +# GITHUB_OAUTH_SCOPE = 'email' +# GITHUB_OAUTH_URL = 'http://127.0.0.1:5000/api/v3/' +# GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:5000/oauth/token' +# GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize' + +# POWERDNS CONFIG +PDNS_STATS_URL = 'http://192.168.100.100:8081/' +PDNS_API_KEY = 'changeme' +PDNS_VERSION = '4.1.1' + +# RECORDS ALLOWED TO EDIT +RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CNAME', 'SPF', 'PTR', 'MX', 'TXT'] + +# EXPERIMENTAL FEATURES +PRETTY_IPV6_PTR = False diff --git a/create_db.py b/create_db.py index 41c874d..2a70f96 100755 --- a/create_db.py +++ b/create_db.py @@ -1,13 +1,16 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +import sys +import time +import os.path +import traceback from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO from app import db from app.models import Role, Setting -import os.path -import time -import sys + def start(): wait_time = get_waittime_from_env() @@ -22,13 +25,14 @@ def get_waittime_from_env(): return int(os.environ.get('WAITFOR_DB', 1)) def connect_db(wait_time): - for i in xrange(0, wait_time): + for i in range(0, wait_time): print("INFO: Wait for database server") sys.stdout.flush() try: db.create_all() return True except: + traceback.print_exc() time.sleep(1) return False @@ -36,14 +40,14 @@ def connect_db(wait_time): def init_roles(db, role_names): # Get key name of data - name_of_roles = map(lambda r: r.name, role_names) + name_of_roles = [r.name for r in role_names] # Query to get current data rows = db.session.query(Role).filter(Role.name.in_(name_of_roles)).all() - name_of_rows = map(lambda r: r.name, rows) + name_of_rows = [r.name for r in rows] # Check which data that need to insert - roles = filter(lambda r: r.name not in name_of_rows, role_names) + roles = [r for r in role_names if r.name not in name_of_rows] # Insert data for role in roles: @@ -52,14 +56,14 @@ def init_roles(db, role_names): def init_settings(db, setting_names): # Get key name of data - name_of_settings = map(lambda r: r.name, setting_names) + name_of_settings = [r.name for r in setting_names] # Query to get current data rows = db.session.query(Setting).filter(Setting.name.in_(name_of_settings)).all() # Check which data that need to insert - name_of_rows = map(lambda r: r.name, rows) - settings = filter(lambda r: r.name not in name_of_rows, setting_names) + name_of_rows = [r.name for r in rows] + settings = [r for r in setting_names if r.name not in name_of_rows] # Insert data for setting in settings: diff --git a/db_downgrade.py b/db_downgrade.py index c001e6c..fa5866e 100755 --- a/db_downgrade.py +++ b/db_downgrade.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO diff --git a/db_migrate.py b/db_migrate.py index 6823469..7d0869e 100755 --- a/db_migrate.py +++ b/db_migrate.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import imp from migrate.versioning import api from app import db diff --git a/db_upgrade.py b/db_upgrade.py index f5ae27b..515b309 100755 --- a/db_upgrade.py +++ b/db_upgrade.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..57b1e16 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,22 @@ +version: "2.1" + +services: + powerdns-admin: + build: + context: . + dockerfile: Dockerfile + image: powerdns-admin + container_name: powerdns-admin + mem_limit: 256M + memswap_limit: 256M + tty: true + command: /usr/bin/supervisord -c /etc/supervisord.conf + ports: + - "9191:9191" + volumes: + - .:/powerdns-admin/ + - "./configs/development.py:/powerdns-admin/config.py" + logging: + driver: json-file + options: + max-size: 50m diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c9271b5..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '2' - -services: - - powerdns-authoritative: - image: winggundamth/powerdns-mysql:trusty - hostname: powerdns-authoritative - depends_on: - - powerdns-authoritative-mariadb - links: - - powerdns-authoritative-mariadb:mysqldb - ports: - - 172.17.0.1:53:53/udp - - 8081:8081 - environment: - - PDNS_DB_HOST=mysqldb - - PDNS_DB_USERNAME=root - - PDNS_DB_NAME=powerdns - - PDNS_DB_PASSWORD=PowerDNSPassword - - PDNS_API_KEY=PowerDNSAPIKey - - powerdns-authoritative-mariadb: - image: mariadb:10.1.15 - hostname: powerdns-authoritative-mariadb - environment: - - MYSQL_DATABASE=powerdns - - MYSQL_ROOT_PASSWORD=PowerDNSPassword - - powerdns-admin: - image: winggundamth/powerdns-admin:trusty - hostname: powerdns-admin - depends_on: - - powerdns-admin-mariadb - - powerdns-authoritative - links: - - powerdns-admin-mariadb:mysqldb - - powerdns-authoritative:powerdns-server - volumes: - - ./:/home/web/powerdns-admin - ports: - - 9393:9393 - environment: - - WAITFOR_DB=60 - - powerdns-admin-mariadb: - image: mariadb:10.1.15 - hostname: powerdns-admin-mariadb - environment: - - MYSQL_DATABASE=powerdns-admin - - MYSQL_ROOT_PASSWORD=PowerDNSAdminPassword diff --git a/requirements.txt b/requirements.txt index 46ab6ba..f7215df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,16 @@ -Flask>=0.10 -Flask-WTF>=0.11 -Flask-Login>=0.2.11 -configobj==5.0.5 -bcrypt==3.1.0 -requests==2.7.0 -python-ldap==2.4.21 -Flask-SQLAlchemy==2.1 -SQLAlchemy==1.0.9 +Flask==0.12.2 +Flask-WTF==0.14.2 +Flask-Login==0.4.1 +Flask-OAuthlib==0.9.4 +Flask-SQLAlchemy==2.3.2 +SQLAlchemy==1.2.5 sqlalchemy-migrate==0.10.0 -pyotp==2.2.1 -qrcode==5.3 -Flask-OAuthlib==0.9.3 -dnspython>=1.12.0 +mysqlclient==1.3.12 +configobj==5.0.6 +bcrypt==3.1.4 +requests==2.18.4 +python-ldap==3.0.0 +pyotp==2.2.6 +qrcode==6.0 +dnspython==1.15.0 +gunicorn==19.7.1 diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..ed604cf --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,23 @@ +[unix_http_server] +file=/tmp/supervisor.sock ; (the path to the socket file) +chown=nobody:nogroup ; socket file uid:gid owner + +[supervisord] +logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) +logfile_backups=10 ; (num of main logfile rotation backups;default 10) +loglevel=info ; (log level;default info; others: debug,warn,trace) +pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +nodaemon=true ; (start in foreground if true;default false) +minfds=1024 ; (min. avail startup file descriptors;default 1024) +minprocs=200 ; (min. avail process descriptors;default 200) + +[program:powerdns-admin] +command=/usr/local/bin/gunicorn -t 120 --workers 4 --bind '0.0.0.0:9191' --log-level info app:app +directory=/powerdns-admin +autostart=true +priority=999 +user=www-data +redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0