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/join/%23public%3abutterbox.local"}) 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/') 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/') 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)