diff --git a/common/beerocks/CMakeLists.txt b/common/beerocks/CMakeLists.txt index 0479907511..6347f7a547 100644 --- a/common/beerocks/CMakeLists.txt +++ b/common/beerocks/CMakeLists.txt @@ -1,10 +1,11 @@ add_subdirectory(bcl) -add_subdirectory(bwl) -add_subdirectory(tlvf) add_subdirectory(btl) +add_subdirectory(bwl) +add_subdirectory(hostapd) add_subdirectory(scripts) +add_subdirectory(tlvf) # BWL depends on BCL, so make sure it's built first add_dependencies(bwl bcl) add_dependencies(btlvf bcl) -add_dependencies(btl btlvf) \ No newline at end of file +add_dependencies(btl btlvf) diff --git a/common/beerocks/bwl/CMakeLists.txt b/common/beerocks/bwl/CMakeLists.txt index 7ec702efad..39ef976aa2 100644 --- a/common/beerocks/bwl/CMakeLists.txt +++ b/common/beerocks/bwl/CMakeLists.txt @@ -109,6 +109,8 @@ elseif(BWL_TYPE STREQUAL "NL80211") ${PLATFORM_STAGING_DIR}/usr/lib ) + list(APPEND BWL_LIBS prplmesh_hostapd) + elseif(BWL_TYPE STREQUAL "DUMMY") set(bwl_platform_sources @@ -179,7 +181,7 @@ if (BUILD_TESTS) PUBLIC $ ) - target_link_libraries(${TEST_PROJECT_NAME} bcl elpp ${BWL_LIBS}) + target_link_libraries(${TEST_PROJECT_NAME} prplmesh_hostapd bcl elpp ${BWL_LIBS}) target_link_libraries(${TEST_PROJECT_NAME} gtest_main) install(TARGETS ${TEST_PROJECT_NAME} DESTINATION bin/tests) add_test(NAME ${TEST_PROJECT_NAME} COMMAND $) diff --git a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp index aa54592dc2..e5e780f6d7 100644 --- a/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp +++ b/common/beerocks/bwl/nl80211/ap_wlan_hal_nl80211.cpp @@ -7,23 +7,21 @@ */ #include "ap_wlan_hal_nl80211.h" - #include #include #include #include #include - -#include - #include - +#include +#include #include #include #include #include #include #include +#include ////////////////////////////////////////////////////////////////////////////// ////////////////////////// Local Module Definitions ////////////////////////// @@ -106,6 +104,46 @@ static uint8_t wpa_bw_to_beerocks_bw(const std::string &chan_width) return (chan_width == "80+80") ? 160 : beerocks::string_utils::stoi(chan_width); } +/// @brief figures out hostapd config file name by the +// interface name and loads its content +static prplmesh::hostapd::Configuration load_hostapd_config(const std::string &radio_iface_name) +{ + std::vector hostapd_cfg_names = { + "/tmp/run/hostapd-phy0.conf", "/tmp/run/hostapd-phy1.conf", "/var/run/hostapd-phy0.conf", + "/var/run/hostapd-phy1.conf", "/var/run/hostapd-phy2.conf", "/var/run/hostapd-phy3.conf"}; + + for (const auto &try_fname : hostapd_cfg_names) { + LOG(DEBUG) << "Trying to load " << try_fname << "..."; + + if (!beerocks::os_utils::file_exists(try_fname)) { + continue; + } + + prplmesh::hostapd::Configuration hostapd_conf(try_fname); + + // try loading + if (!hostapd_conf.load("interface=")) { + LOG(ERROR) << "Failed to load hostapd config file: " << hostapd_conf; + continue; + } + + // check if it is the right one: + // we are looking for the line in the vap that declares this vap: interface=radio_iface_name + // we could equaly ask hostapd_conf if it has this vap, but there is no such interface + // to do this. it should be something like: hostapd_conf.is_vap_exists(radio_iface_name); + if (hostapd_conf.get_vap_value(radio_iface_name, "interface") != radio_iface_name) { + LOG(DEBUG) << radio_iface_name << " does not exists in " << try_fname; + continue; + } + + // all is good, return the conf + return hostapd_conf; + } + + // return an empty one since we couldn't find the + return prplmesh::hostapd::Configuration("file not found"); +} + ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// Implementation /////////////////////////////// ////////////////////////////////////////////////////////////////////////////// @@ -268,7 +306,241 @@ bool ap_wlan_hal_nl80211::update_vap_credentials( std::list &bss_info_conf_list, const std::string &backhaul_wps_ssid, const std::string &backhaul_wps_passphrase) { - //TODO Implement #346 + if (0 == bss_info_conf_list.size()) { + LOG(DEBUG) << "given bss conf list size is zero, no changes to existing hostapd " + "configuration are applied"; + return true; + } + + // Load hostapd config for the radio + prplmesh::hostapd::Configuration conf = load_hostapd_config(m_radio_info.iface_name); + if (!conf) { + LOG(ERROR) << "Autoconfiguration: no hostapd config to apply configuration!"; + return false; + } + + // If a Multi-AP Agent receives an AP-Autoconfiguration WSC message containing one or + // more M2, it shall validate each M2 (based on its 1905 AL MAC address) and configure + // a BSS on the corresponding radio for each of the M2. If the Multi-AP Agent is currently + // operating a BSS with operating parameters that do not completely match any of the M2 in + // the received AP-Autoconfiguration WSC message, it shall tear down that BSS. + + // decalre a function for iterating over bss-conf and ap-vaps + bool abort = false; + auto configure_func = + [&abort, &conf, &bss_info_conf_list, &backhaul_wps_ssid, &backhaul_wps_passphrase]( + const std::string &vap, + std::remove_reference::type::iterator bss_it) { + if (abort) { + return; + } + + if (bss_it != bss_info_conf_list.end()) { + + // we still have data on the input bss-conf list + + // escape I + auto auth_type = + son::wireless_utils::wsc_to_bwl_authentication(bss_it->authentication_type); + if (auth_type == "INVALID") { + LOG(ERROR) << "Autoconfiguration: auth type is 'INVALID'; number: " + << (uint16_t)bss_it->authentication_type; + abort = true; + return; + } + + // escape II + auto enc_type = son::wireless_utils::wsc_to_bwl_encryption(bss_it->encryption_type); + if (enc_type == "INVALID") { + LOG(ERROR) << "Autoconfiguration: enc_type is 'INVALID'; number: " + << int(bss_it->encryption_type); + abort = true; + return; + } + + // escape III + if (conf.get_vap_value(vap, "bssid").empty()) { + LOG(ERROR) << "Failed to get BSSID for vap: " << vap; + abort = true; + return; + } + + // settings + + // Hostapd "wpa" field. + // This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) + // and/or WPA2 (full IEEE 802.11i/RSN): + // bit0 = WPA + // bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) + int wpa = 0; + + // Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The + // entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be + // added to enable SHA256-based stronger algorithms. + // WPA-PSK = WPA-Personal / WPA2-Personal + std::string wpa_key_mgmt; // default to empty -> delete from hostapd config + + // (dot11RSNAConfigPairwiseCiphersTable) + // Pairwise cipher for WPA (v1) (default: TKIP) + // wpa_pairwise=TKIP CCMP + // Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) + // rsn_pairwise=CCMP + std::string wpa_pairwise; // default to empty -> delete from hostapd config + + // WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit + // secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase + // (8..63 characters), wpa_passphrase. + std::string wpa_passphrase; + std::string wpa_psk; + + // ieee80211w: Whether management frame protection (MFP) is enabled + // 0 = disabled (default) + // 1 = optional + // 2 = required + std::string ieee80211w; + + // This parameter can be used to disable caching of PMKSA created through EAP + // authentication. RSN preauthentication may still end up using PMKSA caching if + // it is enabled (rsn_preauth=1). + // 0 = PMKSA caching enabled (default) + // 1 = PMKSA caching disabled + std::string disable_pmksa_caching; + + // Opportunistic Key Caching (aka Proactive Key Caching) + // Allow PMK cache to be shared opportunistically among configured interfaces + // and BSSes (i.e., all configurations within a single hostapd process). + // 0 = disabled (default) + // 1 = enabled + std::string okc; + + // This parameter can be used to disable retransmission of EAPOL-Key frames that + // are used to install keys (EAPOL-Key message 3/4 and group message 1/2). This + // is similar to setting wpa_group_update_count=1 and + std::string wpa_disable_eapol_key_retries; + + // EasyMesh R1 only allows Open and WPA2 PSK auth&encryption methods. + // Quote: A Multi-AP Controller shall set the Authentication Type attribute + // in M2 to indicate WPA2-Personal or Open System Authentication. + // bss_it->authentication_type is a bitfield, but we are not going + // to accept any combinations due to the above limitation. + if (bss_it->authentication_type == WSC::eWscAuth::WSC_AUTH_OPEN) { + wpa = 0x0; + if (bss_it->encryption_type != WSC::eWscEncr::WSC_ENCR_NONE) { + LOG(ERROR) << "Autoconfiguration: " << vap << " encryption set on open VAP"; + abort = true; + return; + } + if (bss_it->network_key.length() > 0) { + LOG(ERROR) + << "Autoconfiguration: " << vap << " network key set for open VAP"; + abort = true; + return; + } + } else if (bss_it->authentication_type == WSC::eWscAuth::WSC_AUTH_WPA2PSK) { + wpa = 0x2; + wpa_key_mgmt.assign("WPA-PSK"); + // Cipher must include AES for WPA2, TKIP is optional + if ((static_cast(bss_it->encryption_type) & + static_cast(WSC::eWscEncr::WSC_ENCR_AES)) == 0) { + LOG(ERROR) + << "Autoconfiguration: " << vap << " CCMP(AES) is required for WPA2"; + abort = true; + return; + } + if ((uint16_t(bss_it->encryption_type) & + uint16_t(WSC::eWscEncr::WSC_ENCR_TKIP)) != 0) { + wpa_pairwise.assign("TKIP CCMP"); + } else { + wpa_pairwise.assign("CCMP"); + } + if (bss_it->network_key.length() < 8 || bss_it->network_key.length() > 64) { + LOG(ERROR) << "Autoconfiguration: " << vap << " invalid network key length " + << bss_it->network_key.length(); + abort = true; + return; + } + if (bss_it->network_key.length() < 64) { + wpa_passphrase.assign(bss_it->network_key); + } else { + wpa_psk.assign(bss_it->network_key); + } + ieee80211w.assign("0"); + disable_pmksa_caching.assign("1"); + okc.assign("0"); + wpa_disable_eapol_key_retries.assign("0"); + } else { + LOG(ERROR) << "Autoconfiguration: " << vap << " invalid authentication type"; + abort = true; + return; + } + + LOG(DEBUG) << "Autoconfiguration for ssid: " << bss_it->ssid + << " auth_type: " << auth_type << " encr_type: " << enc_type + << " network_key: " << bss_it->network_key + << " fronthaul=" << beerocks::string_utils::bool_str(bss_it->fronthaul) + << " backhaul=" << beerocks::string_utils::bool_str(bss_it->backhaul); + + conf.set_create_vap_value(vap, "ssid", bss_it->ssid); + conf.set_create_vap_value(vap, "wps_state", bss_it->fronthaul ? "2" : ""); + conf.set_create_vap_value(vap, "wps_independent", "0"); + conf.set_create_vap_value(vap, "max_num_sta", bss_it->backhaul ? "1" : ""); + + // oddly enough, multi_ap_backhaul_wpa_passphrase has to be + // quoted, while wpa_passphrase does not... + if (bss_it->fronthaul && !backhaul_wps_ssid.empty()) { + conf.set_create_vap_value(vap, "multi_ap_backhaul_ssid", + "\"" + backhaul_wps_ssid + "\""); + conf.set_create_vap_value(vap, "multi_ap_backhaul_wpa_passphrase", + backhaul_wps_passphrase); + } + + // remove when not needed + if (!bss_it->fronthaul && backhaul_wps_ssid.empty()) { + conf.set_create_vap_value(vap, "multi_ap_backhaul_ssid", ""); + conf.set_create_vap_value(vap, "multi_ap_backhaul_wpa_passphrase", ""); + } + + conf.set_create_vap_value(vap, "wpa", wpa); + conf.set_create_vap_value(vap, "okc", okc); + conf.set_create_vap_value(vap, "wpa_key_mgmt", wpa_key_mgmt); + conf.set_create_vap_value(vap, "wpa_pairwise", wpa_pairwise); + conf.set_create_vap_value(vap, "wpa_psk", wpa_psk); + conf.set_create_vap_value(vap, "ieee80211w", ieee80211w); + conf.set_create_vap_value(vap, "wpa_passphrase", wpa_passphrase); + conf.set_create_vap_value(vap, "disable_pmksa_caching", disable_pmksa_caching); + conf.set_create_vap_value(vap, "wpa_disable_eapol_key_retries", + wpa_disable_eapol_key_retries); + + // finally enable the vap (remove any previously set start_disabled and uncomment) + conf.set_create_vap_value(vap, "start_disabled", ""); + conf.uncomment_vap(vap); + + } else { + // no more data in the input bss-conf list + // disable the rest of the vaps + conf.comment_vap(vap); + } + }; + + conf.for_all_ap_vaps(configure_func, bss_info_conf_list.begin(), bss_info_conf_list.end(), + [](const std::string &) { return true; }); + + if (abort) { + return false; + } + + if (!conf.store()) { + LOG(ERROR) << "Autoconfiguration: cannot save hostapd config!"; + return false; + } + + const std::string cmd("UPDATE "); + if (!wpa_ctrl_send_msg(cmd)) { + LOG(ERROR) << "Autoconfiguration: \"" << cmd << "\" command to hostapd has failed"; + return false; + } + + LOG(DEBUG) << "Autoconfiguration: done:\n" << conf; return true; } diff --git a/common/beerocks/hostapd/CMakeLists.txt b/common/beerocks/hostapd/CMakeLists.txt new file mode 100644 index 0000000000..f7b3d5afc3 --- /dev/null +++ b/common/beerocks/hostapd/CMakeLists.txt @@ -0,0 +1,48 @@ + +project(prplmesh_hostapd VERSION ${prplmesh_VERSION}) + +# Set the base path for the current module +set(MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) + +# Build the library +set(hostapd_sources source/configuration.cpp) +add_library(${PROJECT_NAME} ${hostapd_sources}) +set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${prplmesh_VERSION} SOVERSION ${prplmesh_VERSION_MAJOR}) +target_link_libraries(${PROJECT_NAME} elpp ${PRPLMESH_HOSTAPD_LIBS}) +target_include_directories(${PROJECT_NAME} + PRIVATE + ${PLATFORM_INCLUDE_DIR} + PUBLIC + $ +) + +install( + TARGETS ${PROJECT_NAME} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if (BUILD_TESTS) + set(TEST_PROJECT_NAME ${PROJECT_NAME}_unit_tests) + set(unit_tests_sources + ${MODULE_PATH}/unit_tests/configuration_test.cpp + ) + add_executable(${TEST_PROJECT_NAME} + ${unit_tests_sources} + ) + if (COVERAGE) + set_target_properties(${TEST_PROJECT_NAME} PROPERTIES COMPILE_FLAGS "--coverage -fPIC -O0") + set_target_properties(${TEST_PROJECT_NAME} PROPERTIES LINK_FLAGS "--coverage") + endif() + target_include_directories(${TEST_PROJECT_NAME} + PRIVATE + ${PLATFORM_INCLUDE_DIR} + PUBLIC + $ + ) + target_link_libraries(${TEST_PROJECT_NAME} ${PROJECT_NAME} ${PRPLMESH_HOSTAPD_LIBS}) + target_link_libraries(${TEST_PROJECT_NAME} gtest_main) + install(TARGETS ${TEST_PROJECT_NAME} DESTINATION bin/tests) + add_test(NAME ${TEST_PROJECT_NAME} COMMAND $) +endif() diff --git a/common/beerocks/hostapd/README.md b/common/beerocks/hostapd/README.md new file mode 100644 index 0000000000..6b38e522bf --- /dev/null +++ b/common/beerocks/hostapd/README.md @@ -0,0 +1,90 @@ + +# Directory Content +This diectory contains prplMesh utilities to work with hostapd +It has few features as listed below + +## Configuration +Manipulates hostapd configuration. +* load +* save +* add +* remove +* edit +* disable +* find + +## Process (yet to implement) +Manages hostapd process +* start +* stop +* restart + +## Commands (yet to implement) +Manages sending and receiving commands to and from ostapd + +## Events (yet to implement) +Manages events originated by hostapd itself + +# Code + +## namespace +The entire code is under prplmesh::hostapd +* Configuration: there is a class named Configuration +* Process: prplmesh::hostapd::process (yet to implement) +* Commands: prplmesh::hostapd::commands (yet to implement) +* Events: prplmesh::hostapd::event (yet to implement) + +## Limitations +We are updating hostapd's configuration files with the info that we get from +the controller. Once we changed the configuration we send UPDATE command to hostapd +making it re-read the configuration and updating the VAPs accordingly. + +With that approach there are the following limitations: + - all the VAPs are expected to be always present in the config, but to be + commented out if not active. + - any configuration change causing the hostapd config files to be re-generated + and the hostapd processes restarted will override the prplMesh configuration. + +## hostapd Configuration Format + +hostapd has a special format that is NOT an ini like format. +below, between /// BEGIN hostapd.conf /// and /// END hostapd.conf /// is the +format of the file. +note: the string "bss=" may be replaced by the +user in the call to load() with another vap-indicator (e.g. "interface=") + +/// BEGIN hostapd.conf /// + +\### "head" part + +\# everything until the first `bss=` in the file +\# is considered "head" +\# after the first `bss=` "vaps" are presented. +\# so we expect no parametrs that does not belong to vaps +\# after the first `bss=` +\# take a look below for more details +\# +\# note that we don't expect a space between the key and the equal sign +\# the code in seeks for `key=` when a key is needed. +\# therefore `key =` (with space) will fail +\# also, everything immidiatly after the equal sign (=) is +\# part of the value. the space before 11 in this line is part of the value: +bassid= 11:22:33:44:55:66 +key=value + +\### "vaps part - we expect at least one vap to be configured ## + +\# vap (bss and vap are interchangable) +bss=wlan0_0 + +\# this key (ssid) belongs to the previous vap (bss value which is wlan0_0) +ssid=test2 + +# another vap +bss=wlan0_1 + +# this key (bssid) belongs to the previous vap wlan0_1 +bssid=00:13:10:95:fe:0b + +///// END hostapd.conf /// + diff --git a/common/beerocks/hostapd/include/hostapd/configuration.h b/common/beerocks/hostapd/include/hostapd/configuration.h new file mode 100644 index 0000000000..9a29be01bf --- /dev/null +++ b/common/beerocks/hostapd/include/hostapd/configuration.h @@ -0,0 +1,299 @@ +/* SPDX-License-Identifier: BSD-2-Clause-Patent + * + * SPDX-FileCopyrightText: 2016-2020 the prplMesh contributors (see AUTHORS.md) + * + * This code is subject to the terms of the BSD+Patent license. + * See LICENSE file for more details. + */ + +#include +#include +#include +#include +#include + +namespace prplmesh { +namespace hostapd { + +class Configuration { + +public: + /** + * @brief construct a Configurationn object + * @param file_name the name of the configuration file. + */ + explicit Configuration(const std::string &file_name); + + ~Configuration() = default; + Configuration(const Configuration &) = default; + Configuration(Configuration &&) = default; + + /** + * @brief for simple use in if statements + * @return true or false with the following meaning: + * true - configuration is non empty and valid + * false - otherwise + */ + operator bool() const; + + /** + * @brief load the configuration + * @details loads the file this object is constructed with + * @param vap_indications - set the indications that a vap + * configuration section begins. e.g. "bss=" and/or "interface=" + * note that the equal sign is expected to be part of vap_indication + * std::string vap_indication("interface="); + * vap indication can be any of the parameters supplied + * @return *this (as bool, see above) + */ + template + bool load(const StringIndications... vap_indications) + { + return load(std::set{vap_indications...}); + } + + /** + * @brief stores the configuration + * @details stores the internal representation of the configuration + * into the file it was loaded from, effectively changing the configuration + * @return *this (as bool, see above) + */ + bool store(); + + /** + * @brief set key/value in the head section + * @details set the key/vale in the head section, + * either replace or create. + * @note comments in the head section are not supported + * for example, if the key/value line was commented before + * the call to this function, it would be uncommented afterwards + * @param + * - the key to set its value (string) + * - the value (string) + */ + bool set_create_head_value(const std::string &key, const std::string &value); + + /** + * @brief set key/value in the head section + * @details set the key/vale in the head section, + * either replace or create. + * @note comments in the head section are not supported + * for example, if the key/value line was commented before + * the call to this function, it would be uncommented afterwards + * @param + * - the key to set its value (string) + * - the value (int) + */ + bool set_create_head_value(const std::string &key, const int value); + + /** + * @brief get the value of the given key from the head + * @param + * - the key to get its value (string) + * @return a string with the value or empty if not found + */ + std::string get_head_value(const std::string &key); + + /** + * @brief set key/value for the given vap + * @details set the key/vale for the given vap, either replace or create. + * @param + * - the vap to set the value for (string) + * - the key to set its value (string) + * - the value (string) + * @return + * true - the vap exists, values were set. + * false - the given vap was not found + */ + bool set_create_vap_value(const std::string &vap, const std::string &key, + const std::string &value); + + /** + * @brief set key/value for the given vap + * @details set the key/vale for the given vap, either replace or create. + * @param + * - the vap to set the value for (string) + * - the key to set its value (string) + * - the value (int) + * @return + * true - the vap exists, values were set. + * false - the given vap was not found + */ + bool set_create_vap_value(const std::string &vap, const std::string &key, const int value); + + /** + * @brief get the value of the given key for the given vap + * @param + * - the vap to get the value for (string) + * - the key to get its value (string) + * @return a string with the value or empty if not found + */ + std::string get_vap_value(const std::string &vap, const std::string &key); + + /** + * @brief disables vap by adding a comment to it + * e.g: + * before: + * bss=wlan0_0 + * ssid=test2 + * bss=wlan0_1 + * after: + * #bss=wlan0_0 + * #ssid=test2 + * bss=wlan0_1 + * + */ + void comment_vap(const std::string &vap); + + /** + * @brief enables vap by removing comments from it + * e.g: + * before: + * #bss=wlan0_0 + * ##ssid=test2 + * bss=wlan0_1 + * after: + * bss=wlan0_0 + * ssid=test2 + * bss=wlan0_1 + * + */ + void uncomment_vap(const std::string &vap); + + /** + * @brief apply func to all ap vaps + * @details apply func to all vaps that ap_predicate returns + * true for them. this enables leaving STAs vaps untouched for example. + * @param func has the following interface: void f(const std::string &vap); + * @param ap_predicate has the following interface: bool f(const std::string &vap); + */ + template void for_all_ap_vaps(func, ap_predicate); + + /** + * @brief apply func to all ap vaps, increment the given iterator + * with each call to func: f(current_vap, ++user_itertor) + * @details apply func to all vaps that ap_predicate returns true for + * while incrementing the user iterator. + * this functionality enables iterating over two containers + * in paralel: the ap-vaps and the user container. + * @param func has the following interface: template f(const std::string& vap, iter user_iterator) + * @param iter - iterator to the begining of the user sequence + * @param ap_predicate has the following interface: bool f(const std::string &vap); + */ + template + void for_all_ap_vaps(func, iter current, const iter end, ap_predicate); + + /** + * @brief for debug: return the last internal message + * @details each action on this class changes its internal + * message (similar to errno) - for debug usage + * @return string describing the last message + */ + const std::string &get_last_message() const; + +private: + /** + * @brief helper: load configuration based on array of strings + * indicating vap separation. + * @param array of separations + * @return *this as bool + */ + bool load(const std::set &vap_indicators); + + /** + * @brief helper: get exiting vap + * @details any function that works on a specific vap does + * the same: search the vap, set a variable when found + * and set an error state when not found, this function + * does it instead of copy/paste the same code + * @param the calling function, string (to report error) + * @param the requested vap, string + * @return tuple*> (found/not found, a + * pointer to the vap's array) + */ + std::tuple *> get_vap(const std::string &calling_function, + const std::string &vap); + + /** + * @brief helper: check if the line contains the key + * @param line in the format key=value or commented ###key=value + * @param the requested key, string + * @return true/false if the line contains the key + */ + bool is_key_in_line(const std::string &line, const std::string &key) const; + +private: + std::string m_configuration_file; + + // m_ok hoslds the internal state of the configuration + // the user may query the success/fail state of the last command + // simply by reading the value of this variable. + // the access to it is via operator bool() + // m_ok itslef may be changed because of a call to + // may be changed because of const functions, therefore mutable + mutable bool m_ok = false; + + // each string is a line in the original configuration file + // that belongs to the "head" part. read the explenation at + // the end of the cpp file for more details + std::vector m_hostapd_config_head; + + // a map between a vap (the key) to its key/value pairs. + // each string in the value part of the map is the line in the original + // configuration file with the original key=value + // e.g. the following lines in the configuration file: + // bss=wlan0.1 <------------------------------ the key in the map: "wlan0.1" + // ctrl_interface=/var/run/hostapd <---------- each line is an element in the array of the map's value + // ap_isolate=1 + // ap_max_inactivity=60 + // bss_transition=1 + // interworking=1 + // disassoc_low_ack=1 + // bss=wlan0.2 <------------------------------ the key in the map: "wlan0.2" + // ctrl_interface=/var/run/hostXXd + // ap_isolate=1 + // ap_max_inactivity=60 + // bss_transition=0 + // interworking=3 + // creates two entries in the map: + // { "wlan0.1" : ["ctrl_interface=/var/run/hostapd", "ap_isolate=1", "ap_max_inactivity=60", "bss_transition=1", "interworking=1", "disassoc_low_ack=1"] }, + // { "wlan0.2" : ["ctrl_interface=/var/run/hosXXpd", "ap_isolate=1", "ap_max_inactivity=60", "bss_transition=0", "interworking=3"] } + std::map> m_hostapd_config_vaps; + + // see m_ok's comment + mutable std::string m_last_message = "initial state, yet nothing was done"; + + // for logs + friend std::ostream &operator<<(std::ostream &, const Configuration &); +}; + +template +void Configuration::for_all_ap_vaps(func f, ap_predicate pred) +{ + auto f_with_iter = [&f](const std::string &vap, int iter) { f(vap); }; + + int dummy(0); + for_all_ap_vaps(f_with_iter, dummy, 10, pred); +} + +template +void Configuration::for_all_ap_vaps(func f, iter current_iter, const iter end, ap_predicate pred) +{ + for_each(m_hostapd_config_vaps.begin(), m_hostapd_config_vaps.end(), + [this, &f, ¤t_iter, &end, + &pred](const std::pair> &vap) { + if (pred(vap.first)) { + if (end == current_iter) { + f(vap.first, end); + } else { + f(vap.first, current_iter++); + } + } + }); +} + +// for logs +std::ostream &operator<<(std::ostream &, const Configuration &); + +} // namespace hostapd +} // namespace prplmesh diff --git a/common/beerocks/hostapd/source/configuration.cpp b/common/beerocks/hostapd/source/configuration.cpp new file mode 100644 index 0000000000..99f96f1782 --- /dev/null +++ b/common/beerocks/hostapd/source/configuration.cpp @@ -0,0 +1,348 @@ +/* SPDX-License-Identifier: BSD-2-Clause-Patent + * + * SPDX-FileCopyrightText: 2016-2020 the prplMesh contributors (see AUTHORS.md) + * + * This code is subject to the terms of the BSD+Patent license. + * See LICENSE file for more details. + */ + +#include +#include +#include +#include + +namespace prplmesh { +namespace hostapd { + +Configuration::Configuration(const std::string &file_name) : m_configuration_file(file_name) {} + +Configuration::operator bool() const { return m_ok; } + +bool Configuration::load(const std::set &vap_indications) +{ + // please take a look at README.md (common/beerocks/hostapd/README.md) for + // the expected format of hostapd configuration file. + // loading the file relies on the expected format + // otherwise the load fails + + + // for cases when load is called more than once, we + // first clear internal data + m_hostapd_config_head.clear(); + m_hostapd_config_vaps.clear(); + + // strat reading + std::ifstream ifs(m_configuration_file); + std::string line; + + bool parsing_vaps = false; + std::string cur_vap; + + // go over line by line in the file + while (getline(ifs, line)) { + + // skip empty lines + if (std::all_of(line.begin(), line.end(), isspace)) { + continue; + } + + // check if the string belongs to a vap config part and capture which one. + auto end_comment = line.find_first_not_of('#'); + auto end_key = line.find_first_of('='); + + std::string current_key(line,end_comment,end_key+1); + + auto vap_iterator = vap_indications.find(current_key); + if (vap_iterator != vap_indications.end()) { + // from now on we are in the vaps area, all + // key/value pairs belongs to vaps + parsing_vaps = true; + + // copy the vap value + cur_vap = std::string(line, end_key + 1); + } + + // if not a vap line store it in the header part of the config, + // otherwise add to the currently being parsed vap storage. + if (!parsing_vaps) { + m_hostapd_config_head.push_back(line); + } else { + m_hostapd_config_vaps[cur_vap].push_back(line); + } + } + + std::stringstream load_message; + load_message << "load() final message: os - " << strerror(errno) << "; existing vaps - " + << std::boolalpha << parsing_vaps; + m_last_message = load_message.str(); + + // if we've got to parsing vaps and no read errors, assume all is good + m_ok = parsing_vaps && !ifs.bad(); + + // return this as bool + return *this; +} + +bool Configuration::store() +{ + std::ofstream out_file(m_configuration_file, std::ofstream::out | std::ofstream::trunc); + + // store the head + for (const auto &line : m_hostapd_config_head) { + out_file << line << "\n"; + } + + // store the vaps + for (auto &vap : m_hostapd_config_vaps) { + + // add empty line for readability + out_file << "\n"; + + for (auto &line : vap.second) { + out_file << line << "\n"; + } + } + + m_ok = true; + m_last_message = m_configuration_file + " was stored"; + + // close the file + out_file.close(); + if (out_file.fail()) { + m_last_message = strerror(errno); + m_ok = false; + } + + return *this; +} + +bool Configuration::set_create_head_value(const std::string &key, const std::string &value) +{ + // search for the key + std::string key_eq(key + "="); + auto line_iter = std::find_if( + m_hostapd_config_head.begin(), m_hostapd_config_head.end(), + [&key_eq, this](const std::string &line) -> bool { return is_key_in_line(line, key_eq); }); + + // we first delete the key, and if the requested value is non empty + // we push it to the end of the array + + // delete the key-value if found + if (line_iter != m_hostapd_config_head.end()) { + line_iter = m_hostapd_config_head.erase(line_iter); + } else { + m_last_message = + std::string(__FUNCTION__) + " the key '" + key + "' for head was not found"; + } + + // when the new value is provided add the key back with that new value + if (value.length() != 0) { + m_hostapd_config_head.push_back(key_eq + value); + m_last_message = std::string(__FUNCTION__) + " the key '" + key + "' was (re)added to head"; + } else { + m_last_message = std::string(__FUNCTION__) + " the key '" + key + "' was deleted from head"; + } + + m_ok = true; + return *this; +} + +bool Configuration::set_create_head_value(const std::string &key, const int value) +{ + return set_create_head_value(key, std::to_string(value)); +} + +std::string Configuration::get_head_value(const std::string &key) +{ + std::string key_eq(key + "="); + auto line_iter = std::find_if( + m_hostapd_config_head.begin(), m_hostapd_config_head.end(), + [&key_eq, this](const std::string &line) -> bool { return is_key_in_line(line, key_eq); }); + + if (line_iter == m_hostapd_config_head.end()) { + m_last_message = std::string(__FUNCTION__) + " couldn't find requested key in head: " + key; + return ""; + } + + // return from just after the '=' sign to the end of the string + return line_iter->substr(line_iter->find('=') + 1); +} + +bool Configuration::set_create_vap_value(const std::string &vap, const std::string &key, + const std::string &value) +{ + // search for the requested vap + auto find_vap = get_vap(std::string(__FUNCTION__) + " key/value: " + key + '/' + value, vap); + if (!std::get<0>(find_vap)) { + return false; + } + auto existing_vap = std::get<1>(find_vap); + bool existing_vap_commented = existing_vap->front()[0] == '#'; + + std::string key_eq(key + "="); + auto line_iter = std::find_if( + existing_vap->begin(), existing_vap->end(), + [&key_eq, this](const std::string &line) -> bool { return is_key_in_line(line, key_eq); }); + + // we first delete the key, and if the requested value is non empty + // we push it to the end of the array + + // delete the key-value if found + if (line_iter != existing_vap->end()) { + line_iter = existing_vap->erase(line_iter); + } else { + m_last_message = + std::string(__FUNCTION__) + " the key '" + key + "' for vap " + vap + " was not found"; + } + + // when the new value is provided add the key back with that new value + if (value.length() != 0) { + if (existing_vap_commented) { + existing_vap->push_back('#' + key_eq + value); + } else { + existing_vap->push_back(key_eq + value); + } + m_last_message = + std::string(__FUNCTION__) + " the key '" + key + "' for vap " + vap + " was (re)added"; + } else { + m_last_message = + std::string(__FUNCTION__) + " the key '" + key + "' for vap " + vap + " was deleted"; + } + + m_ok = true; + return *this; +} + +bool Configuration::set_create_vap_value(const std::string &vap, const std::string &key, + const int value) +{ + return set_create_vap_value(vap, key, std::to_string(value)); +} + +std::string Configuration::get_vap_value(const std::string &vap, const std::string &key) +{ + // search for the requested vap + auto find_vap = get_vap(std::string(__FUNCTION__), vap); + if (!std::get<0>(find_vap)) { + return ""; + } + const auto &existing_vap = std::get<1>(find_vap); + + // from now on this function is ok with all situations + // (e.g. not finding the requested key) + m_ok = true; + + std::string key_eq(key + "="); + auto line_iter = std::find_if( + existing_vap->begin(), existing_vap->end(), + [&key_eq, this](const std::string &line) -> bool { return is_key_in_line(line, key_eq); }); + + if (line_iter == existing_vap->end()) { + m_last_message = std::string(__FUNCTION__) + + " couldn't find requested key for vap: " + vap + "; requested key: " + key; + return ""; + } + + // return from the just after the '=' sign to the end of the string + return line_iter->substr(line_iter->find('=') + 1); +} + +void Configuration::comment_vap(const std::string &vap) +{ + // search for the requested vap + auto find_vap = get_vap(std::string(__FUNCTION__), vap); + if (!std::get<0>(find_vap)) { + return; + } + const auto &existing_vap = std::get<1>(find_vap); + + std::for_each(existing_vap->begin(), existing_vap->end(), + [](std::string &line) { line.insert(0, 1, '#'); }); +} + +void Configuration::uncomment_vap(const std::string &vap) +{ + // search for the requested vap + auto find_vap = get_vap(std::string(__FUNCTION__), vap); + if (!std::get<0>(find_vap)) { + return; + } + const auto &existing_vap = std::get<1>(find_vap); + + std::for_each(existing_vap->begin(), existing_vap->end(), [](std::string &line) { + auto end_comment = line.find_first_not_of('#'); + if (std::string::npos != end_comment) { + line.erase(0, end_comment); + }; + }); +} + +const std::string &Configuration::get_last_message() const { return m_last_message; } + +std::tuple *> +Configuration::get_vap(const std::string &calling_function, const std::string &vap) +{ + // search for the requested vap - ignore comments + // by searching from the back of the saved vap (rfind) + auto existing_vap = + std::find_if(m_hostapd_config_vaps.begin(), m_hostapd_config_vaps.end(), + [&vap](const std::pair> ¤t_vap) { + return current_vap.first.rfind(vap) != std::string::npos; + }); + + if (existing_vap == m_hostapd_config_vaps.end()) { + m_last_message = calling_function + " couldn't find requested vap: " + vap; + m_ok = false; + return std::make_tuple(false, nullptr); + } + + m_ok = true; + return std::make_tuple(true, &existing_vap->second); +} + +bool Configuration::is_key_in_line(const std::string &line, const std::string &key) const +{ + // we need to make sure when searching for example + // for "ssid", to ignore cases like: + // multi_ap_backhaul_ssid="Multi-AP-24G-2" + // ^^^^^ + // bssid=02:9A:96:FB:59:11 + // ^^^^^ + // and we need to take into consideration + // that the key might be or might not be commented. + // so the search algorithm is: + // - find the requested key and + // - make sure it is either on the first position + // or it has a comment sign just before it + auto found_pos = line.rfind(key); + bool ret = found_pos != std::string::npos && (found_pos == 0 || line.at(found_pos - 1) == '#'); + + return ret; +} + +std::ostream &operator<<(std::ostream &os, const Configuration &conf) +{ + os << "== configuration details ==\n" + << "= ok: " << std::boolalpha << conf.m_ok << '\n' + << "= last message: " << conf.m_last_message << '\n' + << "= file: " << conf.m_configuration_file << '\n' + << "= head: " << '\n'; + + for (const auto &line : conf.m_hostapd_config_head) { + os << line << '\n'; + } + + os << "== vaps (total of: " << conf.m_hostapd_config_vaps.size() << " vaps) ==\n"; + + for (const auto &vap : conf.m_hostapd_config_vaps) { + os << " vap: " << vap.first << "\n"; + for (const auto &line : vap.second) { + os << line << '\n'; + } + } + + return os; +} + +} // namespace hostapd +} // namespace prplmesh diff --git a/common/beerocks/hostapd/unit_tests/configuration_test.cpp b/common/beerocks/hostapd/unit_tests/configuration_test.cpp new file mode 100644 index 0000000000..a85e5ded56 --- /dev/null +++ b/common/beerocks/hostapd/unit_tests/configuration_test.cpp @@ -0,0 +1,589 @@ +/* SPDX-License-Identifier: BSD-2-Clause-Patent + * + * SPDX-FileCopyrightText: 2020 the prplMesh contributors (see AUTHORS.md) + * + * This code is subject to the terms of the BSD+Patent license. + * See LICENSE file for more details. + */ + +#include +#include + +#include + +namespace { + +const std::string vap_indication_1("bss="); +const std::string vap_indication_2("interface="); +const std::string configuration_path("/tmp/"); +const std::string configuration_file_name("omnia.conf"); +const std::string configuration_content( + "driver=nl80211\n" + "logger_syslog=127\n" + "logger_syslog_level=1\n" + "logger_stdout=127\n" + "logger_stdout_level=7\n" + "hw_mode=a\n" + "beacon_int=100\n" + "channel=44\n" + "tx_queue_data2_burst=2.0\n" + "ieee80211n=1\n" + "ht_coex=0\n" + "ht_capab=[HT40+][LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935][DSSS_CCK-40]\n" + "vht_oper_chwidth=1\n" + "vht_oper_centr_freq_seg0_idx=42\n" + "ieee80211ac=1\n" + "vht_capab=[RXLDPC][SHORT-GI-80][TX-STBC-2BY1][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][RX-STBC-1][MAX-MPDU-11454][MAX-A-MPDU-LEN-EXP7]\n" + + "interface=wlan0\n" + "ctrl_interface=/var/run/hostapd\n" + "ap_isolate=1\n" + "bss_load_update_period=60\n" + "chan_util_avg_period=600\n" + "disassoc_low_ack=1\n" + "preamble=1\n" + "wmm_enabled=1\n" + "ignore_broadcast_ssid=0\n" + "uapsd_advertisement_enabled=1\n" + "utf8_ssid=1\n" + "multi_ap=0\n" + "wpa_passphrase=prplBEE$\n" + "wpa_psk_file=/var/run/hostapd-wlan0.psk\n" + "auth_algs=1\n" + "wpa=2\n" + "wpa_pairwise=CCMP\n" + "ssid=prplmesh-front\n" + "bridge=br-lan\n" + "wpa_disable_eapol_key_retries=0\n" + "wpa_key_mgmt=WPA-PSK\n" + "okc=0\n" + "disable_pmksa_caching=1\n" + "dynamic_vlan=0\n" + "vlan_naming=1\n" + "vlan_file=/var/run/hostapd-wlan0.vlan\n" + "bssid=04:f0:21:24:24:17\n" + + "bss=wlan0-1\n" + "ctrl_interface=/var/run/hostapd\n" + "ap_isolate=1\n" + "bss_load_update_period=60\n" + "chan_util_avg_period=600\n" + "disassoc_low_ack=1\n" + "preamble=1\n" + "wmm_enabled=1\n" + "ignore_broadcast_ssid=0\n" + "uapsd_advertisement_enabled=1\n" + "utf8_ssid=1\n" + "multi_ap=0\n" + "wpa_passphrase=prplBEE$\n" + "wpa_psk_file=/var/run/hostapd-wlan0-1.psk\n" + "auth_algs=1\n" + "wpa=2\n" + "wpa_pairwise=CCMP\n" + "ssid=prplmesh-back\n" + "bridge=br-lan\n" + "wpa_disable_eapol_key_retries=0\n" + "wpa_key_mgmt=WPA-PSK\n" + "okc=0\n" + "disable_pmksa_caching=1\n" + "dynamic_vlan=0\n" + "vlan_naming=1\n" + "vlan_file=/var/run/hostapd-wlan0-1.vlan\n" + "wds_sta=1\n" + "bssid=06:f0:21:24:24:17\n" +); + +void clean_start() +{ + // save the content of the string (start clean) + std::ofstream tmp(configuration_path + configuration_file_name); + tmp << configuration_content; + tmp.flush(); +} + +// Suppress cppcheck syntax error for gtest TEST macro +// cppcheck-suppress syntaxError +TEST(configuration_test, load) +{ + //// start prerequsite //// + + clean_start(); + + //// end prerequsite //// + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + EXPECT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + EXPECT_TRUE(conf) << conf; +} + +TEST(configuration_test, store) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // load the dummy configuration file + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + + // construct a configuration + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + // add a value to vap + conf.set_create_vap_value("wlan0-1", "was_i_stroed", "yes_you_were"); + EXPECT_TRUE(conf) << conf; + + // store + conf.store(); + EXPECT_TRUE(conf) << conf; +} + +TEST(configuration_test, set_string_head_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + // replace existing key + conf.set_create_head_value("vht_capab", "this is shorter"); + EXPECT_TRUE(conf) << conf; + + // remove exiting key + conf.set_create_head_value("ht_capab", ""); + EXPECT_TRUE(conf) << conf; + + // add a new key/value + conf.set_create_head_value("new_head", "{pnew->next=phead; phead=pnew;}"); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; +} + +TEST(configuration_test, set_int_head_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + // replace existing key + conf.set_create_head_value("bss_transition", 451); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; +} + +TEST(configuration_test, get_head_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + // get existing key + auto val = conf.get_head_value("logger_syslog"); + EXPECT_EQ(val, "127"); + + // get non existing key + val = conf.get_head_value("out_of_office"); + EXPECT_EQ(val, ""); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; +} + +TEST(configuration_test, set_string_vap_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + conf.set_create_vap_value("wlan0", "ssid", "ran_home"); + EXPECT_TRUE(conf) << conf; + + // replace existing value for existing key for existing vap + conf.set_create_vap_value("wlan0", "disassoc_low_ack", "734"); + EXPECT_TRUE(conf) << conf; + + // add a key/value to exising vap + conf.set_create_vap_value("wlan0", "unit_test_ok", "true"); + EXPECT_TRUE(conf) << conf; + + // remove key/value from existing vap + conf.set_create_vap_value("wlan0-1", "vendor_elements", ""); + EXPECT_TRUE(conf) << conf; + + // set key/value for NON existing vap + conf.set_create_vap_value("no_such", "how_do_you_do", "i_am_doing_fine"); + EXPECT_FALSE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; + + //// end test //// +} + +TEST(configuration_test, set_int_vap_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + // replace existing value for existing key for existing vap + conf.set_create_vap_value("wlan0-1", "ignore_broadcast_ssid", 42); + EXPECT_TRUE(conf) << conf; + + // add a key/value to exising vap + conf.set_create_vap_value("wlan0", "i_am_negative", -24); + EXPECT_TRUE(conf) << conf; + + // try to replace existing value for existing key for existing vap + // with NON-int value. we expect the value to be trancated here + // at the caller site + conf.set_create_vap_value("wlan0", "wmm_enabled", 333.444); + EXPECT_TRUE(conf) << conf; + + //// end test //// +} + +TEST(configuration_test, get_vap_values) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + std::string value; + + // get existing value for existing vap + value = conf.get_vap_value("wlan0", "wpa_passphrase"); + EXPECT_EQ(value, "prplBEE$") << conf; + + // another check - existing value for existing vap + value = conf.get_vap_value("wlan0", "bssid"); + EXPECT_EQ(value, "04:f0:21:24:24:17"); + + // get NON existing value for existing vap + value = conf.get_vap_value("wlan0.1", "does_not_exist"); + EXPECT_EQ(value, "") << conf; + + // try to get for NON existing vap + value = conf.get_vap_value("no_vap", "key"); + EXPECT_EQ(value, "") << conf; + + //// end test //// +} + +TEST(configuration_test, disable_all_ap) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + // identify vap by mode=ap + /* + auto mode_predicate = [&conf](const std::string &vap) { + return conf.get_vap_value(vap, "mode") == "ap"; + }; + */ + + // all vaps are ap vaps + auto all_predicate = [&conf](const std::string &vap) { return true; }; + + // disable by adding a key/value + auto disable_func = [&conf](const std::string vap) { + conf.set_create_vap_value(vap, "start_disabled", 1); + }; + + conf.for_all_ap_vaps(disable_func, all_predicate); + EXPECT_TRUE(conf) << conf; + + // disable by commenting + auto comment_func = [&conf](const std::string vap) { conf.comment_vap(vap); }; + + conf.for_all_ap_vaps(comment_func, all_predicate); + EXPECT_TRUE(conf) << conf; + + // store + conf.store(); + EXPECT_TRUE(conf) << conf; + + //// end prerequsite //// +} + +TEST(configuration_test, enable_all_ap) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + // enable by removing key/value + auto enable_func = [&conf](const std::string vap) { + conf.set_create_vap_value(vap, "start_disabled", ""); + }; + + auto ap_predicate = [&conf](const std::string &vap) { + return conf.get_vap_value(vap, "mode") == "ap"; + }; + + conf.for_all_ap_vaps(enable_func, ap_predicate); + EXPECT_TRUE(conf) << conf; + + // enable by uncommenting + auto uncomment_func = [&conf](const std::string &vap) { conf.uncomment_vap(vap); }; + + conf.for_all_ap_vaps(uncomment_func, ap_predicate); + EXPECT_TRUE(conf) << conf; + + // store + conf.store(); + EXPECT_TRUE(conf) << conf; + + //// end prerequsite //// +} + +TEST(configuration_test, comment_vap) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + conf.comment_vap("wlan0"); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; + + //// end test //// +} + +TEST(configuration_test, uncomment_vap) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + // comment twice! + conf.comment_vap("wlan0"); + conf.comment_vap("wlan0"); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + ; + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + conf.uncomment_vap("wlan0"); + EXPECT_TRUE(conf) << conf; + + conf.uncomment_vap("wlan0"); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; + + // replace existing value for existing key for existing vap + conf.set_create_vap_value("wlan0", "disassoc_low_ack", "734"); + EXPECT_TRUE(conf) << conf; + + // add a key/value to exising vap + conf.set_create_vap_value("wlan0", "unit_test_ok", "true"); + EXPECT_TRUE(conf) << conf; + + // remove key/value from existing vap + conf.set_create_vap_value("wlan0-1", "preamble", ""); + EXPECT_TRUE(conf) << conf; + + // set key/value for NON existing vap + conf.set_create_vap_value("no_such", "how_do_you_do", "i_am_doing_fine"); + EXPECT_FALSE(conf) << conf; + + //// end test //// +} + +TEST(configuration_test, itererate_both_containers) +{ + //// start prerequsite //// + + // save the content of the string (start clean) + clean_start(); + + // construct a configuration + prplmesh::hostapd::Configuration conf(configuration_path + configuration_file_name); + ASSERT_FALSE(conf) << conf; + + // load the dummy configuration file + conf.load(vap_indication_1,vap_indication_2); + + ASSERT_TRUE(conf) << conf; + + //// end prerequsite //// + + //// start test //// + + std::vector ssid = {"00:11:22:33:44:55", "ab:cd:ef:01:02:03"}; + + auto set_ssid = [&conf, &ssid](const std::string vap, std::vector::iterator it) { + if (it != ssid.end()) { + // as long as we didn't finish our containr + // we set the given's vap ssid + conf.set_create_vap_value(vap, "ssid", *it); + } else { + // when we done with our container, we simply + // comment the rest of the vaps + conf.comment_vap(vap); + } + }; + + auto ap_predicate = [&conf](const std::string &vap) { + return conf.get_vap_value(vap, "mode") == "ap"; + }; + + conf.for_all_ap_vaps(set_ssid, ssid.begin(), ssid.end(), ap_predicate); + EXPECT_TRUE(conf) << conf; + + conf.store(); + EXPECT_TRUE(conf) << conf; + + //// end test //// +} + +} // namespace