/* * * 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 #endif #include #include #include #include #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)