| /* |
| * |
| * Wireless daemon for Linux |
| * |
| * Copyright (C) 2019 Intel Corporation. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <stdio.h> |
| |
| #include <ell/ell.h> |
| |
| #include "src/iwd.h" |
| #include "src/module.h" |
| #include "src/common.h" |
| #include "src/network.h" |
| #include "src/util.h" |
| #include "src/ie.h" |
| #include "src/knownnetworks.h" |
| #include "src/storage.h" |
| #include "src/scan.h" |
| |
| static struct l_dir_watch *hs20_dir_watch; |
| static struct l_queue *hs20_settings; |
| |
| struct hs20_config { |
| struct network_info super; |
| char *filename; |
| uint8_t hessid[6]; |
| char **nai_realms; |
| uint8_t *rc; /* roaming consortium */ |
| size_t rc_len; |
| char *object_path; |
| char *name; |
| }; |
| |
| static bool match_filename(const void *a, const void *b) |
| { |
| const struct hs20_config *config = a; |
| const char *filename = b; |
| |
| if (!strcmp(config->filename, filename)) |
| return true; |
| |
| return false; |
| } |
| |
| static void hs20_config_free(void *user_data) |
| { |
| struct hs20_config *config = user_data; |
| |
| l_queue_remove(hs20_settings, config); |
| |
| l_strv_free(config->nai_realms); |
| l_free(config->rc); |
| l_free(config->object_path); |
| l_free(config->filename); |
| l_free(config->name); |
| l_free(config); |
| } |
| |
| static int hotspot_network_touch(struct network_info *info) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| return l_path_touch(config->filename); |
| } |
| |
| static struct l_settings *hotspot_network_open(struct network_info *info) |
| { |
| struct l_settings *settings; |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| settings = l_settings_new(); |
| |
| if (!l_settings_load_from_file(settings, config->filename)) { |
| l_settings_free(settings); |
| return NULL; |
| } |
| |
| return settings; |
| } |
| |
| static void hotspot_network_sync(struct network_info *info, |
| struct l_settings *settings) |
| { |
| char *data; |
| size_t length = 0; |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| data = l_settings_to_data(settings, &length); |
| write_file(data, length, true, "%s", config->filename); |
| l_free(data); |
| } |
| |
| static void hotspot_network_remove(struct network_info *info) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| unlink(config->filename); |
| } |
| |
| static void hotspot_network_free(struct network_info *info) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| hs20_config_free(config); |
| } |
| |
| static const char *hotspot_network_get_path(const struct network_info *info) |
| { |
| char *digest; |
| struct l_checksum *sha; |
| char **realms; |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| if (config->object_path) |
| return config->object_path; |
| |
| sha = l_checksum_new(L_CHECKSUM_SHA256); |
| |
| if (config->nai_realms) { |
| realms = config->nai_realms; |
| |
| while (*realms) { |
| l_checksum_update(sha, *realms, strlen(*realms)); |
| realms++; |
| } |
| } |
| |
| if (config->rc) |
| l_checksum_update(sha, config->rc, config->rc_len); |
| |
| if (!util_mem_is_zero(config->hessid, 6)) |
| l_checksum_update(sha, config->hessid, 6); |
| |
| digest = l_checksum_get_string(sha); |
| l_checksum_free(sha); |
| config->object_path = l_strdup_printf("/%.8s_hotspot", digest); |
| l_free(digest); |
| |
| return config->object_path; |
| } |
| |
| static const char *hotspot_network_get_name(const struct network_info *info) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| return config->name; |
| } |
| |
| static const char *hotspot_network_get_type(const struct network_info *info) |
| { |
| return "hotspot"; |
| } |
| |
| static bool hotspot_match_hessid(const struct network_info *info, |
| const uint8_t *hessid) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| if (util_mem_is_zero(config->hessid, 6) || !hessid) |
| return false; |
| |
| return !memcmp(config->hessid, hessid, 6); |
| } |
| |
| static const uint8_t *hotspot_match_roaming_consortium( |
| const struct network_info *info, |
| const uint8_t *rc_ie, |
| size_t rc_len, |
| size_t *rc_len_out) |
| { |
| const uint8_t *rc1, *rc2, *rc3; |
| size_t rc1_len, rc2_len, rc3_len; |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| if (!config->rc || !rc_ie) |
| return NULL; |
| |
| if (ie_parse_roaming_consortium_from_data(rc_ie, rc_ie[1] + 2, NULL, |
| &rc1, &rc1_len, &rc2, &rc2_len, |
| &rc3, &rc3_len) < 0) |
| return NULL; |
| |
| /* rc1 is guaranteed to be set if the above returns success */ |
| if (rc1_len == config->rc_len && !memcmp(rc1, config->rc, rc1_len)) { |
| if (rc_len_out) |
| *rc_len_out = rc1_len; |
| return rc1; |
| } |
| |
| if (rc2 && rc2_len == config->rc_len && |
| !memcmp(rc2, config->rc, rc2_len)) { |
| if (rc_len_out) |
| *rc_len_out = rc2_len; |
| return rc2; |
| } |
| |
| if (rc3 && rc1_len == config->rc_len && |
| !memcmp(rc3, config->rc, rc3_len)) { |
| if (rc_len_out) |
| *rc_len_out = rc3_len; |
| return rc3; |
| } |
| |
| return NULL; |
| } |
| |
| static bool hotspot_match_nai_realms(const struct network_info *info, |
| const char **nai_realms) |
| { |
| const char **realms = nai_realms; |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| |
| if (!config->nai_realms || !nai_realms) |
| return false; |
| |
| while (*realms) { |
| if (l_strv_contains(config->nai_realms, *realms)) |
| return true; |
| |
| realms++; |
| } |
| |
| return false; |
| } |
| |
| static const struct iovec *hotspot_network_get_ies( |
| const struct network_info *info, |
| struct scan_bss *bss, |
| size_t *num_elems) |
| { |
| static struct iovec iov[2]; |
| static uint8_t hs20_ie[7]; |
| static uint8_t rc_buf[11]; |
| const uint8_t *rc; |
| size_t rc_len; |
| size_t iov_elems = 0; |
| |
| ie_build_hs20_indication(bss->hs20_version, hs20_ie); |
| |
| iov[iov_elems].iov_base = hs20_ie; |
| iov[iov_elems].iov_len = hs20_ie[1] + 2; |
| iov_elems++; |
| |
| rc = hotspot_match_roaming_consortium(info, bss->rc_ie, |
| bss->rc_ie[1] + 2, &rc_len); |
| if (rc) { |
| ie_build_roaming_consortium(rc, rc_len, rc_buf); |
| |
| iov[iov_elems].iov_base = rc_buf; |
| iov[iov_elems].iov_len = rc_buf[1] + 2; |
| iov_elems++; |
| } |
| |
| *num_elems = iov_elems; |
| |
| return iov; |
| } |
| |
| static char *hotspot_network_get_file_path(const struct network_info *info) |
| { |
| struct hs20_config *config = l_container_of(info, struct hs20_config, |
| super); |
| return l_strdup(config->filename); |
| } |
| |
| static struct network_info_ops hotspot_ops = { |
| .open = hotspot_network_open, |
| .touch = hotspot_network_touch, |
| .sync = hotspot_network_sync, |
| .remove = hotspot_network_remove, |
| .free = hotspot_network_free, |
| .get_path = hotspot_network_get_path, |
| .get_name = hotspot_network_get_name, |
| .get_type = hotspot_network_get_type, |
| .get_extra_ies = hotspot_network_get_ies, |
| .get_file_path = hotspot_network_get_file_path, |
| |
| .match_hessid = hotspot_match_hessid, |
| .match_roaming_consortium = hotspot_match_roaming_consortium, |
| .match_nai_realms = hotspot_match_nai_realms, |
| }; |
| |
| static struct hs20_config *hs20_config_new(struct l_settings *settings, |
| char *filename) |
| { |
| struct hs20_config *config; |
| char *hessid_str; |
| char **nai_realms = NULL; |
| size_t rc_len; |
| uint8_t *rc; |
| char *name; |
| bool autoconnect; |
| |
| /* One of HESSID, NAI realms, or Roaming Consortium must be included */ |
| hessid_str = l_settings_get_string(settings, "Hotspot", "HESSID"); |
| |
| nai_realms = l_settings_get_string_list(settings, "Hotspot", |
| "NAIRealmNames", ','); |
| |
| rc = l_settings_get_bytes(settings, "Hotspot", "RoamingConsortium", |
| &rc_len); |
| |
| if (!l_settings_get_bool(settings, "Settings", "AutoConnect", |
| &autoconnect)) |
| autoconnect = true; |
| |
| name = l_settings_get_string(settings, "Hotspot", "Name"); |
| |
| if ((!hessid_str && !nai_realms && !rc) || !name) { |
| l_error("Could not parse hotspot config %s", filename); |
| goto free_values; |
| } |
| |
| config = l_new(struct hs20_config, 1); |
| |
| if (hessid_str) { |
| if (!util_string_to_address(hessid_str, config->hessid)) { |
| l_error("Invalid HESSID in settings"); |
| l_free(config); |
| goto free_values; |
| } |
| |
| l_free(hessid_str); |
| } |
| |
| if (nai_realms) |
| config->nai_realms = nai_realms; |
| |
| if (rc) { |
| /* |
| * WiFi Alliance Hotspot 2.0 Spec - Section 3.1.4 |
| * |
| * "The Consortium OI field is 3 or 5-octet field set to a value |
| * of a roaming consortium OI" |
| */ |
| if (rc && rc_len != 3 && rc_len != 5) { |
| l_warn("invalid RoamingConsortium length %zu", rc_len); |
| l_free(rc); |
| rc = NULL; |
| } |
| |
| config->rc = rc; |
| config->rc_len = rc_len; |
| } |
| |
| config->super.is_autoconnectable = autoconnect; |
| config->super.is_hotspot = true; |
| config->super.type = SECURITY_8021X; |
| config->super.ops = &hotspot_ops; |
| config->super.connected_time = l_path_get_mtime(filename); |
| config->name = name; |
| |
| config->filename = l_strdup(filename); |
| |
| known_networks_add(&config->super); |
| |
| return config; |
| |
| free_values: |
| l_strv_free(nai_realms); |
| l_free(hessid_str); |
| l_free(name); |
| l_free(rc); |
| |
| return NULL; |
| } |
| |
| static void hs20_dir_watch_cb(const char *filename, |
| enum l_dir_watch_event event, |
| void *user_data) |
| { |
| struct l_settings *new; |
| uint64_t connected_time; |
| struct hs20_config *config; |
| |
| L_AUTO_FREE_VAR(char *, full_path) = NULL; |
| |
| /* |
| * Ignore notifications for the actual directory, we can't do |
| * anything about some of them anyway. Only react to |
| * notifications for files in the storage directory. |
| */ |
| if (!filename) |
| return; |
| |
| full_path = storage_get_hotspot_path("%s", filename); |
| |
| switch (event) { |
| case L_DIR_WATCH_EVENT_CREATED: |
| new = l_settings_new(); |
| |
| if (!l_settings_load_from_file(new, full_path)) { |
| l_settings_free(new); |
| return; |
| } |
| |
| config = hs20_config_new(new, full_path); |
| l_settings_free(new); |
| |
| if (!config) |
| return; |
| |
| l_queue_push_head(hs20_settings, config); |
| break; |
| case L_DIR_WATCH_EVENT_REMOVED: |
| config = l_queue_remove_if(hs20_settings, match_filename, |
| full_path); |
| if (!config) |
| return; |
| |
| known_networks_remove(&config->super); |
| |
| /* |
| * TODO: Disconnect any networks using this provisioning file |
| */ |
| |
| break; |
| case L_DIR_WATCH_EVENT_MODIFIED: |
| config = l_queue_find(hs20_settings, match_filename, full_path); |
| if (!config) |
| return; |
| |
| connected_time = l_path_get_mtime(full_path); |
| |
| new = l_settings_new(); |
| |
| if (!l_settings_load_from_file(new, full_path)) { |
| l_settings_free(new); |
| return; |
| } |
| |
| known_network_set_connected_time(&config->super, |
| connected_time); |
| known_network_update(&config->super, new); |
| |
| l_settings_free(new); |
| |
| break; |
| case L_DIR_WATCH_EVENT_ACCESSED: |
| break; |
| case L_DIR_WATCH_EVENT_ATTRIB: |
| config = l_queue_find(hs20_settings, match_filename, full_path); |
| if (!config) |
| return; |
| |
| connected_time = l_path_get_mtime(full_path); |
| known_network_set_connected_time(&config->super, |
| connected_time); |
| |
| break; |
| } |
| } |
| |
| static void hs20_dir_watch_destroy(void *user_data) |
| { |
| hs20_dir_watch = NULL; |
| } |
| |
| static int hotspot_init(void) |
| { |
| DIR *dir; |
| struct dirent *dirent; |
| |
| L_AUTO_FREE_VAR(char *, hs20_dir) = storage_get_hotspot_path(NULL); |
| |
| dir = opendir(hs20_dir); |
| if (!dir) |
| return -ENOENT; |
| |
| hs20_settings = l_queue_new(); |
| |
| while ((dirent = readdir(dir))) { |
| struct hs20_config *config; |
| struct l_settings *s; |
| char *filename; |
| |
| if (dirent->d_type != DT_REG && dirent->d_type != DT_LNK) |
| continue; |
| |
| filename = l_strdup_printf("%s/%s", hs20_dir, dirent->d_name); |
| s = l_settings_new(); |
| |
| if (!l_settings_load_from_file(s, filename)) |
| goto next; |
| |
| config = hs20_config_new(s, filename); |
| |
| if (config) |
| l_queue_push_head(hs20_settings, config); |
| |
| next: |
| l_free(filename); |
| l_settings_free(s); |
| } |
| |
| closedir(dir); |
| |
| hs20_dir_watch = l_dir_watch_new(hs20_dir, hs20_dir_watch_cb, NULL, |
| hs20_dir_watch_destroy); |
| |
| return 0; |
| } |
| |
| static void hotspot_exit(void) |
| { |
| l_dir_watch_destroy(hs20_dir_watch); |
| |
| l_queue_destroy(hs20_settings, NULL); |
| hs20_settings = NULL; |
| } |
| |
| IWD_MODULE(hotspot, hotspot_init, hotspot_exit) |
| IWD_MODULE_DEPENDS(hotspot, known_networks) |