#! /bin/bash
. /usr/lib/netctl/globals
. "$SUBR_DIR/interface"
. "$SUBR_DIR/rfkill"
. "$SUBR_DIR/wpa"
usage() {
cat << END
Usage: wifi-menu [-h | --help] [-o | --obscure] [INTERFACE]
Interactively connect to a wireless network on INTERFACE using netctl.
If only one wireless interface is available, INTERFACE can be omitted.
Arguments:
-h, --help Show this help
-o, --obscure Show asterisks for the characters of the password
and store the password as a hexadecimal string
END
}
# Undo printf escaping in $1
printf_decode() {
printf -- "${1//%/%%}"
}
# Prepare $1 for use in a special quoting context
quote_safe() {
if [[ "$1" = \"* ]]; then
printf '""%s"' "$1"
else
printf "%s" "$1"
fi
}
# Fill PROFILES and ESSIDS with the profile names and essids and fill GENERATED
# with the names of automatically generated profiles for interface $1
init_profiles() {
local i=0 essid profile
while IFS= read -r profile; do
# Sandbox the sourcing of profiles
essid=$(
source "$PROFILE_DIR/$profile" > /dev/null
if [[ "$Interface" = "$1" && -n "$ESSID" ]]; then
if [[ "$ESSID" = \"\"*\" ]]; then
ESSID=${ESSID:2:-1}
fi
printf "%s" "$ESSID"
if [[ "$Description" =~ "Automatically generated" ]]; then
return 2
else
return 1
fi
fi
return 0
)
case $? in
2)
GENERATED+=("$profile")
;&
1)
PROFILES[i]=$profile
ESSIDS[i]=$essid
(( ++i ))
;;
esac
done < <(list_profiles)
}
# Build ENTRIES as an argument list for dialog based on scan results in $1
init_entries() {
local i=0 sep=$'\t' decoded flags signal ssid
while IFS=$'\t' read -r signal flags ssid; do
decoded=$(printf_decode "$ssid")
ENTRIES[i++]="--" # the SSID might look like an option to dialog
if [[ "$CHARMAP" = "UTF-8" ]]; then
ENTRIES[i++]=$decoded
else
ENTRIES[i++]=$ssid
fi
if is_yes "${CONNECTED:-no}" && [[ "$decoded" = "$CONNECTION" ]]; then
ENTRIES[i]="*" # Currently connected
elif in_array "$decoded" "${ESSIDS[@]}"; then
if in_array "$(ssid_to_profile "$decoded")" "${GENERATED[@]}"; then
ENTRIES[i]="." # Automatically generated
else
ENTRIES[i]=":" # Handmade
fi
else
ENTRIES[i]=" " # Not present
fi
if [[ "$flags" =~ WPA2|WPA|WEP ]]; then
ENTRIES[i]+="${sep}${BASH_REMATCH[0],,}"
else
ENTRIES[i]+="${sep}none"
fi
ENTRIES[i]+=" ${sep}${signal}"
(( ++i ))
done < "$1"
}
# Find a profile name for ssid $1
ssid_to_profile() {
local i
for i in $(seq 0 $((${#ESSIDS[@]}-1))); do
if [[ "$1" = "${ESSIDS[i]}" ]]; then
printf "%s" "${PROFILES[i]}"
return 0
fi
done
return 1
}
# Ask the user for the name of the new profile
confirm_profile() {
local msg="Enter a name for the new profile\n"
PROFILE=$(dialog --inputbox "$msg" 10 50 "$PROFILE" --stdout) || return $?
if [[ "$PROFILE" = */* ]]; then
PROFILE=${PROFILE//\//_}
confirm_profile
elif [[ -e "$PROFILE_DIR/$PROFILE" ]]; then
msg="A profile by the name '$PROFILE' already exists.
Do you want to overwrite it?"
dialog --yesno "$msg" 10 50 --stdout || confirm_profile
fi
}
# Create a profile for ssid $1
create_profile() {
local box flags key msg security signal ssid
PROFILE=$(iconv -c -f UTF-8 -t //TRANSLIT <<< "$1")
PROFILE="$INTERFACE-${PROFILE//[?\/]/_}"
[[ -e "$PROFILE_DIR/$PROFILE" ]] && PROFILE+=".wifi-menu"
confirm_profile || return $?
while IFS=$'\t' read -r signal flags ssid; do
[[ "$(printf_decode "$ssid")" != "$1" ]] || break
done < "$NETWORKS"
if [[ "$flags" =~ WPA|WEP ]]; then
security=${BASH_REMATCH[0],,}
else
security=none
fi
if [[ "$flags" =~ PSK|WEP ]]; then
if is_yes "${OBSCURE:-no}"; then
box="--insecure --passwordbox"
else
box="--inputbox"
fi
msg="Enter $security security key for\n'$1'"
key=$(dialog $box "$msg" 10 40 --stdout) || return $?
if [[ "${BASH_REMATCH[0]}" = "WEP" ]]; then
if [[ "$key" = +([[:xdigit:]][[:xdigit:]]) ]]; then
key="\"$key"
else
key=$(quote_safe "$key")
fi
elif [[ ${#key} -ge 8 && ${#key} -le 63 ]]; then
if is_yes "${OBSCURE:-no}"; then
key="\"$(wpa_passphrase "$1" "$key" | sed -n "s/^[[:space:]]*psk=//p")"
else
key=$(quote_safe "$key")
fi
elif [[ ${#key} -eq 64 && "$key" = +([[:xdigit:]]) ]]; then
key="\"$key"
else
return 4
fi
fi
cat << EOF > "$PROFILE_DIR/$PROFILE"
Description='Automatically generated profile by wifi-menu'
Interface=$INTERFACE
Connection=wireless
Security=$security
ESSID=$(printf "%q" "$(quote_safe "$1")")
IP=dhcp
${key+Key=$(printf "%q" "$key")}
EOF
printf "%s" "$PROFILE"
return 0
}
# Connect to ssid $1 using an available profile or an automatically created one
# if none exists
connect_to_ssid() {
local msg
PROFILE=$(ssid_to_profile "$1")
if [[ $? -ne 0 ]]; then
PROFILE=$(create_profile "$1") || return $?
NEW_PROFILE=yes
fi
clear
if systemctl is-active --quiet "netctl-auto@$INTERFACE.service"; then
report_notice "Interface '$INTERFACE' is controlled by netctl-auto"
if is_yes "${NEW_PROFILE:-no}"; then
do_debug systemctl restart "netctl-auto@$INTERFACE.service"
fi
do_debug netctl-auto switch-to "$PROFILE"
elif ! netctl switch-to "$PROFILE"; then
if is_yes "${NEW_PROFILE:-no}"; then
msg=" CONNECTING FAILED
Do you want to keep the generated profile ('$PROFILE')?"
dialog --yesno "$msg" 10 40 --stdout || rm "$PROFILE_DIR/$PROFILE"
clear
fi
return 2
fi
return 0
}
while [[ "$1" = -* ]]; do
case "$1" in
-h|--help)
usage
exit
;;
-o|--obscure)
OBSCURE=yes
shift
;;
-*)
report_error "Invalid option: $1"
usage
exit 255
;;
esac
done
if [[ $# -gt 1 ]]; then
report_error "Too many arguments"
usage
exit 255
fi
ensure_root "$(basename "$0")"
if ! type dialog &> /dev/null; then
exit_error "Please install 'dialog' to use wifi-menu"
fi
CHARMAP=$(locale charmap)
cd / # We do not want to spawn anything that can block unmounting
INTERFACE=$1
if [[ -z "$INTERFACE" ]]; then
INTERFACE=(/sys/class/net/*/wireless/)
if [[ ${#INTERFACE[@]} -ne 1 || ! -d "$INTERFACE" ]]; then
report_error "Invalid interface specification"
usage
exit 255
fi
INTERFACE=${INTERFACE:15:-10}
report_debug "Using interface '$INTERFACE'"
elif ! is_interface "$INTERFACE"; then
exit_error "No such interface: $INTERFACE"
fi
load_interface_config "$INTERFACE"
if [[ "$RFKill" && "$(rf_status "$INTERFACE" "$RFKill")" ]]; then
if ! rf_enable "$INTERFACE" "$RFKill"; then
exit_error "Could not unblock transmission on interface '$INTERFACE'"
fi
RF_UNBLOCKED=yes
fi
echo -n "Scanning for networks... "
if CONNECTION=$(wpa_call "$INTERFACE" status 2> /dev/null | grep -m 1 "^ssid="); then
CONNECTION=$(printf_decode "${CONNECTION#ssid=}")
CONNECTED=yes
fi
NETWORKS=$(wpa_supplicant_scan "$INTERFACE" 3,4,5)
RETURN=$?
if is_yes "${RF_UNBLOCKED:-no}"; then
rf_disable "$INTERFACE" "$RFKill"
fi
if (( RETURN == 0 )); then
trap 'rm -f "$NETWORKS"' EXIT
echo "done"
init_profiles "$INTERFACE"
init_entries "$NETWORKS"
MSG="Select the network you wish to use
Flags description:
* - active connection present
: - handmade profile present
. - automatically generated profile present"
CHOICE=$(dialog --menu "$MSG" 24 50 12 "${ENTRIES[@]}" --stdout)
RETURN=$?
if (( RETURN == 0 )); then
if [[ "$CHARMAP" != "UTF-8" ]]; then
CHOICE=$(printf_decode "$CHOICE")
fi
connect_to_ssid "$CHOICE"
RETURN=$?
fi
else
echo "failed"
RETURN=3
fi
case $RETURN in
0|2) # Connected | Connecting failed
;;
1) # Canceled
clear
;;
3) # No networks found
report_error "No networks found"
;;
4) # Invalid passphrase length (WEP keys have tighter restrictions)
clear
report_error "Passphrase must be 8..63 characters"
;;
255) # ESC or error
clear
report_error "Aborted"
;;
*) # Should not happen
report_error "Unexpected return code from dialog: $RETURN"
RETURN=7
;;
esac
exit $RETURN
# vim: ft=sh ts=4 et sw=4:
. /usr/lib/netctl/globals
. "$SUBR_DIR/interface"
. "$SUBR_DIR/rfkill"
. "$SUBR_DIR/wpa"
usage() {
cat << END
Usage: wifi-menu [-h | --help] [-o | --obscure] [INTERFACE]
Interactively connect to a wireless network on INTERFACE using netctl.
If only one wireless interface is available, INTERFACE can be omitted.
Arguments:
-h, --help Show this help
-o, --obscure Show asterisks for the characters of the password
and store the password as a hexadecimal string
END
}
# Undo printf escaping in $1
printf_decode() {
printf -- "${1//%/%%}"
}
# Prepare $1 for use in a special quoting context
quote_safe() {
if [[ "$1" = \"* ]]; then
printf '""%s"' "$1"
else
printf "%s" "$1"
fi
}
# Fill PROFILES and ESSIDS with the profile names and essids and fill GENERATED
# with the names of automatically generated profiles for interface $1
init_profiles() {
local i=0 essid profile
while IFS= read -r profile; do
# Sandbox the sourcing of profiles
essid=$(
source "$PROFILE_DIR/$profile" > /dev/null
if [[ "$Interface" = "$1" && -n "$ESSID" ]]; then
if [[ "$ESSID" = \"\"*\" ]]; then
ESSID=${ESSID:2:-1}
fi
printf "%s" "$ESSID"
if [[ "$Description" =~ "Automatically generated" ]]; then
return 2
else
return 1
fi
fi
return 0
)
case $? in
2)
GENERATED+=("$profile")
;&
1)
PROFILES[i]=$profile
ESSIDS[i]=$essid
(( ++i ))
;;
esac
done < <(list_profiles)
}
# Build ENTRIES as an argument list for dialog based on scan results in $1
init_entries() {
local i=0 sep=$'\t' decoded flags signal ssid
while IFS=$'\t' read -r signal flags ssid; do
decoded=$(printf_decode "$ssid")
ENTRIES[i++]="--" # the SSID might look like an option to dialog
if [[ "$CHARMAP" = "UTF-8" ]]; then
ENTRIES[i++]=$decoded
else
ENTRIES[i++]=$ssid
fi
if is_yes "${CONNECTED:-no}" && [[ "$decoded" = "$CONNECTION" ]]; then
ENTRIES[i]="*" # Currently connected
elif in_array "$decoded" "${ESSIDS[@]}"; then
if in_array "$(ssid_to_profile "$decoded")" "${GENERATED[@]}"; then
ENTRIES[i]="." # Automatically generated
else
ENTRIES[i]=":" # Handmade
fi
else
ENTRIES[i]=" " # Not present
fi
if [[ "$flags" =~ WPA2|WPA|WEP ]]; then
ENTRIES[i]+="${sep}${BASH_REMATCH[0],,}"
else
ENTRIES[i]+="${sep}none"
fi
ENTRIES[i]+=" ${sep}${signal}"
(( ++i ))
done < "$1"
}
# Find a profile name for ssid $1
ssid_to_profile() {
local i
for i in $(seq 0 $((${#ESSIDS[@]}-1))); do
if [[ "$1" = "${ESSIDS[i]}" ]]; then
printf "%s" "${PROFILES[i]}"
return 0
fi
done
return 1
}
# Ask the user for the name of the new profile
confirm_profile() {
local msg="Enter a name for the new profile\n"
PROFILE=$(dialog --inputbox "$msg" 10 50 "$PROFILE" --stdout) || return $?
if [[ "$PROFILE" = */* ]]; then
PROFILE=${PROFILE//\//_}
confirm_profile
elif [[ -e "$PROFILE_DIR/$PROFILE" ]]; then
msg="A profile by the name '$PROFILE' already exists.
Do you want to overwrite it?"
dialog --yesno "$msg" 10 50 --stdout || confirm_profile
fi
}
# Create a profile for ssid $1
create_profile() {
local box flags key msg security signal ssid
PROFILE=$(iconv -c -f UTF-8 -t //TRANSLIT <<< "$1")
PROFILE="$INTERFACE-${PROFILE//[?\/]/_}"
[[ -e "$PROFILE_DIR/$PROFILE" ]] && PROFILE+=".wifi-menu"
confirm_profile || return $?
while IFS=$'\t' read -r signal flags ssid; do
[[ "$(printf_decode "$ssid")" != "$1" ]] || break
done < "$NETWORKS"
if [[ "$flags" =~ WPA|WEP ]]; then
security=${BASH_REMATCH[0],,}
else
security=none
fi
if [[ "$flags" =~ PSK|WEP ]]; then
if is_yes "${OBSCURE:-no}"; then
box="--insecure --passwordbox"
else
box="--inputbox"
fi
msg="Enter $security security key for\n'$1'"
key=$(dialog $box "$msg" 10 40 --stdout) || return $?
if [[ "${BASH_REMATCH[0]}" = "WEP" ]]; then
if [[ "$key" = +([[:xdigit:]][[:xdigit:]]) ]]; then
key="\"$key"
else
key=$(quote_safe "$key")
fi
elif [[ ${#key} -ge 8 && ${#key} -le 63 ]]; then
if is_yes "${OBSCURE:-no}"; then
key="\"$(wpa_passphrase "$1" "$key" | sed -n "s/^[[:space:]]*psk=//p")"
else
key=$(quote_safe "$key")
fi
elif [[ ${#key} -eq 64 && "$key" = +([[:xdigit:]]) ]]; then
key="\"$key"
else
return 4
fi
fi
cat << EOF > "$PROFILE_DIR/$PROFILE"
Description='Automatically generated profile by wifi-menu'
Interface=$INTERFACE
Connection=wireless
Security=$security
ESSID=$(printf "%q" "$(quote_safe "$1")")
IP=dhcp
${key+Key=$(printf "%q" "$key")}
EOF
printf "%s" "$PROFILE"
return 0
}
# Connect to ssid $1 using an available profile or an automatically created one
# if none exists
connect_to_ssid() {
local msg
PROFILE=$(ssid_to_profile "$1")
if [[ $? -ne 0 ]]; then
PROFILE=$(create_profile "$1") || return $?
NEW_PROFILE=yes
fi
clear
if systemctl is-active --quiet "netctl-auto@$INTERFACE.service"; then
report_notice "Interface '$INTERFACE' is controlled by netctl-auto"
if is_yes "${NEW_PROFILE:-no}"; then
do_debug systemctl restart "netctl-auto@$INTERFACE.service"
fi
do_debug netctl-auto switch-to "$PROFILE"
elif ! netctl switch-to "$PROFILE"; then
if is_yes "${NEW_PROFILE:-no}"; then
msg=" CONNECTING FAILED
Do you want to keep the generated profile ('$PROFILE')?"
dialog --yesno "$msg" 10 40 --stdout || rm "$PROFILE_DIR/$PROFILE"
clear
fi
return 2
fi
return 0
}
while [[ "$1" = -* ]]; do
case "$1" in
-h|--help)
usage
exit
;;
-o|--obscure)
OBSCURE=yes
shift
;;
-*)
report_error "Invalid option: $1"
usage
exit 255
;;
esac
done
if [[ $# -gt 1 ]]; then
report_error "Too many arguments"
usage
exit 255
fi
ensure_root "$(basename "$0")"
if ! type dialog &> /dev/null; then
exit_error "Please install 'dialog' to use wifi-menu"
fi
CHARMAP=$(locale charmap)
cd / # We do not want to spawn anything that can block unmounting
INTERFACE=$1
if [[ -z "$INTERFACE" ]]; then
INTERFACE=(/sys/class/net/*/wireless/)
if [[ ${#INTERFACE[@]} -ne 1 || ! -d "$INTERFACE" ]]; then
report_error "Invalid interface specification"
usage
exit 255
fi
INTERFACE=${INTERFACE:15:-10}
report_debug "Using interface '$INTERFACE'"
elif ! is_interface "$INTERFACE"; then
exit_error "No such interface: $INTERFACE"
fi
load_interface_config "$INTERFACE"
if [[ "$RFKill" && "$(rf_status "$INTERFACE" "$RFKill")" ]]; then
if ! rf_enable "$INTERFACE" "$RFKill"; then
exit_error "Could not unblock transmission on interface '$INTERFACE'"
fi
RF_UNBLOCKED=yes
fi
echo -n "Scanning for networks... "
if CONNECTION=$(wpa_call "$INTERFACE" status 2> /dev/null | grep -m 1 "^ssid="); then
CONNECTION=$(printf_decode "${CONNECTION#ssid=}")
CONNECTED=yes
fi
NETWORKS=$(wpa_supplicant_scan "$INTERFACE" 3,4,5)
RETURN=$?
if is_yes "${RF_UNBLOCKED:-no}"; then
rf_disable "$INTERFACE" "$RFKill"
fi
if (( RETURN == 0 )); then
trap 'rm -f "$NETWORKS"' EXIT
echo "done"
init_profiles "$INTERFACE"
init_entries "$NETWORKS"
MSG="Select the network you wish to use
Flags description:
* - active connection present
: - handmade profile present
. - automatically generated profile present"
CHOICE=$(dialog --menu "$MSG" 24 50 12 "${ENTRIES[@]}" --stdout)
RETURN=$?
if (( RETURN == 0 )); then
if [[ "$CHARMAP" != "UTF-8" ]]; then
CHOICE=$(printf_decode "$CHOICE")
fi
connect_to_ssid "$CHOICE"
RETURN=$?
fi
else
echo "failed"
RETURN=3
fi
case $RETURN in
0|2) # Connected | Connecting failed
;;
1) # Canceled
clear
;;
3) # No networks found
report_error "No networks found"
;;
4) # Invalid passphrase length (WEP keys have tighter restrictions)
clear
report_error "Passphrase must be 8..63 characters"
;;
255) # ESC or error
clear
report_error "Aborted"
;;
*) # Should not happen
report_error "Unexpected return code from dialog: $RETURN"
RETURN=7
;;
esac
exit $RETURN
# vim: ft=sh ts=4 et sw=4: