Compare commits

..
15 Commits
Author SHA1 Message Date
wiktr 7acb426fea IZBI!!!1!!11 2026-09-10 00:38:16 +02:00
wiktr c3d61ada91 hotfix 2026-08-28 14:57:00 +02:00
wiktr 37442c9864 add CSRF protection 2026-08-28 14:48:58 +02:00
wiktr 8b94f36db5 fix Token db interface 2026-08-28 14:47:52 +02:00
wiktr 0bb7ca21cf finish rewrite 2026-08-25 16:20:27 +02:00
wiktr cfa862d4e5 partial frontend rewrite 2026-08-25 15:20:19 +02:00
wiktr 8e143acb1e rewrite backend 2026-08-25 15:16:31 +02:00
wiktr 1b5ea7be3a Add comments 2026-08-25 01:48:03 +02:00
wiktr 07ff654baa update docker compose dns port 2026-08-24 23:13:12 +02:00
wiktr ecc110641b hotfix 2026-08-24 23:11:35 +02:00
wiktr 42fe8d9a76 implement admin panel 2026-08-24 21:49:17 +02:00
wiktr b17b75e043 add admin user list 2026-08-24 18:21:20 +02:00
wiktr abdfcde1c9 add user dashboard 2026-08-24 17:24:08 +02:00
wiktr 21bce2da64 Add user login form 2026-08-24 15:18:45 +02:00
wiktr 0b262c83ab update schema, add db init 2026-08-24 13:35:59 +02:00
12 changed files with 981 additions and 66 deletions

No files matched your search

+1 -1
View File
@@ -11,6 +11,6 @@ RUN pip3 install -r /requirements.txt
COPY ./run.sh /run.sh COPY ./run.sh /run.sh
COPY ./app/ /app COPY ./app/ /app
RUN mkdir /app/hosts.d/ WORKDIR /app
CMD ["/run.sh"] CMD ["/run.sh"]
+55 -14
View File
@@ -1,18 +1,59 @@
MIT License The izbi license 1.01
Copyright (c) 2026 wiktr Permission is hereby granted, free of charge, to any student and/or
graduate of Computer Science - Information Security Management
(pl. Informatyka - Zarządzanie Bezpieczeństwem Informacji "izbi")
from AGH University of Krakow ("AGH"), to freely use, modify and
redistribute the contents of this software, its source code
and any associated documentation ("full software").
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Every file that is written in human-readable format, meant to be read
associated documentation files (the "Software"), to deal in the Software without restriction, including by humans and not used by the software itself (documentation, readme, etc.),
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell has to end with the word "izbi", separated from the rest of the
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the aforementioned file by a newline character.
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial Only other students and/or staff of AGH's Faculty of Computer Science
portions of the Software. (pl. Wydział Informatyki Akademii Górniczo-Hutniczej "WI AGH"),
after being given the verbal (which may be revoked at any time by original authors) or written
permission from the author(s) of the full software may acquire the same rights
to the full software as the students of izbi, free of charge.
The use of this license by people affiliated with WI AGH is permitted only if they
know someone that attends and/or attended izbi.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT Any other person, who is unrelated to WI AGH and wishes to use this software is allowed to,
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO but cannot publish for profit or modify any version of the full software
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER or parts of its source code, including (but not limited to) merging and/or sublicensing,
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE except where such modification is private and cannot be discovered or
USE OR OTHER DEALINGS IN THE SOFTWARE. traced through the Clearnet or Deep web, in which case it is explicitly permitted.
Students of AGH's Faculty of Computer Science, Electronics and Telecommunications
(pl. Wydział Informatyki, Elektroniki i Telekomunikacji "WIET")
are not permitted to use and/or modify the full software.
However if they want to use the full software or its parts
they need to give 1 (one) full can of beer from the brand "Romper",
or any other beer stated in the beer.md file in which (if created),
every author needs to name the brand and type of beer they want to be given.
This beer must be given to every credited author of the full software,
even if parts of the software that the students of WIET used were not made by that person.
After supplying the author(s) with beer, WIET students are acquiring the
same rights as students of WI AGH.
The only people that are not allowed to use the full software in
any way, shape or form, including reading this license or any other file
are any past, present and future deans of WIET.
THE TEXT ABOVE HAS TO APPEAR IN ITS UNCHANGED FORM IN ANY COPIES
AND/OR FORKS OF FULL SOFTWARE THAT WERE NOT CREATED BY THE ORIGINAL
AUTHOR OF THE FULL SOFTWARE. THE ONLY PEOPLE WITH THE RIGHT TO
CHANGE THE LICENSE OF THE FULL SOFTWARE ARE THE AUTHORS
CREDITED IN THE HISTORY OF THE ORIGINAL REPOSITORY (OR SEPARATE
AUTHORS.md file).
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
izbi
+3 -1
View File
@@ -1,3 +1,5 @@
# IZBI DNS # IZBI DNS
Dynamic DNS for `.izbi` domains. Dynamic DNS for `.izbi` domains.
izbi
+32 -31
View File
@@ -1,34 +1,35 @@
import sqlite3 import db
from hashlib import sha256
#def __init__(self, path: str) -> None:
# self.db_connection = sqlite3.connect(path)
# cursor = self.db_connection.cursor()
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS Tokens (
# TOKEN TEXT PRIMARY KEY NOT NULL,
# USER TEXT NOT NULL,
# NAME TEXT NOT NULL
# )
# ''')
#
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS Domains (
# DOMAIN TEXT PRIMARY KEY NOT NULL,
# USER TEXT NOT NULL
# )
# ''')
#
# self.db_connection.commit()
# cursor.close()
# Check if access token can update domain's ip
def check_token_domain(db_path: str, token: str, domain: str) -> bool: def check_token_domain(db_path: str, token: str, domain: str) -> bool:
with sqlite3.connect(db_path) as connection: userid = db.Token.get_user(db_path, token)
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Tokens
JOIN Domains ON Tokens.user = Domains.user
WHERE token = ?
AND domain = ?
''', (token,domain))
return cursor.fetchone()[0] == 1 domain = domain.strip()
return domain in db.User.get_domains(db_path, userid)
# Verify user's passowrd for login. Return UserID on success, None on failure
def user_login(db_path: str, username: str, password: str) -> int | None:
try:
userid = db.User.get_id(db_path, username)
except ValueError:
return None
pwsalt, pwhash = db.User.get_password_hash(db_path, userid)
password_hash = sha256( (pwsalt + password).encode('utf-8')).hexdigest()
if pwhash == password_hash:
return userid
return None
# Change password of user (by user)
def change_user_password(db_path: str, userid: int, oldpass: str, newpass: str) -> None:
username = db.User.get_username(db_path, userid)
if user_login(db_path, username, oldpass) is None:
raise ValueError("Incorrect password")
db.User.set_password(db_path, userid, newpass)
+432
View File
@@ -0,0 +1,432 @@
import sqlite3
from os import urandom
from hashlib import sha256
# Initialize database
def init(db_path: str) -> None:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
# Auth token table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Tokens (
TKHASH TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL,
NAME TEXT NOT NULL
)
''')
# Registered domains table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Domains (
DOMAIN TEXT PRIMARY KEY NOT NULL,
USERID INTEGER NOT NULL
)
''')
# Users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Users (
USERID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
USERNAME TEXT NOT NULL UNIQUE,
PWHASH TEXT NOT NULL,
PWSALT TEXT NOT NULL,
EMAIL TEXT,
IS_ADMIN INTEGER NOT NULL DEFAULT 0
)
''')
# Create a new admin if there are no accounts
cursor.execute('''
SELECT COUNT(*) FROM Users
''')
if cursor.fetchone()[0] == 0:
username = "root"
password = urandom(8).hex()
User.new(db_path, username, password, is_admin=True)
print(f"No users detected, created new admin\nUser: {username}\nPass: {password}", flush=True)
def list_users(db_path: str) -> list[dict]:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT userid, username, is_admin, email FROM Users
''')
users = cursor.fetchall()
users = list(map(lambda x: {"userid": x[0], "username": x[1], "domains": [], "email": x[3], "is_admin": x[2]}, users))
for user in users:
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (user["userid"], ))
domains = cursor.fetchall()
domains = list(map(lambda x: x[0], domains))
user['domains'] = domains
return users
# Collection of getters and setters
class User:
def exists(db_path: str, userid: int) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT COUNT(*) FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0] == 1
def new(db_path: str, username: str, password: str, domains: list = [], is_admin: bool = False, email: str = None) -> int:
username = username.strip()
if not username.isalnum():
raise ValueError("Username has to be alphanumeric")
if len(username) < 1:
raise ValueError("Username has to be at least 1 character long")
if len(password) > 16:
raise ValueError("Username has to be at most 16 characters long")
if len(password) < 8:
raise ValueError("Password has to be at least 8 characters long")
if len(password) > 128:
raise ValueError("Password has to be at most 128 characters long")
if email is not None:
email = email.strip()
if len(email) > 128:
raise ValueError("Email has to be at most 128 characters long, be reasonable")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
pwsalt = urandom(16).hex()
pwhash = sha256( (pwsalt + password).encode('utf-8') ).hexdigest()
cursor.execute('''
INSERT INTO Users (username, pwsalt, pwhash, email, is_admin) VALUES (?, ?, ?, ?, ?)
''', (username, pwsalt, pwhash, email, is_admin))
# Get new user's ID
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
''', (username, ))
userid = cursor.fetchone()[0]
User.set_domains(db_path, userid, domains)
return userid
def delete(db_path: str, userid: int):
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('DELETE FROM Domains WHERE userid = ?', (userid, ))
cursor.execute('DELETE FROM Tokens WHERE userid = ?', (userid, ))
cursor.execute('DELETE FROM Users WHERE userid = ?', (userid, ))
def is_admin(db_path: str, userid: int) -> bool:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT is_admin FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def set_admin(db_path: str, userid: int, is_admin: bool):
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
UPDATE Users
SET is_admin = ?
WHERE userid = ?
''', (is_admin, userid))
def get_username(db_path: str, userid: int) -> str:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT username FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def get_id(db_path: str, username: str) -> int:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT userid FROM Users
WHERE username = ?
''', (username, ))
userid = cursor.fetchone()
if userid is None:
raise ValueError(f"User {username} does not exist")
return userid[0]
def get_domains(db_path: str, userid: int) -> list[str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT domain FROM Domains
WHERE userid = ?
''', (userid, ))
domains = cursor.fetchall()
domains = list(map(lambda x: x[0], domains))
return domains
def set_domains(db_path: str, userid: int, domains: list[str]) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
#TODO domain regex
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
DELETE FROM Domains
WHERE userid = ?
''', (userid, ))
for domain in domains:
domain = domain.strip()
if len(domain) < 1:
continue
cursor.execute('''
INSERT INTO Domains (domain, userid) VALUES (?, ?)
''', (domain, userid))
def get_token_names(db_path: str, userid: int) -> list[str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT name FROM Tokens
WHERE userid = ?
''', (userid, ))
token_names = cursor.fetchall()
token_names = list(map(lambda x: x[0], token_names))
return token_names
def generate_token(db_path: str, userid: int, token_name: str) -> str | None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if len(token_name) < 1:
raise ValueError("Token name has to be at least 1 character long")
if len(token_name) > 16:
raise ValueError("Token name has to be at most 16 characters long")
if not token_name.isalnum():
raise ValueError("Token name has to be alphanumeric")
token_name_list = User.get_token_names(db_path, userid)
if len(token_name_list) >= 5:
raise ValueError("One user can have at most 5 tokens")
if token_name in token_name_list:
raise ValueError("Token name is not unique")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
# Try 10 times in case of collisions
for _ in range(10):
token = urandom(32).hex()
token_hash = sha256(token.encode("utf-8")).hexdigest()
try:
cursor.execute('''
INSERT INTO Tokens (tkhash, userid, name) VALUES (?, ?, ?)
''', (token_hash, userid, token_name))
except sqlite3.IntegrityError:
continue
return token
return None
def revoke_token(db_path: str, userid: int, token_name: str) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if token_name not in User.get_token_names(db_path, userid):
raise ValueError("Token name not found")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
DELETE FROM Tokens
WHERE userid = ?
AND name = ?
''', (userid, token_name))
def get_password_hash(db_path: str, userid: int) -> tuple[str, str]:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT pwsalt, pwhash FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()
def set_password(db_path: str, userid: int, password: str) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if len(password) < 8:
raise ValueError("Password has to be at least 8 characters long")
if len(password) > 128:
raise ValueError("Password has to be at most 128 characters long")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
pwsalt = urandom(16).hex()
pwhash = sha256( (pwsalt + password).encode('utf-8') ).hexdigest()
cursor.execute('''
UPdATE Users
SET pwsalt = ?,
pwhash = ?
WHERE userid = ?
''', (pwsalt, pwhash, userid))
def get_email(db_path: str, userid: int) -> str:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
SELECT email FROM Users
WHERE userid = ?
''', (userid, ))
return cursor.fetchone()[0]
def set_email(db_path: str, userid: int, email: str | None) -> None:
if not User.exists(db_path, userid):
raise ValueError(f"User {userid} does not exist")
if email is not None:
email = email.strip()
if len(email) > 128:
raise ValueError("Email has to be at most 128 characters long, be reasonable")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
cursor.execute('''
UPDATE Users
SET email = ?
WHERE userid = ?
''', (email, userid, ))
# Collection of getters
class Token:
def exists(db_path: str, token: str) -> bool:
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
tkhash = sha256(token.encode('utf-8')).hexdigest()
cursor.execute('''
SELECT COUNT(*) FROM Tokens
WHERE tkhash = ?
''', (tkhash, ))
return cursor.fetchone()[0] == 1
def get_user(db_path: str, token: str) -> int:
if not Token.exists(db_path, token):
raise ValueError("Token does not exist")
with sqlite3.connect(db_path) as connection:
cursor = connection.cursor()
tkhash = sha256(token.encode('utf-8')).hexdigest()
cursor.execute('''
SELECT userid FROM Tokens
WHERE tkhash = ?
''', (tkhash, ))
return cursor.fetchone()[0]
+312 -17
View File
@@ -1,19 +1,27 @@
from flask import Flask, jsonify, request from flask import Flask, jsonify, request, render_template, redirect, session, flash
import auth import auth
import db
import re import re
from os import environ, urandom
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, EmailField, HiddenField
from wtforms.validators import DataRequired
from flask_wtf.csrf import CSRFProtect
HOSTS_PATH = ( environ["HOSTS"] if "HOSTS" in environ else"/app/data/hosts.d/" ) + "/izbi"
DB_PATH = environ["DB_PATH"] if "DB_PATH" in environ else "/app/data/sqlite.db"
HOSTS_PATH = "/app/hosts.d/izbi"
DB_PATH = "/app/sqlite.db"
app = Flask(__name__) app = Flask(__name__)
app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "memcached"
app.config["PERMANENT_SESSION_LIFETIME"] = 3600
app.config["SECRET_KEY"] = environ["SECRET_KEY"] if "SECRET_KEY" in environ else urandom(32).hex()
csrf = CSRFProtect(app)
hosts = {} hosts = {}
# Update hosts file
def update_hosts(path: str, hosts: dict = {}) -> bool: def update_hosts(path: str, hosts: dict = {}) -> bool:
try: try:
hosts_str = "" hosts_str = ""
@@ -28,19 +36,304 @@ def update_hosts(path: str, hosts: dict = {}) -> bool:
return False return False
# check if string is a valid IP
def validate_ip(ip: str) -> bool: def validate_ip(ip: str) -> bool:
exp = r"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$" exp = r"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"
return re.search(exp, ip) return re.search(exp, ip)
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
@app.route('/', methods=["GET"]) # Serve homepage
@app.route('/', methods=["GET", "POST"])
def index():
if session.get("USERID") is not None:
return redirect("/dashboard")
login_form = LoginForm()
if login_form.validate_on_submit():
username = login_form.username.data
password = login_form.password.data
userid = auth.user_login(DB_PATH, username, password)
if userid is None:
flash("Invalid user or password")
return redirect("/")
session["USERID"] = userid
return redirect("/dashboard")
return render_template("index.html", login_form=login_form)
class LogoutForm(FlaskForm):
pass
class GenerateTokenForm(FlaskForm):
token_name = StringField('token-name', validators=[DataRequired()])
class RevokeTokenForm(FlaskForm):
token_name = StringField('token-name', validators=[DataRequired()])
class ChangePasswordForm(FlaskForm):
password = PasswordField('password', validators=[DataRequired()])
new_password = PasswordField('new-password', validators=[DataRequired()])
repeat_password = PasswordField('repeat-password', validators=[DataRequired()])
class CreateUserForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
domains = StringField('domains')
password = PasswordField('password', validators=[DataRequired()])
is_admin = BooleanField('is-admin')
email = EmailField('email')
class UpdateUserForm(FlaskForm): #TODO
pass
class DeleteUserForm(FlaskForm):
userid = HiddenField('userid', validators=[DataRequired()])
# Display dashboard for logged in users
@app.route('/dashboard', methods=["GET"])
def dashboard():
if session.get("USERID") is None:
return redirect("/")
logout_form = LogoutForm()
generate_token_form = GenerateTokenForm()
revoke_token_form = RevokeTokenForm()
change_password_form = ChangePasswordForm()
userid = session.get("USERID")
username = db.User.get_username(DB_PATH, userid)
domains = db.User.get_domains(DB_PATH, userid)
tokens = db.User.get_token_names(DB_PATH, userid)
# If user is admin, display additional options
if db.User.is_admin(DB_PATH, userid):
create_user_form = CreateUserForm()
update_user_form = UpdateUserForm()
delete_user_form = DeleteUserForm()
users = db.list_users(DB_PATH)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form, create_user_form=create_user_form, update_user_form=update_user_form, delete_user_form=delete_user_form,
username=username, domains=domains, tokens=tokens, is_admin=True, users=users
)
return render_template("dashboard.html",
logout_form=logout_form, generate_token_form=generate_token_form, revoke_token_form=revoke_token_form, change_password_form=change_password_form,
username=username, domains=domains, tokens=tokens, is_admin=False
)
# Log out by clearing session data
@app.route('/logout', methods=["POST"])
def logout():
logout_form = LogoutForm()
if logout_form.validate_on_submit():
session.clear()
return redirect("/")
# Generate new access token with a name
@app.route('/generate-token', methods=["POST"])
def generate_token():
if session.get("USERID") is None:
return redirect("/")
generate_token_form = GenerateTokenForm()
if generate_token_form.validate_on_submit():
token_name = generate_token_form.token_name.data
try:
token = db.User.generate_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
token = None
if token is not None:
flash(token)
return redirect("/dashboard")
# Revoke token by name
@app.route('/revoke-token', methods=["POST"])
def revoke_token():
if session.get("USERID") is None:
return redirect("/")
revoke_token_form = RevokeTokenForm()
if revoke_token_form.validate_on_submit():
token_name = revoke_token_form.token_name.data
try:
db.User.revoke_token(DB_PATH, session.get("USERID"), token_name)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
# Change password (by user)
@app.route('/change-password', methods=["POST"])
def change_password():
if session.get("USERID") is None:
return redirect("/")
change_password_form = ChangePasswordForm()
if change_password_form.validate_on_submit():
password = change_password_form.password.data
new_password = change_password_form.new_password.data
repeat_password = change_password_form.repeat_password.data
if new_password != repeat_password:
flash("Passwords do not match")
return redirect("/dashboard")
try:
auth.change_user_password(DB_PATH, session.get("USERID"), password, new_password)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
flash("Password changed successfully")
return redirect("/dashboard")
# Create new user (by admin)
@app.route('/create-user', methods=["POST"])
def create_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
create_user_form = CreateUserForm()
if create_user_form.validate_on_submit():
username = create_user_form.username.data.strip()
password = create_user_form.password.data
domains = create_user_form.domains.data
if domains is None:
domains = []
else:
# Collapse whitespaces
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
is_admin = create_user_form.is_admin.data
email = create_user_form.email.data
if email is not None:
if email == "":
email = None
try:
db.User.new(DB_PATH, username, password, domains, is_admin, email)
except ValueError as e:
flash(str(e))
return redirect("/dashboard")
# Update user's field (by admin)
@app.route('/update-user', methods=["POST"])
def update_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
update_user_form = UpdateUserForm()
if not update_user_form.validate_on_submit():
return redirect("/dashboard")
userid = request.form.get("userid")
if userid is None:
flash("UserID not supplied")
return redirect("/dashboard")
if not db.User.exists(DB_PATH, userid):
flash("User does not exist")
return redirect("/dashboard")
domains = request.form.get('domains')
if domains is not None:
collapse_exp = re.compile(r'\s+')
domains = collapse_exp.sub(' ', domains.strip())
domains = domains.split(" ")
try:
db.User.set_domains(DB_PATH, userid, domains)
except ValueError as e:
flash(str(e))
password = request.form.get("password")
if password is not None:
try:
db.User.set_password(DB_PATH, userid, password)
except ValueError as e:
flash(str(e))
is_admin = request.form.get("is_admin")
if is_admin is not None:
is_admin = is_admin == "true"
db.User.set_admin(DB_PATH, userid, is_admin)
email = request.form.get("email")
if email is not None:
if email == "":
email = None
db.User.set_email(DB_PATH, userid, email)
return redirect("/dashboard")
# Delete user (by admin)
@app.route('/delete-user', methods=["POST"])
def delete_user():
if session.get("USERID") is None:
return redirect("/")
if not db.User.is_admin(DB_PATH, session.get("USERID")):
return redirect("/dashboard")
delete_user_form = DeleteUserForm()
if delete_user_form.validate_on_submit():
userid = delete_user_form.userid.data
if not db.User.exists(DB_PATH, userid):
flash("User does not exst")
return redirect("/dashboard")
db.User.delete(DB_PATH, userid)
return redirect("/dashboard")
# API health endpoint ???
@app.route('/health', methods=["GET"])
def health(): def health():
return jsonify({"status": "healthy", "comment": ""}) return jsonify({"status": "healthy", "comment": ""})
# API for updating domain's IP
@app.route('/update', methods=["GET"]) @app.route('/update', methods=["GET"])
def update_addr(): def update_addr():
try: try:
@@ -50,10 +343,10 @@ def update_addr():
if 'ip' not in request.args: if 'ip' not in request.args:
ip = request.remote_addr ip = request.remote_addr
elif not validate_ip(request.args['ip']): elif validate_ip(request.args['ip']):
return jsonify({"status": "400", "code": "ip-error", "comment": "Invalid IP"}), 400
else:
ip = request.args['ip'] ip = request.args['ip']
else:
return jsonify({"status": "400", "code": "ip-error", "comment": "Invalid IP"}), 400
if 'token' not in request.args: if 'token' not in request.args:
return jsonify({"status": "400", "code": "token-error", "comment": "Missing or invalid token"}), 400 return jsonify({"status": "400", "code": "token-error", "comment": "Missing or invalid token"}), 400
@@ -65,6 +358,7 @@ def update_addr():
success = True success = True
# Update hosts file if something changed
if domain not in hosts or ip != hosts[domain]: if domain not in hosts or ip != hosts[domain]:
hosts[domain] = ip hosts[domain] = ip
success = update_hosts(HOSTS_PATH, hosts) success = update_hosts(HOSTS_PATH, hosts)
@@ -73,10 +367,11 @@ def update_addr():
return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500 return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500
return jsonify({"status": "200", "code": "ok", "comment": "Updated successfully"}) return jsonify({"status": "200", "code": "ok", "comment": "Updated successfully"})
except: except Exception as e:
print(e, flush=True)
return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500 return jsonify({"status": "500", "code": "internal-server-error", "comment": "An error occurred while processing request"}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(host="0.0.0.0", port=8080, debug=True) db.init(DB_PATH)
app.run(host="0.0.0.0", port=8080, debug=False)
+55
View File
@@ -0,0 +1,55 @@
<h2>Administration</h2>
<p>Users:
<table>
<tr><th>Username</th><th>Domains</th><th>Password</th><th>Admin</th><th>Email</th><th>Delete</th></tr>
<form method="POST" action="/create-user"><tr>
{{ create_user_form.hidden_tag() }}
<td>{{ create_user_form.username(placeholder="*Username") }}</td>
<td>{{ create_user_form.domains(placeholder="Space separated domains") }}</td>
<td>{{ create_user_form.password(placeholder="*Password") }}</td>
<td>{{ create_user_form.is_admin() }}</td>
<td>{{ create_user_form.email(placeholder="Email") }}</td>
<td><button type="submit">Add</button></td>
</tr></form>
{% for user in users %}
<tr>
<td>{{ user["username"] }}</td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="text" name="domains" placeholder="space separated domains" value="{{ user["domains"] | join(' ') }}" />
<button type="submit">update domains</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="password" name="password" placeholder="*password" required />
<button type="submit">update password</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="hidden" name="is_admin" value="{% if user["is_admin"] %}false{% else %}true{% endif %}" />
<input type="checkbox" disabled {% if user["is_admin"] %}checked{% endif %} />
<button type="submit">toggle admin</button>
</form></td>
<td><form method="POST" action="/update-user">
{{ update_user_form.hidden_tag() }}
<input type="hidden" name="userid" value="{{ user["userid"] }}" />
<input type="email" name="email" placeholder="email" value="{{ user["email"] if user["email"] }}"/>
<button type="submit">update email</button>
</form></td>
<td><form method="POST" action="/delete-user">
{{ delete_user_form.csrf_token }}
{{ delete_user_form.userid(value=user["userid"]) }}
<button type="submit">Delete</button>
</form></td>
</tr>
{% endfor %}
</table>
</p>
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - IZBI DNS</title>
</head>
<body>
<h2>Hello {{ username }}</h2>
<form method="POST" action="/logout">
{{ logout_form.hidden_tag() }}
<button type="submit">Log&nbsp;Out</button>
</form>
{% with messages = get_flashed_messages()%}
{% if messages%}
{% for message in messages%}
<h3>{{message}}</h3>
{%endfor%}
{%endif%}
{%endwith%}
<p>Your domains:
<ul>{% for domain in domains %}
<li>{{ domain }}</li>
{% endfor %}
</ul>
</p>
<p>Your tokens:
<form method="POST" action="/generate-token">
{{ generate_token_form.hidden_tag() }}
{{ generate_token_form.token_name(placeholder="Token Name") }}
<button type="submit">New&nbsp;Token</button>
</form>
<ul>{% for token in tokens %}
<li>
<form method="POST" action="/revoke-token">
{{ revoke_token_form.hidden_tag() }}
{{ revoke_token_form.token_name(placeholder="Token Name", readonly=True, value=token) }}
<button type="submit">Revoke</button>
</form>
</li>
{% endfor %}
</ul>
</p>
<p>Password change
<form method="POST" action="/change-password">
{{ change_password_form.hidden_tag() }}
{{ change_password_form.password(placeholder="Old Password") }}<br />
{{ change_password_form.new_password(placeholder="New Password") }}<br />
{{ change_password_form.repeat_password(placeholder="Repeat Password") }}<br />
<button type="submit">Change Password</button>
</form>
</p>{% if is_admin %}{% include 'admin.html' %}{% endif %}
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>IZBI DNS</title>
</head>
<body>
<p>Dynamic DNS service for <span style="font-family: monospace">.izbi</span> domains <span style="font-family: monospace">:3</span></p>
{% with messages = get_flashed_messages()%}
{% if messages%}
{% for message in messages%}
<h3>{{message}}</h3>
{%endfor%}
{%endif%}
{%endwith%}
<form method="POST" action="/">
{{ login_form.hidden_tag() }}
{{ login_form.username(placeholder="Username", autofocus=True) }}<br />
{{ login_form.password(placeholder="Password") }}<br />
<button type="submit">log in</button>
</form>
</body>
</html>
+7 -1
View File
@@ -3,6 +3,12 @@ services:
image: izbi-dns image: izbi-dns
build: . build: .
restart: unless-stopped restart: unless-stopped
environment:
- HOSTS_PATH=/app/data/hosts.d/
- DB_PATH=/app/data/sqlite.db
- SECRET_KEY=CHANGEME
ports: ports:
- "127.0.0.1:8080:8080" - "127.0.0.1:8080:8080"
- "127.0.0.1:55:5353/udp" - "53:5353/udp"
volumes:
- "./data/:/app/data/"
+1
View File
@@ -1 +1,2 @@
flask flask
Flask-WTF
+3 -1
View File
@@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
/usr/sbin/dnsmasq -d --hostsdir /app/hosts.d/ -p 5353 & mkdir -p "$HOSTS_PATH"
/usr/sbin/dnsmasq -d --hostsdir "$HOSTS_PATH" -p 5353 &
/usr/local/bin/python3 /app/main.py & /usr/local/bin/python3 /app/main.py &