Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,32 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//modules:module.bzl", "cc_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
cc_everest_module(
name = "Setup",
srcs = glob([
"*.cpp",
"*.hpp",
]),
deps = [
"//lib/everest/run_application",
],
)
cc_test(
name = "Setup_test",
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
srcs = glob(
[
"tests/*.cpp",
"tests/*.hpp",
"WiFiSetup.*",
],
),
includes = ["."],
deps = [
"//lib/everest/run_application",
"@com_github_nlohmann_json//:json",
"@googletest//:gtest_main",
],
)

View File

@@ -0,0 +1,29 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_sources(${MODULE_NAME}
PRIVATE
"WiFiSetup.cpp"
)
target_link_libraries(${MODULE_NAME}
PRIVATE
everest::run_application
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17)
if(EVEREST_CORE_BUILD_TESTING)
include(CTest)
add_subdirectory(tests)
endif()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,245 @@
# Setup module API documentation
This module is responsible for setup tasks that might need privileged access, for example wifi configuration.
If not run as root user, set at least the following capabilities in your EVerest config file: CAP_NET_ADMIN, CAP_NET_RAW, CAP_DAC_OVERRIDE.
They will be passed on to the child processes such as wpa_cli etc.
## Periodically published variables
### everest_api/setup/var/supported_setup_features
This variable is published periodically and contains a JSON object with the supported features in the following form:
```json
{
"localization": true,
"setup_simulation": true,
"setup_wifi": true
}
```
### everest_api/setup/var/hostname
This variable is published periodically and contains the hostname.
## Commands and variables published in response
### everest_api/setup/cmd/scan_wifi
If any arbitrary payload is published to this topic a list of available wifi networks is published on the following topic:
__everest_api/setup/var/wifi_info__
with the following payload format:
```json
[
{
"bssid": "00:11:22:33:44:55",
"flags": [
"WPA2-PSK-CCMP",
"ESS"
],
"frequency": 2437,
"signal_level": -41,
"ssid": "Example"
},
{
"bssid": "66:77:88:99:aa:bb",
"flags": [
"WPA2-PSK-CCMP",
"ESS"
],
"frequency": 5180,
"signal_level": -56,
"ssid": "Example2"
}
]
```
additionally general network device information is published on the following topic:
__everest_api/setup/var/network_device_info__
with the following payload format:
```json
[
{
"blocked": false,
"interface": "wlan0",
"ipv4": ["192.0.2.23"],
"ipv6": ["2001:db8:0:0:0:0:0:23"],
"mac": "00:11:22:33:44:55",
"rfkill_id": "0",
"wireless": true
},
{
"blocked": false,
"interface": "eth0",
"ipv4": "192.0.2.42",
"ipv6": ["2001:db8:0:0:0:0:0:42"],
"mac": "11:22:33:44:55:66",
"rfkill_id": "",
"wireless": false
}
]
```
additionally the list of configured wifi networks is published to the following topic:
__everest_api/setup/var/configured_networks__
with the following payload format:
```json
[
{
"connected": true,
"interface": "wlan0",
"network_id": 0,
"signal_level": -56,
"ssid": "Example"
},
{
"connected": false,
"interface": "wlan0",
"network_id": 1,
"signal_level": -100,
"ssid": "Example2"
}
]
```
### everest_api/setup/cmd/enable_wifi_scanning
If any arbitrary payload is published to this topic the list of available wifi networks and general network device information just mentioned is published periodically.
### everest_api/setup/cmd/disable_wifi_scanning
If any arbitrary payload is published to this topic the list of available wifi networks and general network device information stops being periodically published.
### everest_api/setup/cmd/rfkill_unblock
If a rfkill_id is published to this topic the wifi interface with this id will be unblocked.
### everest_api/setup/cmd/rfkill_block
If a rfkill_id is published to this topic the wifi interface with this id will be blocked.
### everest_api/setup/cmd/list_configured_networks
If any arbitrary payload is published to this topic the list of configured wifi networks is published to the following topic:
__everest_api/setup/var/configured_networks__
with the following payload format:
```json
[
{
"connected": true,
"interface": "wlan0",
"network_id": 0,
"ssid": "Example"
},
{
"connected": false,
"interface": "wlan0",
"network_id": 1,
"ssid": "Example2"
}
]
```
### everest_api/setup/cmd/add_network
To add a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "20fcb529dee0aad11b0568f553942850d06e4c4531c0d75b35345d580b300f78"
}
```
The PSK field can represent the passphrase instead using escaped quotes:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "\"A_valid_passphrase\""
}
```
For open WiFi networks the psk must be an empty string `"psk": ""`.
For hidden networks an optional item is needed:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "\"A_valid_passphrase\"",
"hidden": true
}
```
When `hidden` is not supplied then it is assumed to be false.
### everest_api/setup/cmd/enable_network
To enable a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/disable_network
To disable a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/select_network
To select a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/remove_network
To remove a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/remove_all_networks
If any arbitrary payload is published to this topic all wifi networks will be removed.
### everest_api/setup/cmd/enable_ap
If any arbitrary payload is published to this topic a wireless access point will be enabled on the interface configured in the module config.
### everest_api/setup/cmd/disable_ap
If any arbitrary payload is published to this topic the wireless access point will be disabled.
### everest_api/setup/cmd/check_online_status
If any arbitrary payload is published to this topic a ping will be sent to the host configured in the configuration key "online_check_host". Depending on the success of this ping a status of "online" or "offline" will be reported on the following topic:
__everest_api/setup/var/online_status__
### everest_api/setup/cmd/reboot
If any arbitrary payload is published to this topic the system will reboot.
## Application Info / Localization
### everest_api/setup/cmd/set_mode
If a mode _private_ or _public_ is published to this topic it will be stored permanently.
### everest_api/setup/cmd/set_initialized
If any arbitrary payload is published to this topic the system will be marked as "initialized" permanently.
### everest_api/setup/cmd/reset_initialized
If any arbitrary payload is published to this topic the system will be marked as "uninitialized" permanently.
### everest_api/setup/cmd/change_default_language
You can set a [three-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) to be set as the default language which will be stored permanently.
### everest_api/setup/cmd/change_current_language
You can set a [three-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) to be set as the current language.
### everest_api/setup/cmd/get_application_info
If any arbitrary payload is published to this topic a application info object is published to the following topic:
__everest_api/setup/var/application_info__
with the following payload format:
```json
{
"initialized": true,
"mode": "private",
"default_language": "eng",
"current_language": "ger"
}
```

View File

@@ -0,0 +1,707 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Setup.hpp"
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <locale>
#include <everest/run_application/run_application.hpp>
#include <fmt/core.h>
using namespace everest::run_application;
namespace module {
// set WifiConfigureClass to the class to use for configuring WiFi
typedef WpaCliSetup WifiConfigureClass;
void to_json(json& j, const NetworkDeviceInfo& k) {
j = json::object({{"interface", k.interface},
{"wireless", k.wireless},
{"blocked", k.blocked},
{"rfkill_id", k.rfkill_id},
{"ipv4", k.ipv4},
{"ipv6", k.ipv6},
{"mac", k.mac},
{"link_type", k.link_type}});
}
void to_json(json& j, const WifiCredentials& k) {
j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}});
}
void from_json(const json& j, WifiCredentials& k) {
k.interface = j.at("interface");
k.ssid = j.at("ssid");
k.psk = j.at("psk");
k.hidden = false;
// optional item
auto it = j.find("hidden");
if ((it != j.end() && *it)) {
k.hidden = true;
}
}
void to_json(json& j, const InterfaceAndNetworkId& k) {
j = json::object({{"interface", k.interface}, {"network_id", k.network_id}});
}
void from_json(const json& j, InterfaceAndNetworkId& k) {
k.interface = j.at("interface");
k.network_id = j.at("network_id");
}
void to_json(json& j, const SupportedSetupFeatures& k) {
j = json::object(
{{"setup_wifi", k.setup_wifi}, {"localization", k.localization}, {"setup_simulation", k.setup_simulation}});
}
void to_json(json& j, const ApplicationInfo& k) {
j = json::object({{"initialized", k.initialized},
{"mode", k.mode},
{"default_language", k.default_language},
{"current_language", k.current_language},
{"release_metadata_file", k.release_metadata_file}});
}
//------------------------------------------------------------------------------
// JSON conversion for WifiConfigureClass types
static void to_json(json& j, const WifiConfigureClass::WifiNetworkStatus& k) {
j = json::object({{"interface", k.interface},
{"network_id", k.network_id},
{"ssid", k.ssid},
{"connected", k.connected},
{"signal_level", k.signal_level}});
}
static void to_json(json& j, const WifiConfigureClass::WifiScan& k) {
auto flags_array = json::array();
flags_array = k.flags;
j = json::object({{"bssid", k.bssid},
{"ssid", k.ssid},
{"frequency", k.frequency},
{"signal_level", k.signal_level},
{"flags", flags_array}});
}
//------------------------------------------------------------------------------
void Setup::init() {
// Set default locale "C" when no locale is set at all
try {
std::locale loc("");
} catch (const std::runtime_error& e) {
setenv("LC_ALL", "C", 1);
}
}
void Setup::ready() {
this->discover_network_thread = std::thread([this]() {
while (true) {
if ((this->config.setup_wifi) && (wifi_scan_enabled)) {
this->discover_network();
}
this->publish_hostname();
std::this_thread::sleep_for(std::chrono::seconds(5));
}
});
this->publish_application_info_thread = std::thread([this]() {
while (true) {
this->publish_supported_features();
this->publish_application_info();
this->publish_ap_state();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
std::string set_mode_cmd = this->cmd_base + "set_mode";
this->mqtt.subscribe(set_mode_cmd, [this](const std::string& data) { this->set_mode(data); });
std::string set_initialized_cmd = this->cmd_base + "set_initialized";
this->mqtt.subscribe(set_initialized_cmd, [this](const std::string& data) { this->set_initialized(true); });
std::string reset_initialized_cmd = this->cmd_base + "reset_initialized";
this->mqtt.subscribe(reset_initialized_cmd, [this](const std::string& data) { this->set_initialized(false); });
std::string change_default_language_cmd = this->cmd_base + "change_default_language";
this->mqtt.subscribe(change_default_language_cmd,
[this](const std::string& data) { this->set_default_language(data); });
std::string change_current_language_cmd = this->cmd_base + "change_current_language";
this->mqtt.subscribe(change_current_language_cmd,
[this](const std::string& data) { this->set_current_language(data); });
std::string get_application_info_cmd = this->cmd_base + "get_application_info";
this->mqtt.subscribe(get_application_info_cmd,
[this](const std::string& data) { this->publish_application_info(); });
if (this->config.setup_wifi) {
std::string rfkill_unblock_cmd = this->cmd_base + "rfkill_unblock";
this->mqtt.subscribe(rfkill_unblock_cmd, [this](const std::string& data) { this->rfkill_unblock(data); });
std::string rfkill_block_cmd = this->cmd_base + "rfkill_block";
this->mqtt.subscribe(rfkill_block_cmd, [this](const std::string& data) { this->rfkill_block(data); });
std::string list_configured_networks_cmd = this->cmd_base + "list_configured_networks";
this->mqtt.subscribe(list_configured_networks_cmd,
[this](const std::string& data) { this->publish_configured_networks(); });
std::string add_network_cmd = this->cmd_base + "add_network";
this->mqtt.subscribe(add_network_cmd, [this](const std::string& data) {
WifiCredentials wifi_credentials = json::parse(data);
WifiConfigureClass wifi;
this->add_and_enable_network(wifi_credentials.interface, wifi_credentials.ssid, wifi_credentials.psk,
wifi_credentials.hidden);
wifi.save_config(wifi_credentials.interface);
this->publish_configured_networks();
});
std::string enable_network_cmd = this->cmd_base + "enable_network";
this->mqtt.subscribe(enable_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.enable_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string disable_network_cmd = this->cmd_base + "disable_network";
this->mqtt.subscribe(disable_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.disable_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string select_network_cmd = this->cmd_base + "select_network";
this->mqtt.subscribe(select_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.select_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string remove_network_cmd = this->cmd_base + "remove_network";
this->mqtt.subscribe(remove_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.remove_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string scan_wifi_cmd = this->cmd_base + "scan_wifi";
this->mqtt.subscribe(scan_wifi_cmd, [this](const std::string& data) { this->discover_network(); });
std::string enable_wifi_scanning_cmd = this->cmd_base + "enable_wifi_scanning";
this->mqtt.subscribe(enable_wifi_scanning_cmd,
[this](const std::string& data) { this->wifi_scan_enabled = true; });
std::string disable_wifi_scanning_cmd = this->cmd_base + "disable_wifi_scanning";
this->mqtt.subscribe(disable_wifi_scanning_cmd,
[this](const std::string& data) { this->wifi_scan_enabled = false; });
std::string remove_all_networks_cmd = this->cmd_base + "remove_all_networks";
this->mqtt.subscribe(remove_all_networks_cmd, [this](const std::string& data) {
this->remove_all_networks();
this->publish_configured_networks();
});
std::string reboot_cmd = this->cmd_base + "reboot";
this->mqtt.subscribe(reboot_cmd, [this](const std::string& data) { this->reboot(); });
std::string check_online_status_cmd = this->cmd_base + "check_online_status";
this->mqtt.subscribe(check_online_status_cmd, [this](const std::string& data) { this->check_online_status(); });
std::string enable_ap_cmd = this->cmd_base + "enable_ap";
this->mqtt.subscribe(enable_ap_cmd, [this](const std::string& data) { enable_ap(); });
std::string disable_ap_cmd = this->cmd_base + "disable_ap";
this->mqtt.subscribe(disable_ap_cmd, [this](const std::string& data) { disable_ap(); });
}
}
void Setup::publish_supported_features() {
SupportedSetupFeatures supported_setup_features;
supported_setup_features.setup_wifi = this->config.setup_wifi;
supported_setup_features.localization = this->config.localization;
supported_setup_features.setup_simulation = this->config.setup_simulation;
std::string supported_setup_features_var = this->var_base + "supported_setup_features";
json supported_setup_features_json = supported_setup_features;
this->mqtt.publish(supported_setup_features_var, supported_setup_features_json.dump());
}
void Setup::publish_application_info() {
ApplicationInfo application_info;
application_info.initialized = this->get_initialized();
application_info.mode = this->get_mode();
application_info.default_language = this->get_default_language();
application_info.current_language = this->get_current_language();
application_info.release_metadata_file = this->info.paths.etc / this->config.release_metadata_file;
std::string application_info_var = this->var_base + "application_info";
json application_info_json = application_info;
this->mqtt.publish(application_info_var, application_info_json.dump());
}
void Setup::publish_hostname() {
std::string hostname_var = this->var_base + "hostname";
this->mqtt.publish(hostname_var, this->get_hostname());
}
void Setup::publish_ap_state() {
std::string ap_state_var = this->var_base + "ap_state";
auto hostapd_enabled_output = run_application("systemctl", {"is-active", "--quiet", "hostapd"});
if (hostapd_enabled_output.exit_code == 0) {
this->ap_state = "enabled";
} else {
this->ap_state = "disabled";
}
this->mqtt.publish(ap_state_var, this->ap_state);
}
void Setup::set_default_language(std::string language) {
this->r_store->call_store("everest_localization_default_language", language);
}
std::string Setup::get_default_language() {
auto language = this->r_store->call_load("everest_localization_default_language");
if (!std::holds_alternative<std::string>(language)) {
return "unknown";
}
return std::get<std::string>(language);
}
void Setup::set_current_language(const std::string& language) {
this->current_language = language;
}
std::string Setup::get_current_language() {
if (this->current_language.empty()) {
this->current_language = this->get_default_language();
}
return this->current_language;
}
void Setup::set_mode(std::string mode) {
this->r_store->call_store("everest_mode", mode);
}
std::string Setup::get_mode() {
auto mode = this->r_store->call_load("everest_mode");
if (!std::holds_alternative<std::string>(mode)) {
return "unknown";
}
return std::get<std::string>(mode);
}
void Setup::set_initialized(bool initialized) {
this->r_store->call_store("everest_initialized", initialized);
}
bool Setup::get_initialized() {
if (this->config.initialized_by_default) {
return true;
}
auto initialized = this->r_store->call_load("everest_initialized");
if (!std::holds_alternative<bool>(initialized)) {
return false;
}
return std::get<bool>(initialized);
}
void Setup::discover_network() {
std::vector<NetworkDeviceInfo> device_info = this->get_network_devices();
this->populate_rfkill_status(device_info);
this->populate_ip_addresses(device_info);
std::string network_device_info_var = this->var_base + "network_device_info";
json device_info_json = json::array();
device_info_json = device_info;
this->mqtt.publish(network_device_info_var, device_info_json.dump());
auto wifi_info = this->scan_wifi(device_info);
std::string wifi_info_var = this->var_base + "wifi_info";
json wifi_info_json = json::array();
wifi_info_json = wifi_info;
this->mqtt.publish(wifi_info_var, wifi_info_json.dump());
this->publish_configured_networks();
}
std::string Setup::read_type_file(const fs::path& type_path) {
if (!fs::exists(type_path)) {
return "";
}
std::ifstream ifs(type_path.c_str());
std::string type_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
// trim newlines
type_file.erase(std::remove(type_file.begin(), type_file.end(), '\n'), type_file.end());
return type_file;
}
std::vector<NetworkDeviceInfo> Setup::get_network_devices() {
auto sys_net_path = fs::path("/sys/class/net");
auto sys_virtual_net_path = fs::path("/sys/devices/virtual/net");
std::vector<NetworkDeviceInfo> device_info;
for (auto&& net_it : fs::directory_iterator(sys_net_path)) {
auto net_path = net_it.path();
auto type_path = net_path / "type";
if (!fs::exists(type_path)) {
continue;
}
std::string type_file = this->read_type_file(type_path);
auto interface = net_path.filename();
auto virtual_interface = sys_virtual_net_path / interface;
// check if type is ethernet:
if (type_file == "1") {
if (fs::exists(virtual_interface)) {
continue;
}
auto device = NetworkDeviceInfo();
device.interface = interface.string();
device.link_type = "ether";
// check if its wireless or not:
auto wireless_path = net_path / "wireless";
if (fs::exists(wireless_path)) {
device.wireless = true;
auto phy80211_path = net_path / "phy80211";
for (auto&& rfkill_it : fs::directory_iterator(phy80211_path)) {
auto phy_file_path = rfkill_it.path().filename().string();
std::string rfkill = "rfkill";
if (phy_file_path.find(rfkill) == 0) {
phy_file_path.erase(0, rfkill.size());
device.rfkill_id = phy_file_path;
break;
}
}
}
device_info.push_back(device);
} else if (type_file == "65534") {
if (!fs::exists(virtual_interface)) {
continue;
}
auto virtual_type_path = virtual_interface / "type";
if (!fs::exists(virtual_type_path)) {
continue;
}
std::string virtual_type_file = this->read_type_file(virtual_type_path);
if (virtual_type_file == type_file) {
// assume it's a vpn, but check ip link
auto ip_output = run_application("ip", {"--json", "-details", "link", "show", interface});
if (ip_output.exit_code != 0) {
continue;
}
const auto ip_json = json::parse(ip_output.output);
if (ip_json.size() < 1) {
continue;
}
const auto& entry = ip_json.at(0);
if (entry.contains("linkinfo") and entry.at("linkinfo").contains("info_kind")) {
auto device = NetworkDeviceInfo();
device.interface = interface.string();
device.link_type = entry.at("linkinfo").at("info_kind");
device_info.push_back(device);
}
}
}
}
return device_info;
}
void Setup::populate_rfkill_status(std::vector<NetworkDeviceInfo>& device_info) {
auto rfkill_output = run_application("rfkill", {"--json"});
if (rfkill_output.exit_code != 0) {
return;
}
auto rfkill_json = json::parse(rfkill_output.output);
for (auto rfkill_object : rfkill_json.items()) {
for (auto rfkill_device : rfkill_object.value()) {
for (auto& device : device_info) {
int device_id = rfkill_device.at("id");
if (!device.rfkill_id.empty() && std::to_string(device_id) == device.rfkill_id) {
if (rfkill_device.at("soft") == "blocked") {
device.blocked = true;
}
break;
}
}
}
}
}
bool Setup::rfkill_unblock(std::string rfkill_id) {
auto network_devices = this->get_network_devices();
this->populate_rfkill_status(network_devices);
bool found = false;
for (auto device : network_devices) {
if (device.rfkill_id == rfkill_id) {
if (!device.blocked) {
return true;
}
found = true;
break;
}
}
if (!found) {
return false;
}
auto rfkill_output = run_application("rfkill", {"unblock", rfkill_id});
if (rfkill_output.exit_code != 0) {
return false;
}
return true;
}
bool Setup::rfkill_block(std::string rfkill_id) {
auto network_devices = this->get_network_devices();
this->populate_rfkill_status(network_devices);
bool found = false;
for (auto device : network_devices) {
if (device.rfkill_id == rfkill_id) {
if (device.blocked) {
return true;
}
found = true;
break;
}
}
if (!found) {
return false;
}
auto rfkill_output = run_application("rfkill", {"block", rfkill_id});
if (rfkill_output.exit_code != 0) {
return false;
}
return true;
}
void Setup::publish_configured_networks() {
auto network_devices = this->get_network_devices();
WpaCliSetup::WifiNetworkStatusList all_wifi_networks;
for (auto device : network_devices) {
if (!device.wireless) {
continue;
}
WifiConfigureClass wifi;
auto network_list = wifi.list_networks_status(device.interface);
for (auto& i : network_list) {
all_wifi_networks.push_back(std::move(i));
}
}
std::string network_list_var = this->var_base + "configured_networks";
json configured_networks_json = json::array();
configured_networks_json = all_wifi_networks;
this->mqtt.publish(network_list_var, configured_networks_json.dump());
}
bool Setup::add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk,
bool hidden) {
WifiConfigureClass wifi;
std::string net_if = interface;
if (net_if.empty()) {
EVLOG_warning << "Attempting to add a network without an interface, attempting to use the first one";
auto network_devices = this->get_network_devices();
for (auto device : network_devices) {
if (device.wireless) {
net_if = device.interface;
break;
}
}
}
auto network_id = wifi.add_network(net_if);
bool bResult = network_id != -1;
bResult = bResult && wifi.set_network(net_if, network_id, ssid, psk, hidden);
bResult = bResult && wifi.enable_network(net_if, network_id);
return bResult;
}
bool Setup::remove_all_networks() {
auto network_devices = this->get_network_devices();
std::uint32_t remove_fail = 0;
for (auto device : network_devices) {
if (!device.wireless) {
continue;
}
WifiConfigureClass wifi;
auto networks = wifi.list_networks(device.interface);
for (auto network : networks) {
if (!wifi.remove_network(device.interface, network.network_id)) {
remove_fail++;
}
}
wifi.save_config(device.interface);
}
return remove_fail == 0;
}
bool Setup::reboot() {
bool success = true;
auto reboot_output = run_application("systemctl", {"reboot"});
if (reboot_output.exit_code != 0) {
success = false;
}
return success;
}
bool Setup::is_online() {
bool success = true;
auto reboot_output = run_application("ping", {"-c", "1", this->config.online_check_host});
if (reboot_output.exit_code != 0) {
success = false;
}
return success;
}
void Setup::check_online_status() {
std::string online_status_var = this->var_base + "online_status";
if (this->is_online()) {
this->mqtt.publish(online_status_var, "online");
} else {
this->mqtt.publish(online_status_var, "offline");
}
}
void Setup::enable_ap() {
auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "disconnect"});
if (wpa_cli_output.exit_code != 0) {
EVLOG_error << "Could not disconnect from wireless LAN";
}
auto start_hostapd_output = run_application("systemctl", {"start", "hostapd"});
if (start_hostapd_output.exit_code != 0) {
EVLOG_error << "Could not start hostapd";
}
auto start_dnsmasq_output = run_application("systemctl", {"start", "dnsmasq"});
if (start_dnsmasq_output.exit_code != 0) {
EVLOG_error << "Could not start dnsmasq";
}
auto add_static_ip_output =
run_application("ip", {"addr", "add", this->config.ap_ipv4, "dev", this->config.ap_interface});
if (add_static_ip_output.exit_code != 0) {
EVLOG_error << "Could not add static ip to interface " << this->config.ap_interface;
}
}
void Setup::disable_ap() {
auto del_static_ip_output =
run_application("ip", {"addr", "del", this->config.ap_ipv4, "dev", this->config.ap_interface});
if (del_static_ip_output.exit_code != 0) {
EVLOG_error << "Could not del static ip " << this->config.ap_ipv4 << " from interface "
<< this->config.ap_interface;
}
auto stop_dnsmasq_output = run_application("systemctl", {"stop", "dnsmasq"});
if (stop_dnsmasq_output.exit_code != 0) {
EVLOG_error << "Could not stop dnsmasq";
}
auto stop_hostapd_output = run_application("systemctl", {"stop", "hostapd"});
if (stop_hostapd_output.exit_code != 0) {
EVLOG_error << "Could not stop hostapd";
}
auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "reconnect"});
if (wpa_cli_output.exit_code != 0) {
EVLOG_error << "Could not reconnect to wireless LAN";
}
}
static void add_addr_infos_to_device(const json& addr_infos, NetworkDeviceInfo& device) {
for (const auto& addr_info : addr_infos) {
if (addr_info.at("family") == "inet") {
device.ipv4.push_back(addr_info.at("local"));
} else if (addr_info.at("family") == "inet6") {
device.ipv6.push_back(addr_info.at("local"));
}
}
}
void Setup::populate_ip_addresses(std::vector<NetworkDeviceInfo>& device_info) {
auto ip_output = run_application("ip", {"--json", "address", "show"});
if (ip_output.exit_code != 0) {
return;
}
const auto ip_json = json::parse(ip_output.output);
for (const auto& ip_object : ip_json) {
const std::string ifname = ip_object.at("ifname");
auto device = std::find_if(device_info.begin(), device_info.end(),
[&ifname](NetworkDeviceInfo& device) { return device.interface == ifname; });
if (device == device_info.end()) {
continue;
}
if (ip_object.contains("address")) {
device->mac = ip_object.at("address");
}
add_addr_infos_to_device(ip_object.at("addr_info"), *device);
}
}
WifiConfigureClass::WifiScanList Setup::scan_wifi(const std::vector<NetworkDeviceInfo>& device_info) {
WifiConfigureClass::WifiScanList wifi_info;
WifiConfigureClass wifi;
for (auto device : device_info) {
if (!device.wireless) {
continue;
}
auto dev_list = wifi.scan_wifi(device.interface);
wifi_info.insert(wifi_info.end(), dev_list.begin(), dev_list.end());
}
return wifi_info;
}
std::string Setup::get_hostname() {
auto hostname_output = run_application("hostname", {});
if (hostname_output.exit_code == 0 && hostname_output.split_output.size() > 0) {
return hostname_output.split_output.at(0);
}
return "";
}
} // namespace module

View File

@@ -0,0 +1,189 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef SETUP_HPP
#define SETUP_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for required interface implementations
#include <generated/interfaces/kvs/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include "WiFiSetup.hpp"
#include <regex>
namespace module {
namespace fs = std::filesystem;
struct WifiCredentials {
std::string interface;
std::string ssid;
std::string psk;
bool hidden;
operator std::string() {
json wifi_credentials = *this;
return wifi_credentials.dump();
}
};
void to_json(json& j, const WifiCredentials& k);
void from_json(const json& j, WifiCredentials& k);
struct InterfaceAndNetworkId {
std::string interface;
int network_id;
operator std::string() {
json remove_wifi = *this;
return remove_wifi.dump();
}
};
void to_json(json& j, const InterfaceAndNetworkId& k);
void from_json(const json& j, InterfaceAndNetworkId& k);
struct NetworkDeviceInfo {
std::string interface;
bool wireless = false;
bool blocked = false;
std::string rfkill_id;
std::vector<std::string> ipv4;
std::vector<std::string> ipv6;
std::string mac;
std::string link_type;
operator std::string() {
json device_info = *this;
return device_info.dump();
}
};
void to_json(json& j, const NetworkDeviceInfo& k);
struct SupportedSetupFeatures {
bool setup_wifi;
bool localization;
bool setup_simulation;
operator std::string() {
json supported_setup_features = *this;
return supported_setup_features.dump();
}
};
void to_json(json& j, const SupportedSetupFeatures& k);
struct ApplicationInfo {
bool initialized;
std::string mode;
std::string default_language;
std::string current_language;
std::string release_metadata_file;
operator std::string() {
json application_info = *this;
return application_info.dump();
}
};
void to_json(json& j, const ApplicationInfo& k);
} // namespace module
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
bool setup_wifi;
bool localization;
bool setup_simulation;
std::string online_check_host;
bool initialized_by_default;
std::string release_metadata_file;
std::string ap_interface;
std::string ap_ipv4;
};
class Setup : public Everest::ModuleBase {
public:
Setup() = delete;
Setup(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<kvsIntf> r_store,
Conf& config) :
ModuleBase(info), mqtt(mqtt_provider), r_store(std::move(r_store)), config(config){};
Everest::MqttProvider& mqtt;
const std::unique_ptr<kvsIntf> r_store;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
std::string api_base = "everest_api/setup/";
std::string var_base = api_base + "var/";
std::string cmd_base = api_base + "cmd/";
std::thread discover_network_thread;
std::thread publish_application_info_thread;
bool wifi_scan_enabled = false;
std::string ap_state = "unknown";
void publish_supported_features();
void publish_application_info();
void publish_hostname();
void publish_ap_state();
void set_default_language(std::string language);
std::string get_default_language();
std::string current_language;
void set_current_language(const std::string& language);
std::string get_current_language();
void set_mode(std::string mode);
std::string get_mode();
void set_initialized(bool initialized);
bool get_initialized();
void discover_network();
std::string read_type_file(const fs::path& type_path);
std::vector<NetworkDeviceInfo> get_network_devices();
void populate_rfkill_status(std::vector<NetworkDeviceInfo>& device_info);
bool rfkill_unblock(std::string rfkill_id);
bool rfkill_block(std::string rfkill_id);
void publish_configured_networks();
bool add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk,
bool hidden = false);
bool remove_all_networks();
bool reboot();
bool is_online();
void check_online_status();
void enable_ap();
void disable_ap();
void populate_ip_addresses(std::vector<NetworkDeviceInfo>& device_info);
WpaCliSetup::WifiScanList scan_wifi(const std::vector<NetworkDeviceInfo>& device_info);
std::string get_hostname();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // SETUP_HPP

View File

@@ -0,0 +1,578 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "WiFiSetup.hpp"
#include <filesystem>
#include <functional>
#include <regex>
#include <sstream>
#include <thread>
#include <utility>
#include <everest/run_application/run_application.hpp>
using namespace everest::run_application;
/**
* @file
* @brief wpa_cli command failure detection
*
* `wpa_cli` sets an exit code of 0 unless the command is malformed.
* Failures are presented via text to stdout.
* Hence checking for failure to remove a network would mean checking
* the output for OK or FAIL.
*
* This is common across all calls to `wpa_cli`.
*/
namespace {
inline int hex_digit_to_nibble(std::uint8_t c) {
int result{-1};
if ((c >= '0') && (c <= '9')) {
result = static_cast<std::uint8_t>(c - '0');
} else if ((c >= 'a') && (c <= 'f')) {
result = static_cast<std::uint8_t>(c - 'a') + 10;
} else if ((c >= 'A') && (c <= 'F')) {
result = static_cast<std::uint8_t>(c - 'A') + 10;
}
return result;
}
inline int hex_to_int(std::uint8_t high, std::uint8_t low) {
int result{-1};
const auto h = hex_digit_to_nibble(high);
const auto l = hex_digit_to_nibble(low);
if ((h != -1) && (l != -1)) {
const auto hc = static_cast<std::uint8_t>(h) << 4U;
const auto lc = static_cast<std::uint8_t>(l);
result = static_cast<int>(hc | lc);
}
return result;
}
inline std::uint8_t nibble_to_hex_digit(std::uint8_t c) {
std::uint8_t result{};
c &= 0x0fU;
if (c <= 9) {
result = c + '0';
} else {
result = (c - 10) + 'a';
}
return result;
}
inline void int_to_hex(std::uint8_t& high, std::uint8_t& low, std::uint8_t c) {
high = nibble_to_hex_digit(c >> 4U);
low = nibble_to_hex_digit(c);
}
} // namespace
namespace module {
constexpr const char* wpa_cli = "/usr/sbin/wpa_cli";
constexpr const int not_connected_rssi = -100; // -100 dBm is the minimum for wifi
bool WpaCliSetup::do_scan(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return false;
}
auto output = run_application(wpa_cli, {"-i", interface, "scan"});
return output.exit_code == 0;
}
WpaCliSetup::WifiScanList WpaCliSetup::do_scan_results(const std::string& interface) {
WifiScanList result = {};
auto output = run_application(wpa_cli, {"-i", interface, "scan_results"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
if (scan_results.size() >= 2) {
// skip header
for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end();
++scan_results_it) {
std::vector<std::string> columns;
std::istringstream stream(*scan_results_it);
for (std::string value; std::getline(stream, value, '\t');) {
columns.push_back(std::move(value));
}
if (columns.size() >= 5) {
WifiScan info;
info.bssid = columns[0];
info.ssid = columns[4];
info.frequency = std::stoi(columns[1]);
info.signal_level = std::stoi(columns[2]);
info.flags = std::move(parse_flags(columns[3]));
result.push_back(std::move(info));
}
}
}
}
return result;
}
WpaCliSetup::Status WpaCliSetup::do_status(const std::string& interface) {
Status result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "status"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
for (auto& scan_result : scan_results) {
std::vector<std::string> columns;
std::istringstream ss(scan_result);
for (std::string value; std::getline(ss, value, '=');) {
columns.push_back(std::move(value));
}
if (columns.size() == 2) {
result[columns[0]] = columns[1];
}
}
}
}
return result;
}
WpaCliSetup::Poll WpaCliSetup::do_signal_poll(const std::string& interface) {
Poll result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "signal_poll"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
for (auto& scan_result : scan_results) {
std::vector<std::string> columns;
std::istringstream ss(scan_result);
for (std::string value; std::getline(ss, value, '=');) {
columns.push_back(std::move(value));
}
if (columns.size() == 2) {
result[columns[0]] = columns[1];
}
}
}
}
return result;
}
WpaCliSetup::flags_t WpaCliSetup::parse_flags(const std::string& flags) {
const std::regex flags_regex("\\[(.*?)\\]");
flags_t parsed_flags;
for (auto it = std::sregex_iterator(flags.begin(), flags.end(), flags_regex); it != std::sregex_iterator(); ++it) {
parsed_flags.push_back((*it).str(1));
}
return parsed_flags;
}
int WpaCliSetup::add_network(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return -1;
}
auto output = run_application(wpa_cli, {"-i", interface, "add_network"});
if ((output.exit_code != 0) || (output.split_output.size() != 1)) {
return -1;
}
return std::stoi(output.split_output.at(0));
}
bool WpaCliSetup::set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, network_security_t mode, bool hidden) {
/*
* configuring a network needs:
* - ssid "<SSID>"
* - psk "<Passphrase>" or ABCDEF0123456789... (for WPA2)
* - sae_password "<Passphrase>" (for WPA3)
* - key_mgmt NONE (for open networks)
* - scan_ssid 1 (for hidden networks)
*
* Support for WPA3 requires:
* - key_mgmt WPA-PSK WPA-PSK-SHA256 SAE or SAE if WPA3 only
* - sae_password replaces psk, WPA3 doesn't support PreSharedKey (64 hex digits)
* the passphrase is required
* - for interworking WPA2 and WPA3 the passphrase is needed
* - psk with hex digits (PreSharedKey) doesn't work
*/
/*
* From wpa_supplicant/hostapd
* ieee80211w: Whether management frame protection (MFP) is enabled
* 0 = disabled (default)
* 1 = optional
* 2 = required
* The most common configuration options for this based on the PMF (protected
* management frames) certification program are:
* PMF enabled: ieee80211w=1 and wpa_key_mgmt=WPA-EAP WPA-EAP-SHA256
* PMF required: ieee80211w=2 and wpa_key_mgmt=WPA-EAP-SHA256
* (and similarly for WPA-PSK and WPA-PSK-SHA256 if WPA2-Personal is used)
* WPA3-Personal-only mode: ieee80211w=2 and wpa_key_mgmt=SAE
*/
constexpr std::uint8_t wpa2_psk_size = 64U;
if (!is_wifi_interface(interface)) {
return false;
}
if (psk.empty()) {
// force security mode to none
mode = network_security_t::none;
}
const char* key_mgt = nullptr;
const char* psk_name = nullptr;
const char* ieee80211w = nullptr;
switch (mode) {
case network_security_t::none:
key_mgt = "NONE";
break;
case network_security_t::wpa2_only:
key_mgt = "WPA-PSK";
psk_name = "psk";
break;
case network_security_t::wpa3_only:
key_mgt = "SAE";
psk_name = "sae_password";
ieee80211w = "2";
break;
case network_security_t::wpa2_and_wpa3:
default:
if (psk.size() == wpa2_psk_size) {
// WPA3 doesn't support PSK (hex digits), it needs a passphrase
key_mgt = "WPA-PSK";
psk_name = "psk";
} else if (psk.size() > wpa2_psk_size) {
// WPA2 doesn't support passphrases > 63 characters
key_mgt = "SAE";
psk_name = "sae_password";
ieee80211w = "2";
} else {
key_mgt = "WPA-PSK WPA-PSK-SHA256 SAE";
psk_name = "psk";
ieee80211w = "1";
}
break;
}
auto network_id_string = std::to_string(network_id);
// de-escaping SSID strings in wpa_supplicant is not reliable.
// hence providing the SSID as a string of hex digits
auto ssid_parameter = ssid_to_hex(ssid);
auto output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ssid", ssid_parameter});
if ((output.exit_code == 0) && (psk_name != nullptr)) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, psk_name, psk});
}
if (output.exit_code == 0) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "key_mgmt", key_mgt});
}
if ((output.exit_code == 0) && (ieee80211w != nullptr)) {
output =
run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ieee80211w", ieee80211w});
}
if (hidden && (output.exit_code == 0)) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "scan_ssid", "1"});
}
return output.exit_code == 0;
}
bool WpaCliSetup::enable_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "enable_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::disable_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "disable_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::select_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "select_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::remove_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "remove_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::save_config(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return false;
}
auto output = run_application(wpa_cli, {"-i", interface, "save_config"});
return output.exit_code == 0;
}
WpaCliSetup::WifiScanList WpaCliSetup::scan_wifi(const std::string& interface) {
WifiScanList result = {};
if (do_scan(interface)) {
// FIXME: is there a proper signal to check if the scan is ready? Maybe in the socket based interface
std::this_thread::sleep_for(std::chrono::seconds(3));
result = std::move(do_scan_results(interface));
}
return result;
}
WpaCliSetup::WifiNetworkList WpaCliSetup::list_networks(const std::string& interface) {
WifiNetworkList result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "list_networks"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
if (scan_results.size() >= 2) {
// skip header
for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end();
++scan_results_it) {
std::vector<std::string> columns;
std::istringstream ss(*scan_results_it);
for (std::string value; std::getline(ss, value, '\t');) {
columns.push_back(std::move(value));
}
if (columns.size() >= 2) {
WifiNetwork info;
info.network_id = std::stoi(columns[0]);
info.ssid = columns[1];
result.push_back(std::move(info));
}
}
}
}
}
return result;
}
WpaCliSetup::WifiNetworkStatusList WpaCliSetup::list_networks_status(const std::string& interface) {
WifiNetworkStatusList result = {};
if (is_wifi_interface(interface)) {
auto network_list = list_networks(interface);
auto status_map = do_status(interface);
int connected_rssi = not_connected_rssi;
// signal_poll raises errors when not connected
if (status_map["wpa_state"] == "COMPLETED") {
auto signal_map = do_signal_poll(interface);
if (auto it = signal_map.find("RSSI"); it != signal_map.end()) {
connected_rssi = std::stoi(it->second);
}
}
for (auto& i : network_list) {
WifiNetworkStatus net;
net.interface = interface;
net.network_id = i.network_id;
net.ssid = i.ssid;
net.connected = false;
net.signal_level = not_connected_rssi;
auto id_it = status_map.find("id");
auto ssid_it = status_map.find("ssid");
if ((id_it != status_map.end()) && (ssid_it != status_map.end()) &&
(std::stoi(id_it->second) == i.network_id) && (ssid_it->second == i.ssid)) {
net.connected = true;
net.signal_level = connected_rssi;
}
result.push_back(net);
}
}
return result;
}
bool WpaCliSetup::is_wifi_interface(const std::string& interface) {
// check if /sys/class/net/<interface>/wireless exists
auto path = std::filesystem::path("/sys/class/net");
path /= interface;
path /= "wireless";
return std::filesystem::exists(path);
}
int Ssid::standard(std::uint8_t c) {
int output{-1};
if (c == '\\') {
handler = &Ssid::backslash;
} else {
output = static_cast<int>(c);
}
return output;
}
int Ssid::backslash(std::uint8_t c) {
int output{static_cast<int>(c)};
handler = &Ssid::standard;
switch (c) {
case '"':
case '\\':
break;
case 'n':
output = '\n';
break;
case 'r':
output = '\r';
break;
case 't':
output = '\t';
break;
case 'e':
output = '\033';
break;
case 'x':
handler = &Ssid::hex_1;
output = -1;
break;
default:
// malformed
error = true;
output = -1;
break;
}
return output;
}
int Ssid::hex_1(std::uint8_t c) {
tmp = static_cast<std::uint8_t>(c);
handler = &Ssid::hex_2;
return -1;
}
int Ssid::hex_2(std::uint8_t c) {
int output{-1};
handler = &Ssid::standard;
const auto res = hex_to_int(tmp, c);
if (res != -1) {
output = res;
} else {
error = true;
// malformed
}
return output;
}
std::string Ssid::to_hex(const std::string& ssid) {
std::ostringstream ss;
handler = &Ssid::standard;
error = false;
for (const auto& c : ssid) {
const auto output = std::invoke(handler, this, static_cast<std::uint8_t>(c));
if (error) {
break;
}
if (output != -1) {
ss << std::hex << std::setw(2) << std::setfill('0') << output;
}
}
return (error) ? std::string{} : ss.str();
}
void Ssid::encode(int c, std::ostringstream& ss) {
if ((c < 0) || (c > 255)) {
error = true;
} else {
switch (c) {
case '"':
ss << R"(\")";
break;
case '\\':
ss << R"(\\)";
break;
case '\033':
ss << R"(\e)";
break;
case '\n':
ss << R"(\n)";
break;
case '\r':
ss << R"(\r)";
break;
case '\t':
ss << R"(\t)";
break;
default: {
if ((c >= 32) && (c <= 126)) {
ss << static_cast<char>(c);
} else {
std::uint8_t high{};
std::uint8_t low{};
int_to_hex(high, low, c);
ss << R"(\x)" << static_cast<char>(high) << static_cast<char>(low);
}
break;
}
}
}
}
std::string Ssid::from_hex(const std::string& hex) {
std::ostringstream ss;
bool high{true};
error = (hex.size() % 2) != 0;
for (const auto& c : hex) {
if (error) {
break;
}
if (high) {
tmp = static_cast<std::uint8_t>(c);
high = false;
} else {
const auto output = hex_to_int(tmp, static_cast<std::uint8_t>(c));
encode(output, ss);
high = true;
}
}
return (error) ? std::string{} : ss.str();
}
std::string WpaCliSetup::hex_to_ssid(const std::string& hex) {
Ssid converter;
return converter.from_hex(hex);
}
std::string WpaCliSetup::ssid_to_hex(const std::string& ssid) {
Ssid converter;
return converter.to_hex(ssid);
}
} // namespace module

View File

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef WIFISETUP_HPP
#define WIFISETUP_HPP
#include <cstdint>
#include <map>
#include <sstream>
#include <string>
#include <vector>
/**
* SSID encoding
* From Wikipedia:
* "SSIDs can be zero to 32 octets long, and are, for convenience, usually
* in a natural language, such as English"
*
* wpa-cli escapes SSID strings in scan results; character values 32..126 are
* not converted. The following conversions are applied:
* - \" double quote
* - \\ backslash
* - \e escape (\033)
* - \n newline
* - \r return
* - \t tab
* - \xnn for other values
*
* JSON strings are UTF-8 with \ converted to \\
*/
namespace module {
class Ssid {
private:
using fn_p = int (Ssid::*)(std::uint8_t c);
fn_p handler{nullptr};
std::uint8_t tmp{0};
bool error{false};
int standard(std::uint8_t c);
int backslash(std::uint8_t c);
int hex_1(std::uint8_t c);
int hex_2(std::uint8_t c);
void encode(int c, std::ostringstream& ss);
public:
std::string to_hex(const std::string& ssid);
std::string from_hex(const std::string& hex);
};
class WpaCliSetup {
public:
using flags_t = std::vector<std::string>;
enum class network_security_t : std::uint8_t {
none,
wpa2_only,
wpa3_only,
wpa2_and_wpa3,
};
struct WifiScan {
std::string bssid;
std::string ssid;
flags_t flags;
int frequency;
int signal_level;
};
using WifiScanList = std::vector<WifiScan>;
struct WifiNetworkStatus {
std::string interface;
std::string ssid;
int network_id;
int signal_level;
bool connected;
};
using WifiNetworkStatusList = std::vector<WifiNetworkStatus>;
struct WifiNetwork {
std::string ssid;
int network_id;
};
using WifiNetworkList = std::vector<WifiNetwork>;
using Status = std::map<std::string, std::string>;
using Poll = std::map<std::string, std::string>;
protected:
virtual bool do_scan(const std::string& interface);
virtual WifiScanList do_scan_results(const std::string& interface);
virtual Status do_status(const std::string& interface);
virtual Poll do_signal_poll(const std::string& interface);
virtual flags_t parse_flags(const std::string& flags);
public:
virtual ~WpaCliSetup() = default;
virtual int add_network(const std::string& interface);
virtual bool set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, network_security_t mode, bool hidden);
virtual bool set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, bool hidden) {
return set_network(interface, network_id, ssid, psk, network_security_t::wpa2_and_wpa3, hidden);
}
virtual bool enable_network(const std::string& interface, int network_id);
virtual bool disable_network(const std::string& interface, int network_id);
virtual bool select_network(const std::string& interface, int network_id);
virtual bool remove_network(const std::string& interface, int network_id);
virtual bool save_config(const std::string& interface);
virtual WifiScanList scan_wifi(const std::string& interface);
virtual WifiNetworkList list_networks(const std::string& interface);
virtual WifiNetworkStatusList list_networks_status(const std::string& interface);
virtual bool is_wifi_interface(const std::string& interface);
static std::string hex_to_ssid(const std::string& hex);
static std::string ssid_to_hex(const std::string& ssid);
};
} // namespace module
#endif // WIFISETUP_HPP

View File

@@ -0,0 +1,44 @@
description: >-
The EVerest Setup module for setting up a LAN or WIFI network connection. This module needs privileged access and
should not run during normal operations
config:
setup_wifi:
description: Allow wifi setup
type: boolean
default: false
localization:
description: Enable localization support
type: boolean
default: false
setup_simulation:
description: Allow simulation setup
type: boolean
default: false
online_check_host:
description: Hostname or IP to use to check for internet connectivity
type: string
default: lfenergy.org
initialized_by_default:
description: Always report as if the charger was initialized
type: boolean
default: true
release_metadata_file:
description: Location of the release metadata file relative to the EVerest prefix
type: string
default: "release.json"
ap_interface:
description: Wifi interface for AP mode
type: string
default: wlan0
ap_ipv4:
description: IPv4 address of the AP
type: string
default: "192.168.1.1/24"
requires:
store:
interface: kvs
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Kai-Uwe Hermann

View File

@@ -0,0 +1,19 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_setup_tests)
add_executable(${TEST_TARGET_NAME})
target_include_directories(${TEST_TARGET_NAME} PUBLIC ${GTEST_INCLUDE_DIRS} . ..)
target_sources(${TEST_TARGET_NAME} PRIVATE
RunApplicationStub.cpp
WiFiSetupTest.cpp
../WiFiSetup.cpp
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
GTest::gtest_main
nlohmann_json::nlohmann_json
everest::run_application
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "RunApplicationStub.hpp"
#include <gtest/gtest.h>
#include <utility>
using namespace everest::run_application;
namespace stub {
RunApplication* RunApplication::active_p = nullptr;
RunApplication::RunApplication() :
results({
{"add_network", {{}, {{"0"}}, 0}},
{"set_network", {{}, {{"OK"}}, 0}},
{"enable_network", {{}, {{"OK"}}, 0}},
{"disable_network", {{}, {{"OK"}}, 0}},
{"select_network", {{}, {{"OK"}}, 0}},
{"remove_network", {{}, {{"OK"}}, 0}},
{"save_config", {{}, {{"OK"}}, 0}},
// scan_wifi uses scan and scan_results
{"scan", {{}, {{"OK"}}, 0}},
{"scan_results",
{{},
{
{"bssid / frequency / signal level / flags / ssid"},
},
0}},
{"list_networks",
{{},
{
{"network id / ssid / bssid / flags"},
},
0}},
// list_networks_status uses list_networks status signal_poll
{"status",
{{},
{
{"wpa_state=INACTIVE"},
{"p2p_device_address=c2:ee:40:b0:57:b8"},
{"address=c0:ee:40:b0:57:b8"},
{"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"},
},
0}},
{"signal_poll", {{}, {{"FAIL"}}, 0}},
}),
signal_poll_called(false),
psk_called(false),
sae_password_called(false),
key_mgmt_called(false),
scan_ssid_called(false),
ieee80211w_called(false),
key_mgmt_value(),
ieee80211w_value() {
active_p = this;
}
RunApplication::~RunApplication() {
active_p = nullptr;
}
CmdOutput RunApplication::run_application(const std::string& name, std::vector<std::string> args) {
CmdOutput result = {{}, {}, -1};
EXPECT_EQ(name, "/usr/sbin/wpa_cli");
EXPECT_EQ(args[0], "-i");
if (args[2] == "signal_poll") {
signal_poll_called = true;
} else if (args[2] == "set_network") {
if (args[4] == "psk") {
psk_called = true;
} else if (args[4] == "sae_password") {
sae_password_called = true;
} else if (args[4] == "key_mgmt") {
key_mgmt_called = true;
key_mgmt_value = args[5];
} else if (args[4] == "ieee80211w") {
ieee80211w_called = true;
ieee80211w_value = args[5];
} else if (args[4] == "scan_ssid") {
scan_ssid_called = true;
}
}
auto it = results.find(args[2]);
if (it != results.end()) {
result = it->second;
if (!result.split_output.empty() && result.output.empty()) {
for (auto& line : result.output) {
result.output += line + "\n";
}
}
}
return result;
}
} // namespace stub
namespace everest::run_application {
CmdOutput run_application(const std::string& name, std::vector<std::string> args,
const std::function<CmdControl(const std::string& output_line)> output_callback) {
CmdOutput result = {{}, {}, -1};
if (stub::RunApplication::active_p != nullptr) {
result = std::move(stub::RunApplication::active_p->run_application(name, args));
}
return result;
}
} // namespace everest::run_application

View File

@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef RUNAPPLICATIONSTUB_HPP
#define RUNAPPLICATIONSTUB_HPP
#include <everest/run_application/run_application.hpp>
#include <map>
#include <string>
namespace stub {
class RunApplication {
public:
static RunApplication* active_p;
std::map<std::string, everest::run_application::CmdOutput> results;
bool signal_poll_called;
bool psk_called;
bool sae_password_called;
bool key_mgmt_called;
bool scan_ssid_called;
bool ieee80211w_called;
std::string key_mgmt_value;
std::string ieee80211w_value;
RunApplication();
virtual ~RunApplication();
virtual everest::run_application::CmdOutput run_application(const std::string& name, std::vector<std::string> args);
};
} // namespace stub
namespace everest::run_application {
CmdOutput run_application(const std::string& name, std::vector<std::string> args,
const std::function<CmdControl(const std::string& output_line)> output_callback);
}
#endif // RUNAPPLICATIONSTUB_HPP

View File

@@ -0,0 +1,726 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "RunApplicationStub.hpp"
#include <WiFiSetup.hpp>
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
namespace {
using namespace module;
using nlohmann::json;
constexpr const char* example_psk = "e3003974af901976485f3e655b455791dcc20a5380f42a7839de3bfdc9d70d71";
constexpr const char* example_password = "LetMeIn2";
constexpr const char* example_long_password = "e3003974af901976485f3e655b455791dcc20a5380f42a7839de3bfdc9d70d71X";
class WpaCliSetupTest : public WpaCliSetup {
public:
// override to support testing
virtual bool is_wifi_interface(const std::string& interface) override {
if (interface == "ap0") {
return false;
} else if (interface == "eth0") {
return false;
}
return true;
};
};
struct WifiCredentials {
std::string interface;
std::string ssid;
std::string psk;
bool hidden;
operator std::string() {
json wifi_credentials = *this;
return wifi_credentials.dump();
}
};
void to_json(json& j, const WifiCredentials& k) {
j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}});
}
void from_json(const json& j, WifiCredentials& k) {
k.interface = j.at("interface");
k.ssid = j.at("ssid");
k.psk = j.at("psk");
k.hidden = false;
// optional item
auto it = j.find("hidden");
if ((it != j.end() && *it)) {
k.hidden = true;
}
}
//-----------------------------------------------------------------------------
// SSID conversions
TEST(Ssid, toHex) {
Ssid ssid;
EXPECT_EQ(ssid.to_hex("0123456789"), "30313233343536373839");
EXPECT_EQ(ssid.to_hex("abcdef"), "616263646566");
EXPECT_EQ(ssid.to_hex("ABCDEF"), "414243444546");
EXPECT_EQ(ssid.to_hex(R"(\" \\ \e\n\r\t)"), "22205c201b0a0d09");
EXPECT_EQ(ssid.to_hex(R"(\x00\x01\xfd)"), "0001fd");
}
TEST(Ssid, fromHex) {
Ssid ssid;
EXPECT_EQ(ssid.from_hex("30313233343536373839"), "0123456789");
EXPECT_EQ(ssid.from_hex("616263646566"), "abcdef");
EXPECT_EQ(ssid.from_hex("414243444546"), "ABCDEF");
EXPECT_EQ(ssid.from_hex("22205c201b0a0d09"), R"(\" \\ \e\n\r\t)");
EXPECT_EQ(ssid.from_hex("0001fd"), R"(\x00\x01\xfd)");
}
TEST(Ssid, complete) {
Ssid ssid;
std::stringstream ss;
for (std::uint16_t i = 0; i < 256; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << i;
}
const std::string values_str(ss.str());
const auto result_str = ssid.from_hex(values_str);
const auto result_hex = ssid.to_hex(result_str);
// std::cout << result_str << std::endl;
// std::cout << result_hex << std::endl;
EXPECT_EQ(values_str, result_hex);
}
TEST(Ssid, unusual) {
Ssid ssid;
// allowing upper case hex digits
EXPECT_EQ(ssid.from_hex("E0E1E2E3E4E5E6E7E8E9EaEbEcEdEeEfEAEBECEDEEEF"),
R"(\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xea\xeb\xec\xed\xee\xef)");
EXPECT_EQ(
ssid.to_hex(R"(\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xEA\xEB\xEC\xED\xEE\xEF)"),
"e0e1e2e3e4e5e6e7e8e9eaebecedeeefeaebecedeeef");
// parsing errors
EXPECT_EQ(ssid.to_hex(R"(123\?456)"), "");
EXPECT_EQ(ssid.to_hex(R"(123\?)"), "");
EXPECT_EQ(ssid.to_hex(R"(\?456)"), "");
EXPECT_EQ(ssid.to_hex(R"(\034)"), "");
EXPECT_EQ(ssid.to_hex(R"(\03)"), "");
EXPECT_EQ(ssid.to_hex(R"(\0)"), "");
EXPECT_EQ(ssid.to_hex(R"(\x)"), "");
EXPECT_EQ(ssid.to_hex(R"(\xz)"), "");
EXPECT_EQ(ssid.to_hex(R"(\xaz)"), "");
EXPECT_EQ(ssid.from_hex(R"(G123)"), "");
EXPECT_EQ(ssid.from_hex(R"(12G3)"), "");
EXPECT_EQ(ssid.from_hex(R"(123G)"), "");
EXPECT_EQ(ssid.from_hex(R"(1234568)"), "");
}
TEST(Ssid, json) {
/*
* worked example
* ssid=PP€-310034
* scan_results
* bssid / frequency / signal level / flags / ssid
* c2:ee:40:10:57:b8 2417 -45 [WPA2-PSK-CCMP][ESS] PP\xe2\x82\xac-310034
*
* MQTT
* everest_api/setup/var/wifi_info: [
* {"bssid":"c2:ee:40:10:57:b8","flags":["WPA2-PSK-CCMP","ESS"],"frequency":2417,"signal_level":-46,"ssid":"PP\\xe2\\x82\\xac-310034"}]
*/
Ssid conv;
std::string ssid_hex{"535349443de282ac3132"};
std::string ssid = conv.from_hex(ssid_hex);
WifiCredentials wifi = {"eth0", ssid, "\"psk\"", false};
nlohmann::json j;
to_json(j, wifi);
const auto transmitted = j.dump();
WifiCredentials received = json::parse(transmitted);
EXPECT_EQ(received.ssid, ssid);
const auto rec_ssid_hex = conv.to_hex(received.ssid);
EXPECT_EQ(rec_ssid_hex, ssid_hex);
}
//-----------------------------------------------------------------------------
// add_network()
TEST(add_network, wired) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_EQ(obj.add_network("eth0"), -1);
}
TEST(add_network, wireless) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_NE(obj.add_network("wlan0"), -1);
}
TEST(add_network, access_point) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_EQ(obj.add_network("ap0"), -1);
}
//-----------------------------------------------------------------------------
// set_network()
TEST(set_network, none_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, none_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, none_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_password, WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_psk) {
// with a PSK result is same as wpa2_only
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_psk,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_password) {
// configure for both
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "1");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK WPA-PSK-SHA256 SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_password_long) {
// configure for WPA3 only
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_long_password,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "LetMeIn2", false));
ASSERT_TRUE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, open) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "OpenNet", "", false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, hidden_wpa2) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "LetMeIn3", true));
ASSERT_TRUE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.scan_ssid_called);
}
TEST(set_network, hidden_open) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "", true));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.scan_ssid_called);
}
//-----------------------------------------------------------------------------
// enable_network()
TEST(enable_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.enable_network("wlan0", 0));
}
TEST(enable_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["enable_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.enable_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// disable_network()
TEST(disable_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.disable_network("wlan0", 0));
}
TEST(disable_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["disable_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.disable_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// select_network()
TEST(select_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.select_network("wlan0", 0));
}
TEST(select_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["select_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.select_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// remove_network()
TEST(remove_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.remove_network("wlan0", 0));
}
TEST(remove_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["remove_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.remove_network("wlan0", 1));
}
TEST(remove_network, fail) {
stub::RunApplication ra;
ra.results["remove_network"] = {{}, {{"Invalid REMOVE_NETWORK command - at least 1 argument is required."}}, 255};
WpaCliSetupTest obj;
ASSERT_FALSE(obj.remove_network("wlan0", -99));
}
//-----------------------------------------------------------------------------
// save_config()
TEST(save_config, success) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.save_config("wlan0"));
}
TEST(save_config, fail) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_FALSE(obj.save_config("ap0"));
}
//-----------------------------------------------------------------------------
// scan_wifi()
TEST(scan_wifi, none) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_TRUE(res.empty());
}
TEST(scan_wifi, some) {
stub::RunApplication ra;
ra.results["scan_results"] = {
{},
{
{"bssid / frequency / signal level / flags / ssid"},
{"14:49:bc:06:81:19\t2412\t-72\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"},
{"6a:82:8c:38:b2:a1\t2412\t-93\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
},
0};
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 2);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19");
EXPECT_EQ(res[0].ssid, "PlusnetWireless");
EXPECT_EQ(res[0].frequency, 2412);
EXPECT_EQ(res[0].signal_level, -72);
EXPECT_EQ(res[0].flags, expected);
EXPECT_EQ(res[1].bssid, "6a:82:8c:38:b2:a1");
EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[1].frequency, 2412);
EXPECT_EQ(res[1].signal_level, -93);
EXPECT_EQ(res[1].flags, expected);
}
TEST(scan_wifi, more) {
stub::RunApplication ra;
ra.results["scan_results"] = {
{},
{
{"bssid / frequency / signal level / flags / ssid"},
{"14:49:bc:06:81:19\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"},
{"14:49:bc:06:81:1b\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"00:1e:42:33:62:07\t2462\t-89\t[WPA2-PSK-CCMP+TKIP][ESS][UTF-8]\tRUT950_6207"},
{"b4:ba:9d:16:e2:ba\t2437\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tSKYLZMEY"},
{"14:49:bc:06:81:1c\t2412\t-72\t[ESS]\tTesting123"},
{"36:49:5b:f8:e1:07\t2412\t-92\t[ESS]\tEE WiFi"},
{"14:49:bc:06:81:18\t2412\t-73\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"6a:82:8c:38:b2:a1\t2412\t-88\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"18:82:8c:38:b2:a5\t2412\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tBT-3GAG3M"},
{"6a:82:8c:38:b2:a6\t2412\t-92\t[ESS]\tEE WiFi"},
},
0};
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 10);
module::WpaCliSetup::flags_t expected1 = {"WPA2-PSK-CCMP", "ESS"};
module::WpaCliSetup::flags_t expected2 = {"WPA2-PSK-CCMP+TKIP", "ESS", "UTF-8"};
module::WpaCliSetup::flags_t expected3 = {"WPA2-PSK-CCMP", "WPS", "ESS"};
module::WpaCliSetup::flags_t expected4 = {"ESS"};
EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19");
EXPECT_EQ(res[0].ssid, "PlusnetWireless");
EXPECT_EQ(res[0].frequency, 2412);
EXPECT_EQ(res[0].signal_level, -71);
EXPECT_EQ(res[0].flags, expected1);
EXPECT_EQ(res[1].bssid, "14:49:bc:06:81:1b");
EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[1].frequency, 2412);
EXPECT_EQ(res[1].signal_level, -71);
EXPECT_EQ(res[1].flags, expected1);
EXPECT_EQ(res[2].bssid, "00:1e:42:33:62:07");
EXPECT_EQ(res[2].ssid, "RUT950_6207");
EXPECT_EQ(res[2].frequency, 2462);
EXPECT_EQ(res[2].signal_level, -89);
EXPECT_EQ(res[2].flags, expected2);
EXPECT_EQ(res[3].bssid, "b4:ba:9d:16:e2:ba");
EXPECT_EQ(res[3].ssid, "SKYLZMEY");
EXPECT_EQ(res[3].frequency, 2437);
EXPECT_EQ(res[3].signal_level, -92);
EXPECT_EQ(res[3].flags, expected3);
EXPECT_EQ(res[4].bssid, "14:49:bc:06:81:1c");
EXPECT_EQ(res[4].ssid, "Testing123");
EXPECT_EQ(res[4].frequency, 2412);
EXPECT_EQ(res[4].signal_level, -72);
EXPECT_EQ(res[4].flags, expected4);
EXPECT_EQ(res[5].bssid, "36:49:5b:f8:e1:07");
EXPECT_EQ(res[5].ssid, "EE WiFi");
EXPECT_EQ(res[5].frequency, 2412);
EXPECT_EQ(res[5].signal_level, -92);
EXPECT_EQ(res[5].flags, expected4);
EXPECT_EQ(res[6].bssid, "14:49:bc:06:81:18");
EXPECT_EQ(res[6].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[6].frequency, 2412);
EXPECT_EQ(res[6].signal_level, -73);
EXPECT_EQ(res[6].flags, expected1);
EXPECT_EQ(res[7].bssid, "6a:82:8c:38:b2:a1");
EXPECT_EQ(res[7].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[7].frequency, 2412);
EXPECT_EQ(res[7].signal_level, -88);
EXPECT_EQ(res[7].flags, expected1);
EXPECT_EQ(res[8].bssid, "18:82:8c:38:b2:a5");
EXPECT_EQ(res[8].ssid, "BT-3GAG3M");
EXPECT_EQ(res[8].frequency, 2412);
EXPECT_EQ(res[8].signal_level, -92);
EXPECT_EQ(res[8].flags, expected3);
EXPECT_EQ(res[9].bssid, "6a:82:8c:38:b2:a6");
EXPECT_EQ(res[9].ssid, "EE WiFi");
EXPECT_EQ(res[9].frequency, 2412);
EXPECT_EQ(res[9].signal_level, -92);
EXPECT_EQ(res[9].flags, expected4);
}
//-----------------------------------------------------------------------------
// list_networks()
TEST(list_networks, none) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_TRUE(res.empty());
}
TEST(list_networks, one) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 1);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
}
TEST(list_networks, two) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
{"1\tPlusnetWireless\tany\t[CURRENT]"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 2);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
EXPECT_EQ(res[1].network_id, 1);
EXPECT_EQ(res[1].ssid, "PlusnetWireless");
}
//-----------------------------------------------------------------------------
// list_networks_status()
TEST(list_networks, not_connected) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.list_networks_status("wlan0");
ASSERT_TRUE(res.empty());
ASSERT_FALSE(ra.signal_poll_called);
}
TEST(list_networks, connected) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
{"1\tPlusnetWireless\tany\t[CURRENT]"},
{"2\tHiddenNet\tany\t[DISABLED]"},
},
0};
ra.results["status"] = {{},
{
{"bssid=14:49:bc:06:81:19"},
{"freq=2412"},
{"ssid=PlusnetWireless"},
{"id=1"},
{"mode=station"},
{"wifi_generation=4"},
{"pairwise_cipher=CCMP"},
{"group_cipher=CCMP"},
{"key_mgmt=WPA2-PSK"},
{"wpa_state=COMPLETED"},
{"ip_address=172.25.1.11"},
{"p2p_device_address=c2:ee:40:b0:57:b8"},
{"address=c0:ee:40:b0:57:b8"},
{"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"},
},
0};
ra.results["signal_poll"] = {{},
{
{"RSSI=-73"},
{"LINKSPEED=54"},
{"NOISE=9999"},
{"FREQUENCY=2412"},
{"WIDTH=20 MHz"},
{"CENTER_FRQ1=2412"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks_status("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_TRUE(ra.signal_poll_called);
ASSERT_EQ(res.size(), 3);
EXPECT_EQ(res[0].interface, "wlan0");
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
EXPECT_FALSE(res[0].connected);
EXPECT_EQ(res[0].signal_level, -100);
EXPECT_EQ(res[1].interface, "wlan0");
EXPECT_EQ(res[1].network_id, 1);
EXPECT_EQ(res[1].ssid, "PlusnetWireless");
EXPECT_TRUE(res[1].connected);
EXPECT_EQ(res[1].signal_level, -73);
EXPECT_EQ(res[2].interface, "wlan0");
EXPECT_EQ(res[2].network_id, 2);
EXPECT_EQ(res[2].ssid, "HiddenNet");
EXPECT_FALSE(res[2].connected);
EXPECT_EQ(res[2].signal_level, -100);
}
//-----------------------------------------------------------------------------
// is_wifi_interface()
// not tested as it is checking for files in /proc so depends on the
// machine the tests are running on
} // namespace