diff --git a/app/css/all.css b/app/css/all.css index d9de52e3..023eb15b 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -75,8 +75,8 @@ License: GNU General Public License v3.0 .service-status-down { color: #f80107 !important; - animation: flash 1s linear infinite; } + @keyframes flash { 50% { opacity: 0; @@ -88,6 +88,8 @@ License: GNU General Public License v3.0 height: 20rem; border: 1px solid #d1d3e2; 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 { diff --git a/app/css/custom.php b/app/css/custom.php index ff87b3b2..33a59c58 100644 --- a/app/css/custom.php +++ b/app/css/custom.php @@ -42,6 +42,12 @@ body { background-color: #fff; } +.btn-primary.disabled { + color: !important; + border-color: !important; + background-color: #fff !important; +} + .card-footer, .modal-footer { background-color: #f2f1f0; } diff --git a/config/config.php b/config/config.php index 4589092d..630f1ef7 100755 --- a/config/config.php +++ b/config/config.php @@ -3,6 +3,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_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth'); define('RASPI_WIFI_AP_INTERFACE', 'wlan0'); 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_ADBLOCK_ENABLED', false); define('RASPI_OPENVPN_ENABLED', false); +define('RASPI_VPN_PROVIDER_ENABLED', false); define('RASPI_WIREGUARD_ENABLED', false); define('RASPI_TORPROXY_ENABLED', false); define('RASPI_CONFAUTH_ENABLED', true); diff --git a/config/vpn-providers.json b/config/vpn-providers.json new file mode 100644 index 00000000..3108295f --- /dev/null +++ b/config/vpn-providers.json @@ -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" + } + } + ] +} diff --git a/includes/defaults.php b/includes/defaults.php index 6544cfa0..cd109b1c 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -8,6 +8,7 @@ $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_VERSION' => '2.9.7', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', + 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth', 'RASPI_WIFI_AP_INTERFACE' => 'wlan0', 'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap', @@ -45,6 +46,7 @@ $defaults = [ 'RASPI_DHCP_ENABLED' => true, 'RASPI_ADBLOCK_ENABLED' => false, 'RASPI_OPENVPN_ENABLED' => false, + 'RASPI_VPN_PROVIDER_ENABLED' => false, 'RASPI_WIREGUARD_ENABLED' => false, 'RASPI_TORPROXY_ENABLED' => false, 'RASPI_CONFAUTH_ENABLED' => true, diff --git a/includes/functions.php b/includes/functions.php index 24a42f15..e5797507 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -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 */ /** @@ -669,6 +687,7 @@ function initializeApp() $_SESSION["theme_url"] = getThemeOpt(); $_SESSION["toggleState"] = getSidebarState(); $_SESSION["bridgedEnabled"] = getBridgedState(); + $_SESSION["providerID"] = getProviderID(); } function getThemeOpt() @@ -709,6 +728,17 @@ function getBridgedState() 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 * diff --git a/includes/page_actions.php b/includes/page_actions.php index b460dd43..ed64a33e 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -27,6 +27,9 @@ case "/wg_conf": DisplayWireGuardConfig(); break; + case "/provider_conf": + DisplayProviderConfig(); + break; case "/torproxy_conf": DisplayTorProxyConfig(); break; diff --git a/includes/provider.php b/includes/provider.php new file mode 100755 index 00000000..b117e693 --- /dev/null +++ b/includes/provider.php @@ -0,0 +1,316 @@ +addMessage(sprintf(_('Expected %s binary not found at: %s'), $providerName, $binPath), 'warning'); + $status->addMessage(sprintf(_('Visit the installation instructions 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; +} + diff --git a/includes/sidebar.php b/includes/sidebar.php index ada2a0b0..13558354 100755 --- a/includes/sidebar.php +++ b/includes/sidebar.php @@ -60,6 +60,11 @@ + +
%s
"
+msgstr "Installed Linux CLI: %s
"
+
+msgid "Current %s
connection status is displayed below."
+msgstr "Current %s
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"
+
diff --git a/templates/provider.php b/templates/provider.php
new file mode 100755
index 00000000..810a0c04
--- /dev/null
+++ b/templates/provider.php
@@ -0,0 +1,51 @@
+
+
+ class="btn btn-outline btn-primary " name="SaveProviderSettings" value="" />
+
+ class="btn btn-success " name="StartProviderVPN" value="" />
+
+ class="btn btn-warning " name="StopProviderVPN" value="" />
+
+
+
+
+ %s"), $providerVersion); ?>
+%s connection status is displayed below."), strtolower($providerName)); ?>
+ +