diff --git a/.gitignore b/.gitignore index a2a77b49..245d6fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ yarn-error.log includes/config.php rootCA.pem vendor +.env diff --git a/api/auth.py b/api/auth.py new file mode 100644 index 00000000..bef3c6ae --- /dev/null +++ b/api/auth.py @@ -0,0 +1,24 @@ +import os +from fastapi.security.api_key import APIKeyHeader +from fastapi import Security, HTTPException +from starlette.status import HTTP_403_FORBIDDEN +from dotenv import load_dotenv + +load_dotenv() + +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" + ) + diff --git a/api/main.py b/api/main.py new file mode 100644 index 00000000..8e98647d --- /dev/null +++ b/api/main.py @@ -0,0 +1,156 @@ +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 + + +tags_metadata = [ +] +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(api_key: APIKey = Depends(auth.get_api_key)): + 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/hotspot"]) +async def get_ap(api_key: APIKey = Depends(auth.get_api_key)): + 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.get("/clients/{wireless_interface}", tags=["Clients"]) +async def get_clients(wireless_interface, api_key: APIKey = Depends(auth.get_api_key)): + 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(api_key: APIKey = Depends(auth.get_api_key)): + 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(api_key: APIKey = Depends(auth.get_api_key)): + return{ +'domains': json.loads(dns.adblockdomains()) +} + +@app.get("/dns/hostnames", tags=["DNS"]) +async def get_hostnames(api_key: APIKey = Depends(auth.get_api_key)): + return{ +'hostnames': json.loads(dns.adblockhostnames()) +} + +@app.get("/dns/upstream", tags=["DNS"]) +async def get_upstream(api_key: APIKey = Depends(auth.get_api_key)): + return{ +'upstream_nameserver': dns.upstream_nameserver() +} + +@app.get("/dns/logs", tags=["DNS"]) +async def get_dnsmasq_logs(api_key: APIKey = Depends(auth.get_api_key)): + return(dns.dnsmasq_logs()) + + +@app.get("/ddns", tags=["DDNS"]) +async def get_ddns(api_key: APIKey = Depends(auth.get_api_key)): + 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(api_key: APIKey = Depends(auth.get_api_key)): + return json.loads(firewall.firewall_rules()) + +@app.get("/networking", tags=["Networking"]) +async def get_networking(api_key: APIKey = Depends(auth.get_api_key)): + return{ +'interfaces': json.loads(networking.interfaces()), +'throughput': json.loads(networking.throughput()) +} + +@app.get("/openvpn", tags=["OpenVPN"]) +async def get_openvpn(api_key: APIKey = Depends(auth.get_api_key)): + 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, api_key: APIKey = Depends(auth.get_api_key)): + return{ +'client_config': openvpn.client_config_list(config) +} + +@app.get("/wireguard", tags=["WireGuard"]) +async def get_wireguard(api_key: APIKey = Depends(auth.get_api_key)): + return{ +'client_configs': wireguard.configs(), +'client_config_names': wireguard.client_config_names(), +'client_config_active': wireguard.client_config_active() +} + diff --git a/api/modules/ap.py b/api/modules/ap.py new file mode 100644 index 00000000..25dffdad --- /dev/null +++ b/api/modules/ap.py @@ -0,0 +1,64 @@ +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 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 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 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 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 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 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 channel(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep channel= | cut -d'=' -f2", 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 ieee80211n(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ieee80211n= | cut -d'=' -f2", 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 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 wpa(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa= | cut -d'=' -f2", 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 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 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 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..63caf40d --- /dev/null +++ b/api/modules/client.py @@ -0,0 +1,38 @@ +import subprocess +import json + +def get_active_clients_amount(interface): + arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True) + mac_addresses = arp_output.stdout.splitlines() + + if mac_addresses: + grep_pattern = '|'.join(mac_addresses) + output = subprocess.run(['grep', '-iwE', grep_pattern, '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True) + return len(output.stdout.splitlines()) + else: + return 0 + +def get_active_clients(interface): + arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True) + arp_mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:]) + + dnsmasq_output = subprocess.run(['cat', '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True) + active_clients = [] + + for line in dnsmasq_output.stdout.splitlines(): + fields = line.split() + mac_address = fields[1] + + if mac_address in arp_mac_addresses: + client_data = { + "timestamp": int(fields[0]), + "mac_address": fields[1], + "ip_address": fields[2], + "hostname": fields[3], + "client_id": fields[4], + } + active_clients.append(client_data) + + json_output = json.dumps(active_clients, indent=2) + return json_output + 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..9000cfc1 --- /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(["cat", f"/etc/openvpn/client/{client_config}"], capture_output=True, text=True).stdout.strip() + return output.split('\n') + +#TODO: where is the logfile?? +#TODO: is service connected? 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..36eaa2b6 --- /dev/null +++ b/api/modules/wireguard.py @@ -0,0 +1,23 @@ +import subprocess +import re + +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]) + +#TODO: where is the logfile?? +#TODO: is service connected? diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 00000000..7c82a713 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.109.0 +uvicorn==0.25.0 +psutil==5.9.8 +python-dotenv==1.0.1 + diff --git a/app/js/custom.js b/app/js/custom.js index 8cbeeecf..febf7609 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -122,6 +122,10 @@ $(document).on("click", "#gen_wpa_passphrase", function(e) { $('#txtwpapassphrase').val(genPassword(63)); }); +$(document).on("click", "#gen_apikey", function(e) { + $('#txtapikey').val(genPassword(32).toLowerCase()); +}); + $(document).on("click", "#js-clearhostapd-log", function(e) { var csrfToken = $('meta[name=csrf_token]').attr('content'); $.post('ajax/logging/clearlog.php?',{'logfile':'/tmp/hostapd.log', 'csrf_token': csrfToken},function(data){ diff --git a/composer.json b/composer.json index ed4f5e8f..ad52f66b 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,15 @@ } ], "require": { - "php": "^7.0" + "php": "^8.2", + "phpoption/phpoption": "^1.9", + "ext-mbstring": "*" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2.0", "phpcompatibility/php-compatibility": "^9.3.5", - "squizlabs/php_codesniffer": "^3.5.5" + "squizlabs/php_codesniffer": "^3.9.0", + "ext-simplexml": "*" }, "scripts": { "lint": "parallel-lint . --exclude vendor", diff --git a/config/config.php b/config/config.php index a252a556..2950632d 100755 --- a/config/config.php +++ b/config/config.php @@ -4,6 +4,7 @@ define('RASPI_BRAND_TEXT', 'RaspAP'); define('RASPI_CONFIG', '/etc/raspap'); define('RASPI_CONFIG_NETWORK', RASPI_CONFIG.'/networking/defaults.json'); define('RASPI_CONFIG_PROVIDERS', 'config/vpn-providers.json'); +define('RASPI_CONFIG_API', RASPI_CONFIG.'/api'); define('RASPI_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth'); define('RASPI_WIFI_AP_INTERFACE', 'wlan0'); define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap'); @@ -59,6 +60,7 @@ define('RASPI_CHANGETHEME_ENABLED', true); define('RASPI_VNSTAT_ENABLED', true); define('RASPI_SYSTEM_ENABLED', true); define('RASPI_MONITOR_ENABLED', false); +define('RASPI_RESTAPI_ENABLED', false); // Locale settings define('LOCALE_ROOT', 'locale'); diff --git a/includes/defaults.php b/includes/defaults.php index 6f359c23..5e4f8fa0 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -9,6 +9,7 @@ $defaults = [ 'RASPI_VERSION' => '3.0.9', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', + 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', 'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth', 'RASPI_WIFI_AP_INTERFACE' => 'wlan0', 'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap', diff --git a/includes/page_actions.php b/includes/page_actions.php index ed64a33e..de4a1cd0 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -45,6 +45,9 @@ case "/system_info": DisplaySystem($extraFooterScripts); break; + case "/restapi_conf": + DisplayRestAPI(); + break; case "/about": DisplayAbout(); break; diff --git a/includes/restapi.php b/includes/restapi.php new file mode 100644 index 00000000..68207b2b --- /dev/null +++ b/includes/restapi.php @@ -0,0 +1,94 @@ +load(); + + // set defaults + $apiKey = $_ENV['RASPAP_API_KEY']; + + if (!RASPI_MONITOR_ENABLED) { + if (isset($_POST['SaveAPIsettings'])) { + if (isset($_POST['txtapikey'])) { + $apiKey = trim($_POST['txtapikey']); + if (strlen($apiKey) == 0) { + $status->addMessage('Please enter a valid API key', 'danger'); + } else { + $return = saveAPISettings($status, $apiKey, $dotenv); + $status->addMessage('Restarting restapi.service', 'info'); + exec('sudo /bin/systemctl stop restapi.service', $return); + sleep(1); + exec('sudo /bin/systemctl start restapi.service', $return); + } + } + } elseif (isset($_POST['StartRestAPIservice'])) { + $status->addMessage('Attempting to start restapi.service', 'info'); + exec('sudo /bin/systemctl start restapi.service', $return); + foreach ($return as $line) { + $status->addMessage($line, 'info'); + } + } elseif (isset($_POST['StopRestAPIservice'])) { + $status->addMessage('Attempting to stop restapi.service', 'info'); + exec('sudo /bin/systemctl stop restapi.service', $return); + foreach ($return as $line) { + $status->addMessage($line, 'info'); + } + } + } + exec("ps aux | grep -v grep | grep uvicorn", $output, $return); + $serviceStatus = !empty($output) ? "up" : "down"; + + exec("sudo systemctl status restapi.service", $output, $return); + array_shift($output); + $serviceLog = implode("\n", $output); + + if ($serviceStatus == "up") { + $docUrl = getDocUrl(); + $faicon = ""; + $docMsg = sprintf(_("RestAPI docs are accessible here%s"),$docUrl, $faicon); + } + + echo renderTemplate("restapi", compact( + "status", + "apiKey", + "serviceStatus", + "serviceLog", + "docMsg" + )); +} + +/** + * Saves RestAPI settings + * + * @param object status + * @param object dotenv + * @param string $apiKey + */ +function saveAPISettings($status, $apiKey, $dotenv) +{ + $status->addMessage('Saving API key', 'info'); + $dotenv->set('RASPAP_API_KEY', $apiKey); + return $status; +} + +// Returns a url for fastapi's automatic docs +function getDocUrl() +{ + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://'; + $server_name = $_SERVER['SERVER_NAME']; + $port = 8081; + $url = $protocol . $server_name .':'. $port . '/docs'; + return $url; +} + diff --git a/includes/sidebar.php b/includes/sidebar.php index 34545fc0..b83b8764 100755 --- a/includes/sidebar.php +++ b/includes/sidebar.php @@ -80,12 +80,17 @@ + +
restapi.service
status is displayed below."
+msgstr "Current restapi.service
status is displayed below."
+
+msgid "RestAPI docs are accessible here%s"
+msgstr "RestAPI docs are accessible here%s"
+
+msgid "Restarting restapi.service"
+msgstr "Restarting restapi.service"
+
diff --git a/src/RaspAP/DotEnv/DotEnv.php b/src/RaspAP/DotEnv/DotEnv.php
new file mode 100644
index 00000000..1e8397a1
--- /dev/null
+++ b/src/RaspAP/DotEnv/DotEnv.php
@@ -0,0 +1,90 @@
+
+ * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
+ */
+
+declare(strict_types=1);
+
+namespace RaspAP\DotEnv;
+
+class DotEnv
+{
+ protected $envFile;
+ protected $data = [];
+
+ public function __construct($envFile = RASPI_CONFIG_API. '/.env')
+ {
+ $this->envFile = $envFile;
+ }
+
+ public function load()
+ {
+ if (!file_exists($this->envFile)) {
+ $this->createEnv();
+ }
+
+ if (file_exists($this->envFile)) {
+ $this->data = parse_ini_file($this->envFile);
+ foreach ($this->data as $key => $value) {
+ if (!getenv($key)) {
+ putenv("$key=$value");
+ $_ENV[$key] = $value;
+ }
+ }
+ } else {
+ throw new Exception(".env file '{$this->envFile}' not found.");
+ }
+ }
+
+ public function set($key, $value)
+ {
+ $this->data[$key] = $value;
+ putenv("$key=$value");
+ $this->store($key, $value);
+ }
+
+ public function get($key)
+ {
+ return getenv($key);
+ }
+
+ public function getAll()
+ {
+ return $this->data;
+ }
+
+ public function unset($key)
+ {
+ unset($_ENV[$key]);
+ return $this;
+ }
+
+ private function store($key, $value)
+ {
+ $content = file_get_contents($this->envFile);
+ $content = preg_replace("/^$key=.*/m", "$key=$value", $content, 1, $count);
+ if ($count === 0) {
+ // if key doesn't exist, append it
+ $content .= "$key=$value\n";
+ }
+ file_put_contents("/tmp/.env", $content);
+ system('sudo mv /tmp/.env '.$this->envFile, $result);
+ if ($result !== 0) {
+ throw new Exception("Unable to move .env file: ". $this->envFile);
+ }
+ }
+
+ protected function createEnv()
+ {
+ exec('sudo touch '. escapeshellarg($this->envFile), $output, $result);
+ if ($result !== 0) {
+ throw new Exception("Unable to create .env file: ". $this->envFile);
+ }
+ }
+}
+
diff --git a/templates/restapi.php b/templates/restapi.php
new file mode 100644
index 00000000..fc1d1437
--- /dev/null
+++ b/templates/restapi.php
@@ -0,0 +1,52 @@
+
+
+ " />
+
+ " />
+
+ " />
+
+
+
+
+ restapi.service status is displayed below."); ?>
+