├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ ├── common-debian.sh │ └── node-debian.sh ├── .github ├── pull_request_template.md └── workflows │ ├── docs.yml │ ├── swift-arm.yml │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── DarwinWLAN │ ├── CWInterface.swift │ ├── CWNetwork.swift │ ├── CWWiFiClient.swift │ └── DarwinWLAN.swift ├── LinuxWLAN │ ├── Interfaces.swift │ ├── LinuxWLAN.swift │ ├── NetworkInterface.swift │ └── Scan.swift ├── WLAN │ ├── AsyncStream.swift │ ├── BSSID.swift │ ├── ByteSwap.swift │ ├── ByteValue.swift │ ├── Error.swift │ ├── Interface.swift │ ├── Network.swift │ ├── SSID.swift │ ├── ScanRecord.swift │ └── WLANManager.swift └── wirelesstool │ ├── Error.swift │ └── WLANTool.swift ├── Tests └── WLANTests │ └── WLANTests.swift └── ci_scripts └── ci_post_clone.sh /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Swift version: 5.6-focal, 5.5, 5.4, 5.3, 5.2, 5.1, 4.2 2 | ARG VARIANT=5.7.3-jammy 3 | FROM swift:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="false" 9 | 10 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=vscode 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | COPY library-scripts/common-debian.sh /tmp/library-scripts/ 15 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 16 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 17 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/library-scripts 18 | 19 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 20 | ARG NODE_VERSION="none" 21 | ENV NVM_DIR=/usr/local/share/nvm 22 | ENV NVM_SYMLINK_CURRENT=true \ 23 | PATH=${NVM_DIR}/current/bin:${PATH} 24 | COPY library-scripts/node-debian.sh /tmp/library-scripts/ 25 | RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ 26 | && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 27 | 28 | # [Optional] Uncomment this section to install additional OS packages you may want. 29 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 30 | # && apt-get -y install --no-install-recommends 31 | 32 | # [Optional] Uncomment this line to install global node packages. 33 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/swift 3 | { 4 | "name": "Swift", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a Swift version 9 | "VARIANT": "5.7.3-jammy", 10 | // Options 11 | "NODE_VERSION": "lts/*", 12 | "UPGRADE_PACKAGES": "true" 13 | } 14 | }, 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined" 19 | ], 20 | 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | // Configure properties specific to VS Code. 24 | "vscode": { 25 | // Set *default* container specific settings.json values on container create. 26 | "settings": { 27 | "lldb.library": "/usr/lib/liblldb.so" 28 | }, 29 | 30 | // Add the IDs of extensions you want installed when the container is created. 31 | "extensions": [ 32 | "sswg.swift-lang" 33 | ] 34 | } 35 | }, 36 | 37 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 38 | // "forwardPorts": [], 39 | 40 | // Use 'postCreateCommand' to run commands after the container is created. 41 | // "postCreateCommand": "", 42 | 43 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 44 | "remoteUser": "vscode" 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust[0-9] \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/node-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] 11 | 12 | export NVM_DIR=${1:-"/usr/local/share/nvm"} 13 | export NODE_VERSION=${2:-"lts"} 14 | USERNAME=${3:-"automatic"} 15 | UPDATE_RC=${4:-"true"} 16 | INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" 17 | export NVM_VERSION="0.38.0" 18 | 19 | set -e 20 | 21 | if [ "$(id -u)" -ne 0 ]; then 22 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 23 | exit 1 24 | fi 25 | 26 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 27 | rm -f /etc/profile.d/00-restore-env.sh 28 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 29 | chmod +x /etc/profile.d/00-restore-env.sh 30 | 31 | # Determine the appropriate non-root user 32 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 33 | USERNAME="" 34 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 35 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 36 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 37 | USERNAME=${CURRENT_USER} 38 | break 39 | fi 40 | done 41 | if [ "${USERNAME}" = "" ]; then 42 | USERNAME=root 43 | fi 44 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 45 | USERNAME=root 46 | fi 47 | 48 | updaterc() { 49 | if [ "${UPDATE_RC}" = "true" ]; then 50 | echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." 51 | if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then 52 | echo -e "$1" >> /etc/bash.bashrc 53 | fi 54 | if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then 55 | echo -e "$1" >> /etc/zsh/zshrc 56 | fi 57 | fi 58 | } 59 | 60 | # Function to run apt-get if needed 61 | apt_get_update_if_needed() 62 | { 63 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 64 | echo "Running apt-get update..." 65 | apt-get update 66 | else 67 | echo "Skipping apt-get update." 68 | fi 69 | } 70 | 71 | # Checks if packages are installed and installs them if not 72 | check_packages() { 73 | if ! dpkg -s "$@" > /dev/null 2>&1; then 74 | apt_get_update_if_needed 75 | apt-get -y install --no-install-recommends "$@" 76 | fi 77 | } 78 | 79 | # Ensure apt is in non-interactive to avoid prompts 80 | export DEBIAN_FRONTEND=noninteractive 81 | 82 | # Install dependencies 83 | check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr 84 | 85 | # Install yarn 86 | if type yarn > /dev/null 2>&1; then 87 | echo "Yarn already installed." 88 | else 89 | # Import key safely (new method rather than deprecated apt-key approach) and install 90 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg 91 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list 92 | apt-get update 93 | apt-get -y install --no-install-recommends yarn 94 | fi 95 | 96 | # Adjust node version if required 97 | if [ "${NODE_VERSION}" = "none" ]; then 98 | export NODE_VERSION= 99 | elif [ "${NODE_VERSION}" = "lts" ]; then 100 | export NODE_VERSION="lts/*" 101 | fi 102 | 103 | # Create a symlink to the installed version for use in Dockerfile PATH statements 104 | export NVM_SYMLINK_CURRENT=true 105 | 106 | # Install the specified node version if NVM directory already exists, then exit 107 | if [ -d "${NVM_DIR}" ]; then 108 | echo "NVM already installed." 109 | if [ "${NODE_VERSION}" != "" ]; then 110 | su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" 111 | fi 112 | exit 0 113 | fi 114 | 115 | # Create nvm group, nvm dir, and set sticky bit 116 | if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then 117 | groupadd -r nvm 118 | fi 119 | umask 0002 120 | usermod -a -G nvm ${USERNAME} 121 | mkdir -p ${NVM_DIR} 122 | chown :nvm ${NVM_DIR} 123 | chmod g+s ${NVM_DIR} 124 | su ${USERNAME} -c "$(cat << EOF 125 | set -e 126 | umask 0002 127 | # Do not update profile - we'll do this manually 128 | export PROFILE=/dev/null 129 | ls -lah /home/${USERNAME}/.nvs || : 130 | curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash 131 | source ${NVM_DIR}/nvm.sh 132 | if [ "${NODE_VERSION}" != "" ]; then 133 | nvm alias default ${NODE_VERSION} 134 | fi 135 | nvm clear-cache 136 | EOF 137 | )" 2>&1 138 | # Update rc files 139 | if [ "${UPDATE_RC}" = "true" ]; then 140 | updaterc "$(cat < /dev/null 2>&1; then 153 | to_install="${to_install} make" 154 | fi 155 | if ! type gcc > /dev/null 2>&1; then 156 | to_install="${to_install} gcc" 157 | fi 158 | if ! type g++ > /dev/null 2>&1; then 159 | to_install="${to_install} g++" 160 | fi 161 | if ! type python3 > /dev/null 2>&1; then 162 | to_install="${to_install} python3-minimal" 163 | fi 164 | if [ ! -z "${to_install}" ]; then 165 | apt_get_update_if_needed 166 | apt-get -y install ${to_install} 167 | fi 168 | fi 169 | 170 | echo "Done!" 171 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Issue** 2 | 3 | Fixes #1. 4 | 5 | **What does this PR Do?** 6 | 7 | Description of the changes in this pull request. 8 | 9 | **Where should the reviewer start?** 10 | 11 | `main.swift` 12 | 13 | **Sweet giphy showing how you feel about this PR** 14 | 15 | ![Giphy](https://media.giphy.com/media/rkDXJA9GoWR2/giphy.gif) 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | 7 | jobs: 8 | # Create documentation 9 | docs: 10 | name: Documentation 11 | runs-on: macOS-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Xcode Version 16 | run: | 17 | xcodebuild -version 18 | swift --version 19 | - name: Swift Version 20 | run: swift --version 21 | - name: Generate documentation 22 | uses: sersoft-gmbh/swifty-docs-action@v1 23 | - name: Upload docs 24 | uses: actions/upload-artifact@v1 25 | if: always() 26 | with: 27 | name: docs 28 | path: docs 29 | -------------------------------------------------------------------------------- /.github/workflows/swift-arm.yml: -------------------------------------------------------------------------------- 1 | name: Swift ARM 2 | on: [push] 3 | jobs: 4 | 5 | linux-armv7-crosscompile-build: 6 | name: Crosscompile for Linux Armv7 7 | runs-on: ubuntu-latest 8 | container: colemancda/swift-armv7:latest-prebuilt 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Swift Version 13 | run: swift --version 14 | - name: Build (Release) 15 | run: | 16 | cd $SRC_ROOT 17 | export SWIFT_PACKAGE_SRCDIR=$GITHUB_WORKSPACE 18 | export SWIFT_PACKAGE_BUILDDIR=$SWIFT_PACKAGE_SRCDIR/.build 19 | $SRC_ROOT/build-swift-package.sh 20 | - name: Archive Build artifacts 21 | uses: actions/upload-artifact@v3 22 | with: 23 | name: linux-armv7-crosscompile-test 24 | path: .build/*/*.xctest 25 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | on: [push] 3 | jobs: 4 | 5 | macos: 6 | name: macOS 7 | runs-on: macos-latest 8 | steps: 9 | - name: Install Swift 10 | uses: slashmo/install-swift@v0.3.0 11 | with: 12 | version: 5.7.3 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Swift Version 16 | run: swift --version 17 | - name: Build (Debug) 18 | run: swift build -c debug 19 | - name: Build (Release) 20 | run: swift build -c release 21 | - name: Test (Debug) 22 | run: swift test -c debug 23 | 24 | linux: 25 | name: Linux 26 | strategy: 27 | matrix: 28 | swift: [5.7.3] 29 | runs-on: ubuntu-20.04 30 | steps: 31 | - name: Install Swift 32 | uses: slashmo/install-swift@v0.3.0 33 | with: 34 | version: ${{ matrix.swift }} 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | - name: Swift Version 38 | run: swift --version 39 | - name: Build (Debug) 40 | run: swift build -c debug 41 | - name: Build (Release) 42 | run: swift build -c release 43 | - name: Test (Debug) 44 | run: swift test -c debug 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | .swiftpm/ 42 | Package.resolved 43 | *.xcodeproj 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | import class Foundation.ProcessInfo 4 | 5 | // force building as dynamic library 6 | let dynamicLibrary = ProcessInfo.processInfo.environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] != nil 7 | let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrary ? .dynamic : nil 8 | 9 | var package = Package( 10 | name: "WLAN", 11 | platforms: [ 12 | .macOS(.v10_15), 13 | .iOS(.v13), 14 | .watchOS(.v6), 15 | .tvOS(.v13), 16 | ], 17 | products: [ 18 | .library( 19 | name: "WLAN", 20 | type: libraryType, 21 | targets: ["WLAN"] 22 | ), 23 | .library( 24 | name: "DarwinWLAN", 25 | targets: ["DarwinWLAN"] 26 | ), 27 | .library( 28 | name: "LinuxWLAN", 29 | type: libraryType, 30 | targets: ["LinuxWLAN"] 31 | ) 32 | ], 33 | dependencies: [ 34 | .package( 35 | url: "https://github.com/PureSwift/Netlink.git", 36 | .branch("master") 37 | ) 38 | ], 39 | targets: [ 40 | .target( 41 | name: "WLAN" 42 | ), 43 | .target( 44 | name: "DarwinWLAN", 45 | dependencies: [ 46 | .target(name: "WLAN") 47 | ] 48 | ), 49 | .target( 50 | name: "LinuxWLAN", 51 | dependencies: [ 52 | .target( 53 | name: "WLAN" 54 | ), 55 | .product( 56 | name: "Netlink", 57 | package: "Netlink" 58 | ), 59 | .product( 60 | name: "NetlinkGeneric", 61 | package: "Netlink" 62 | ), 63 | .product( 64 | name: "Netlink80211", 65 | package: "Netlink" 66 | ) 67 | ] 68 | ), 69 | .executableTarget( 70 | name: "wirelesstool", 71 | dependencies: [ 72 | .target( 73 | name: "WLAN" 74 | ), 75 | .target( 76 | name: "DarwinWLAN", 77 | condition: .when(platforms: [.macOS]) 78 | ), 79 | .target( 80 | name: "LinuxWLAN", 81 | condition: .when(platforms: [.linux]) 82 | ) 83 | ] 84 | ), 85 | .testTarget( 86 | name: "WLANTests", 87 | dependencies: [ 88 | .target( 89 | name: "WLAN" 90 | ), 91 | ] 92 | ), 93 | ] 94 | ) 95 | 96 | // SwiftPM command plugins are only supported by Swift version 5.6 and later. 97 | #if swift(>=5.6) 98 | let buildDocs = ProcessInfo.processInfo.environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] != nil 99 | if buildDocs { 100 | package.dependencies += [ 101 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 102 | ] 103 | } 104 | #endif 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WLAN 2 | Wireless LAN (WiFi) API for Swift (Supports Linux) 3 | 4 | ## Overview 5 | 6 | This library provides a cross-platform API for interacting WiFi hardware and scanning for networks. 7 | On macOS the [CoreWLAN](https://developer.apple.com/documentation/corewlan) framework is used to communicate with the Darwin kernel, while on Linux a [Netlink](https://en.wikipedia.org/wiki/Netlink) socket is used to communicate with the Linux kernel. 8 | -------------------------------------------------------------------------------- /Sources/DarwinWLAN/CWInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWInterface.swift 3 | // WLANDarwin 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // 7 | 8 | #if canImport(CoreWLAN) 9 | 10 | import Foundation 11 | import CoreWLAN 12 | import WLAN 13 | 14 | internal extension WLANInterface { 15 | 16 | init(_ coreWLAN: CWInterface) { 17 | 18 | guard let interfaceName = coreWLAN.interfaceName 19 | else { fatalError("Invalid values") } 20 | 21 | self.init(name: interfaceName) 22 | } 23 | } 24 | 25 | internal extension CWInterface { 26 | 27 | func network(for network: WLANNetwork) throws -> CWNetwork { 28 | 29 | guard let wlanNetwork = self.cachedScanResults()? 30 | .first(where: { $0.ssidData == network.ssid.data }) 31 | else { throw WLANError.invalidNetwork(network) } 32 | 33 | return wlanNetwork 34 | } 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/DarwinWLAN/CWNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWNetwork.swift 3 | // WLANDarwin 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // 7 | 8 | #if canImport(CoreWLAN) 9 | 10 | import Foundation 11 | import CoreWLAN 12 | import WLAN 13 | 14 | internal extension WLANNetwork { 15 | 16 | init(_ coreWLAN: CWNetwork) { 17 | 18 | guard let ssidData = coreWLAN.ssidData, 19 | let ssid = SSID(data: ssidData) 20 | else { fatalError("Invalid values") } 21 | 22 | let bssid: BSSID 23 | if let bssidString = coreWLAN.bssid { 24 | bssid = BSSID(rawValue: bssidString) ?? .zero 25 | } else { 26 | bssid = .zero // dont have permissions 27 | } 28 | 29 | self.init(ssid: ssid, bssid: bssid) 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/DarwinWLAN/CWWiFiClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CWWiFiClient.swift 3 | // WLANDarwin 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // 7 | 8 | #if canImport(CoreWLAN) 9 | 10 | import Foundation 11 | import CoreWLAN 12 | import WLAN 13 | 14 | internal extension CWWiFiClient { 15 | 16 | func interface(for interface: WLANInterface) throws -> CWInterface { 17 | 18 | guard let wlanInterface = self.interfaces()? 19 | .first(where: { $0.interfaceName == interface.name }) 20 | else { throw WLANError.invalidInterface(interface) } 21 | 22 | return wlanInterface 23 | } 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/DarwinWLAN/DarwinWLAN.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreWLAN.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/3/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if canImport(CoreWLAN) 10 | 11 | import Foundation 12 | import CoreWLAN 13 | import WLAN 14 | 15 | /// Darwin WLAN Manager 16 | /// 17 | /// Query AirPort interfaces and choose wireless networks. 18 | public final class DarwinWLANManager: WLANManager { 19 | 20 | // MARK: - Properties 21 | 22 | internal let client: CWWiFiClient 23 | 24 | internal let queue = DispatchQueue(label: "org.pureswift.DarwinWLAN") 25 | 26 | // MARK: - Initialization 27 | 28 | deinit { 29 | client.delegate = nil 30 | } 31 | 32 | public init() { 33 | self.client = CWWiFiClient() 34 | self.client.delegate = self 35 | } 36 | 37 | // MARK: - Methods 38 | 39 | /// Returns the default Wi-Fi interface. 40 | public var interface: WLANInterface? { 41 | get async { 42 | return await withCheckedContinuation { continuation in 43 | queue.async { [unowned self] in 44 | let interface = self.client.interface() 45 | .map { WLANInterface($0) } 46 | continuation.resume(returning: interface) 47 | } 48 | } 49 | } 50 | } 51 | 52 | /** 53 | Returns all available Wi-Fi interfaces. 54 | 55 | - Returns: An array of `WLANInterface`, representing all of the available Wi-Fi interfaces in the system. 56 | */ 57 | public var interfaces: [WLANInterface] { 58 | get async { 59 | return await withCheckedContinuation { continuation in 60 | queue.async { [unowned self] in 61 | let interfaces = (self.client.interfaces() ?? []) 62 | .map { WLANInterface($0) } 63 | continuation.resume(returning: interfaces) 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | Sets the interface power state. 71 | 72 | - Parameter power: A Boolean value corresponding to the power state. NO indicates the "OFF" state. 73 | - Parameter interface: The network interface. 74 | */ 75 | public func setPower(_ power: Bool, for interface: WLANInterface) async throws { 76 | return try await withCheckedThrowingContinuation { continuation in 77 | queue.async { [unowned self] in 78 | do { 79 | let interface = try self.client.interface(for: interface) 80 | try interface.setPower(power) 81 | continuation.resume() 82 | } catch { 83 | continuation.resume(throwing: error) 84 | } 85 | } 86 | } 87 | } 88 | 89 | /** 90 | Scans for networks. 91 | 92 | If ssid parameter is present, a directed scan will be performed by the interface, otherwise a broadcast scan will be performed. This method will block for the duration of the scan. 93 | 94 | - Note: Returned networks will not contain BSSID information unless Location Services is enabled and the user has authorized the calling app to use location services. 95 | 96 | - Parameter ssid: The SSID for which to scan. 97 | - Parameter interface: The network interface. 98 | */ 99 | public func scan( 100 | with interface: WLANInterface 101 | ) async throws -> AsyncWLANScan { 102 | return AsyncWLANScan { continuation in 103 | let task = Task { 104 | var foundNetworks = Set() 105 | while Task.isCancelled == false { 106 | do { 107 | let networks = try await self._scan(with: interface) 108 | for network in networks { 109 | // yield new values only 110 | guard foundNetworks.contains(network) == false else { 111 | return 112 | } 113 | foundNetworks.insert(network) 114 | continuation.yield(network) 115 | } 116 | } 117 | catch { 118 | continuation.finish(throwing: error) 119 | } 120 | } 121 | } 122 | // make child task, cancel 123 | continuation.onTermination = { 124 | task.cancel() 125 | } 126 | } 127 | } 128 | 129 | internal func _scan( 130 | with interface: WLANInterface 131 | ) async throws -> Set { 132 | return try await withCheckedThrowingContinuation { continuation in 133 | queue.async { [unowned self] in 134 | do { 135 | let interface = try self.client.interface(for: interface) 136 | let networks = try interface.scanForNetworks(withSSID: nil) // blocking call 137 | let value = Set(networks.lazy.map({ WLANNetwork($0) })) 138 | continuation.resume(returning: value) 139 | } 140 | catch { 141 | continuation.resume(throwing: error) 142 | } 143 | } 144 | } 145 | } 146 | 147 | /** 148 | Associates to a given network using the given network passphrase. 149 | 150 | - Parameter network: The network to which the interface will associate. 151 | - Parameter password: The network passphrase or key. Required for association to WEP, WPA Personal, and WPA2 Personal networks. 152 | - Parameter interface: The network interface. 153 | */ 154 | public func associate( 155 | to network: WLANNetwork, 156 | password: String? = nil, 157 | for interface: WLANInterface 158 | ) async throws { 159 | return try await withCheckedThrowingContinuation { continuation in 160 | queue.async { [unowned self] in 161 | do { 162 | let interface = try self.client.interface(for: interface) 163 | let network = try interface.network(for: network) 164 | try interface.associate(to: network, password: password) 165 | continuation.resume() 166 | } catch { 167 | continuation.resume(throwing: error) 168 | } 169 | } 170 | } 171 | } 172 | 173 | /** 174 | Disassociates from the current network. 175 | 176 | This method has no effect if the interface is not associated to a network. 177 | */ 178 | public func disassociate(interface: WLANInterface) async throws { 179 | return try await withCheckedThrowingContinuation { continuation in 180 | queue.async { [unowned self] in 181 | do { 182 | let interface = try self.client.interface(for: interface) 183 | interface.disassociate() 184 | continuation.resume() 185 | } catch { 186 | continuation.resume(throwing: error) 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | internal extension DarwinWLANManager { 194 | 195 | /// CWWiFiClient Delegate 196 | /// 197 | /// https://developer.apple.com/documentation/corewlan/cweventdelegate 198 | @objc(DarwinWLANManagerDelegate) 199 | final class Delegate: NSObject /* CWWiFiEventDelegate */ { 200 | 201 | private unowned var manager: DarwinWLANManager 202 | 203 | init(_ manager: DarwinWLANManager) { 204 | self.manager = manager 205 | } 206 | 207 | /// Tells the delegate that the current BSSID has changed. 208 | @objc 209 | func bssidDidChangeForWiFiInterface(withName name: String) { 210 | 211 | } 212 | 213 | /// Tells the delegate that the connection to the Wi-Fi subsystem is temporarily interrupted. 214 | @objc 215 | func clientConnectionInterrupted() { 216 | 217 | } 218 | 219 | /// Tells the delegate that the connection to the Wi-Fi subsystem is permanently invalidated. 220 | @objc 221 | func clientConnectionInvalidated() { 222 | 223 | } 224 | 225 | /// Tells the delegate that the currently adopted country code has changed. 226 | @objc 227 | func countryCodeDidChangeForWiFiInterface(withName name: String) { 228 | 229 | } 230 | 231 | /// Tells the delegate that the Wi-Fi link state changed. 232 | @objc 233 | func linkDidChangeForWiFiInterface(withName name: String) { 234 | 235 | } 236 | 237 | /// Tells the delegate that the link quality has changed. 238 | @objc 239 | func linkQualityDidChangeForWiFiInterface(withName name: String, rssi: Int, transmitRate: Double) { 240 | 241 | } 242 | 243 | /// Tells the delegate that the operating mode has changed. 244 | func modeDidChangeForWiFiInterface(withName name: String) { 245 | 246 | } 247 | 248 | /// Tells the delegate that the Wi-Fi power state changed. 249 | func powerStateDidChangeForWiFiInterface(withName name: String) { 250 | 251 | } 252 | 253 | /// Tells the delegate that the Wi-Fi interface's scan cache has been updated with new results. 254 | func scanCacheUpdatedForWiFiInterface(withName name: String) { 255 | 256 | } 257 | 258 | /// 259 | func ssidDidChangeForWiFiInterface(withName name: String) { 260 | 261 | } 262 | 263 | } 264 | } 265 | 266 | #endif 267 | -------------------------------------------------------------------------------- /Sources/LinuxWLAN/Interfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 4/22/22. 6 | // 7 | 8 | import Foundation 9 | import SystemPackage 10 | import WLAN 11 | import Netlink 12 | import NetlinkGeneric 13 | import Netlink80211 14 | 15 | internal extension LinuxWLANManager { 16 | 17 | /// Issue NL80211_CMD_GET_WIPHY to the kernel and get wireless info. 18 | func getWiphy(_ interface: UInt32) async throws -> NL80211Wiphy { 19 | // Setup which command to run. 20 | let command = NL80211GetWiphyCommand(interface: interface) 21 | let message = try newMessage(command, flags: [.request]) 22 | // Send the message. 23 | try await socket.send(message.data) 24 | // Retrieve the kernel's answer 25 | let messages = try await socket.recieve(NetlinkGenericMessage.self) 26 | let decoder = NetlinkAttributeDecoder() 27 | // parse response 28 | guard let response = messages.first, 29 | let interface = try? decoder.decode(NL80211Wiphy.self, from: response) 30 | else { throw NetlinkSocketError.invalidData(Data()) } 31 | return interface 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/LinuxWLAN/LinuxWLAN.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WLANProtocol.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(Linux) 10 | import Glibc 11 | #elseif os(macOS) || os(iOS) 12 | import Darwin.C 13 | #endif 14 | 15 | import Foundation 16 | import SystemPackage 17 | import WLAN 18 | import Netlink 19 | import NetlinkGeneric 20 | import Netlink80211 21 | 22 | /** 23 | Linux WLAN Manager 24 | 25 | A wrapper around the entire Wi-Fi subsystem that you use to access interfaces. 26 | */ 27 | public actor LinuxWLANManager: WLANManager { 28 | 29 | // MARK: - Properties 30 | 31 | internal let socket: NetlinkSocket 32 | 33 | internal let controller: NetlinkGenericFamilyController 34 | 35 | internal private(set) var sequenceNumber: UInt32 = 0 36 | 37 | internal private(set) var interfaceCache = [WLANInterface: InterfaceCache]() 38 | 39 | internal private(set) var scanCache = [WLANNetwork: NL80211ScanResult]() 40 | 41 | // MARK: - Initialization 42 | 43 | public init() async throws { 44 | // Open socket to kernel. 45 | // Create file descriptor and bind socket. 46 | let socket = try await NetlinkSocket(.generic) 47 | // Find the "nl80211" driver ID. 48 | let controller = try await socket.resolve(name: .nl80211) // Find the "nl80211" driver ID. 49 | self.socket = socket 50 | self.controller = controller 51 | // allocate buffer 52 | self.interfaceCache.reserveCapacity(10) 53 | self.scanCache.reserveCapacity(10) 54 | } 55 | 56 | // MARK: - Methods 57 | 58 | /// Returns the default Wi-Fi interface. 59 | public var interface: WLANInterface? { 60 | get async { 61 | return await interfaces.first 62 | } 63 | } 64 | 65 | /** 66 | Returns all available Wi-Fi interfaces. 67 | 68 | - Returns: An array of `WLANInterface`, representing all of the available Wi-Fi interfaces in the system. 69 | */ 70 | public var interfaces: [WLANInterface] { 71 | get async { 72 | do { 73 | try await refreshInterfaces() 74 | return interfaceCache 75 | .lazy 76 | .sorted(by: { $0.value.id < $1.value.id }) 77 | .map { $0.key } 78 | } 79 | catch { 80 | assertionFailure("Unable to get interfaces. \(error.localizedDescription)") 81 | return [] 82 | } 83 | } 84 | } 85 | 86 | /** 87 | Sets the interface power state. 88 | 89 | - Parameter power: A Boolean value corresponding to the power state. NO indicates the "OFF" state. 90 | - Parameter interface: The network interface. 91 | */ 92 | public func setPower(_ power: Bool, for interface: WLANInterface) async throws { 93 | 94 | } 95 | 96 | /** 97 | Disassociates from the current network. 98 | 99 | This method has no effect if the interface is not associated to a network. 100 | */ 101 | public func disassociate(interface: WLANInterface) async throws { 102 | 103 | } 104 | 105 | // MARK: - Internal Methods 106 | 107 | internal func interface(for interfaceName: WLANInterface) throws -> InterfaceCache { 108 | guard let interface = interfaceCache[interfaceName] else { 109 | throw WLANError.invalidInterface(interfaceName) 110 | } 111 | return interface 112 | } 113 | 114 | internal func newMessage( 115 | _ command: NetlinkGenericCommand, 116 | flags: NetlinkMessageFlag = 0, 117 | version: NetlinkGenericVersion = 0, 118 | payload: Data = Data() 119 | ) -> NetlinkGenericMessage { 120 | return NetlinkGenericMessage( 121 | type: NetlinkMessageType(rawValue: UInt16(controller.id.rawValue)), 122 | flags: flags, 123 | sequence: newSequence(), 124 | process: ProcessID.current.rawValue, 125 | command: command, 126 | version: version, 127 | payload: payload 128 | ) 129 | } 130 | 131 | internal func newMessage( 132 | _ command: T, 133 | flags: NetlinkMessageFlag = 0 134 | ) throws -> NetlinkGenericMessage { 135 | let encoder = NetlinkAttributeEncoder() 136 | let commandData = try encoder.encode(command) 137 | return newMessage(T.command, flags: flags, version: T.version, payload: commandData) 138 | } 139 | 140 | private func newSequence() -> UInt32 { 141 | if sequenceNumber == .max { 142 | sequenceNumber = 0 143 | } else { 144 | sequenceNumber += 1 145 | } 146 | return sequenceNumber 147 | } 148 | 149 | internal func refreshInterfaces() async throws { 150 | let interfaces = try NetworkInterface.interfaces() 151 | self.interfaceCache.removeAll(keepingCapacity: true) 152 | self.interfaceCache.reserveCapacity(interfaces.count) 153 | for interface in interfaces { 154 | do { 155 | let key = WLANInterface(name: interface.name) 156 | let id = try NetworkInterface.index(for: interface) 157 | let wiphy = try await getWiphy(id) 158 | let cacheValue = InterfaceCache( 159 | id: id, 160 | interface: interface, 161 | wiphy: wiphy 162 | ) 163 | self.interfaceCache[key] = cacheValue 164 | } catch { 165 | continue 166 | } 167 | } 168 | } 169 | 170 | @discardableResult 171 | internal func cache(_ scanResult: NL80211ScanResult) -> WLANNetwork { 172 | let network = WLANNetwork(scanResult) 173 | self.scanCache[network] = scanResult 174 | return network 175 | } 176 | 177 | internal func resetScanResultsCache() { 178 | self.scanCache.removeAll(keepingCapacity: true) 179 | } 180 | } 181 | 182 | // MARK: - Supporting Types 183 | 184 | internal extension LinuxWLANManager { 185 | 186 | struct InterfaceCache: Equatable, Hashable, Identifiable { 187 | 188 | let id: UInt32 189 | 190 | let interface: NetworkInterface 191 | 192 | let wiphy: NL80211Wiphy 193 | } 194 | } 195 | 196 | internal protocol NetlinkWLANMessage: Encodable { 197 | 198 | static var command: NetlinkGenericCommand { get } 199 | 200 | static var version: NetlinkGenericVersion { get } 201 | } 202 | 203 | extension NL80211GetWiphyCommand: NetlinkWLANMessage { } 204 | 205 | extension NL80211GetInterfaceCommand: NetlinkWLANMessage { } 206 | 207 | extension NL80211TriggerScanCommand: NetlinkWLANMessage { } 208 | 209 | extension NL80211GetScanResultsCommand: NetlinkWLANMessage { } 210 | -------------------------------------------------------------------------------- /Sources/LinuxWLAN/NetworkInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkInterface.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // 7 | // 8 | 9 | #if os(Linux) 10 | import Glibc 11 | #elseif canImport(Darwin) 12 | import Darwin 13 | #endif 14 | 15 | import SystemPackage 16 | 17 | /// UNIX Network Interface 18 | internal struct NetworkInterface: Equatable, Hashable { 19 | 20 | /// Interface name. 21 | public let name: String 22 | } 23 | 24 | internal extension NetworkInterface { 25 | 26 | static func interfaces() throws -> [NetworkInterface] { 27 | 28 | var addressLinkedList: UnsafeMutablePointer? = nil 29 | 30 | guard getifaddrs(&addressLinkedList) == 0 31 | else { throw Errno(rawValue: errno) } 32 | 33 | var interfaces = [NetworkInterface]() 34 | var nextElement = addressLinkedList 35 | while let interface = nextElement?.pointee { 36 | nextElement = interface.ifa_next 37 | guard interface.ifa_addr?.pointee.sa_family == sa_family_t(AF_PACKET) 38 | else { continue } 39 | let name = String(cString: interface.ifa_name) 40 | interfaces.append(NetworkInterface(name: name)) 41 | } 42 | 43 | return interfaces 44 | } 45 | 46 | /// Returns the index of the network interface corresponding to the name 47 | static func index(for interface: NetworkInterface) throws -> UInt32 { 48 | let index = if_nametoindex(interface.name) 49 | guard index != 0 else { throw Errno(rawValue: errno) } 50 | return index 51 | } 52 | } 53 | 54 | #if !os(Linux) 55 | var AF_PACKET: CInt { fatalError("AF_PACKET is Linux-only") } 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/LinuxWLAN/Scan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Netlink80211.swift 3 | // LinuxWLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/7/18. 6 | // 7 | 8 | #if os(Linux) 9 | import Glibc 10 | #elseif os(macOS) || os(iOS) 11 | import Darwin.C 12 | #endif 13 | 14 | import Foundation 15 | import SystemPackage 16 | import WLAN 17 | import Netlink 18 | import NetlinkGeneric 19 | import Netlink80211 20 | 21 | public extension LinuxWLANManager { 22 | 23 | /** 24 | Scans for networks. 25 | 26 | If ssid parameter is present, a directed scan will be performed by the interface, otherwise a broadcast scan will be performed. This method will block for the duration of the scan. 27 | 28 | - Parameter interface: The network interface. 29 | */ 30 | func scan(with interface: WLANInterface) async throws -> AsyncWLANScan { 31 | do { 32 | // get cached interface index 33 | let interface = try self.interface(for: interface) 34 | // register for `scan` multicast group 35 | guard let scanGroup = controller.multicastGroups.first(where: { $0.name == NetlinkGenericMulticastGroupName.NL80211.scan }) 36 | else { throw Errno.notSupported } 37 | // subscribe to group 38 | try socket.subscribe(to: scanGroup.id) 39 | // start scanning on wireless interface. 40 | try await triggerScan(interface: interface.id) 41 | // reset cache 42 | resetScanResultsCache() 43 | // stream 44 | return AsyncWLANScan { continuation in 45 | let task = Task { 46 | // wait 47 | while Task.isCancelled == false { 48 | do { 49 | // attempt to read messages 50 | let messages = try await socket.recieve(NetlinkGenericMessage.self) 51 | let hasNewScanResults = messages.contains(where: { $0.command == NetlinkGenericCommand.NL80211.newScanResults }) 52 | guard hasNewScanResults else { 53 | try await Task.sleep(nanoseconds: 100_000_000) 54 | continue 55 | } 56 | let scanResults = try await scanResults(interface: interface.id) 57 | // cache new results 58 | for scanResult in scanResults { 59 | let key = WLANNetwork(scanResult) 60 | let isNew = self.scanCache.keys.contains(key) == false 61 | self.cache(scanResult) 62 | if isNew { 63 | continuation.yield(key) 64 | } 65 | } 66 | } 67 | catch _ as NetlinkErrorMessage { 68 | continue 69 | } 70 | } 71 | } 72 | continuation.onTermination = { [weak socket] in 73 | try? socket?.unsubscribe(from: scanGroup.id) 74 | task.cancel() 75 | } 76 | } 77 | } 78 | catch let errorMessage as NetlinkErrorMessage { 79 | throw errorMessage.error ?? errorMessage 80 | } 81 | } 82 | } 83 | 84 | internal extension LinuxWLANManager { 85 | 86 | /// Issue NL80211_CMD_TRIGGER_SCAN to the kernel and wait for it to finish. 87 | func triggerScan(interface: UInt32) async throws { 88 | 89 | // build command 90 | let command = NL80211TriggerScanCommand(interface: interface) 91 | 92 | // Setup which command to run. 93 | let message = try newMessage(command, flags: [.request]) 94 | 95 | // Send the message. 96 | try await socket.send(message.data) 97 | } 98 | 99 | /// Issue NL80211_CMD_GET_SCAN. 100 | func scanResults(interface: UInt32) async throws -> [NL80211ScanResult] { 101 | 102 | // Add message attribute, specify which interface to use. 103 | let command = NL80211GetScanResultsCommand(interface: interface) 104 | 105 | // Setup which command to run. 106 | let message = try newMessage(command, flags: 773) 107 | 108 | // Send the message. 109 | try await socket.send(message.data) 110 | 111 | // Retrieve the kernel's answer 112 | let messages = try await socket.recieve(NetlinkGenericMessage.self) 113 | let decoder = NetlinkAttributeDecoder() 114 | return messages.compactMap { try? decoder.decode(NL80211ScanResult.self, from: $0) } 115 | } 116 | } 117 | 118 | internal extension WLANNetwork { 119 | 120 | init(_ scanResult: NL80211ScanResult) { 121 | let ssidLength = min(Int(scanResult.bss.informationElements[1]), 32) 122 | let ssid = SSID(data: scanResult.bss.informationElements[2 ..< 2 + ssidLength]) ?? "" 123 | let bssid = BSSID(bigEndian: BSSID(bytes: scanResult.bss.bssid.bytes)) 124 | self.init(ssid: ssid, bssid: bssid) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/WLAN/AsyncStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncStream.swift 3 | // 4 | // 5 | // Created by Alsey Coleman Miller on 4/22/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// WLAN Async Scan sequence 11 | public struct AsyncWLANScan : AsyncSequence { 12 | 13 | public typealias Element = WLANNetwork 14 | 15 | internal typealias Storage = AsyncIndefiniteStream.Storage 16 | 17 | let storage: Storage 18 | 19 | public init( 20 | bufferSize: Int = 100, 21 | _ build: @escaping ((Element) -> ()) async throws -> () 22 | ) { 23 | self.init(.init(bufferSize: bufferSize, build)) 24 | } 25 | 26 | public init( 27 | bufferSize: Int = 100, 28 | _ build: (Continuation) -> () 29 | ) { 30 | let stream = AsyncIndefiniteStream(bufferSize: bufferSize) { 31 | build(Continuation($0.continuation, $0.storage)) 32 | } 33 | self.init(stream) 34 | } 35 | 36 | internal init(_ stream: AsyncIndefiniteStream) { 37 | self.storage = stream.storage 38 | } 39 | 40 | public func makeAsyncIterator() -> AsyncIterator { 41 | return AsyncIterator(storage.stream.makeAsyncIterator()) 42 | } 43 | 44 | public func stop() { 45 | storage.stop() 46 | } 47 | 48 | public var isScanning: Bool { 49 | return storage.isExecuting 50 | } 51 | } 52 | 53 | public extension AsyncWLANScan { 54 | 55 | struct AsyncIterator: AsyncIteratorProtocol { 56 | 57 | private(set) var iterator: AsyncThrowingStream.AsyncIterator 58 | 59 | init(_ iterator: AsyncThrowingStream.AsyncIterator) { 60 | self.iterator = iterator 61 | } 62 | 63 | @inline(__always) 64 | public mutating func next() async throws -> Element? { 65 | return try await iterator.next() 66 | } 67 | } 68 | } 69 | 70 | public extension AsyncWLANScan { 71 | 72 | final class Continuation { 73 | 74 | let continuation: AsyncThrowingStream.Continuation 75 | 76 | let storage: Storage 77 | 78 | fileprivate init( 79 | _ continuation: AsyncThrowingStream.Continuation, 80 | _ storage: Storage 81 | ) { 82 | self.continuation = continuation 83 | self.storage = storage 84 | } 85 | 86 | public var onTermination: (() -> ())? { 87 | get { storage.onTermination } 88 | set { storage.onTermination = newValue } 89 | } 90 | 91 | public func yield(_ value: Element) { 92 | continuation.yield(value) 93 | } 94 | 95 | public func finish(throwing error: Error) { 96 | continuation.finish(throwing: error) 97 | } 98 | } 99 | } 100 | 101 | public extension AsyncWLANScan { 102 | 103 | func first() async throws -> Element? { 104 | for try await element in self { 105 | self.stop() 106 | return element 107 | } 108 | return nil 109 | } 110 | } 111 | 112 | /// Async Stream that will produce values until `stop()` is called or task is cancelled. 113 | internal struct AsyncIndefiniteStream : AsyncSequence { 114 | 115 | let storage: Storage 116 | 117 | public init( 118 | bufferSize: Int = 100, 119 | _ build: @escaping ((Element) -> ()) async throws -> () 120 | ) { 121 | let storage = Storage() 122 | let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in 123 | let task = Task { 124 | do { 125 | try await build({ continuation.yield($0) }) 126 | } 127 | catch _ as CancellationError { } // end 128 | catch { 129 | continuation.finish(throwing: error) 130 | } 131 | } 132 | storage.continuation = continuation 133 | #if swift(>=5.6) 134 | continuation.onTermination = { [weak storage] in 135 | switch $0 { 136 | case .cancelled: 137 | storage?.stop() 138 | default: 139 | break 140 | } 141 | } 142 | #endif 143 | storage.onTermination = { 144 | // cancel task when `stop` is called 145 | task.cancel() 146 | } 147 | } 148 | storage.stream = stream 149 | self.storage = storage 150 | } 151 | 152 | public init( 153 | bufferSize: Int = 100, 154 | _ build: (Continuation) -> () 155 | ) { 156 | let storage = Storage() 157 | let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in 158 | storage.continuation = continuation 159 | #if swift(>=5.6) 160 | continuation.onTermination = { [weak storage] in 161 | switch $0 { 162 | case .cancelled: 163 | storage?.stop() 164 | default: 165 | break 166 | } 167 | } 168 | #endif 169 | build(Continuation(continuation, storage)) 170 | } 171 | storage.stream = stream 172 | self.storage = storage 173 | } 174 | 175 | public func makeAsyncIterator() -> AsyncIterator { 176 | return storage.makeAsyncIterator() 177 | } 178 | 179 | public func stop() { 180 | storage.stop() 181 | } 182 | 183 | public var isExecuting: Bool { 184 | storage.isExecuting 185 | } 186 | } 187 | 188 | extension AsyncIndefiniteStream { 189 | 190 | struct AsyncIterator: AsyncIteratorProtocol { 191 | 192 | private(set) var iterator: AsyncThrowingStream.AsyncIterator 193 | 194 | fileprivate init(_ iterator: AsyncThrowingStream.AsyncIterator) { 195 | self.iterator = iterator 196 | } 197 | 198 | @inline(__always) 199 | public mutating func next() async throws -> Element? { 200 | return try await iterator.next() 201 | } 202 | } 203 | } 204 | 205 | extension AsyncIndefiniteStream { 206 | 207 | final class Continuation { 208 | 209 | let continuation: AsyncThrowingStream.Continuation 210 | 211 | let storage: Storage 212 | 213 | fileprivate init( 214 | _ continuation: AsyncThrowingStream.Continuation, 215 | _ storage: Storage 216 | ) { 217 | self.continuation = continuation 218 | self.storage = storage 219 | } 220 | 221 | public var onTermination: (() -> ())? { 222 | get { storage.onTermination } 223 | set { storage.onTermination = newValue } 224 | } 225 | 226 | public func yield(_ value: Element) { 227 | continuation.yield(value) 228 | } 229 | 230 | public func finish(throwing error: Error) { 231 | continuation.finish(throwing: error) 232 | } 233 | } 234 | } 235 | 236 | internal extension AsyncIndefiniteStream { 237 | 238 | final class Storage { 239 | 240 | var isExecuting: Bool { 241 | get { 242 | lock.lock() 243 | let value = _isExecuting 244 | lock.unlock() 245 | return value 246 | } 247 | } 248 | 249 | private var _isExecuting = true 250 | 251 | let lock = NSLock() 252 | 253 | var stream: AsyncThrowingStream! 254 | 255 | var continuation: AsyncThrowingStream.Continuation! 256 | 257 | var onTermination: (() -> ())? 258 | 259 | deinit { 260 | stop() 261 | } 262 | 263 | init() { } 264 | 265 | func stop() { 266 | // end stream 267 | continuation.finish() 268 | // cleanup 269 | lock.lock() 270 | defer { lock.unlock() } 271 | guard _isExecuting else { return } 272 | _isExecuting = false 273 | // cleanup / stop scanning / cancel child task 274 | onTermination?() 275 | } 276 | 277 | func makeAsyncIterator() -> AsyncIterator { 278 | return AsyncIterator(stream.makeAsyncIterator()) 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /Sources/WLAN/BSSID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BSSID.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(Linux) 10 | import Glibc 11 | #elseif canImport(Darwin) 12 | import Darwin 13 | #endif 14 | 15 | import Foundation 16 | 17 | /// WLAN BSSID. 18 | /// 19 | /// Represents a unique 48-bit identifier that follows MAC address conventions. 20 | /// 21 | /// - Note: The value is always stored in host byte order. 22 | public struct BSSID: ByteValue { 23 | 24 | // MARK: - ByteValueType 25 | 26 | /// Raw BSSID 6 byte (48 bit) value. 27 | public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) 28 | 29 | public static var bitWidth: Int { return 48 } 30 | 31 | // MARK: - Properties 32 | 33 | public var bytes: ByteValue 34 | 35 | // MARK: - Initialization 36 | 37 | public init(bytes: ByteValue = (0, 0, 0, 0, 0, 0)) { 38 | self.bytes = bytes 39 | } 40 | } 41 | 42 | public extension BSSID { 43 | 44 | /// The minimum representable value in this type. 45 | static var min: BSSID { return BSSID(bytes: (.min, .min, .min, .min, .min, .min)) } 46 | 47 | /// The maximum representable value in this type. 48 | static var max: BSSID { return BSSID(bytes: (.max, .max, .max, .max, .max, .max)) } 49 | 50 | static var zero: BSSID { return .min } 51 | } 52 | 53 | // MARK: - Data 54 | 55 | public extension BSSID { 56 | 57 | static var length: Int { return 6 } 58 | 59 | init?(data: Data) { 60 | 61 | guard data.count == BSSID.length 62 | else { return nil } 63 | 64 | self.bytes = (data[0], data[1], data[2], data[3], data[4], data[5]) 65 | } 66 | 67 | var data: Data { 68 | 69 | return Data([bytes.0, bytes.1, bytes.2, bytes.3, bytes.4, bytes.5]) 70 | } 71 | } 72 | 73 | // MARK: - Byte Swap 74 | 75 | extension BSSID: ByteSwap { 76 | 77 | /// A representation of this BSSID with the byte order swapped. 78 | public var byteSwapped: BSSID { 79 | 80 | return BSSID(bytes: (bytes.5, bytes.4, bytes.3, bytes.2, bytes.1, bytes.0)) 81 | } 82 | } 83 | 84 | // MARK: - RawRepresentable 85 | 86 | extension BSSID: RawRepresentable { 87 | 88 | /// Converts a 48 bit ethernet number to its string representation. 89 | public init?(rawValue: String) { 90 | 91 | // verify string 92 | guard rawValue.utf8.count == 17 93 | else { return nil } 94 | 95 | var bytes: ByteValue = (0, 0, 0, 0, 0, 0) 96 | 97 | let components = rawValue.components(separatedBy: ":") 98 | 99 | guard components.count == 6 100 | else { return nil } 101 | 102 | for (index, string) in components.enumerated() { 103 | 104 | guard let byte = UInt8(string, radix: 16) 105 | else { return nil } 106 | 107 | withUnsafeMutablePointer(to: &bytes) { 108 | $0.withMemoryRebound(to: UInt8.self, capacity: 6) { 109 | $0.advanced(by: index).pointee = byte 110 | } 111 | } 112 | } 113 | 114 | self.init(bigEndian: BSSID(bytes: bytes)) 115 | } 116 | 117 | public var rawValue: String { 118 | 119 | let bytes = self.bigEndian.bytes 120 | 121 | return String(format: "%02X:%02X:%02X:%02X:%02X:%02X", bytes.0, bytes.1, bytes.2, bytes.3, bytes.4, bytes.5) 122 | } 123 | } 124 | 125 | // MARK: - Equatable 126 | 127 | extension BSSID: Equatable { 128 | 129 | public static func == (lhs: BSSID, rhs: BSSID) -> Bool { 130 | 131 | return lhs.bytes.0 == rhs.bytes.0 132 | && lhs.bytes.1 == rhs.bytes.1 133 | && lhs.bytes.2 == rhs.bytes.2 134 | && lhs.bytes.3 == rhs.bytes.3 135 | && lhs.bytes.4 == rhs.bytes.4 136 | && lhs.bytes.5 == rhs.bytes.5 137 | } 138 | } 139 | 140 | // MARK: - Hashable 141 | 142 | extension BSSID: Hashable { 143 | 144 | public var hashValue: Int { 145 | 146 | let int64Bytes = (bytes.0, bytes.1, bytes.2, bytes.3, bytes.4, bytes.5, UInt8(0), UInt8(0)) // 2 bytes for padding 147 | 148 | let int64Value = unsafeBitCast(int64Bytes, to: Int64.self) 149 | 150 | return int64Value.hashValue 151 | } 152 | } 153 | 154 | // MARK: - CustomStringConvertible 155 | 156 | extension BSSID: CustomStringConvertible { 157 | 158 | public var description: String { return rawValue } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/WLAN/ByteSwap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ByteSwap.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | /// A Bluetooth value that is stored in the CPU native endianess format. 10 | public protocol ByteSwap { 11 | 12 | /// A representation of this integer with the byte order swapped. 13 | var byteSwapped: Self { get } 14 | } 15 | 16 | public extension ByteSwap { 17 | 18 | /// Creates an instance from its little-endian representation, changing the 19 | /// byte order if necessary. 20 | /// 21 | /// - Parameter value: A value to use as the little-endian representation of 22 | /// the new instance. 23 | init(littleEndian value: Self) { 24 | #if _endian(little) 25 | self = value 26 | #else 27 | self = value.byteSwapped 28 | #endif 29 | } 30 | 31 | /// Creates an instance from its big-endian representation, changing the byte 32 | /// order if necessary. 33 | /// 34 | /// - Parameter value: A value to use as the big-endian representation of the 35 | /// new instance. 36 | init(bigEndian value: Self) { 37 | #if _endian(big) 38 | self = value 39 | #else 40 | self = value.byteSwapped 41 | #endif 42 | } 43 | 44 | /// The little-endian representation of this value. 45 | /// 46 | /// If necessary, the byte order of this value is reversed from the typical 47 | /// byte order of this address. On a little-endian platform, for any 48 | /// address `x`, `x == x.littleEndian`. 49 | var littleEndian: Self { 50 | #if _endian(little) 51 | return self 52 | #else 53 | return byteSwapped 54 | #endif 55 | } 56 | 57 | /// The big-endian representation of this value. 58 | /// 59 | /// If necessary, the byte order of this value is reversed from the typical 60 | /// byte order of this address. On a big-endian platform, for any 61 | /// address `x`, `x == x.bigEndian`. 62 | var bigEndian: Self { 63 | #if _endian(big) 64 | return self 65 | #else 66 | return byteSwapped 67 | #endif 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/WLAN/ByteValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ByteValueType.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | /// Stores a primitive value. 10 | /// 11 | /// Useful for Swift wrappers for primitive byte types. 12 | public protocol ByteValue: Equatable { 13 | 14 | associatedtype ByteValue 15 | 16 | /// Returns the the primitive byte type. 17 | var bytes: ByteValue { get } 18 | 19 | /// Initializes with the primitive the primitive byte type. 20 | init(bytes: ByteValue) 21 | 22 | /// The number of bits used for the underlying binary representation of values of this type. 23 | static var bitWidth: Int { get } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/WLAN/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | public enum WLANError: Error { 10 | 11 | /// Invalid interface specified. 12 | case invalidInterface(WLANInterface) 13 | 14 | /// Invalid network specified. 15 | case invalidNetwork(WLANNetwork) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/WLAN/Interface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WLAN.swift 3 | // PureSwift 4 | // 5 | // Created by Alsey Coleman Miller on 7/3/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | /// Encapsulates an IEEE 802.11 interface. 10 | public struct WLANInterface: Equatable, Hashable, Codable { 11 | 12 | /// The BSD name of the interface. 13 | public let name: String 14 | 15 | public init(name: String) { 16 | self.name = name 17 | } 18 | } 19 | 20 | // MARK: - CustomStringConvertible 21 | 22 | extension WLANInterface: CustomStringConvertible { 23 | 24 | public var description: String { 25 | return name 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/WLAN/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Encapsulates an IEEE 802.11 network. 13 | */ 14 | public struct WLANNetwork: Equatable, Hashable { 15 | 16 | /// The service set identifier (SSID) for the network, returned as data. 17 | /// 18 | /// The SSID is defined as 1-32 octets. 19 | public let ssid: SSID 20 | 21 | /// The basic service set identifier (BSSID) for the network. 22 | public let bssid: BSSID 23 | 24 | public init(ssid: SSID, 25 | bssid: BSSID) { 26 | 27 | self.ssid = ssid 28 | self.bssid = bssid 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/WLAN/SSID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSID.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Service Set Identifier 13 | 14 | An SSID is a unique ID that consists of 1-32 octets and is used for naming wireless networks. When multiple wireless networks overlap in a certain location, SSIDs make sure that data gets sent to the correct destination. 15 | */ 16 | public struct SSID: Equatable, Hashable { 17 | 18 | /// Maximum Length 19 | internal static let length = (min: 1, max: 32) 20 | 21 | // MARK: - Properties 22 | 23 | public let data: Data 24 | 25 | // MARK: - Initialization 26 | 27 | public init?(data: Data) { 28 | 29 | guard data.count <= SSID.length.max, 30 | data.count >= SSID.length.min 31 | else { return nil } 32 | 33 | self.data = data 34 | } 35 | 36 | public init?(string: String) { 37 | 38 | guard let data = string.data(using: .utf8) 39 | else { return nil } 40 | 41 | self.init(data: data) 42 | } 43 | } 44 | 45 | // MARK: - CustomStringConvertible 46 | 47 | extension SSID: CustomStringConvertible { 48 | 49 | public var description: String { 50 | 51 | if let string = String.init(data: self.data, encoding: .utf8) { 52 | 53 | return string 54 | 55 | } else { 56 | 57 | return self.data.description 58 | } 59 | } 60 | } 61 | 62 | // MARK: - ExpressibleByStringLiteral 63 | 64 | extension SSID: ExpressibleByStringLiteral { 65 | 66 | public init(stringLiteral string: String) { 67 | 68 | if string.isEmpty { 69 | self.data = Data(" ".utf8) 70 | } else { 71 | // truncate if neccesary 72 | self.data = Data(string.utf8.prefix(SSID.length.max)) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/WLAN/ScanRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanRecord.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct WLANScanRecord { 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/WLAN/WLANManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WLANProtocol.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/4/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | WLAN Manager 13 | 14 | A wrapper around the entire Wi-Fi subsystem that you use to access interfaces. 15 | */ 16 | public protocol WLANManager: AnyObject { 17 | 18 | /// Returns the default Wi-Fi interface. 19 | var interface: WLANInterface? { get async } 20 | 21 | /** 22 | Returns all available Wi-Fi interfaces. 23 | 24 | - Returns: An array of `WLANInterface`, representing all of the available Wi-Fi interfaces in the system. 25 | */ 26 | var interfaces: [WLANInterface] { get async } 27 | 28 | /** 29 | Scans for networks. 30 | 31 | If ssid parameter is present, a directed scan will be performed by the interface, otherwise a broadcast scan will be performed. This method will block for the duration of the scan. 32 | 33 | - Parameter interface: The network interface. 34 | */ 35 | func scan(with interface: WLANInterface) async throws -> AsyncWLANScan 36 | 37 | /** 38 | Sets the interface power state. 39 | 40 | - Parameter power: A Boolean value corresponding to the power state. NO indicates the "OFF" state. 41 | - Parameter interface: The network interface. 42 | */ 43 | func setPower(_ power: Bool, for interface: WLANInterface) async throws 44 | 45 | /** 46 | Disassociates from the current network. 47 | 48 | This method has no effect if the interface is not associated to a network. 49 | */ 50 | func disassociate(interface: WLANInterface) async throws 51 | } 52 | -------------------------------------------------------------------------------- /Sources/wirelesstool/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // wirelesstool 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CommandError: Error { 11 | 12 | case noInterface 13 | } 14 | -------------------------------------------------------------------------------- /Sources/wirelesstool/WLANTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // WLAN 4 | // 5 | // Created by Alsey Coleman Miller on 7/5/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | #if os(Linux) 10 | import Glibc 11 | #elseif os(macOS) 12 | import Darwin.C 13 | #endif 14 | 15 | import Foundation 16 | import WLAN 17 | 18 | #if os(macOS) 19 | import DarwinWLAN 20 | #elseif os(Linux) 21 | import LinuxWLAN 22 | #endif 23 | 24 | @main 25 | struct WLANTool { 26 | 27 | static func main() async throws { 28 | 29 | #if os(macOS) 30 | let manager = DarwinWLANManager() 31 | #elseif os(Linux) 32 | let manager = try await LinuxWLANManager() 33 | #endif 34 | 35 | guard let interface = await manager.interface 36 | else { throw CommandError.noInterface } 37 | 38 | print("Interface: \(interface)") 39 | let stream = try await manager.scan(with: interface) 40 | Task { 41 | try await Task.sleep(nanoseconds: 30 * 1_000_000_000) 42 | stream.stop() 43 | } 44 | print("Networks:") 45 | var counter = 0 46 | for try await network in stream { 47 | counter += 1 48 | print("\(counter). \(network.ssid)") 49 | if network.bssid != .zero { 50 | print(" " + network.bssid.rawValue) 51 | } 52 | } 53 | print("Found \(counter) networks") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/WLANTests/WLANTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WLANTests.swift 3 | // PureSwift 4 | // 5 | // Created by Alsey Coleman Miller on 7/3/18. 6 | // Copyright © 2018 PureSwift. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import WLAN 12 | 13 | final class WLANTests: XCTestCase { 14 | 15 | func testSSID() { 16 | 17 | XCTAssertNil(SSID(string: ""), "SSID must be 1-32 octets") 18 | XCTAssertNil(SSID(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"), "SSID must be 1-32 octets") 19 | 20 | XCTAssertEqual(SSID(string: "ColemanCDA")?.description, "ColemanCDA") 21 | XCTAssertEqual(SSID(string: "ColemanCDA"), "ColemanCDA") 22 | } 23 | 24 | func testBSSID() { 25 | 26 | XCTAssertNil(BSSID(rawValue: "")) 27 | XCTAssertNil(BSSID(rawValue: "D8C77141C1DB")) 28 | XCTAssertNil(BSSID(rawValue: "D8:C7:71:41:C1:DB:")) 29 | 30 | XCTAssertEqual(BSSID(rawValue: "D8:C7:71:41:C1:DB")?.description, "D8:C7:71:41:C1:DB") 31 | XCTAssertEqual(BSSID(rawValue: "18:A6:F7:99:81:90")?.description, "18:A6:F7:99:81:90") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ci_scripts/ci_post_clone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | swift package generate-xcodeproj 3 | --------------------------------------------------------------------------------