def https_disable(): certs = Certificates() if not certs.set_apache_config(ssl=False): return error("failed disabling ssl for apache") cfg["config"]["https_port"] = None cfg.save() # we need to "re-wire" the proxy to port 80 on activated TLS add_msg = "" if cfg["config"]["proxy_active"]: subdomain = cfg["config"]["proxy_domain"].split(".")[0] scheme = "http" token = cfg["config"]["nk_token"] proxy_tunnel = ProxyTunnel() try: proxy_port = proxy_tunnel.setup(token, subdomain, scheme) except ProxySetupError as e: log.error(exc_info=e) add_msg = "(but register at proxy-server error: " + repr(e) + ")" except Exception as e: log.error(exc_info=e) add_msg = "(unexpected error during proxy setup)" cfg["config"]["proxy_port"] = proxy_port cfg.save() return success("HTTPS disabled " + add_msg)
def backup(): devs = partitions.backup_devices data = { "devices": devs, "backups": backup_restore.find_backups([dev["path"] for dev in devs]), "last_backup": cfg["config"]["last_backup"], } return success(data=data)
def https_enable(): board.set("tls", { "what": "enable", "state": "pending", }) job_queue.put("EnableHTTPS") return success("HTTPS enabling pending")
def test_http(): nc = Nextcloud() res, req = nc.check_reachability() data = yaml.load(res.text) data["data"]["ipv4"] = [req["ipv4"], req["domain"]] data["data"]["ipv6"] = [ "[" + req["ipv6"] + "]" if req["ipv6"] else "", req["domain"] ] return success(data["msg"][0], data=data["data"])
def https(): dct = { "domain": cfg.get("config", {}).get("domain"), "email": cfg.get("config", {}).get("email"), "https": cfg["config"]["https_port"] is not None, "dns_mode": cfg["config"]["dns_mode"] } return success(data=dct)
def certs_clear(): if cfg["config"]["https_port"]: return error("cannot clear certificates while HTTPS is active") certs = Certificates() certs.clear_certs() return success("Cleared all certificates")
def system_settings(): if request.method == "GET": return success( data={ "log_lvl": cfg["config"]["log_lvl"], "expert_mode": cfg["config"]["expert_mode"] }) elif request.method == "POST": pass
def umount_storage(name): if ".." in name or "/" in name: return error("invalid name") mount_target = f"/media/{name}" if mount_target not in partitions.mounted: return error("not mounted") if partitions.umount_partition(mount_target): return success("Unmounting successful") else: return error("Failed unmount, check logs...")
def ssh_set(): auth_p_dir = Path("/home/nextuser/.ssh") if not auth_p_dir.exists(): os.makedirs(auth_p_dir.as_posix()) auth_p = auth_p_dir / "authorized_keys" if request.method == "GET": ip = local_ip() if not auth_p.exists(): return success(data={"pubkey": "", "local_ip": ip}) with auth_p.open() as fd: return success(data={"pubkey": fd.read(), "local_ip": ip}) elif request.method == "POST": pubkey = request.form.get("pubkey") with auth_p.open("w") as fd: fd.write(pubkey.strip() + "\n") log.info(f"setting ssh pub key: {pubkey}") return success()
def getcerts(): dct = { "cert": None, "domain": cfg["config"]["domain"], } if dct.get("domain"): certs = Certificates() my_cert = certs.get_cert(dct.get("domain")) if my_cert: dct["cert"] = my_cert return success(data=dct)
def restore_start(): src_path = request.form.get("src_path") if not backup_restore.check_backup(src_path): msg = "Invalid backup, cannot restore" log.error(msg) return error(msg) log.info(f"Initiating restore from: {src_path}") job_kwargs = {"tar_path": src_path, "mode": "restore"} job_queue.put(("BackupRestore", job_kwargs)) return success("restore started")
def get_logs(): log_dir = Path("/srv/logdump") journal_logs = { "nextbox-daemon": "journald.nextbox-daemon.log", "nextbox-compose": "journald.nextbox-compose.log" } cmd_dump_journalctl = "journalctl --no-pager -n 1000 -xeu {daemon} > {log_dir}/{filename}" var_logfiles = [ "/var/log/dpkg.log", "/var/log/unattended-upgrades/unattended-upgrades.log", "/var/log/unattended-upgrades/unattended-upgrades-dpkg.log", "/var/log/unattended-upgrades/unattended-upgrades-shutdown.log", "/var/log/kern.log", "/var/log/letsencrypt/letsencrypt.log", LOG_FILENAME ] logfiles = [] # cleanup logdump dir shutil.rmtree(log_dir.as_posix()) os.makedirs(log_dir.as_posix()) for unit, fn in journal_logs.items(): cmd = cmd_dump_journalctl.format(daemon=unit, filename=fn, log_dir=log_dir) CommandRunner(cmd, block=True, shell=True) logfiles.append(log_dir / fn) for path in var_logfiles: if Path(path).exists(): shutil.copy(path, log_dir.as_posix()) logfiles.append(log_dir / Path(path).name) hash_file = Path(log_dir) / "sha256.txt" CommandRunner(f"sha256sum {log_dir.as_posix()}/* > {hash_file.as_posix()}", shell=True, block=True) logfiles.append(hash_file) zip_path = "/srv/nextbox-logs.zip" with ZipFile(zip_path, "w") as fd: for path in logfiles: fd.write(path, path.name) with open(zip_path, "rb") as fd: return success(data={"zip": b64encode(fd.read())})
def register_proxy(): # assemble data for key in request.form: if key == "nk_token": token = request.form.get(key) elif key == "proxy_domain": proxy_domain = request.form.get(key) subdomain = proxy_domain.split(".")[0] scheme = "https" if cfg["config"]["https_port"] else "http" proxy_tunnel = ProxyTunnel() try: proxy_port = proxy_tunnel.setup(token, subdomain, scheme) except ProxySetupError as e: cfg["config"]["proxy_active"] = False cfg.save() log.error(str(e)) return error(str(e)) except Exception as e: cfg["config"]["proxy_active"] = False cfg.save() msg = "unexpected error during proxy setup" log.error(msg, exc_info=e) return error(msg) # configure nextcloud nc = Nextcloud() try: nc.set_config("overwriteprotocol", "https") nc.set_config("overwritecondaddr", "^172\\.18\\.238\\.1$") nc.set_config("trusted_proxies", ["172.18.238.1"]) except NextcloudError as e: cfg["config"]["proxy_active"] = False cfg.save() msg = "could not configure nextcloud for proxy usage" log.error(msg, exc_info=e) return error(msg) cfg["config"]["proxy_domain"] = proxy_domain cfg["config"]["proxy_active"] = True cfg["config"]["proxy_port"] = proxy_port cfg.save() # ensure trusted domains are set job_queue.put("TrustedDomains") return success("Proxy successfully registered")
def get_status(): # add pkginfo if not yet inside status-board if not board.contains_key("pkginfo"): board.set("pkginfo", {"version": nextbox_version()}) keys = board.get_keys() # update ips, not more often than 1min if board.is_older_than("ips", 60): dns = DNSManager() board.set("ips", {"ipv4": dns.get_ipv4(), "ipv6": dns.get_ipv6()}) # return all board data keys = board.get_keys() status_data = {key: board.get(key) for key in keys} return success(data=status_data)
def test_proxy(): domain = cfg["config"]["proxy_domain"] what = "https" url = f"{what}://{domain}" if not domain: return error("no proxy-domain set") out = {"result": None, "domain": domain} try: res = requests.get(url, timeout=2) out["result"] = True out["nextcloud"] = "Nextcloud" in res.text except (requests.exceptions.ConnectionError, requests.exceptions.SSLError): out["result"] = False out["nextcloud"] = False return success("tested proxy for reachability", data=out)
def mount_storage(device, name=None): devs = partitions.block_devices mounted = partitions.mounted found = False for dev in devs.values(): for part in dev["parts"].values(): if part["name"] == device: found = True if part["mounted"]: return error("device already mounted") if part["special"]: return error("cannot mount special device") if not found: return error("Could not find device!") if name is None: for idx in range(1, 11): _name = f"extra-{idx}" mount_target = f"/media/{_name}" if mount_target not in mounted: name = _name break if name is None: return error("cannot determine mount target, too many mounts?") if ".." in device or "/" in device: return error("invalid device") if ".." in name or "/" in name: return error("invalid name") mount_target = f"/media/{name}" mount_device = f"/dev/{device}" if not os.path.exists(mount_target): os.makedirs(mount_target) if partitions.mount_partition(mount_device, mount_target): return success("Mounting successful") else: return error("Failed mounting, check logs...")
def dyndns_register(): data = {} for key in request.form: if key in ["domain", "email"]: data[key] = request.form.get(key) data["password"] = None headers = {"Content-Type": "application/json"} req = urllib.request.Request(DYNDNS_DESEC_REGISTER, method="POST", data=json.dumps(data).encode("utf-8"), headers=headers) try: res = urllib.request.urlopen(req).read().decode("utf-8") except urllib.error.HTTPError as e: desc = e.read() return error(f"Could not complete registration", data=json.loads(desc)) return success(data=json.loads(res))
def test_ddclient(): cr = CommandRunner([DDCLIENT_BIN, "-verbose", "-foreground", "-force"], block=True) cr.log_output() for line in cr.output: if "SUCCESS:" in line: return success("DDClient test: OK") if "Request was throttled" in line: pat = "available in ([0-9]*) seconds" try: waitfor = int(re.search(pat, line).groups()[0]) + 5 except: waitfor = 10 return error("DDClient test: Not OK", data={ "reason": "throttled", "waitfor": waitfor }) return error("DDClient test: Not OK", data={"reason": "unknown"})
def backup_start(): tar_path = request.form.get("tar_path") found = False for dev in partitions.backup_devices: if tar_path.startswith(dev["path"]): found = dev break if not found: msg = "Invalid backup location provided" log.error(msg) return error(msg) log.info( f"Initiating backup onto: {dev['friendly_name']} @ {dev['path']} with target: {tar_path}" ) job_kwargs = {"tar_path": tar_path, "mode": "backup"} job_queue.put(("BackupRestore", job_kwargs)) return success("backup started")
def test_resolve4(): ip_type = request.path.split("/")[-1] domain = cfg["config"]["domain"] resolve_ip = None ext_ip = None if not domain: return error("no domain is set") dns = DNSManager() dns.clear_dns_caches() if ip_type == "ipv4": resolve_ip = dns.resolve_ipv4(domain) ext_ip = dns.get_ipv4() else: resolve_ip = dns.resolve_ipv6(domain) ext_ip = dns.get_ipv6() log.info( f"resolving '{domain}' to IP: {resolve_ip}, external IP: {ext_ip}") data = {"ip": ext_ip, "resolve_ip": resolve_ip, "domain": domain} # if not both "resolve" and "getip" are successful, we have failed if resolve_ip is None or ext_ip is None: log.error(f"failed resolving and/or getting external {ip_type}") return error("Resolve test: Not OK", data=data) # resolving to wrong ip if resolve_ip != ext_ip: log.warning(f"Resolved {ip_type} does not match external {ip_type}") log.warning("This might indicate a bad DynDNS configuration") return error("Resolve test: Not OK", data=data) # all good! return success("Resolve test: OK", data=data)
def backup_status_clear(): board.delete_key("backup_restore") return success()
def storage(): return success(data=partitions.block_devices)
def show_log(num_lines=50): ret = tail(LOG_FILENAME, num_lines) return error(f"could not read log: {LOG_FILENAME}") if ret is None \ else success(data=ret[:-1])
def handle_config(): if request.method == "GET": data = dict(cfg["config"]) try: data["conf"] = Path(DDCLIENT_CONFIG_PATH).read_text("utf-8") except FileNotFoundError: data["conf"] = "" return success(data=data) # save dyndns related values to configuration elif request.method == "POST": run_jobs = [] for key in request.form: val = request.form.get(key) # special config-value 'conf' represents ddclient-config-contents if key == "conf": old_conf = Path(DDCLIENT_CONFIG_PATH).read_text("utf-8") if old_conf != val: log.info("writing ddclient config and restarting service") Path(DDCLIENT_CONFIG_PATH).write_text(val, "utf-8") elif len(val.strip()) == 0: log.info("writing empty ddclient config") Path(DDCLIENT_CONFIG_PATH).write_text(val, "utf-8") services.stop("ddclient") services.disable("ddclient") if len(val.strip()) > 0: services.enable("ddclient") services.restart("ddclient") run_jobs.append("DynDNSUpdate") elif key in AVAIL_CONFIGS and val is not None: # only allow valid DYNDNS_MODES if key == "dns_mode" and val not in DYNDNS_MODES: log.warning( f"key: 'dns_mode' has invalid value: {val} - skipping") continue # start DynDNS update on "desec_done" elif key == "dns_mode" and val == "desec_done": run_jobs.append("DynDNSUpdate") # start TrustedDomains update on new domain elif "domain" in key: run_jobs.append("TrustedDomains") # deactivate proxy on request elif key == "proxy_active" and val.lower() == "false": proxy_tunnel = ProxyTunnel() proxy_tunnel.stop() # skip if 'val' is empty elif val is None: log.debug(f"skipping key: '{key}' -> no value provided") continue # convert to bool, ugly? if val.lower() in ["true", "false"]: val = val.lower() == "true" # put key-value into cfg and save (yes, saving each value) cfg["config"][key] = val log.debug(f"saving key: '{key}' with value: '{val}'") cfg.save() # run jobs collected during configuration update if len(run_jobs) > 0: for job in run_jobs: job_queue.put(job) return success("DynDNS configuration saved")
def service_operation(name, operation): if not services.check(name, operation): return error("not allowed") dct = services.exec(name, operation) return success(data=dct)
def poweroff(): log.info("POWER-OFF - by /poweroff request") cr = CommandRunner("poweroff") if cr.returncode != 0: return error("failed executing: 'poweroff'") return success(data={})
def reboot(): log.info("REBOOTING NOW - by /reboot request") cr = CommandRunner("reboot") if cr.returncode != 0: return error("failed executing: 'reboot'") return success(data={})
def backup_status(): return success(data=board.get("backup_restore"))