Merge pull request #1519 from NL-TCH/REST-API

RestAPI installer integrated
This commit is contained in:
Bill Zimmerman 2024-03-09 14:05:30 +01:00 committed by GitHub
commit 2de012cc85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1003 additions and 3 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ yarn-error.log
includes/config.php
rootCA.pem
vendor
.env

24
api/auth.py Normal file
View file

@ -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"
)

156
api/main.py Normal file
View file

@ -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()
}

64
api/modules/ap.py Normal file
View file

@ -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)

38
api/modules/client.py Normal file
View file

@ -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

24
api/modules/ddns.py Normal file
View file

@ -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()

30
api/modules/dhcp.py Normal file
View file

@ -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

38
api/modules/dns.py Normal file
View file

@ -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

4
api/modules/firewall.py Normal file
View file

@ -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()

68
api/modules/networking.py Normal file
View file

@ -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

41
api/modules/openvpn.py Normal file
View file

@ -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?

86
api/modules/system.py Normal file
View file

@ -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'

23
api/modules/wireguard.py Normal file
View file

@ -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?

5
api/requirements.txt Normal file
View file

@ -0,0 +1,5 @@
fastapi==0.109.0
uvicorn==0.25.0
psutil==5.9.8
python-dotenv==1.0.1

View file

@ -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){

View file

@ -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",

View file

@ -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');

View file

@ -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',

View file

@ -45,6 +45,9 @@
case "/system_info":
DisplaySystem($extraFooterScripts);
break;
case "/restapi_conf":
DisplayRestAPI();
break;
case "/about":
DisplayAbout();
break;

94
includes/restapi.php Normal file
View file

@ -0,0 +1,94 @@
<?php
require_once 'includes/functions.php';
require_once 'config.php';
/**
* Handler for RestAPI settings
*/
function DisplayRestAPI()
{
// initialize status object
$status = new \RaspAP\Messages\StatusMessage;
// create instance of DotEnv
$dotenv = new \RaspAP\DotEnv\DotEnv;
$dotenv->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 = "<i class=\"text-gray-500 fas fa-external-link-alt ml-1\"></i>";
$docMsg = sprintf(_("RestAPI docs are accessible <a href=\"%s\" target=\"_blank\">here%s</a>"),$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;
}

View file

@ -80,6 +80,11 @@
<a class="nav-link" href="data_use"><i class="fas fa-chart-bar fa-fw mr-2"></i><span class="nav-label"><?php echo _("Data usage"); ?></a>
</li>
<?php endif; ?>
<?php if (RASPI_RESTAPI_ENABLED) : ?>
<li class="nav-item">
<a class="nav-link" href="restapi_conf"><i class="fas fa-puzzle-piece mr-2"></i><span class="nav-label"><?php echo _("RestAPI"); ?></a>
</li>
<?php endif; ?>
<?php if (RASPI_SYSTEM_ENABLED) : ?>
<li class="nav-item">
<a class="nav-link" href="system_info"><i class="fas fa-cube fa-fw mr-2"></i><span class="nav-label"><?php echo _("System"); ?></a>

View file

@ -47,6 +47,7 @@ require_once 'includes/about.php';
require_once 'includes/openvpn.php';
require_once 'includes/wireguard.php';
require_once 'includes/provider.php';
require_once 'includes/restapi.php';
require_once 'includes/torproxy.php';
initializeApp();

View file

@ -57,6 +57,7 @@ function _install_raspap() {
_configure_networking
_prompt_install_adblock
_prompt_install_openvpn
_prompt_install_restapi
_install_extra_features
_prompt_install_wireguard
_prompt_install_vpn_providers
@ -502,6 +503,24 @@ 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
fi
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 +581,33 @@ function _create_openvpn_scripts() {
_install_status 0
}
# Install and enable RestAPI configuration option
function _install_restapi() {
_install_log "Installing and enabling RestAPI"
sudo mv "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder"
if ! command -v python3 &> /dev/null; then
echo "Python is not installed. Installing Python..."
sudo apt update
sudo apt install -y python3 python3-pip
echo "Python installed successfully."
else
echo "Python is already installed."
sudo apt install python3-pip -y
fi
python3 -m pip install -r "$raspap_dir/api/requirements.txt" --break-system-packages || _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"
echo "Enabling RestAPI management option"
sudo sed -i "s/\('RASPI_RESTAPI_ENABLED', \)false/\1true/g" "$webroot_dir/includes/config.php" || _install_status 1 "Unable to modify config.php"
_install_status 0
}
# Fetches latest files from github to webroot
function _download_latest_files() {
_install_log "Cloning latest files from GitHub"

View file

@ -26,6 +26,11 @@ www-data ALL=(ALL) NOPASSWD:/bin/systemctl start openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl enable openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl disable openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl start restapi.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop restapi.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl status restapi.service
www-data ALL=(ALL) NOPASSWD:/bin/touch /etc/raspap/api/.env
www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/.env /etc/raspap/api/.env
www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/ovpn/* /etc/openvpn/client/*.conf
www-data ALL=(ALL) NOPASSWD:/usr/bin/ln -s /etc/openvpn/client/*.conf /etc/openvpn/client/*.conf
www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/openvpn/client/*.conf

View file

@ -40,6 +40,7 @@ OPTIONS:
-y, --yes, --assume-yes Assumes "yes" as an answer to all prompts
-c, --cert, --certificate Installs an SSL certificate for lighttpd
-o, --openvpn <flag> Used with -y, --yes, sets OpenVPN install option (0=no install)
-s, --rest, --restapi <flag> Used with -y, --yes, sets RestAPI install option (0=no install)
-a, --adblock <flag> Used with -y, --yes, sets Adblock install option (0=no install)
-w, --wireguard <flag> Used with -y, --yes, sets WireGuard install option (0=no install)
-e, --provider <value> Used with -y, --yes, sets the VPN provider install option
@ -94,6 +95,7 @@ function _parse_params() {
upgrade=0
update=0
ovpn_option=1
restapi_option=1
adblock_option=1
wg_option=1
insiders=0
@ -111,6 +113,10 @@ function _parse_params() {
ovpn_option="$2"
shift
;;
-s|--rest|--restapi)
restapi_option="$2"
shift
;;
-a|--adblock)
adblock_option="$2"
shift

View file

@ -0,0 +1,16 @@
[Unit]
Description=raspap-restapi
After=network.target
[Service]
User=pi
WorkingDirectory=/etc/raspap/api
LimitNOFILE=4096
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081
ExecStop=/bin/kill -HUP ${MAINPID}
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target

Binary file not shown.

View file

@ -1528,3 +1528,35 @@ msgstr "RaspAP Exception"
msgid "An exception occurred"
msgstr "An exception occurred"
#: includes/restapi.php
msgid "RestAPI"
msgstr "RestAPI"
msgid "RestAPI settings"
msgstr "RestAPI settings"
msgid "Start RestAPI service"
msgstr "Start RestAPI service"
msgid "Stop RestAPI service"
msgstr "Stop RestAPI service"
msgid "API Key"
msgstr "API Key"
msgid "Saving API key"
msgstr "Saving API key"
msgid "RestAPI status"
msgstr "RestAPI status"
msgid "Current <code>restapi.service</code> status is displayed below."
msgstr "Current <code>restapi.service</code> status is displayed below."
msgid "RestAPI docs are accessible <a href=\"%s\" target=\"_blank\">here%s</a>"
msgstr "RestAPI docs are accessible <a href=\"%s\" target=\"_blank\">here%s</a>"
msgid "Restarting restapi.service"
msgstr "Restarting restapi.service"

View file

@ -0,0 +1,90 @@
<?php
/**
* DotEnv parser/writer class
*
* @description Reads and sets key/value pairs to .env
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @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);
}
}
}

52
templates/restapi.php Normal file
View file

@ -0,0 +1,52 @@
<?php ob_start() ?>
<?php if (!RASPI_MONITOR_ENABLED) : ?>
<input type="submit" class="btn btn-outline btn-primary" name="SaveAPIsettings" value="<?php echo _("Save settings"); ?>" />
<?php if ($serviceStatus == 'down') : ?>
<input type="submit" class="btn btn-success" name="StartRestAPIservice" value="<?php echo _("Start RestAPI service"); ?>" />
<?php else : ?>
<input type="submit" class="btn btn-warning" name="StopRestAPIservice" value="<?php echo _("Stop RestAPI service"); ?>" />
<?php endif; ?>
<?php endif ?>
<?php $buttons = ob_get_clean(); ob_end_clean() ?>
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<i class="fas fa-puzzle-piece mr-2"></i><?php echo _("RestAPI"); ?>
</div>
<div class="col">
<button class="btn btn-light btn-icon-split btn-sm service-status float-right">
<span class="icon text-gray-600"><i class="fas fa-circle service-status-<?php echo $serviceStatus ?>"></i></span>
<span class="text service-status">restapi.service <?php echo _($serviceStatus) ?></span>
</button>
</div>
</div><!-- /.row -->
</div><!-- /.card-header -->
<div class="card-body">
<?php $status->showMessages(); ?>
<form role="form" action="restapi_conf" method="POST" class="needs-validation" novalidate>
<?php echo CSRFTokenFieldTag() ?>
<!-- Nav tabs -->
<ul class="nav nav-tabs">
<li class="nav-item"><a class="nav-link active" id="restapisettingstab" href="#restapisettings" data-toggle="tab"><?php echo _("Settings"); ?></a></li>
<li class="nav-item"><a class="nav-link" id="restapistatustab" href="#restapistatus" data-toggle="tab"><?php echo _("Status"); ?></a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<?php echo renderTemplate("restapi/general", $__template_data) ?>
<?php echo renderTemplate("restapi/status", $__template_data) ?>
</div><!-- /.tab-content -->
<?php echo $buttons ?>
</form>
</div><!-- /.card-body -->
<div class="card-footer"></div>
</div><!-- /.card -->
</div><!-- /.col-lg-12 -->
</div><!-- /.row -->

View file

@ -0,0 +1,27 @@
<div class="tab-pane active" id="restapisettings">
<h4 class="mt-3"><?php echo ("RestAPI settings") ;?></h4>
<div class="row">
<div class="form-group col-lg-12 mt-2">
<div class="row">
<div class="col-md-6">
<?php echo $docMsg; ?>
</div>
</div>
<div class="row mt-3">
<div class="form-group col-md-6" required>
<label for="txtapikey"><?php echo _("API Key"); ?></label>
<div class="input-group">
<input type="text" class="form-control" id="txtapikey" name="txtapikey" value="<?php echo htmlspecialchars($apiKey, ENT_QUOTES); ?>" required />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="gen_apikey"><i class="fas fa-magic"></i></button>
</div>
<div class="invalid-feedback">
<?php echo _("Please provide a valid API key."); ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /.tab-pane | general tab -->

View file

@ -0,0 +1,11 @@
<!-- status tab -->
<div class="tab-pane fade" id="restapistatus">
<h4 class="mt-3 mb-3"><?php echo _("RestAPI status") ;?></h4>
<p><?php echo _("Current <code>restapi.service</code> status is displayed below."); ?></p>
<div class="row">
<div class="form-group col-md-8 mt-2">
<textarea class="logoutput"><?php echo htmlspecialchars($serviceLog, ENT_QUOTES); ?></textarea>
</div>
</div>
</div><!-- /.tab-pane -->