diff --git a/README.md b/README.md index 7c48a87..234908d 100755 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ The Application Developmers Guide is the best document to read first. - Sets the device description to visually show the LAN/WAN/WWAN/Modem/IP Verify status. - **python_module_list** - This app will log the python version and modules in the device. It is intended to help with app development to show the python environment within the device. +- **rproxy** + - A reverse proxy similar to port forwarding, except traffic forwarded to a + udp/tcp target will be sourced from the router's IP. This reverse proxy can + be dynamically added to clients as they connect. - **shell_sample** - Provides example how to execute commands at OS shell: "ls - al". - **send_to_server** diff --git a/make.py b/make.py index 690fae1..a15f1a5 100755 --- a/make.py +++ b/make.py @@ -176,6 +176,7 @@ def package(): package_script_path = os.path.join('tools', 'bin', 'package_application.py') app_path = os.path.join(g_app_name) scan_for_cr(app_path) + setup_script(app_path) try: subprocess.check_output('{} {} {}'.format(g_python_cmd, package_script_path, app_path), shell=True) @@ -197,6 +198,7 @@ def package_all(): for app in app_dirs: app_path = os.path.join(app) scan_for_cr(app_path) + setup_script(app_path) try: print('Build app: {}'.format(app_path)) subprocess.check_output('{} {} {}'.format(g_python_cmd, package_script_path, app_path), shell=True) @@ -207,6 +209,22 @@ def package_all(): return success +def setup_script(app_path): + # check app_path for setup.py and execute it + setup_path = os.path.join(app_path, 'setup.py') + if os.path.isfile(setup_path): + cwd = os.getcwd() + os.chdir(app_path) + print('Running setup.py for {}'.format(app_path)) + try: + out = subprocess.check_output('{} {}'.format(g_python_cmd, 'setup.py'), stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + print ('[ERROR]: Exit code != 0') + out = e.output.decode() + print(out) + os.chdir(cwd) + + # Get the SDK status from the NCOS device def status(): status_tree = '/status/system/sdk' diff --git a/rproxy/csclient.py b/rproxy/csclient.py new file mode 100755 index 0000000..5423c80 --- /dev/null +++ b/rproxy/csclient.py @@ -0,0 +1,608 @@ +""" +NCOS communication module for SDK applications. + +Copyright (c) 2022 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. +""" + + +import json +import os +import re +import select +import socket +import threading +import logging.handlers +import signal +import sys + +try: + import traceback +except ImportError: + traceback = None + + +class SdkCSException(Exception): + pass + + +class CSClient(object): + """ + The CSClient class is the NCOS SDK mechanism for communication between apps and the router tree/config store. + Instances of this class communicate with the router using either an explicit socket or with http method calls. + + Apps running locally on the router use a socket on the router to send commands from the app to the router tree + and to receive data (JSON) from the router tree. + + Apps running remotely use the requests library to send HTTP method calls to the router and to receive data from + the router tree. This allows one to use an IDE to run and debug the application on a the computer. Although, + there are limitations with respect to the device hardware access (i.e. serial, USB, etc.). + """ + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return cls in cls._instances + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, app_name, init=False): + self.app_name = app_name + self.ncos = '/var/mnt/sdk' in os.getcwd() # Running in NCOS + handlers = [logging.StreamHandler()] + if 'linux' in sys.platform: + handlers.append(logging.handlers.SysLogHandler(address='/dev/log')) + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s: %(message)s', datefmt='%b %d %H:%M:%S', + handlers=handlers) + self.logger = logging.getLogger(app_name) + if not init: + return + + def get(self, base, query='', tree=0): + """ + Constructs and sends a get request to retrieve specified data from a device. + + The behavior of this method is contextual: + - If the app is installed on (and executed from) a device, it directly queries the router tree to retrieve the + specified data. + - If the app running remotely from a computer it calls the HTTP GET method to retrieve the specified data. + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + + """ + if 'linux' in sys.platform: + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd).get('data') + else: + # Running in a computer so use http to send the get to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.get(device_api, auth=self._get_auth(device_ip, username, password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text).get('data') + + def decrypt(self, base, query='', tree=0): + """ + Constructs and sends a decrypt/get request to retrieve specified data from a device. + + The behavior of this method is contextual: + - If the app is installed on (and executed from) a device, it directly queries the router tree to retrieve the + specified data. + - If the app running remotely from a computer it calls the HTTP GET method to retrieve the specified data. + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + + """ + if 'linux' in sys.platform: + cmd = "decrypt\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd).get('data') + else: + # Running in a computer and can't actually send the alert. + print('Decrypt is only available when running the app in NCOS.') + + def put(self, base, value='', query='', tree=0): + """ + Constructs and sends a put request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP PUT method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + tree: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + value = json.dumps(value) + if 'linux' in sys.platform: + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.put(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def post(self, base, value='', query=''): + """ + Constructs and sends a post request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP POST method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + value: Not required. + query: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + value = json.dumps(value) + if 'linux' in sys.platform: + cmd = f"post\n{base}\n{query}\n{value}\n" + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.post(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def patch(self, value): + """ + Constructs and sends a patch request to update or add specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly updates or adds the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP PUT method to update or add the specified + data. + + Args: + value: list containing dict of add/changes, and list of removals: [{add},[remove]] + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + + if 'linux' in sys.platform: + if value[0].get("config"): + adds = value[0] + else: + adds = {"config": value[0]} + adds = json.dumps(adds) + removals = json.dumps(value[1]) + cmd = f"patch\n{adds}\n{removals}\n" + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/'.format(device_ip) + + try: + response = requests.patch(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(json.dumps(value))}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def delete(self, base, query=''): + """ + Constructs and sends a delete request to delete specified data to the device router tree. + + The behavior of this method is contextual: + - If the app is installed on(and executed from) a device, it directly deletes the specified data to + the router tree. + - If the app running remotely from a computer it calls the HTTP DELETE method to update or add the specified + data. + + + Args: + base: String representing a path to a resource on a router tree, + (i.e. '/config/system/logging/level'). + query: Not required. + + Returns: + A dictionary containing the response (i.e. {"success": True, "data:": {}} + """ + if 'linux' in sys.platform: + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the device. + import requests + device_ip, username, password = self._get_device_access_info() + device_api = 'http://{}/api/{}/{}'.format(device_ip, base, query) + + try: + response = requests.delete(device_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=self._get_auth(device_ip, username, password), + data={"data": '{}'.format(base)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: device at {} did not respond.".format(device_ip)) + return None + + return json.loads(response.text) + + def alert(self, value=''): + """ + Constructs and sends a custom alert to NCM for the device. Apps calling this method must be running + on the target device to send the alert. If invoked while running on a computer, then only a log is output. + + Args: + + app_name: String name of your application. + value: String to displayed for the alert. + + Returns: + Success: None + Failure: An error + """ + if 'linux' in sys.platform: + cmd = "alert\n{}\n{}\n".format(self.app_name, value) + return self._dispatch(cmd) + else: + # Running in a computer and can't actually send the alert. + print('Alert is only available when running the app in NCOS.') + print('Alert Text: {}'.format(value)) + + def log(self, value=''): + """ + Adds an INFO log to the device SYSLOG. + + Args: + value: String text for the log. + + Returns: + None + """ + if self.ncos: + # Running in NCOS so write to the logger + self.logger.info(value) + elif 'linux' in sys.platform: + # Running in Linux (container?) so write to stdout + with open('/dev/stdout', 'w') as log: + log.write(f'{self.app_name}: {value}\n') + else: + # Running in a computer so just use print for the log. + print(value) + + + def _get_auth(self, device_ip, username, password): + # This is only needed when the app is running in a computer. + # Returns the proper HTTP Auth for the global username and password. + # Digest Auth is used for NCOS 6.4 and below while Basic Auth is + # used for NCOS 6.5 and up. + import requests + from http import HTTPStatus + + use_basic = False + device_api = 'http://{}/api/status/product_info'.format(device_ip) + + try: + response = requests.get(device_api, auth=requests.auth.HTTPBasicAuth(username, password)) + if response.status_code == HTTPStatus.OK: + use_basic = True + + except: + use_basic = False + + if use_basic: + return requests.auth.HTTPBasicAuth(username, password) + else: + return requests.auth.HTTPDigestAuth(username, password) + + @staticmethod + def _get_device_access_info(): + # Should only be called when running in a computer. It will return the + # dev_client_ip, dev_client_username, and dev_client_password as defined in + # the sdk section of the sdk_settings.ini file. + device_ip = '' + device_username = '' + device_password = '' + + if 'linux' not in sys.platform: + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + device_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + device_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + device_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return device_ip, device_username, device_password + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(errmsg) + return result + + +class EventingCSClient(CSClient): + running = False + registry = {} + eids = 1 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.on = self.register + self.un = self.unregister + + def start(self): + if self.running: + self.log(f"Eventing Config Store {self.pid} already running") + return + self.running = True + self.pid = os.getpid() + self.f = '/var/tmp/csevent_%d.sock' % self.pid + try: + os.unlink(self.f) + except FileNotFoundError: + pass + self.event_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.event_sock.bind(self.f) + self.event_sock.listen() # backlog is optional. already set on value found in /proc + self.event_sock.setblocking(False) + self.eloop = threading.Thread(target=self._handle_events) + self.eloop.start() + + def stop(self): + if not self.running: + return + self.log(f"Stopping") + for k in list(self.registry.keys()): + self.unregister(k) + self.event_sock.close() + os.unlink(self.f) + self.running = False + + def _handle_events(self): + poller = select.poll() + poller.register(self.event_sock, + select.POLLIN | select.POLLERR | select.POLLHUP) # I don't unregsiter this in cleaning up! + while self.running: + try: + events = poller.poll(1000) + for f, ev in events: + if ev & (select.POLLERR | select.POLLHUP): + self.log("Hangup/error received. Stopping") + self.stop() # TODO: restart w/ cached registrations. Will no longer be an error case + + if ev & select.POLLIN: + conn, addr = self.event_sock.accept() + result = self._receive(conn) + eid = int(result['data']['id']) + try: + cb = self.registry[eid]['cb'] + args = self.registry[eid]['args'] + try: + # PUTting just a string to config store results in a json encoded string returned. + # e.g. set /config/system/logging/level "debug", result['data']['cfg'] is '"debug"' + cfg = json.loads(result['data']['cfg']) + except TypeError as e: + # Non-string path + cfg = result['data']['cfg'] + try: + cb_return = cb(result['data']['path'], cfg, args) + except: + if traceback: + traceback.print_exc() + self.log(f"Exception during callback for {str(self.registry[eid])}") + if result['data']['action'] == 'get': # We've something to send back. + # config_store_receiver expects json + cb_return = json.JSONEncoder().encode(cb_return) + conn.sendall( + cb_return.encode()) # No dispatch. Config store receiver will put to config store. + except (NameError, ValueError) as e: + self.log(f"Could not find register data for eid {eid}") + except OSError as e: + self.log(f"OSError: {e}") + raise + + def register(self, action: object, path: object, callback: object, *args: object) -> object: + if not self.running: + self.start() + # what about multiple registration? + eid = self.eids + self.eids += 1 + self.registry[eid] = {'cb': callback, 'action': action, 'path': path, 'args': args} + cmd = "register\n{}\n{}\n{}\n{}\n".format(self.pid, eid, action, path) + return self._dispatch(cmd) + + def unregister(self, eid): + ret = "" + try: + e = self.registry[eid] + except KeyError: + pass + else: + if self.running: + cmd = "unregister\n{}\n{}\n{}\n{}\n".format(self.pid, eid, e['action'], e['path']) + ret = self._dispatch(cmd) + del self.registry[eid] + return ret + + +def clean_up_reg(signal, frame): + """ + When 'cppython remote_port_forward.py' gets a SIGTERM, config_store_receiver.py doesn't + clean up registrations. Even if it did, the comm module can't rely on an external service + to clean up. + """ + EventingCSClient('CSClient').stop() + sys.exit(0) + + +signal.signal(signal.SIGTERM, clean_up_reg) diff --git a/rproxy/package.ini b/rproxy/package.ini new file mode 100644 index 0000000..23b479a --- /dev/null +++ b/rproxy/package.ini @@ -0,0 +1,14 @@ +[rproxy] +uuid = 39fb6bda-d05c-4e92-8b18-e3948789f5f1 +vendor = Cradlepoint +notes = rproxy +version_major = 0 +version_minor = 0 +version_patch = 15 +auto_start = true +restart = true +reboot = true +firmware_major = 7 +firmware_minor = 23 +fimrware_patch = 50 + diff --git a/rproxy/readme.txt b/rproxy/readme.txt new file mode 100644 index 0000000..b3cc1d7 --- /dev/null +++ b/rproxy/readme.txt @@ -0,0 +1,39 @@ +Application Name +================ +rproxy + + +NCOS Devices Supported +====================== +ALL + + +Application Purpose +=================== +A reverse proxy similar to port forwarding, except traffic forwarded to a +udp/tcp target will be sourced from the router's IP. This reverse proxy can +be dynamically added to clients as they connect. + + +Usage +===== +Configure config/system/sdk/appdata with entries for each port your want to proxy +and the target IP/port in the form local_addr:local_port:remote_addr:remote_port:proto +For example: + +| Name | Value | Notes | +| ---- | ----- | ----- | +| rproxy.0 | 80:192.168.0.5:80 | Listens on 127.0.0.1:80 and forwards tcp to 192.168.0.5:80 +| rproxy.1 | 127.0.0.1:80:192.168.0.5:80:tcp | Same as above except explicit +| rproxy.2 | 192.168.0.1:53:192.168.0.5:53:udp | Listens on 192.168.0.1 udp port 53 and forwards to 192.168.0.5:53 + +For dynamic clients, add a client entry to config/system/sdk/appdata with rproxy.auto in the form +local_host:local_port_ranges:remote_port:protocol. Each client that connects on the LAN network +will get a port in the range specified and traffic will be forwarded to the remote port. +For example: + +| Name | Value | Notes | +| ---- | ----- | ----- | +| rproxy.auto.0 | 20020-20030:22 | Listens on a port in the range 20020-20030 and forwards to port 22 on each lan client +| rproxy.auto.1 | 127.0.0.1:20020-20030:22:tcp | Same as above except explicit +| rpxoxy.auto.2 | 80,8000-8010:80:tcp | ranges can be specified with commas diff --git a/rproxy/run.py b/rproxy/run.py new file mode 100755 index 0000000..df10f86 --- /dev/null +++ b/rproxy/run.py @@ -0,0 +1,122 @@ +import subprocess +import time +from csclient import CSClient +import shlex +import threading +from ipaddress import ip_address + + +cs = CSClient('rproxy') + +LOGGER = cs.logger + + +def get_binary(): + arch = subprocess.check_output(["uname", "-m"], text=True).strip() + if arch == "armv7l": + return "./rproxy_arm" + elif arch == "x86_64": + return "./rproxy_amd64" + elif arch == "aarch64": + return "./rproxy_arm64" + else: + raise Exception(f"unsupported architecture {arch}") + + +def run_cmd(local_host, local_port, remote_host, remote_port, protocol): + binary = get_binary() + return shlex.split(f"{binary} -b {local_host}:{local_port} -r {remote_host}:{remote_port} -p {protocol}") + + +def populate_auto_rproxy_config(): + + def ranges(s): + for r in s.split(','): + r = r.split('-') + first = int(r[0]) + last = int(r[-1]) + yield from range(first, last+1) + while True: + last += 1 + if last > 65535: + break + yield last + + appdata = cs.get("/config/system/sdk/appdata") + cmd_strings = set() + + for r in (j['value'] for j in appdata if j['name'].startswith('rproxy.auto')): + # e.g. local_host:local_port_ranges:remote_port:protocol + parts = r.split(':') + if "." not in parts[0]: + parts.insert(0, "127.0.0.1") + if len(parts) < 4: + parts.append("TCP") + local_host = parts[0] + local_port_ranges = ranges(parts[1]) + remote_port = parts[2] + protocol = "UDP" if parts[3].upper() == "UDP" else "TCP" + + current_clients = cs.get("/status/lan/clients") + for client in current_clients: + if ip_address(client['ip_address']).version == 4: + cmd = f"{local_host}:{next(local_port_ranges)}:{client['ip_address']}:{remote_port}:{protocol}" + cmd_strings.add(cmd) + + return cmd_strings + + +def get_rproxy_config(): + appdata = cs.get("/config/system/sdk/appdata") + return set(j['value'] for j in appdata if (j['name'].startswith('rproxy') and not j['name'].startswith('rproxy.auto'))) + + +def main(): + LOGGER.info("starting rproxy") + rproxy = populate_auto_rproxy_config() + rproxy.update(get_rproxy_config()) + + def output_thread(proc): + for line in proc.stdout: + LOGGER.info(line) + + ps = [] + ts = [] + for r in rproxy: + # e.g. 0.0.0.0:80:192.168.0.5:80:tcp + cmd = r.split(':') + if "." not in cmd[0]: + cmd.insert(0, "127.0.0.1") + if len(cmd) < 5: + cmd.append("TCP") + local_host = cmd[0] + local_port = cmd[1] + remote_host = cmd[2] + remote_port = cmd[3] + protocol = "UDP" if cmd[4].upper() == "UDP" else "TCP" + cmd = run_cmd(local_host, local_port, remote_host, remote_port, protocol) + LOGGER.info(f"starting rproxy: {cmd}") + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + t = threading.Thread(target=output_thread, args=(p,)) + t.daemon = True + t.start() + ps.append(p) + ts.append(t) + + # periodically check for changes in rproxy config + LOGGER.info("checking for rproxy config changes every 10 seconds") + while True: + time.sleep(10) + new_rproxy = populate_auto_rproxy_config() + new_rproxy.update(get_rproxy_config()) + if rproxy != new_rproxy: + LOGGER.info(f"rproxy config changed from {rproxy} to {new_rproxy}") + for p in ps: + p.kill() + break + + +if __name__ == "__main__": + # cleanup any instances + subprocess.run(["killall", "rproxy"]) + main() \ No newline at end of file diff --git a/rproxy/setup.py b/rproxy/setup.py new file mode 100755 index 0000000..6e58aee --- /dev/null +++ b/rproxy/setup.py @@ -0,0 +1,64 @@ +import requests +import tarfile +import os + + +def download_and_extract_tar_gz(url, target_folder): + try: + # Create the target folder if it doesn't exist + if not os.path.exists(target_folder): + os.makedirs(target_folder) + + # Download the tar.gz file + print(f"Downloading {url}...") + response = requests.get(url) + if response.status_code != 200: + print(f"Failed to download the file from {url}. Status code: {response.status_code}") + return + + # Save the file in the target folder + filename = os.path.join(target_folder, list(filter(None, url.split("/")))[-1]) + with open(filename, "wb") as file: + file.write(response.content) + + # Extract the contents of the tar.gz file + with tarfile.open(filename, "r:gz") as tar: + tar.extractall(target_folder) + + # Delete the tar.gz file after extraction + os.remove(filename) + + print(f"Extracting {filename} completed successfully.") + except Exception as e: + print(f"An error occurred: {e}") + + +def move_file(source, target): + try: + os.rename(source, target) + print(f"Files moved from {source} to {target} successfully.") + except Exception as e: + print(f"An error occurred moving {source} to {target}: {e}") + + +def add_executable_perm(filename): + try: + # Add executable permissions to the file + os.chmod(filename, 0o755) + + print(f"Executable permissions for {filename} added successfully.") + except Exception as e: + print(f"An error occurred adding permissions for {filename}: {e}") + + +if __name__ == "__main__": + dl = { + 'arm': 'rproxy-arm-unknown-linux-musleabihf.tar.gz', + 'amd64': 'rproxy-x86_64-unknown-linux-musl.tar.gz', + 'arm64': 'rproxy-aarch64-unknown-linux-musl.tar.gz' + } + + for arch in ['arm', 'amd64', 'arm64']: + download_and_extract_tar_gz(f'https://github.com/cayspekko/buildproxy/releases/download/v0.1.0/{dl[arch]}', './') + move_file("rproxy", f"rproxy_{arch}") + add_executable_perm(f'./rproxy_{arch}') diff --git a/rproxy/start.sh b/rproxy/start.sh new file mode 100755 index 0000000..8f63ffb --- /dev/null +++ b/rproxy/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython run.py \ No newline at end of file