mirror of
https://github.com/billz/raspap-webgui.git
synced 2025-03-01 10:31:47 +00:00
Merge pull request #1416 from RaspAP/feat/vpn-providers
Custom VPN provider support
This commit is contained in:
commit
4f3e00fe56
@ -75,8 +75,8 @@ License: GNU General Public License v3.0
|
|||||||
|
|
||||||
.service-status-down {
|
.service-status-down {
|
||||||
color: #f80107 !important;
|
color: #f80107 !important;
|
||||||
animation: flash 1s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash {
|
@keyframes flash {
|
||||||
50% {
|
50% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -88,6 +88,8 @@ License: GNU General Public License v3.0
|
|||||||
height: 20rem;
|
height: 20rem;
|
||||||
border: 1px solid #d1d3e2;
|
border: 1px solid #d1d3e2;
|
||||||
border-radius: .35rem;
|
border-radius: .35rem;
|
||||||
|
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dhcp-static-leases {
|
.dhcp-static-leases {
|
||||||
|
@ -42,6 +42,12 @@ body {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary.disabled {
|
||||||
|
color: <?php echo $color; ?> !important;
|
||||||
|
border-color: <?php echo $color; ?> !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.card-footer, .modal-footer {
|
.card-footer, .modal-footer {
|
||||||
background-color: #f2f1f0;
|
background-color: #f2f1f0;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
define('RASPI_BRAND_TEXT', 'RaspAP');
|
define('RASPI_BRAND_TEXT', 'RaspAP');
|
||||||
define('RASPI_CONFIG', '/etc/raspap');
|
define('RASPI_CONFIG', '/etc/raspap');
|
||||||
define('RASPI_CONFIG_NETWORK', RASPI_CONFIG.'/networking/defaults.json');
|
define('RASPI_CONFIG_NETWORK', RASPI_CONFIG.'/networking/defaults.json');
|
||||||
|
define('RASPI_CONFIG_PROVIDERS', 'config/vpn-providers.json');
|
||||||
define('RASPI_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth');
|
define('RASPI_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth');
|
||||||
define('RASPI_WIFI_AP_INTERFACE', 'wlan0');
|
define('RASPI_WIFI_AP_INTERFACE', 'wlan0');
|
||||||
define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap');
|
define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap');
|
||||||
@ -43,6 +44,7 @@ define('RASPI_NETWORK_ENABLED', true);
|
|||||||
define('RASPI_DHCP_ENABLED', true);
|
define('RASPI_DHCP_ENABLED', true);
|
||||||
define('RASPI_ADBLOCK_ENABLED', false);
|
define('RASPI_ADBLOCK_ENABLED', false);
|
||||||
define('RASPI_OPENVPN_ENABLED', false);
|
define('RASPI_OPENVPN_ENABLED', false);
|
||||||
|
define('RASPI_VPN_PROVIDER_ENABLED', false);
|
||||||
define('RASPI_WIREGUARD_ENABLED', false);
|
define('RASPI_WIREGUARD_ENABLED', false);
|
||||||
define('RASPI_TORPROXY_ENABLED', false);
|
define('RASPI_TORPROXY_ENABLED', false);
|
||||||
define('RASPI_CONFAUTH_ENABLED', true);
|
define('RASPI_CONFAUTH_ENABLED', true);
|
||||||
|
55
config/vpn-providers.json
Normal file
55
config/vpn-providers.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ExpressVPN",
|
||||||
|
"bin_path": "/usr/bin/expressvpn",
|
||||||
|
"install_page": "https://www.expressvpn.com/support/vpn-setup/app-for-linux/",
|
||||||
|
"account_page": "https://www.expressvpn.com/subscriptions",
|
||||||
|
"cmd_overrides": {
|
||||||
|
"countries": "list all",
|
||||||
|
"log": "diagnostics",
|
||||||
|
"version": "-v"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"status": "\/not connected\/",
|
||||||
|
"pattern": "\/^(.{2,5})\\s(?:.{1,28})(.{1,26}).*$\/",
|
||||||
|
"replace": "$1,$2",
|
||||||
|
"slice": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Mullvad VPN",
|
||||||
|
"bin_path": "/usr/bin/mullvad",
|
||||||
|
"install_page": "https://mullvad.net/en/download/vpn/linux",
|
||||||
|
"account_page": "https://mullvad.net/en/account",
|
||||||
|
"cmd_overrides": {
|
||||||
|
"account": "account get",
|
||||||
|
"countries": "relay list",
|
||||||
|
"log": "status -v",
|
||||||
|
"version": "--version"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"status": "\/disconnected\/",
|
||||||
|
"pattern": "\/^(.*),.*$\/",
|
||||||
|
"replace": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "NordVPN",
|
||||||
|
"bin_path": "/usr/bin/nordvpn",
|
||||||
|
"install_page": "https://nordvpn.com/download/linux/",
|
||||||
|
"account_page": "https://my.nordaccount.com/dashboard/",
|
||||||
|
"cmd_overrides": {
|
||||||
|
"log": "status"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"status": "\/status: disconnected\/",
|
||||||
|
"pattern": "(\\w+)\\s+",
|
||||||
|
"replace": "$1,$1\\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -8,6 +8,7 @@ $defaults = [
|
|||||||
'RASPI_BRAND_TEXT' => 'RaspAP',
|
'RASPI_BRAND_TEXT' => 'RaspAP',
|
||||||
'RASPI_VERSION' => '2.9.7',
|
'RASPI_VERSION' => '2.9.7',
|
||||||
'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json',
|
'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json',
|
||||||
|
'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json',
|
||||||
'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth',
|
'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth',
|
||||||
'RASPI_WIFI_AP_INTERFACE' => 'wlan0',
|
'RASPI_WIFI_AP_INTERFACE' => 'wlan0',
|
||||||
'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap',
|
'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap',
|
||||||
@ -45,6 +46,7 @@ $defaults = [
|
|||||||
'RASPI_DHCP_ENABLED' => true,
|
'RASPI_DHCP_ENABLED' => true,
|
||||||
'RASPI_ADBLOCK_ENABLED' => false,
|
'RASPI_ADBLOCK_ENABLED' => false,
|
||||||
'RASPI_OPENVPN_ENABLED' => false,
|
'RASPI_OPENVPN_ENABLED' => false,
|
||||||
|
'RASPI_VPN_PROVIDER_ENABLED' => false,
|
||||||
'RASPI_WIREGUARD_ENABLED' => false,
|
'RASPI_WIREGUARD_ENABLED' => false,
|
||||||
'RASPI_TORPROXY_ENABLED' => false,
|
'RASPI_TORPROXY_ENABLED' => false,
|
||||||
'RASPI_CONFAUTH_ENABLED' => true,
|
'RASPI_CONFAUTH_ENABLED' => true,
|
||||||
|
@ -169,6 +169,24 @@ function getDefaultNetOpts($svc,$key)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a value for the specified VPN provider
|
||||||
|
*
|
||||||
|
* @param numeric $id
|
||||||
|
* @param string $key
|
||||||
|
* @return object $json
|
||||||
|
*/
|
||||||
|
function getProviderValue($id,$key)
|
||||||
|
{
|
||||||
|
$obj = json_decode(file_get_contents(RASPI_CONFIG_PROVIDERS), true);
|
||||||
|
if ($obj === null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$id--;
|
||||||
|
return $obj['providers'][$id][$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Functions to write ini files */
|
/* Functions to write ini files */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -669,6 +687,7 @@ function initializeApp()
|
|||||||
$_SESSION["theme_url"] = getThemeOpt();
|
$_SESSION["theme_url"] = getThemeOpt();
|
||||||
$_SESSION["toggleState"] = getSidebarState();
|
$_SESSION["toggleState"] = getSidebarState();
|
||||||
$_SESSION["bridgedEnabled"] = getBridgedState();
|
$_SESSION["bridgedEnabled"] = getBridgedState();
|
||||||
|
$_SESSION["providerID"] = getProviderID();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThemeOpt()
|
function getThemeOpt()
|
||||||
@ -709,6 +728,17 @@ function getBridgedState()
|
|||||||
return $arrHostapdConf['BridgedEnable'];
|
return $arrHostapdConf['BridgedEnable'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns VPN provider ID, if defined
|
||||||
|
function getProviderID()
|
||||||
|
{
|
||||||
|
if (RASPI_VPN_PROVIDER_ENABLED) {
|
||||||
|
$arrProvider = parse_ini_file(RASPI_CONFIG.'/provider.ini');
|
||||||
|
if (isset($arrProvider['providerID'])) {
|
||||||
|
return $arrProvider['providerID'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the format of a CIDR notation string
|
* Validates the format of a CIDR notation string
|
||||||
*
|
*
|
||||||
|
@ -27,6 +27,9 @@
|
|||||||
case "/wg_conf":
|
case "/wg_conf":
|
||||||
DisplayWireGuardConfig();
|
DisplayWireGuardConfig();
|
||||||
break;
|
break;
|
||||||
|
case "/provider_conf":
|
||||||
|
DisplayProviderConfig();
|
||||||
|
break;
|
||||||
case "/torproxy_conf":
|
case "/torproxy_conf":
|
||||||
DisplayTorProxyConfig();
|
DisplayTorProxyConfig();
|
||||||
break;
|
break;
|
||||||
|
316
includes/provider.php
Executable file
316
includes/provider.php
Executable file
@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Display VPN provider configuration
|
||||||
|
*/
|
||||||
|
function DisplayProviderConfig()
|
||||||
|
{
|
||||||
|
// initialize status object
|
||||||
|
$status = new \RaspAP\Messages\StatusMessage;
|
||||||
|
|
||||||
|
// set defaults
|
||||||
|
$id = $_SESSION["providerID"];
|
||||||
|
$binPath = getProviderValue($id, "bin_path");
|
||||||
|
$providerName = getProviderValue($id, "name");
|
||||||
|
$providerVersion = getProviderVersion($id, $binPath);
|
||||||
|
$installPage = getProviderValue($id, "install_page");
|
||||||
|
$publicIP = get_public_ip();
|
||||||
|
$serviceStatus = 'down';
|
||||||
|
$statusDisplay = 'down';
|
||||||
|
|
||||||
|
if (!file_exists($binPath)) {
|
||||||
|
$status->addMessage(sprintf(_('Expected %s binary not found at: %s'), $providerName, $binPath), 'warning');
|
||||||
|
$status->addMessage(sprintf(_('Visit the <a href="%s" target="_blank">installation instructions</a> for %s\'s Linux CLI.'), $installPage, $providerName), 'warning');
|
||||||
|
$ctlState = 'disabled';
|
||||||
|
$providerVersion = 'not found';
|
||||||
|
} elseif (empty($providerVersion)) {
|
||||||
|
$status->addMessage(sprintf(_('Unable to execute %s binary found at: %s'), $providerName, $binPath), 'warning');
|
||||||
|
$status->addMessage(_('Check that binary is executable and permissions exist in raspap.sudoers'), 'warning');
|
||||||
|
$ctlState = 'disabled';
|
||||||
|
$providerVersion = 'not found';
|
||||||
|
} else {
|
||||||
|
// fetch provider status
|
||||||
|
$serviceStatus = getProviderStatus($id, $binPath);
|
||||||
|
$statusDisplay = $serviceStatus == "down" ? "inactive" : "active";
|
||||||
|
|
||||||
|
// fetch provider log
|
||||||
|
$providerLog = getProviderLog($id, $binPath, $country);
|
||||||
|
|
||||||
|
// fetch account info
|
||||||
|
$accountInfo = getAccountInfo($id, $binPath, $providerName);
|
||||||
|
$accountLink = getProviderValue($id, "account_page");
|
||||||
|
|
||||||
|
// fetch available countries
|
||||||
|
$countries = getCountries($id, $binPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RASPI_MONITOR_ENABLED) {
|
||||||
|
if (isset($_POST['SaveProviderSettings'])) {
|
||||||
|
if (isset($_POST['country'])) {
|
||||||
|
$country = trim($_POST['country']);
|
||||||
|
if (strlen($country) == 0) {
|
||||||
|
$status->addMessage('Select a country from the server location list', 'danger');
|
||||||
|
} else {
|
||||||
|
$return = saveProviderConfig($status, $binPath, $country, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['StartProviderVPN'])) {
|
||||||
|
$status->addMessage('Attempting to connect VPN provider', 'info');
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'connect');
|
||||||
|
exec("sudo $binPath $cmd", $return);
|
||||||
|
$return = stripArtifacts($return);
|
||||||
|
foreach ($return as $line) {
|
||||||
|
if (strlen(trim($line)) > 0) {
|
||||||
|
$line = preg_replace('/\e\[\?[0-9]*l\s(.*)\e.*$/', '$1', $line);
|
||||||
|
$line = preg_replace('/\e\[0m\e\[[0-9;]*m(.*)/', '$1', $line);
|
||||||
|
$status->addMessage($line, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['StopProviderVPN'])) {
|
||||||
|
$status->addMessage('Attempting to disconnect VPN provider', 'info');
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'disconnect');
|
||||||
|
exec("sudo $binPath $cmd", $return);
|
||||||
|
$return = stripArtifacts($return);
|
||||||
|
foreach ($return as $line) {
|
||||||
|
if (strlen(trim($line)) > 0) {
|
||||||
|
$line = preg_replace('/\[1;33;49m(.*)\[0m/', '$1', $line);
|
||||||
|
$status->addMessage($line, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo renderTemplate(
|
||||||
|
"provider", compact(
|
||||||
|
"status",
|
||||||
|
"serviceStatus",
|
||||||
|
"statusDisplay",
|
||||||
|
"providerName",
|
||||||
|
"providerVersion",
|
||||||
|
"accountInfo",
|
||||||
|
"accountLink",
|
||||||
|
"countries",
|
||||||
|
"country",
|
||||||
|
"providerLog",
|
||||||
|
"publicIP",
|
||||||
|
"ctlState"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates VPN provider settings
|
||||||
|
*
|
||||||
|
* @param object $status
|
||||||
|
* @param string $binPath
|
||||||
|
* @param string $country
|
||||||
|
* @param integer $id (optional)
|
||||||
|
*/
|
||||||
|
function saveProviderConfig($status, $binPath, $country, $id = null)
|
||||||
|
{
|
||||||
|
$status->addMessage(sprintf(_('Attempting to connect to %s'),$country), 'info');
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'connect');
|
||||||
|
// mullvad requires relay set location before connect
|
||||||
|
if ($id == 2) {
|
||||||
|
exec("sudo $binPath relay set location $country", $return);
|
||||||
|
exec("sudo $binPath $cmd", $return);
|
||||||
|
} else {
|
||||||
|
exec("sudo $binPath $cmd $country", $return);
|
||||||
|
}
|
||||||
|
$return = stripArtifacts($return);
|
||||||
|
foreach ($return as $line) {
|
||||||
|
if ( strlen(trim($line)) >0 ) {
|
||||||
|
$status->addMessage($line, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes artifacts from shell_exec string values
|
||||||
|
*
|
||||||
|
* @param string $output
|
||||||
|
* @param string $pattern
|
||||||
|
* @return string $result
|
||||||
|
*/
|
||||||
|
function stripArtifacts($output, $pattern = null)
|
||||||
|
{
|
||||||
|
$result = preg_replace('/[-\/\n\t\\\\'.$pattern.'|]/', '', $output);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an override for provider CLI
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $group
|
||||||
|
* @param string $item
|
||||||
|
* @return string $override
|
||||||
|
*/
|
||||||
|
function getCliOverride($id, $group, $item)
|
||||||
|
{
|
||||||
|
$obj = json_decode(file_get_contents(RASPI_CONFIG_PROVIDERS), true);
|
||||||
|
if ($obj === null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$id--;
|
||||||
|
if ($obj['providers'][$id][$group][$item] === null) {
|
||||||
|
return $item;
|
||||||
|
} else {
|
||||||
|
return $obj['providers'][$id][$group][$item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives VPN provider status
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $binPath
|
||||||
|
* @return string $status
|
||||||
|
*/
|
||||||
|
function getProviderStatus($id, $binPath)
|
||||||
|
{
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'status');
|
||||||
|
$pattern = getCliOverride($id, 'regex', 'status');
|
||||||
|
exec("sudo $binPath $cmd", $cmd_raw);
|
||||||
|
$cmd_raw = strtolower(stripArtifacts($cmd_raw[0]));
|
||||||
|
|
||||||
|
if (!empty($cmd_raw[0])) {
|
||||||
|
if (preg_match($pattern, $cmd_raw, $match)) {
|
||||||
|
$status = "down";
|
||||||
|
} else {
|
||||||
|
$status = "up";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$status = "down";
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves available countries
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $binPath
|
||||||
|
* @return array $countries
|
||||||
|
*/
|
||||||
|
function getCountries($id, $binPath)
|
||||||
|
{
|
||||||
|
$countries = [];
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'countries');
|
||||||
|
$pattern = getCliOverride($id, 'regex', 'pattern');
|
||||||
|
$replace = getCliOverride($id, 'regex', 'replace');
|
||||||
|
$slice = getCliOverride($id, 'regex', 'slice');
|
||||||
|
exec("sudo $binPath $cmd", $output);
|
||||||
|
|
||||||
|
// CLI country output differs considerably between different providers.
|
||||||
|
// Ideally, custom parsing would be avoided in favor of a pure regex solution
|
||||||
|
switch ($id) {
|
||||||
|
case 1: // expressvpn
|
||||||
|
$output = array_slice($output, $slice);
|
||||||
|
foreach ($output as $item) {
|
||||||
|
$item = preg_replace($pattern, $replace, $item);
|
||||||
|
$parts = explode(',', $item);
|
||||||
|
$key = trim($parts[0]);
|
||||||
|
$value = trim($parts[1]);
|
||||||
|
$countries[$key] = $value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: // mullvad
|
||||||
|
foreach ($output as $item) {
|
||||||
|
$item = preg_replace($pattern, $replace, $item);
|
||||||
|
if (strlen(trim($item) >0)) {
|
||||||
|
preg_match('/\s+([a-z0-9-]+)\s.*$/', $item, $match);
|
||||||
|
if (count($match) > 1) {
|
||||||
|
$key = $match[1];
|
||||||
|
$item = str_pad($item, strlen($item)+16,' ', STR_PAD_LEFT);
|
||||||
|
$countries[$key] = $item;
|
||||||
|
} else {
|
||||||
|
preg_match('/\(([a-z]+)\)/', $item, $match);
|
||||||
|
$key = $match[1];
|
||||||
|
if (strlen($match[1]) == 3) {
|
||||||
|
$item = str_pad($item, strlen($item)+8,' ', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
$countries[$key] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: // nordvpn
|
||||||
|
$output = stripArtifacts($output,'\s');
|
||||||
|
$arrTmp = explode(",", $output[0]);
|
||||||
|
$countries = array_combine($arrTmp, $arrTmp);
|
||||||
|
foreach ($countries as $key => $value) {
|
||||||
|
$countries[$key] = str_replace("_", " ", $value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$select = array(' ' => _("Select a country..."));
|
||||||
|
$countries = $select + $countries;
|
||||||
|
return $countries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves provider log
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $binPath
|
||||||
|
* @param string $country
|
||||||
|
* @return string $log
|
||||||
|
*/
|
||||||
|
function getProviderLog($id, $binPath, &$country)
|
||||||
|
{
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'log');
|
||||||
|
exec("sudo $binPath $cmd", $cmd_raw);
|
||||||
|
$output = stripArtifacts($cmd_raw);
|
||||||
|
foreach ($output as $item) {
|
||||||
|
if (preg_match('/Country: (\w+)/', $item, $match)) {
|
||||||
|
$country = $match[1];
|
||||||
|
}
|
||||||
|
$providerLog.= ltrim($item) .PHP_EOL;
|
||||||
|
}
|
||||||
|
return $providerLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves provider version information
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $binPath
|
||||||
|
* @return string $version
|
||||||
|
*/
|
||||||
|
function getProviderVersion($id, $binPath)
|
||||||
|
{
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'version');
|
||||||
|
$version = shell_exec("sudo $binPath $cmd");
|
||||||
|
$version = preg_replace('/^[^\w]+\s*/', '', $version);
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves provider account info
|
||||||
|
*
|
||||||
|
* @param integer $id
|
||||||
|
* @param string $binPath
|
||||||
|
* @param string $providerName
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function getAccountInfo($id, $binPath, $providerName)
|
||||||
|
{
|
||||||
|
$cmd = getCliOverride($id, 'cmd_overrides', 'account');
|
||||||
|
exec("sudo $binPath $cmd", $acct);
|
||||||
|
|
||||||
|
foreach ($acct as &$item) {
|
||||||
|
$item = preg_replace('/^[^\w]+\s*/', '', $item);
|
||||||
|
}
|
||||||
|
if (empty($acct)) {
|
||||||
|
$msg = sprintf(_("Account information not available from %s's Linux CLI."), $providerName);
|
||||||
|
$acct[] = $msg;
|
||||||
|
}
|
||||||
|
return $acct;
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,11 @@
|
|||||||
<a class="nav-link" href="wg_conf"><span class="ra-wireguard mr-2"></span><span class="nav-label"><?php echo _("WireGuard"); ?></a>
|
<a class="nav-link" href="wg_conf"><span class="ra-wireguard mr-2"></span><span class="nav-label"><?php echo _("WireGuard"); ?></a>
|
||||||
</li>
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (RASPI_VPN_PROVIDER_ENABLED) : ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="provider_conf"><i class="fas fa-shield-alt fa-fw mr-2"></i><span class="nav-label"><?php echo _(getProviderValue($_SESSION["providerID"], "name")); ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (RASPI_TORPROXY_ENABLED) : ?>
|
<?php if (RASPI_TORPROXY_ENABLED) : ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="torproxy_conf"><i class="fas fa-eye-slash fa-fw mr-2"></i><span class="nav-label"><?php echo _("TOR proxy"); ?></a>
|
<a class="nav-link" href="torproxy_conf"><i class="fas fa-eye-slash fa-fw mr-2"></i><span class="nav-label"><?php echo _("TOR proxy"); ?></a>
|
||||||
|
@ -45,6 +45,7 @@ require_once 'includes/data_usage.php';
|
|||||||
require_once 'includes/about.php';
|
require_once 'includes/about.php';
|
||||||
require_once 'includes/openvpn.php';
|
require_once 'includes/openvpn.php';
|
||||||
require_once 'includes/wireguard.php';
|
require_once 'includes/wireguard.php';
|
||||||
|
require_once 'includes/provider.php';
|
||||||
require_once 'includes/torproxy.php';
|
require_once 'includes/torproxy.php';
|
||||||
|
|
||||||
initializeApp();
|
initializeApp();
|
||||||
|
@ -59,6 +59,7 @@ function _install_raspap() {
|
|||||||
_prompt_install_openvpn
|
_prompt_install_openvpn
|
||||||
_install_mobile_clients
|
_install_mobile_clients
|
||||||
_prompt_install_wireguard
|
_prompt_install_wireguard
|
||||||
|
_prompt_install_vpn_providers
|
||||||
_patch_system_files
|
_patch_system_files
|
||||||
_install_complete
|
_install_complete
|
||||||
}
|
}
|
||||||
@ -90,7 +91,7 @@ function _config_installation() {
|
|||||||
fi
|
fi
|
||||||
_install_log "Configure ${opt[2]}"
|
_install_log "Configure ${opt[2]}"
|
||||||
_get_linux_distro
|
_get_linux_distro
|
||||||
echo "Detected OS: ${DESC}"
|
echo "Detected OS: ${DESC} ${LONG_BIT}-bit"
|
||||||
echo "Using GitHub repository: ${repo} ${branch} branch"
|
echo "Using GitHub repository: ${repo} ${branch} branch"
|
||||||
echo "Configuration directory: ${raspap_dir}"
|
echo "Configuration directory: ${raspap_dir}"
|
||||||
echo -n "lighttpd root: ${webroot_dir}? [Y/n]: "
|
echo -n "lighttpd root: ${webroot_dir}? [Y/n]: "
|
||||||
@ -126,6 +127,7 @@ function _get_linux_distro() {
|
|||||||
RELEASE=$(lsb_release -sr)
|
RELEASE=$(lsb_release -sr)
|
||||||
CODENAME=$(lsb_release -sc)
|
CODENAME=$(lsb_release -sc)
|
||||||
DESC=$(lsb_release -sd)
|
DESC=$(lsb_release -sd)
|
||||||
|
LONG_BIT=$(getconf LONG_BIT)
|
||||||
elif [ -f /etc/os-release ]; then # freedesktop.org
|
elif [ -f /etc/os-release ]; then # freedesktop.org
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
OS=$ID
|
OS=$ID
|
||||||
@ -239,7 +241,7 @@ function _install_dependencies() {
|
|||||||
# Set dconf-set-selections
|
# Set dconf-set-selections
|
||||||
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
|
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
|
||||||
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
|
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
|
||||||
sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package vnstat qrencode || _install_status 1 "Unable to install dependencies"
|
sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package vnstat qrencode jq || _install_status 1 "Unable to install dependencies"
|
||||||
_install_status 0
|
_install_status 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,6 +409,69 @@ function _install_adblock() {
|
|||||||
_install_status 0
|
_install_status 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prompt to install VPN providers
|
||||||
|
function _prompt_install_vpn_providers() {
|
||||||
|
_install_log "Configure VPN provider support (Beta)"
|
||||||
|
echo -n "Enable VPN provider client configuration? [Y/n]: "
|
||||||
|
if [ "$assume_yes" == 0 ]; then
|
||||||
|
read answer < /dev/tty
|
||||||
|
if [ "$answer" != "${answer#[Nn]}" ]; then
|
||||||
|
_install_status 0 "(Skipped)"
|
||||||
|
else
|
||||||
|
_install_provider
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "(Skipped)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install VPN provider client configuration
|
||||||
|
function _install_provider() {
|
||||||
|
echo -e "Select an option from the list:"
|
||||||
|
while true; do
|
||||||
|
json="$webroot_dir/config/"vpn-providers.json
|
||||||
|
while IFS='|' read -r key value; do
|
||||||
|
options["$key"]="$value"
|
||||||
|
done< <(jq -r '.providers[] | "\(.id)|\(.name)|\(.bin_path)"' "$json")
|
||||||
|
|
||||||
|
# display provider options
|
||||||
|
for key in "${!options[@]}"; do
|
||||||
|
echo " $key) ${options[$key]%%|*}"
|
||||||
|
done
|
||||||
|
echo " 0) None"
|
||||||
|
echo -n "Choose an option: "
|
||||||
|
read answer < /dev/tty
|
||||||
|
|
||||||
|
if [ "$answer" != "${answer#[0]}" ]; then
|
||||||
|
_install_status 0 "(Skipped)"
|
||||||
|
break
|
||||||
|
elif [[ "$answer" =~ ^[0-9]+$ ]] && [[ -n ${options[$answer]+abc} ]]; then
|
||||||
|
selected="${options[$answer]}"
|
||||||
|
echo "Configuring support for ${selected%%|*}"
|
||||||
|
bin_path=${selected#*|}
|
||||||
|
if ! grep -q "$bin_path" "$webroot_dir/installers/raspap.sudoers"; then
|
||||||
|
echo "Adding $bin_path to raspap.sudoers"
|
||||||
|
echo "www-data ALL=(ALL) NOPASSWD:$bin_path *" | sudo tee -a "$webroot_dir/installers/raspap.sudoers" > /dev/null || _install_status 1 "Unable to modify raspap.sudoers"
|
||||||
|
fi
|
||||||
|
echo "Enabling administration option for ${selected%%|*}"
|
||||||
|
sudo sed -i "s/\('RASPI_VPN_PROVIDER_ENABLED', \)false/\1true/g" "$webroot_dir/includes/config.php" || _install_status 1 "Unable to modify config.php"
|
||||||
|
|
||||||
|
echo "Adding VPN provider to $raspap_dir/provider.ini"
|
||||||
|
if [ ! -f "$raspap_dir/provider.ini" ]; then
|
||||||
|
sudo touch "$raspap_dir/provider.ini"
|
||||||
|
echo "providerID = $answer" | sudo tee "$raspap_dir/provider.ini" > /dev/null || _install_status 1 "Unable to create $raspap_dir/provider.ini"
|
||||||
|
elif ! grep -q "providerID = $answer" "$raspap_dir/provider.ini"; then
|
||||||
|
echo "providerID = $answer" | sudo tee "$raspap_dir/provider.ini" > /dev/null || _install_status 1 "Unable to write to $raspap_dir/provider.ini"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_install_status 0
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Invalid choice. Select a valid option:"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Prompt to install openvpn
|
# Prompt to install openvpn
|
||||||
function _prompt_install_openvpn() {
|
function _prompt_install_openvpn() {
|
||||||
_install_log "Configure OpenVPN support"
|
_install_log "Configure OpenVPN support"
|
||||||
@ -498,7 +563,13 @@ function _download_latest_files() {
|
|||||||
_install_status 3
|
_install_status 3
|
||||||
echo "Insiders please read this: https://docs.raspap.com/insiders/#authentication"
|
echo "Insiders please read this: https://docs.raspap.com/insiders/#authentication"
|
||||||
fi
|
fi
|
||||||
git clone --branch $branch --depth 1 -c advice.detachedHead=false $git_source_url /tmp/raspap-webgui || _install_status 1 "Unable to download files from github"
|
git clone --branch $branch --depth 1 -c advice.detachedHead=false $git_source_url /tmp/raspap-webgui || clone=false
|
||||||
|
if [ "$clone" = false ]; then
|
||||||
|
_install_status 1 "Unable to download files from github"
|
||||||
|
echo "The installer cannot continue." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
sudo mv /tmp/raspap-webgui $webroot_dir || _install_status 1 "Unable to move raspap-webgui to web root"
|
sudo mv /tmp/raspap-webgui $webroot_dir || _install_status 1 "Unable to move raspap-webgui to web root"
|
||||||
|
|
||||||
if [ "$upgrade" == 1 ]; then
|
if [ "$upgrade" == 1 ]; then
|
||||||
|
Binary file not shown.
@ -1293,3 +1293,66 @@ msgstr "Enable Firewall"
|
|||||||
|
|
||||||
msgid "Apply changes"
|
msgid "Apply changes"
|
||||||
msgstr "Apply changes"
|
msgstr "Apply changes"
|
||||||
|
|
||||||
|
#: includes/provider.php
|
||||||
|
|
||||||
|
msgid "Account details"
|
||||||
|
msgstr "Account details"
|
||||||
|
|
||||||
|
msgid "My account"
|
||||||
|
msgstr "My account"
|
||||||
|
|
||||||
|
msgid "Server location"
|
||||||
|
msgstr "Server location"
|
||||||
|
|
||||||
|
msgid "Choosing <strong>Save settings</strong> will connect to the selected country."
|
||||||
|
msgstr "Choosing <strong>Save settings</strong> will connect to the selected country."
|
||||||
|
|
||||||
|
msgid "Choosing <strong>Connect %s</strong> will connect to a recommended server."
|
||||||
|
msgstr "Choosing <strong>Connect %s</strong> will connect to a recommended server."
|
||||||
|
|
||||||
|
msgid "Select a country from the server location list"
|
||||||
|
msgstr "Select a country from the server location list"
|
||||||
|
|
||||||
|
msgid "Select a country..."
|
||||||
|
msgstr "Select a country..."
|
||||||
|
|
||||||
|
msgid "Account information not available from %s's Linux CLI."
|
||||||
|
msgstr "Account information not available from %s's Linux CLI."
|
||||||
|
|
||||||
|
msgid "Attempting to connect to %s"
|
||||||
|
msgstr "Attempting to connect to %s"
|
||||||
|
|
||||||
|
msgid "Attempting to connect VPN provider"
|
||||||
|
msgstr "Attempting to connect VPN provider"
|
||||||
|
|
||||||
|
msgid "Attempting to disconnect VPN provider"
|
||||||
|
msgstr "Attempting to disconnect VPN provider"
|
||||||
|
|
||||||
|
msgid "Expected %s binary not found at: %s"
|
||||||
|
msgstr "Expected %s binary not found at: %s"
|
||||||
|
|
||||||
|
msgid "Visit the <a href=\"%s\" target=\"_blank\">installation instructions</a> for %s's Linux CLI."
|
||||||
|
msgstr "Visit the <a href=\"%s\" target=\"_blank\">installation instructions</a> for %s's Linux CLI."
|
||||||
|
|
||||||
|
msgid "Unable to execute %s binary found at: %s"
|
||||||
|
msgstr "Unable to execute %s binary found at: %s"
|
||||||
|
|
||||||
|
msgid "Check that binary is executable and permissions exist in raspap.sudoers"
|
||||||
|
msgstr "Check that binary is executable and permissions exist in raspap.sudoers"
|
||||||
|
|
||||||
|
msgid "Installed Linux CLI: <code>%s</code>"
|
||||||
|
msgstr "Installed Linux CLI: <code>%s</code>"
|
||||||
|
|
||||||
|
msgid "Current <code>%s</code> connection status is displayed below."
|
||||||
|
msgstr "Current <code>%s</code> connection status is displayed below."
|
||||||
|
|
||||||
|
msgid "Information provided by %s"
|
||||||
|
msgstr "Information provided by %s"
|
||||||
|
|
||||||
|
msgid "Connect %s"
|
||||||
|
msgstr "Connect %s"
|
||||||
|
|
||||||
|
msgid "Disconnect %s"
|
||||||
|
msgstr "Disconnect %s"
|
||||||
|
|
||||||
|
51
templates/provider.php
Executable file
51
templates/provider.php
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
<?php ob_start() ?>
|
||||||
|
<?php if (!RASPI_MONITOR_ENABLED) : ?>
|
||||||
|
<input type="submit" <?php echo $ctlState; ?> class="btn btn-outline btn-primary <?php echo $ctlState; ?>" name="SaveProviderSettings" value="<?php echo _("Save settings"); ?>" />
|
||||||
|
<?php if ($serviceStatus == 'down') : ?>
|
||||||
|
<input type="submit" <?php echo $ctlState; ?> class="btn btn-success <?php echo $ctlState; ?>" name="StartProviderVPN" value="<?php echo sprintf(_("Connect %s"), $providerName); ?>" />
|
||||||
|
<?php else : ?>
|
||||||
|
<input type="submit" <?php echo $ctlState; ?> class="btn btn-warning <?php echo $ctlState; ?>" name="StopProviderVPN" value="<?php echo sprintf(_("Disconnect %s"), $providerName); ?>" />
|
||||||
|
<?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-shield-alt fa-fw mr-2"></i><?php echo _($providerName); ?>
|
||||||
|
</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"><?php echo strtolower($providerName); ?> <?php echo _($statusDisplay) ?></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.row -->
|
||||||
|
</div><!-- /.card-header -->
|
||||||
|
<div class="card-body">
|
||||||
|
<?php $status->showMessages(); ?>
|
||||||
|
<form role="form" action="provider_conf" enctype="multipart/form-data" method="POST">
|
||||||
|
<?php echo CSRFTokenFieldTag() ?>
|
||||||
|
<!-- Nav tabs -->
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="nav-item"><a class="nav-link active" id="clienttab" href="#providerclient" data-toggle="tab"><?php echo _("Settings"); ?></a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" id="loggingtab" href="#providerstatus" data-toggle="tab"><?php echo _("Status"); ?></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab panes -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<?php echo renderTemplate("provider/general", $__template_data) ?>
|
||||||
|
<?php echo renderTemplate("provider/status", $__template_data) ?>
|
||||||
|
</div><!-- /.tab-content -->
|
||||||
|
|
||||||
|
<?php echo $buttons ?>
|
||||||
|
</form>
|
||||||
|
</div><!-- /.card-body -->
|
||||||
|
<div class="card-footer"><?php echo sprintf( _("Information provided by %s"), strtolower($providerName)); ?></div>
|
||||||
|
</div><!-- /.card -->
|
||||||
|
</div><!-- /.col-lg-12 -->
|
||||||
|
</div><!-- /.row -->
|
||||||
|
|
48
templates/provider/general.php
Normal file
48
templates/provider/general.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<div class="tab-pane active" id="providerclient">
|
||||||
|
<h4 class="mt-3"><?php echo sprintf(_("%s settings"), $providerName) ;?></h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-lg-12 mt-2 mb-2">
|
||||||
|
<div class="row ml-1">
|
||||||
|
<div class="info-item col-xs-3">
|
||||||
|
<i class="fas fa-globe-americas mr-1"></i><?php echo _("IPv4 Address"); ?>
|
||||||
|
</div>
|
||||||
|
<div class="info-value col-xs-3">
|
||||||
|
<?php echo htmlspecialchars($publicIP, ENT_QUOTES); ?><a class="text-gray-500" href="https://ipapi.co/<?php echo($publicIP); ?>" target="_blank" rel="noopener noreferrer"><i class="fas fa-external-link-alt ml-2"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($accountInfo)) : ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mt-1">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5><?php echo _("Account details"); ?></h5>
|
||||||
|
<?php foreach ($accountInfo as $item) {
|
||||||
|
echo '<small>'. $item .'</small><br>';
|
||||||
|
} ?>
|
||||||
|
<a href="<?php echo($accountLink); ?>" target="_blank" class="btn btn-warning btn-sm mt-2"><i class="fas fa-external-link-alt ml-1 mr-1"></i><?php echo _("My account") ?></a>
|
||||||
|
</div><!-- /.card-body -->
|
||||||
|
</div><!-- /.card -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6 mt-3">
|
||||||
|
<h5><?php echo _("Server location"); ?></h5>
|
||||||
|
<div>
|
||||||
|
<small><?php echo _("Choosing <strong>Save settings</strong> will connect to the selected country."); ?></small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<small><?php echo sprintf(_("Choosing <strong>Connect %s</strong> will connect to a recommended server."), $providerName); ?></small>
|
||||||
|
</div>
|
||||||
|
<label for="cbxhwmode"><?php echo _("Country") ;?></label>
|
||||||
|
<?php SelectorOptions('country', $countries, $country, 'cbxcountry'); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.tab-pane | general tab -->
|
||||||
|
|
14
templates/provider/status.php
Normal file
14
templates/provider/status.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!-- logging tab -->
|
||||||
|
<div class="tab-pane fade" id="providerstatus">
|
||||||
|
<h4 class="mt-3 mb-3"><?php echo sprintf(_("%s status"), $providerName) ;?></h4>
|
||||||
|
|
||||||
|
<p><?php echo sprintf(_("Installed Linux CLI: <code>%s</code>"), $providerVersion); ?></p>
|
||||||
|
<p><?php echo sprintf(_("Current <code>%s</code> connection status is displayed below."), strtolower($providerName)); ?></p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-8 mt-2">
|
||||||
|
<textarea class="logoutput"><?php echo htmlspecialchars($providerLog, ENT_QUOTES); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.tab-pane -->
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user