From 2ce8351b1aeb59037de6279576c55dc9d3983ca8 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Thu, 8 Feb 2024 23:24:30 +0100 Subject: [PATCH] initial install test --- api/auth.py | 20 ++++ api/main.py | 229 +++++++++++++++++++++++++++++++++++++ api/modules/ap.py | 112 ++++++++++++++++++ api/modules/client.py | 28 +++++ api/modules/ddns.py | 24 ++++ api/modules/dhcp.py | 30 +++++ api/modules/dns.py | 38 ++++++ api/modules/firewall.py | 4 + api/modules/networking.py | 68 +++++++++++ api/modules/openvpn.py | 41 +++++++ api/modules/restart.py | 7 ++ api/modules/system.py | 86 ++++++++++++++ api/modules/wireguard.py | 26 +++++ api/requirements.txt | 3 + installers/common.sh | 32 ++++++ installers/raspbian.sh | 5 + installers/restapi.service | 14 +++ 17 files changed, 767 insertions(+) create mode 100644 api/auth.py create mode 100644 api/main.py create mode 100644 api/modules/ap.py create mode 100644 api/modules/client.py create mode 100644 api/modules/ddns.py create mode 100644 api/modules/dhcp.py create mode 100644 api/modules/dns.py create mode 100644 api/modules/firewall.py create mode 100644 api/modules/networking.py create mode 100644 api/modules/openvpn.py create mode 100644 api/modules/restart.py create mode 100644 api/modules/system.py create mode 100644 api/modules/wireguard.py create mode 100644 api/requirements.txt create mode 100644 installers/restapi.service diff --git a/api/auth.py b/api/auth.py new file mode 100644 index 00000000..2a716e2d --- /dev/null +++ b/api/auth.py @@ -0,0 +1,20 @@ +import os +from fastapi.security.api_key import APIKeyHeader +from fastapi import Security, HTTPException +from starlette.status import HTTP_403_FORBIDDEN + +apikey=os.getenv('RASPAP_API_KEY') +#if env not set, set the api key to "insecure" +if apikey == None: + apikey = "insecure" + +print(apikey) +api_key_header = APIKeyHeader(name="access_token", auto_error=False) + +async def get_api_key(api_key_header: str = Security(api_key_header)): + if api_key_header ==apikey: + return api_key_header + else: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="403: Unauthorized" + ) \ No newline at end of file diff --git a/api/main.py b/api/main.py new file mode 100644 index 00000000..a3153f0c --- /dev/null +++ b/api/main.py @@ -0,0 +1,229 @@ +from fastapi import FastAPI, Depends +from fastapi.security.api_key import APIKey +import auth + +import json + +import modules.system as system +import modules.ap as ap +import modules.client as client +import modules.dns as dns +import modules.dhcp as dhcp +import modules.ddns as ddns +import modules.firewall as firewall +import modules.networking as networking +import modules.openvpn as openvpn +import modules.wireguard as wireguard +import modules.restart as restart + + +tags_metadata = [ + { + "name": "system", + "description": "All information regarding the underlying system." + }, + { + "name": "accesspoint/hostpost", + "description": "Get and change all information regarding the hotspot" + } +] +app = FastAPI( + title="API for Raspap", + openapi_tags=tags_metadata, + version="0.0.1", + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + } +) + +@app.get("/system", tags=["system"]) +async def get_system(): + return{ +'hostname': system.hostname(), +'uptime': system.uptime(), +'systime': system.systime(), +'usedMemory': system.usedMemory(), +'processorCount': system.processorCount(), +'LoadAvg1Min': system.LoadAvg1Min(), +'systemLoadPercentage': system.systemLoadPercentage(), +'systemTemperature': system.systemTemperature(), +'hostapdStatus': system.hostapdStatus(), +'operatingSystem': system.operatingSystem(), +'kernelVersion': system.kernelVersion(), +'rpiRevision': system.rpiRevision() +} + +@app.get("/ap", tags=["accesspoint/hostpost"]) +async def get_ap(): + return{ +'driver': ap.driver(), +'ctrl_interface': ap.ctrl_interface(), +'ctrl_interface_group': ap.ctrl_interface_group(), +'auth_algs': ap.auth_algs(), +'wpa_key_mgmt': ap.wpa_key_mgmt(), +'beacon_int': ap.beacon_int(), +'ssid': ap.ssid(), +'channel': ap.channel(), +'hw_mode': ap.hw_mode(), +'ieee80211n': ap.ieee80211n(), +'wpa_passphrase': ap.wpa_passphrase(), +'interface': ap.interface(), +'wpa': ap.wpa(), +'wpa_pairwise': ap.wpa_pairwise(), +'country_code': ap.country_code(), +'ignore_broadcast_ssid': ap.ignore_broadcast_ssid() +} + +@app.post("/ap", tags=["accesspoint/hostpost"]) +async def post_ap(driver=None, + ctrl_interface=None, + ctrl_interface_group=None, + auth_algs=None, + wpa_key_mgmt=None, + beacon_int=None, + ssid=None, + channel=None, + hw_mode=None, + ieee80211n=None, + wpa_passphrase=None, + interface=None, + wpa=None, + wpa_pairwise=None, + country_code=None, + ignore_broadcast_ssid=None, + api_key: APIKey = Depends(auth.get_api_key)): + if driver != None: + ap.set_driver(driver) + if ctrl_interface != None: + ap.set_ctrl_interface(ctrl_interface) + if ctrl_interface_group !=None: + ap.set_ctrl_interface_group(ctrl_interface_group) + if auth_algs != None: + ap.set_auth_algs(auth_algs) + if wpa_key_mgmt != None: + ap.set_wpa_key_mgmt(wpa_key_mgmt) + if beacon_int != None: + ap.set_beacon_int(beacon_int) + if ssid != None: + ap.set_ssid(ssid) + if channel != None: + ap.set_channel(channel) + if hw_mode != None: + ap.set_hw_mode(hw_mode) + if ieee80211n != None: + ap.set_ieee80211n(ieee80211n) + if wpa_passphrase != None: + ap.set_wpa_passphrase(wpa_passphrase) + if interface != None: + ap.set_interface(interface) + if wpa != None: + ap.set_wpa(wpa) + if wpa_pairwise != None: + ap.set_wpa_pairwise(wpa_pairwise) + if country_code != None: + ap.set_country_code(country_code) + if ignore_broadcast_ssid != None: + ap.set_ignore_broadcast_ssid(ignore_broadcast_ssid) + + +@app.get("/clients/{wireless_interface}", tags=["Clients"]) +async def get_clients(wireless_interface): + return{ +'active_clients_amount': client.get_active_clients_amount(wireless_interface), +'active_clients': json.loads(client.get_active_clients(wireless_interface)) +} + +@app.get("/dhcp", tags=["DHCP"]) +async def get_dhcp(): + return{ +'range_start': dhcp.range_start(), +'range_end': dhcp.range_end(), +'range_subnet_mask': dhcp.range_subnet_mask(), +'range_lease_time': dhcp.range_lease_time(), +'range_gateway': dhcp.range_gateway(), +'range_nameservers': dhcp.range_nameservers() +} + +@app.get("/dns/domains", tags=["DNS"]) +async def get_domains(): + return{ +'domains': json.loads(dns.adblockdomains()) +} +@app.get("/dns/hostnames", tags=["DNS"]) +async def get_hostnames(): + return{ +'hostnames': json.loads(dns.adblockhostnames()) +} + +@app.get("/dns/upstream", tags=["DNS"]) +async def get_upstream(): + return{ +'upstream_nameserver': dns.upstream_nameserver() +} + +@app.get("/dns/logs", tags=["DNS"]) +async def get_dnsmasq_logs(): + return(dns.dnsmasq_logs()) + + +@app.get("/ddns", tags=["DDNS"]) +async def get_ddns(): + return{ +'use': ddns.use(), +'method': ddns.method(), +'protocol': ddns.protocol(), +'server': ddns.server(), +'login': ddns.login(), +'password': ddns.password(), +'domain': ddns.domain() +} + +@app.get("/firewall", tags=["Firewall"]) +async def get_firewall(): + return json.loads(firewall.firewall_rules()) + +@app.get("/networking", tags=["Networking"]) +async def get_networking(): + return{ +'interfaces': json.loads(networking.interfaces()), +'throughput': json.loads(networking.throughput()) +} + +@app.get("/openvpn", tags=["OpenVPN"]) +async def get_openvpn(): + return{ +'client_configs': openvpn.client_configs(), +'client_config_names': openvpn.client_config_names(), +'client_config_active': openvpn.client_config_active(), +'client_login_names': openvpn.client_login_names(), +'client_login_active': openvpn.client_login_active() +} + +@app.get("/openvpn/{config}", tags=["OpenVPN"]) +async def client_config_list(config): + return{ +'client_config': openvpn.client_config_list(config) +} + +@app.get("/wireguard", tags=["WireGuard"]) +async def get_wireguard(): + return{ +'client_configs': wireguard.configs(), +'client_config_names': wireguard.client_config_names(), +'client_config_active': wireguard.client_config_active() +} + +@app.get("/wireguard/{config}", tags=["WireGuard"]) +async def client_config_list(config): + return{ +'client_config': wireguard.client_config_list(config) +} + +@app.post("/restart/webgui") +async def restart_webgui(): + restart.webgui() + +@app.post("/restart/adblock") +async def restart_adblock(): + restart.adblock() \ No newline at end of file diff --git a/api/modules/ap.py b/api/modules/ap.py new file mode 100644 index 00000000..2d6fd8e3 --- /dev/null +++ b/api/modules/ap.py @@ -0,0 +1,112 @@ +import subprocess +import json + +def driver(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep driver= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_driver(driver): + return subprocess.run(f"sudo sed -i 's/^driver=.*/driver={driver}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ctrl_interface(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ctrl_interface(ctrl_interface): + return subprocess.run(f"sudo sed -i 's/^ctrl_interface=.*/ctrl_interface={ctrl_interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ctrl_interface_group(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface_group= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ctrl_interface_group(ctrl_interface_group): + return subprocess.run(f"sudo sed -i 's/^ctrl_interface_group=.*/ctrl_interface_group={ctrl_interface_group}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def auth_algs(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep auth_algs= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_auth_algs(auth_algs): + return subprocess.run(f"sudo sed -i 's/^auth_algs=.*/auth_algs={auth_algs}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_key_mgmt(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_key_mgmt= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_key_mgmt(wpa_key_mgmt): + return subprocess.run(f"sudo sed -i 's/^wpa_key_mgmt=.*/wpa_key_mgmt={wpa_key_mgmt}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def beacon_int(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep beacon_int= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_beacon_int(beacon_int): + return subprocess.run(f"sudo sed -i 's/^beacon_int=.*/beacon_int={beacon_int}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ssid(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ssid= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ssid(ssid): + return subprocess.run(f"sudo sed -i 's/^ssid=.*/ssid={ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def channel(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep channel= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_channel(channel): + return subprocess.run(f"sudo sed -i 's/^channel=.*/channel={channel}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def hw_mode(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep hw_mode= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_hw_mode(hw_mode): + return subprocess.run(f"sudo sed -i 's/^hw_mode=.*/hw_mode={hw_mode}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ieee80211n(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ieee80211n= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ieee80211n(ieee80211n): + return subprocess.run(f"sudo sed -i 's/^ieee80211n=.*/ieee80211n={ieee80211n}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_passphrase(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_passphrase= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_passphrase(wpa_passphrase): + return subprocess.run(f"sudo sed -i 's/^wpa_passphrase=.*/wpa_passphrase={wpa_passphrase}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def interface(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_interface(interface): + return subprocess.run(f"sudo sed -i 's/^interface=.*/interface={interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa(wpa): + return subprocess.run(f"sudo sed -i 's/^wpa=.*/wpa={wpa}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_pairwise(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_pairwise= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_pairwise(wpa_pairwise): + return subprocess.run(f"sudo sed -i 's/^wpa_pairwise=.*/wpa_pairwise={wpa_pairwise}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def country_code(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep country_code= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_country_code(country_code): + return subprocess.run(f"sudo sed -i 's/^country_code=.*/country_code={country_code}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ignore_broadcast_ssid(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ignore_broadcast_ssid= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ignore_broadcast_ssid(ignore_broadcast_ssid): + return subprocess.run(f"sudo sed -i 's/^ignore_broadcast_ssid=.*/ignore_broadcast_ssid={ignore_broadcast_ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def logging(): + log_output = subprocess.run(f"cat /tmp/hostapd.log", shell=True, capture_output=True, text=True).stdout.strip() + logs = {} + + for line in log_output.split('\n'): + parts = line.split(': ') + if len(parts) >= 2: + interface, message = parts[0], parts[1] + if interface not in logs: + logs[interface] = [] + logs[interface].append(message) + + return json.dumps(logs, indent=2) \ No newline at end of file diff --git a/api/modules/client.py b/api/modules/client.py new file mode 100644 index 00000000..2e894cd5 --- /dev/null +++ b/api/modules/client.py @@ -0,0 +1,28 @@ +import subprocess +import json + +def get_active_clients_amount(interface): + output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) + return(len(output.stdout.splitlines())) + +def get_active_clients(interface): + #does not run like intended, but it works.... + output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) + clients_list = [] + + for line in output.stdout.splitlines(): + fields = line.split() + + client_data = { + "timestamp": int(fields[0]), + "mac_address": fields[1], + "ip_address": fields[2], + "hostname": fields[3], + "client_id": fields[4], + } + + clients_list.append(client_data) + + json_output = json.dumps(clients_list, indent=2) + + return json_output \ No newline at end of file diff --git a/api/modules/ddns.py b/api/modules/ddns.py new file mode 100644 index 00000000..e7ab3ebf --- /dev/null +++ b/api/modules/ddns.py @@ -0,0 +1,24 @@ +import subprocess + +def use(): + return subprocess.run("cat /etc/ddclient.conf | grep use= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def method(): + #get the contents of the line below "use=" + return subprocess.run("awk '/^use=/ {getline; print}' /etc/ddclient.conf | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def protocol(): + return subprocess.run("cat /etc/ddclient.conf | grep protocol= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def server(): + return subprocess.run("cat /etc/ddclient.conf | grep server= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def login(): + return subprocess.run("cat /etc/ddclient.conf | grep login= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def password(): + return subprocess.run("cat /etc/ddclient.conf | grep password= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def domain(): + #get the contents of the line below "password=" + return subprocess.run("awk '/^password=/ {getline; print}' /etc/ddclient.conf", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/dhcp.py b/api/modules/dhcp.py new file mode 100644 index 00000000..887bb29b --- /dev/null +++ b/api/modules/dhcp.py @@ -0,0 +1,30 @@ +import subprocess +import json + +def range_start(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f1", shell=True, capture_output=True, text=True).stdout.strip() + +def range_end(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def range_subnet_mask(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f3", shell=True, capture_output=True, text=True).stdout.strip() + +def range_lease_time(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f4", shell=True, capture_output=True, text=True).stdout.strip() + +def range_gateway(): + return subprocess.run("cat /etc/dhcpcd.conf | grep routers | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def range_nameservers(): + output = subprocess.run("cat /etc/dhcpcd.conf", shell=True, capture_output=True, text=True).stdout.strip() + + nameservers = [] + + lines = output.split('\n') + for line in lines: + if "static domain_name_server" in line: + servers = line.split('=')[1].strip().split() + nameservers.extend(servers) + + return nameservers \ No newline at end of file diff --git a/api/modules/dns.py b/api/modules/dns.py new file mode 100644 index 00000000..17fb4d1e --- /dev/null +++ b/api/modules/dns.py @@ -0,0 +1,38 @@ +import subprocess +import json + +def adblockdomains(): + output = subprocess.run("cat /etc/raspap/adblock/domains.txt", shell=True, capture_output=True, text=True).stdout.strip() + domains =output.split('\n') + domainlist=[] + for domain in domains: + if domain.startswith('#') or domain=="": + continue + domainlist.append(domain.split('=/')[1]) + return domainlist + +def adblockhostnames(): + output = subprocess.run("cat /etc/raspap/adblock/hostnames.txt", shell=True, capture_output=True, text=True).stdout.strip() + hostnames = output.split('\n') + hostnamelist=[] + for hostname in hostnames: + if hostname.startswith('#') or hostname=="": + continue + hostnamelist.append(hostname.replace('0.0.0.0 ','')) + return hostnamelist + +def upstream_nameserver(): + return subprocess.run("awk '/nameserver/ {print $2}' /run/dnsmasq/resolv.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def dnsmasq_logs(): + output = subprocess.run("cat /var/log/dnsmasq.log", shell=True, capture_output=True, text=True).stdout.strip() + log_entries = [] + for line in output.split("\n"): + fields = line.split(" ") + log_dict = { + 'timestamp': ' '.join(fields[:3]), + 'process': fields[3][:-1], # Remove the trailing colon + 'message': ' '.join(fields[4:]), + } + log_entries.append(log_dict) + return log_entries \ No newline at end of file diff --git a/api/modules/firewall.py b/api/modules/firewall.py new file mode 100644 index 00000000..004b9e4a --- /dev/null +++ b/api/modules/firewall.py @@ -0,0 +1,4 @@ +import subprocess + +def firewall_rules(): + return subprocess.run("cat /etc/raspap/networking/firewall/iptables_rules.json", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/networking.py b/api/modules/networking.py new file mode 100644 index 00000000..989e0eb4 --- /dev/null +++ b/api/modules/networking.py @@ -0,0 +1,68 @@ +import psutil +import json + +def throughput(): + interface_info = {} + + # Get network interfaces + interfaces = psutil.net_if_stats() + + for interface, stats in interfaces.items(): + if interface.startswith("lo") or interface.startswith("docker"): + # Skip loopback and docker interface + continue + + try: + # Get network traffic statistics + traffic_stats = psutil.net_io_counters(pernic=True)[interface] + rx_packets = traffic_stats[1] + rx_bytes = traffic_stats[0] + tx_packets = traffic_stats[3] + tx_bytes = traffic_stats[4] + + interface_info[interface] = { + "RX_packets": rx_packets, + "RX_bytes": rx_bytes, + "TX_packets": tx_packets, + "TX_bytes": tx_bytes + } + except KeyError: + # Handle the case where network interface statistics are not available + pass + + return json.dumps(interface_info, indent=2) + +def interfaces(): + interface_info = {} + + # Get network interfaces + interfaces = psutil.net_if_addrs() + + for interface, addrs in interfaces.items(): + if interface.startswith("lo") or interface.startswith("docker"): + # Skip loopback and docker interface + continue + + ip_address = None + netmask = None + mac_address = None + + for addr in addrs: + if addr.family == 2: # AF_INET corresponds to the integer value 2 + # IPv4 address + ip_address = addr.address + netmask = addr.netmask + + # Get MAC address + for addr in psutil.net_if_addrs().get(interface, []): + if addr.family == psutil.AF_LINK: + mac_address = addr.address + + interface_info[interface] = { + "IP_address": ip_address, + "Netmask": netmask, + "MAC_address": mac_address + } + return json.dumps(interface_info, indent=2) + +#TODO: migrate to vnstat, to lose psutil dependency \ No newline at end of file diff --git a/api/modules/openvpn.py b/api/modules/openvpn.py new file mode 100644 index 00000000..b8ad1f96 --- /dev/null +++ b/api/modules/openvpn.py @@ -0,0 +1,41 @@ +import subprocess + +def client_configs(): + return subprocess.run("find /etc/openvpn/client/ -type f | wc -l", shell=True, capture_output=True, text=True).stdout.strip() + +def client_config_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/openvpn/client/ | grep -v "^client.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + if "_client" in client: + config_names_dict ={'config':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_login_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/openvpn/client/ | grep -v "^client.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + if "_login" in client: + config_names_dict ={'login':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_config_active(): + output = subprocess.run('''ls -al /etc/openvpn/client/ | grep "client.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/openvpn/client/") + return(active_config[1]) + +def client_login_active(): + output = subprocess.run('''ls -al /etc/openvpn/client/ | grep "login.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/openvpn/client/") + return(active_config[1]) + +def client_config_list(client_config): + output = subprocess.run(f"cat /etc/openvpn/client/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() + return output.split('\n') + +#TODO: where is the logfile?? +#TODO: is service connected? \ No newline at end of file diff --git a/api/modules/restart.py b/api/modules/restart.py new file mode 100644 index 00000000..18eb1b57 --- /dev/null +++ b/api/modules/restart.py @@ -0,0 +1,7 @@ +import subprocess + +def webgui(): + return subprocess.run("sudo /etc/raspap/lighttpd/configport.sh --restart", shell=True, capture_output=True, text=True).stdout.strip() + +def adblock(): + return subprocess.run("sudo /bin/systemctl restart dnsmasq.service", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/system.py b/api/modules/system.py new file mode 100644 index 00000000..82367779 --- /dev/null +++ b/api/modules/system.py @@ -0,0 +1,86 @@ +import subprocess + +revisions = { + '0002': 'Model B Revision 1.0', + '0003': 'Model B Revision 1.0 + ECN0001', + '0004': 'Model B Revision 2.0 (256 MB)', + '0005': 'Model B Revision 2.0 (256 MB)', + '0006': 'Model B Revision 2.0 (256 MB)', + '0007': 'Model A', + '0008': 'Model A', + '0009': 'Model A', + '000d': 'Model B Revision 2.0 (512 MB)', + '000e': 'Model B Revision 2.0 (512 MB)', + '000f': 'Model B Revision 2.0 (512 MB)', + '0010': 'Model B+', + '0013': 'Model B+', + '0011': 'Compute Module', + '0012': 'Model A+', + 'a01041': 'a01041', + 'a21041': 'a21041', + '900092': 'PiZero 1.2', + '900093': 'PiZero 1.3', + '9000c1': 'PiZero W', + 'a02082': 'Pi 3 Model B', + 'a22082': 'Pi 3 Model B', + 'a32082': 'Pi 3 Model B', + 'a52082': 'Pi 3 Model B', + 'a020d3': 'Pi 3 Model B+', + 'a220a0': 'Compute Module 3', + 'a020a0': 'Compute Module 3', + 'a02100': 'Compute Module 3+', + 'a03111': 'Model 4B Revision 1.1 (1 GB)', + 'b03111': 'Model 4B Revision 1.1 (2 GB)', + 'c03111': 'Model 4B Revision 1.1 (4 GB)', + 'c03111': 'Model 4B Revision 1.1 (4 GB)', + 'a03140': 'Compute Module 4 (1 GB)', + 'b03140': 'Compute Module 4 (2 GB)', + 'c03140': 'Compute Module 4 (4 GB)', + 'd03140': 'Compute Module 4 (8 GB)', + 'c04170': 'Pi 5 (4 GB)', + 'd04170': 'Pi 5 (8 GB)' +} + +def hostname(): + return subprocess.run("hostname", shell=True, capture_output=True, text=True).stdout.strip() + +def uptime(): + return subprocess.run("uptime -p", shell=True, capture_output=True, text=True).stdout.strip() + +def systime(): + return subprocess.run("date", shell=True, capture_output=True, text=True).stdout.strip() + +def usedMemory(): + return round(float(subprocess.run("free -m | awk 'NR==2{total=$2 ; used=$3 } END { print used/total*100}'", shell=True, capture_output=True, text=True).stdout.strip()),2) + +def processorCount(): + return int(subprocess.run("nproc --all", shell=True, capture_output=True, text=True).stdout.strip()) + +def LoadAvg1Min(): + return round(float(subprocess.run("awk '{print $1}' /proc/loadavg", shell=True, capture_output=True, text=True).stdout.strip()),2) + +def systemLoadPercentage(): + return round((float(LoadAvg1Min())*100)/float(processorCount()),2) + +def systemTemperature(): + try: + output = subprocess.run("cat /sys/class/thermal/thermal_zone0/temp", shell=True, capture_output=True, text=True).stdout.strip() + return round(float(output)/1000,2) + except ValueError: + return 0 + +def hostapdStatus(): + return int(subprocess.run("pidof hostapd | wc -l", shell=True, capture_output=True, text=True).stdout.strip()) + +def operatingSystem(): + return subprocess.run('''grep PRETTY_NAME /etc/os-release | cut -d= -f2- | sed 's/"//g' ''', shell=True, capture_output=True, text=True).stdout.strip() + +def kernelVersion(): + return subprocess.run("uname -r", shell=True, capture_output=True, text=True).stdout.strip() + +def rpiRevision(): + output = subprocess.run("grep Revision /proc/cpuinfo | awk '{print $3}'", shell=True, capture_output=True, text=True).stdout.strip() + try: + return revisions[output] + except KeyError: + return 'Unknown Device' \ No newline at end of file diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py new file mode 100644 index 00000000..dd33970b --- /dev/null +++ b/api/modules/wireguard.py @@ -0,0 +1,26 @@ +import subprocess + +def configs(): + #ignore symlinks, because wg0.conf is in production the main config, but in insiders it is a symlink + return subprocess.run("find /etc/wireguard/ -type f | wc -l", shell=True, capture_output=True, text=True).stdout.strip() + +def client_config_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/wireguard/ | grep -v "^wg0.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + config_names_dict ={'config':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_config_active(): + output = subprocess.run('''ls -al /etc/wireguard/ | grep "wg0.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/wireguard/") + return(active_config[1]) + +def client_config_list(client_config): + output = subprocess.run(f"cat /etc/wireguard/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() + return output.split('\n') + +#TODO: where is the logfile?? +#TODO: is service connected? \ No newline at end of file diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 00000000..d0b660e0 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.109.0 +uvicorn==0.25.0 +psutil==5.9.8 \ No newline at end of file diff --git a/installers/common.sh b/installers/common.sh index 8761f941..610d447c 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -57,6 +57,7 @@ function _install_raspap() { _configure_networking _prompt_install_adblock _prompt_install_openvpn + _prompt_isntall_restapi _install_extra_features _prompt_install_wireguard _prompt_install_vpn_providers @@ -502,6 +503,23 @@ function _prompt_install_openvpn() { fi } +# Prompt to install restapi +function _prompt_install_restapi() { + _install_log "Configure restapi" + echo -n "Install and enable RestAPI? [Y/n]: " + if [ "$assume_yes" == 0 ]; then + read answer < /dev/tty + if [ "$answer" != "${answer#[Nn]}" ]; then + _install_status 0 "(Skipped)" + else + _install_restapi + elif [ "$restapi_option" == 1 ]; then + _install_restapi + else + echo "(Skipped)" + fi +} + # Prompt to install WireGuard function _prompt_install_wireguard() { _install_log "Configure WireGuard support" @@ -562,6 +580,20 @@ function _create_openvpn_scripts() { _install_status 0 } +# Install and enable RestAPI configuration option +function _install_restapi() { + _install_log "Installing and enabling RestAPI" + sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" + python -m pip install -r "$raspap_dir/api/requirements.txt" || _install_status 1 " Unable to install pip modules" + + echo "Moving restapi systemd unit control file to /lib/systemd/system/" + sudo mv $webroot_dir/installers/restapi.service /lib/systemd/system/ || _install_status 1 "Unable to move restapi.service file" + sudo systemctl daemon-reload + sudo systemctl enable restapi.service || _install_status 1 "Failed to enable restapi.service" + + _install_status 0 +} + # Fetches latest files from github to webroot function _download_latest_files() { _install_log "Cloning latest files from GitHub" diff --git a/installers/raspbian.sh b/installers/raspbian.sh index 6217f57e..ba43c04d 100755 --- a/installers/raspbian.sh +++ b/installers/raspbian.sh @@ -94,6 +94,7 @@ function _parse_params() { upgrade=0 update=0 ovpn_option=1 + restapi_option=0 adblock_option=1 wg_option=1 insiders=0 @@ -111,6 +112,10 @@ function _parse_params() { ovpn_option="$2" shift ;; + --api|--rest|--restapi) + restapi_option="$2" + shift + ;; -a|--adblock) adblock_option="$2" shift diff --git a/installers/restapi.service b/installers/restapi.service new file mode 100644 index 00000000..ce195603 --- /dev/null +++ b/installers/restapi.service @@ -0,0 +1,14 @@ +[Unit] +Description=raspap-restapi +After=network.target + +[Service] +User=root +WorkingDirectory=/etc/raspap/api +LimitNOFILE=4096 +ExecStart=/usr/bin/python uvicorn main:app --host 0.0.0.0 --port 8081 +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target \ No newline at end of file