248 lines
11 KiB
Python
248 lines
11 KiB
Python
from app import app
|
|
from flask import render_template, flash, redirect, url_for, send_file
|
|
from app.forms import LoginForm, SettingsForm
|
|
from flask_login import login_user, current_user, logout_user, login_required
|
|
import sqlalchemy as sa
|
|
from app import db
|
|
from app.models import User, Setting
|
|
import os
|
|
import json
|
|
from flask_babel import _
|
|
import base64
|
|
import random
|
|
from wordfreq import top_n_list
|
|
import secrets
|
|
import string
|
|
import glob
|
|
import time
|
|
import qrcode
|
|
|
|
CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname', 'ssh_access_settings', 'root_account_settings', 'root_password']
|
|
|
|
|
|
def gen_username() -> str:
|
|
words = top_n_list("en", 5000)
|
|
prefix = random.randint(1000, 9999)
|
|
return f"{random.choice(words)}{random.choice(words)}{prefix}"
|
|
|
|
def gen_password() -> str:
|
|
characters = string.ascii_letters + string.digits
|
|
password = ''.join(secrets.choice(characters) for i in range(20))
|
|
return password
|
|
def get_file_suffix(path: str) -> str:
|
|
return os.path.splitext(path)[1].strip('.') # in case of multiple exts, like .tar.gz, it only splits on the last one
|
|
|
|
def get_file_icon_url(path: str) -> str:
|
|
if os.path.isdir(path):
|
|
return url_for("static", filename=f"images/extension-icons/directory.svg")
|
|
suffix = get_file_suffix(path)
|
|
if suffix:
|
|
if suffix in ['apk', 'deb', 'dmg', 'exe', 'jpg', 'mp3', 'pbf', 'pdf', 'png',
|
|
'ppt', 'pptx']:
|
|
return url_for("static", filename=f"images/extension-icons/ext-{suffix}.svg")
|
|
return url_for("static", filename=f"images/extension-icons/ext-unknown.svg")
|
|
|
|
def get_last_modified(path: str) -> str:
|
|
mod_time = time.ctime(os.path.getmtime(path))
|
|
#print(mod_time)
|
|
#dt = datetime.strptime(mod_time, "%a %b %d %H:%M:%S %Y")
|
|
#formatted_time = dt.strftime("%Y-%m-%d %H:%M")
|
|
return mod_time
|
|
|
|
def get_files_in_path(path: str):
|
|
file_list = []
|
|
if os.path.exists(path):
|
|
list_of_files = glob.glob(path + "/*", recursive=True)
|
|
file_list = [
|
|
{"name": x.replace(path, "").strip("/"), "path": x, "is_file": os.path.isfile(x),
|
|
"is_dir": os.path.isdir(x),
|
|
"filetype": get_file_suffix(x), "last_modified": get_last_modified(x),
|
|
"icon_url": get_file_icon_url(x),
|
|
"relative_path": x.replace(app.config["BUTTERBOX_USB_PATH"], "")} for x
|
|
in list_of_files]
|
|
print(file_list)
|
|
return file_list
|
|
|
|
def get_setting(name) -> str:
|
|
setting = db.session.scalar(sa.select(Setting).where(Setting.key == name))
|
|
return str(setting.value)
|
|
|
|
def set_setting(name: str, value: str):
|
|
setting = db.session.scalar(sa.select(Setting).where(Setting.key == name))
|
|
setting.value = value
|
|
db.session.add(setting)
|
|
|
|
def dump_settings(filename: str) -> None:
|
|
settings = db.session.execute(sa.select(Setting)).scalars().all()
|
|
settings_dict = {s.key: s.value for s in settings}
|
|
#settings_dict.pop('admin_password')
|
|
with open(filename, "w") as f:
|
|
json.dump(settings_dict, f, indent=4)
|
|
@app.route('/')
|
|
@app.route('/index')
|
|
def index():
|
|
enable_chat = get_setting("enable_chat")
|
|
enable_app_store = get_setting("enable_app_store")
|
|
enable_map_viewer = get_setting("enable_map_viewer")
|
|
enable_file_viewer = get_setting("enable_file_viewer")
|
|
enable_deltachat = get_setting("enable_deltachat")
|
|
enable_wifi_sharing = get_setting("enable_wifi_sharing")
|
|
service_array = []
|
|
usb_inserted = False # actual test of whether USB is inserted
|
|
if os.path.exists(app.config["BUTTERBOX_USB_PATH"]):
|
|
usb_inserted = True
|
|
usb_has_maps = False # actual test of whether USB has maps folder
|
|
usb_has_appstore = False # actual test of whether USB has an appstore
|
|
if enable_wifi_sharing == 'true':
|
|
service_array.append({"name": "Share WiFi", "image": url_for("static", filename="images/share-icon.svg"), "url": url_for("share")})
|
|
if enable_deltachat == 'true':
|
|
service_array.append({"name": "Secure Messaging", "image": url_for("static", filename="images/deltachat-icon.svg"), "url": url_for("messaging") })
|
|
if enable_chat == 'true':
|
|
service_array.append({"name": "Message Board", "image": url_for("static", filename="images/chat-icon.png"), "url": f"{app.config["CONVENE_INSTALL_PATH"]}/#/room/%23public%3a{get_setting('butterbox_hostname')}.lan"})
|
|
if enable_app_store == 'true' and usb_has_appstore:
|
|
service_array.append({"name": "Apps", "image": url_for("static", filename="images/appstore-icon.svg")})
|
|
if enable_map_viewer == 'true' and usb_has_maps:
|
|
service_array.append({"name": "Offline Maps", "image": url_for("static", filename="images/maps-icon.png")})
|
|
if enable_file_viewer == 'true':
|
|
name = "Files"
|
|
if not usb_inserted:
|
|
name = "Insert USB to browse files"
|
|
service_array.append({
|
|
"name": name,
|
|
"image": url_for("static", filename="images/explore-icon.svg"),
|
|
"url": url_for("files", path=""),})
|
|
return render_template('index.html', title='Home', get_setting=get_setting, services=service_array)
|
|
|
|
@app.route('/files/', defaults={'path': ''})
|
|
@app.route('/files/<path:path>')
|
|
def files(path):
|
|
base_path = app.config["BUTTERBOX_USB_PATH"]
|
|
if not os.path.exists(base_path):
|
|
return redirect(url_for('index'))
|
|
current_path = os.path.join(base_path, path)
|
|
render_files = []
|
|
if os.path.exists(current_path):
|
|
render_files = get_files_in_path(current_path)
|
|
else:
|
|
return redirect(url_for('files', path=""))
|
|
return render_template('usb-file-viewer.html', title='File Viewer', current_path=current_path, render_files=render_files, get_setting=get_setting)
|
|
|
|
@app.route('/serve_file/<path:filepath>')
|
|
def serve_file(filepath):
|
|
full_path = os.path.realpath(filepath)
|
|
if not filepath.startswith(app.config["BUTTERBOX_USB_PATH"]):
|
|
redirect(url_for('files', path=""))
|
|
return send_file(full_path)
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('admin'))
|
|
form = LoginForm()
|
|
form.remember_me.data = False
|
|
if form.validate_on_submit():
|
|
user = db.session.scalar(sa.select(User).where(User.username == form.username.data))
|
|
if user is None or not user.check_password(form.password.data):
|
|
flash(_('Invalid username or password'))
|
|
return redirect(url_for('login'))
|
|
login_user(user)
|
|
return redirect(url_for('admin'))
|
|
return render_template('login.html', title='Sign in', form=form, get_setting=get_setting)
|
|
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
logout_user()
|
|
return redirect(url_for('admin'))
|
|
@app.route('/admin', methods=['GET', 'POST'])
|
|
@login_required
|
|
def admin():
|
|
raspap_installed = os.path.exists("/var/www/html/raspap")
|
|
form = SettingsForm()
|
|
populate_settings = ['butterbox_name', 'wifi_password', 'ssid', 'butterbox_hostname', 'root_account_settings', 'ssh_access_settings', 'root_password', 'admin_password']
|
|
bool_settings = ['enable_access_point','enable_file_viewer', 'enable_map_viewer', 'enable_app_store', 'enable_chat', 'enable_deltachat', 'enable_wifi_sharing']
|
|
populate_settings.extend(bool_settings)
|
|
if not form.is_submitted():
|
|
for s in populate_settings:
|
|
if s in bool_settings:
|
|
getattr(form, s).data = (get_setting(s) == "true")
|
|
else:
|
|
print(s, get_setting(s))
|
|
getattr(form, s).data = get_setting(s)
|
|
non_admin_settings_changed = False
|
|
if form.validate_on_submit():
|
|
if form.submit.data:
|
|
for s in populate_settings:
|
|
new_value = getattr(form, s).data
|
|
if s in bool_settings:
|
|
new_value = str(new_value).lower() # all settings are str fow now
|
|
if s == 'butterbox_hostname':
|
|
new_value = new_value.lower().replace(" ", "")
|
|
existing_value = get_setting(s)
|
|
if new_value != existing_value:
|
|
print(f"New value was changed for {s}. Existing value was {existing_value}, new value was {new_value}")
|
|
set_setting(s, new_value)
|
|
non_admin_settings_changed = True
|
|
if s in CHANGES_REQUIRING_RESTART:
|
|
app.config['SETTINGS_CHANGED'] = True
|
|
|
|
new_logo = form.butterbox_logo.data
|
|
if new_logo.filename:
|
|
logo_stream = form.butterbox_logo.data.stream
|
|
b64_logo = base64.b64encode(logo_stream.read()).decode('utf-8')
|
|
file_mimetype = form.butterbox_logo.data.mimetype
|
|
new_value = f"data:{file_mimetype};base64,{b64_logo}"
|
|
existing_value = get_setting('butterbox_logo')
|
|
if new_value != existing_value:
|
|
set_setting('butterbox_logo', new_value)
|
|
non_admin_settings_changed = True
|
|
new_admin_password = form.admin_password.data
|
|
if new_admin_password:
|
|
admin_user = db.session.scalar(sa.select(User).where(User.username == 'admin'))
|
|
if not admin_user.check_password(new_admin_password):
|
|
admin_user.set_password(new_admin_password)
|
|
db.session.add(admin_user)
|
|
non_admin_settings_changed = True
|
|
if app.config['SETTINGS_CHANGED']:
|
|
flash(_("⚠️ Some settings may not fully take effect until the Butter Box restarts. Click 'Apply Changes' to restart."))
|
|
else:
|
|
if non_admin_settings_changed:
|
|
flash(
|
|
_("Settings successfully changed."))
|
|
db.session.commit()
|
|
|
|
if form.apply_changes.data:
|
|
set_setting('apply_changes', "true")
|
|
dump_settings("settings.txt")
|
|
flash(_("⚠️ Changes applied! If needed, the system will restart. This may take up to two minutes."))
|
|
|
|
return render_template('admin.html', raspap_installed=raspap_installed, get_setting=get_setting, form=form)
|
|
|
|
|
|
@app.route('/messaging', methods=['GET', 'POST'])
|
|
def messaging():
|
|
return render_template('messaging.html', get_setting=get_setting)
|
|
|
|
@app.route("/deltachat_credentials", methods=["POST"])
|
|
def generate_random_deltachat_credentials():
|
|
ip = app.config['BUTTERBOX_DEFAULT_IP']
|
|
username = gen_username()
|
|
password = gen_password()
|
|
|
|
flash(f"Username: {username}")
|
|
flash(f"Password: {password}")
|
|
flash(f"IP: {ip}")
|
|
return redirect(url_for("messaging"))
|
|
|
|
@app.route('/share')
|
|
def share():
|
|
display_wifi_password = False
|
|
wifi_password = get_setting("wifi_password")
|
|
if wifi_password:
|
|
display_wifi_password = True
|
|
wifi_ssid = get_setting("ssid")
|
|
wifi_encryption_type = "WPA2"
|
|
img = qrcode.make(f"WIFI:T:{wifi_encryption_type};S:{wifi_ssid};P:{wifi_password};;")
|
|
img.save("app/static/images/wifi_qr_code.png")
|
|
return render_template('share.html', get_setting=get_setting, display_wifi_password=display_wifi_password)
|
|
|