From 26aa9dc1bfd85afb632d1ab5676a7f0fedbbe3f7 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Wed, 20 Sep 2023 20:12:47 +0200 Subject: [PATCH] Added repository server and APK sharing --- sbapp/Makefile | 17 +++++- sbapp/buildozer.spec | 8 +-- sbapp/main.py | 134 +++++++++++++++++++++++++++++++++++++++++ sbapp/sideband/core.py | 63 +++++++++++++++++++ sbapp/ui/layouts.py | 96 +++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 6 deletions(-) diff --git a/sbapp/Makefile b/sbapp/Makefile index da6841f..911be60 100644 --- a/sbapp/Makefile +++ b/sbapp/Makefile @@ -6,6 +6,8 @@ clean: @echo Cleaning... -(rm ./__pycache__ -r) -(rm ./app_storage -r) + -(rm ./share/pkg/* -r) + -(rm ./share/mirrors/* -r) -(rm ./bin -r) cleanlibs: @@ -53,15 +55,26 @@ else @(sleep 2) endif +fetchshare: + cp ../../dist_archive/rns-*-py3-none-any.whl ./share/pkg/ + cp ../../dist_archive/rnspure-*-py3-none-any.whl ./share/pkg/ + cp ../../dist_archive/lxmf-*-py3-none-any.whl ./share/pkg/ + cp ../../dist_archive/nomadnet-*-py3-none-any.whl ./share/pkg/ + cp ../../dist_archive/rnsh-*-py3-none-any.whl ./share/pkg/ + cp ../../dist_archive/RNode_Firmware_1.64_Source.zip ./share/pkg/ + cp -r ../../dist_archive/reticulum.network ./share/mirrors/ + cp ../../dist_archive/Reticulum\ Manual.pdf ./share/mirrors/Reticulum_Manual.pdf + cp ../../dist_archive/Reticulum\ Manual.epub ./share/mirrors/Reticulum_Manual.epub + release: buildozer android release postbuild: $(MAKE) cleanrns -apk: prepare prebake pacthfiles release postbuild +apk: prepare prebake pacthfiles fetchshare release postbuild -devapk: prepare prebake pacthfiles debug postbuild +devapk: prepare prebake pacthfiles fetchshare debug postbuild version: @(echo $$(python ./gv.py)) diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index 0758327..d5b759b 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -4,13 +4,13 @@ package.name = sideband package.domain = io.unsigned source.dir = . -source.include_exts = py,png,jpg,jpeg,ttf,kv,pyi,typed,so,0,1,2,3,atlas,frag -source.include_patterns = assets/* +source.include_exts = py,png,jpg,jpeg,webp,ttf,kv,pyi,typed,so,0,1,2,3,atlas,frag,html,css,js,whl,zip,gz,woff2,pdf,epub +source.include_patterns = assets/*,share/* source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements,precompiled/*,parked/*,./setup.py,Makef*,./Makefile,Makefile version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py -android.numeric_version = 20230912 +android.numeric_version = 20230920 # Cryptography recipe is currently broken, using RNS-internal crypto for now requirements = kivy==2.2.1,libbz2,pillow,qrcode==7.3.1,usb4a,usbserial4a @@ -25,7 +25,7 @@ android.presplash_color = #00000000 orientation = portrait fullscreen = 0 -android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT +android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT,ACCESS_NETWORK_STATE android.api = 30 android.minapi = 24 android.ndk = 25b diff --git a/sbapp/main.py b/sbapp/main.py index 43fd19d..45a09fd 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -110,6 +110,7 @@ class SidebandApp(MDApp): self.settings_ready = False self.connectivity_ready = False self.hardware_ready = False + self.repository_ready = False self.hardware_rnode_ready = False self.hardware_modem_ready = False self.hardware_serial_ready = False @@ -1673,6 +1674,139 @@ class SidebandApp(MDApp): def close_connectivity_action(self, sender=None): self.open_conversations(direction="right") + ### Repository screen + ###################################### + def repository_action(self, sender=None, direction="left"): + self.repository_init() + self.root.ids.screen_manager.transition.direction = direction + self.root.ids.screen_manager.current = "repository_screen" + self.root.ids.nav_drawer.set_state("closed") + if not RNS.vendor.platformutils.is_android(): + self.widget_hide(self.root.ids.repository_enable_button) + self.widget_hide(self.root.ids.repository_disable_button) + self.widget_hide(self.root.ids.repository_download_button) + self.root.ids.repository_info.text = "\nThe [b]Repository Webserver[/b] feature is currently only available on mobile devices." + + + self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current) + + def repository_update_info(self, sender=None): + info = "Sideband includes a small repository of useful software and guides related to the Sideband and Reticulum ecosystem. You can start this repository to allow other people on your local network to download software and information directly from this device, without needing an Internet connection.\n\n" + info += "If you want to share the Sideband application itself via the repository server, you must first download it into the local repository, using the \"Update Content\" button below.\n\n" + info += "To make the repository available on your local network, simply start it below, and it will become browsable on a local IP address for anyone connected to the same WiFi or wired network.\n\n" + if self.sideband.webshare_server != None: + if RNS.vendor.platformutils.is_android(): + def getIP(): + adrs = [] + try: + from jnius import autoclass + import ipaddress + mActivity = autoclass('org.kivy.android.PythonActivity').mActivity + SystemProperties = autoclass('android.os.SystemProperties') + Context = autoclass('android.content.Context') + connectivity_manager = mActivity.getSystemService(Context.CONNECTIVITY_SERVICE) + ns = connectivity_manager.getAllNetworks() + if not ns == None and len(ns) > 0: + for n in ns: + lps = connectivity_manager.getLinkProperties(n) + las = lps.getLinkAddresses() + for la in las: + ina = la.getAddress() + ha = ina.getHostAddress() + if not ina.isLinkLocalAddress(): + adrs.append(ha) + + except Exception as e: + RNS.log("Error while getting repository IP address: "+str(e), RNS.LOG_ERROR) + return None + + return adrs + + ips = getIP() + if ips == None or len(ips) == 0: + info += "The repository server is running, but the local device IP address could not be determined.\n\nYou can access the repository by pointing a browser to: http://DEVICE_IP:4444/" + else: + ipstr = "" + for ip in ips: + ipstr += "http://"+str(ip)+":4444/\n" + ms = "" if len(ips) == 1 else "es" + info += "The repository server is running at the following address"+ms+":\n"+ipstr + + self.root.ids.repository_enable_button.disabled = True + self.root.ids.repository_disable_button.disabled = False + + else: + self.root.ids.repository_enable_button.disabled = False + self.root.ids.repository_disable_button.disabled = True + + info += "\n" + self.root.ids.repository_info.text = info + + def repository_start_action(self, sender=None): + self.sideband.start_webshare() + Clock.schedule_once(self.repository_update_info, 1.0) + + def repository_stop_action(self, sender=None): + self.sideband.stop_webshare() + Clock.schedule_once(self.repository_update_info, 0.75) + + def repository_download_action(self, sender=None): + def update_job(sender=None): + try: + import requests + + # Get release info + apk_version = None + apk_url = None + pkgname = None + try: + release_url = "https://api.github.com/repos/markqvist/sideband/releases" + with requests.get(release_url) as response: + releases = response.json() + release = releases[0] + assets = release["assets"] + for asset in assets: + if asset["name"].lower().endswith(".apk"): + apk_url = asset["browser_download_url"] + pkgname = asset["name"] + apk_version = release["tag_name"] + RNS.log(f"Found version {apk_version} artefact {pkgname} at {apk_url}") + except Exception as e: + self.root.ids.repository_update.text = f"Downloading release info failed with the error:\n"+str(e) + return + + self.root.ids.repository_update.text = "Downloading: "+str(apk_url) + with requests.get(apk_url, stream=True) as response: + with open("./dl_tmp", "wb") as tmp_file: + cs = 32*1024 + tds = 0 + for chunk in response.iter_content(chunk_size=cs): + tmp_file.write(chunk) + tds += cs + self.root.ids.repository_update.text = "Downloaded "+RNS.prettysize(tds)+" of "+str(pkgname) + + os.rename("./dl_tmp", f"./share/pkg/{pkgname}") + self.root.ids.repository_update.text = f"Added {pkgname} to the repository!" + except Exception as e: + self.root.ids.repository_update.text = f"Downloading contents failed with the error:\n"+str(e) + + self.root.ids.repository_update.text = "Starting package download..." + def start_update_job(sender=None): + threading.Thread(target=update_job, daemon=True).start() + Clock.schedule_once(start_update_job, 0.5) + + def repository_init(self, sender=None): + if not self.repository_ready: + self.root.ids.hardware_scrollview.effect_cls = ScrollEffect + + self.repository_update_info() + + self.root.ids.repository_update.text = "" + self.repository_ready = True + + def close_repository_action(self, sender=None): + self.open_conversations(direction="right") + ### Hardware screen ###################################### def hardware_action(self, sender=None, direction="left"): diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 1d19de2..f910678 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -95,6 +95,7 @@ class SidebandCore(): self.log_verbose = verbose self.owner_app = owner_app self.reticulum = None + self.webshare_server = None self.app_dir = plyer.storagepath.get_home_dir()+"/.config/sideband" if self.app_dir.startswith("file://"): @@ -130,6 +131,7 @@ class SidebandCore(): self.log_dir = self.app_dir+"/app_storage/" self.tmp_dir = self.app_dir+"/app_storage/tmp" self.exports_dir = self.app_dir+"/exports" + self.webshare_dir = "./share/" self.first_run = True self.saving_configuration = False @@ -2056,6 +2058,67 @@ class SidebandCore(): self._db_setstate("core.started", True) RNS.log("Sideband Core "+str(self)+" started") + def stop_webshare(self): + if self.webshare_server != None: + self.webshare_server.shutdown() + self.webshare_server = None + + def start_webshare(self): + if self.webshare_server == None: + def webshare_job(): + from http import server + import socketserver + import json + + webshare_dir = self.webshare_dir + port = 4444 + class RequestHandler(server.SimpleHTTPRequestHandler): + def do_GET(self): + serve_root = webshare_dir + if "?" in self.path: + self.path = self.path.split("?")[0] + path = serve_root + self.path + if self.path == "/": + path = serve_root + "/index.html" + if "/.." in self.path: + self.send_response(403) + self.end_headers() + self.write("Forbidden".encode("utf-8")) + elif self.path == "/pkglist": + try: + self.send_response(200) + self.send_header("Content-type", "text/json") + self.end_headers() + json_result = json.dumps(os.listdir(serve_root+"/pkg")) + self.wfile.write(json_result.encode("utf-8")) + except Exception as e: + self.send_response(500) + self.end_headers() + RNS.log("Error listing directory "+str(path)+": "+str(e), RNS.LOG_ERROR) + es = "Error" + self.wfile.write(es.encode("utf-8")) + else: + try: + with open(path, 'rb') as f: + data = f.read() + self.send_response(200) + self.end_headers() + self.wfile.write(data) + except Exception as e: + self.send_response(500) + self.end_headers() + RNS.log("Error serving file "+str(path)+": "+str(e), RNS.LOG_ERROR) + es = "Error" + self.wfile.write(es.encode("utf-8")) + + with socketserver.TCPServer(("", port), RequestHandler) as webserver: + self.webshare_server = webserver + webserver.serve_forever() + self.webshare_server = None + RNS.log("Webshare server closed", RNS.LOG_DEBUG) + + threading.Thread(target=webshare_job, daemon=True).start() + def request_lxmf_sync(self, limit = None): if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 0d52841..e4839f4 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -1207,6 +1207,93 @@ MDNavigationLayout: disabled: False active: False + MDScreen: + name: "repository_screen" + + BoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Share Software & Guides" + anchor_title: "left" + elevation: 0 + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.ids.screen_manager.app.close_repository_action(self)], + ] + + ScrollView: + id: repository_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "8dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(28), dp(48), dp(28), dp(16)] + + MDLabel: + text: "Repository Server\\n" + font_style: "H6" + + MDLabel: + id: repository_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(0), dp(35), dp(0), dp(35)] + + MDRectangleFlatIconButton: + id: repository_enable_button + icon: "wifi" + text: "Start Repository Server" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.ids.screen_manager.app.repository_start_action(self) + + MDRectangleFlatIconButton: + id: repository_disable_button + icon: "wifi-off" + text: "Stop Repository Server" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.ids.screen_manager.app.repository_stop_action(self) + disabled: True + + MDRectangleFlatIconButton: + id: repository_download_button + icon: "download-multiple" + text: "Update Contents" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.ids.screen_manager.app.repository_download_action(self) + disabled: False + + MDLabel: + id: repository_update + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + MDScreen: name: "hardware_screen" @@ -1880,6 +1967,15 @@ MDNavigationLayout: on_release: root.ids.screen_manager.app.guide_action(self) + OneLineIconListItem: + text: "Repository" + on_release: root.ids.screen_manager.app.repository_action(self) + + IconLeftWidget: + icon: "book-multiple" + on_release: root.ids.screen_manager.app.guide_action(self) + + OneLineIconListItem: id: app_version_info text: ""