#!/usr/bin/env bash # connect to iot services THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) DFS_SKIP_ARG_PARSE=1 source "$THIS_DIR/../tools/common.sh" unset DFS_SKIP_ARG_PARSE RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}} RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0} EXTRA_SSH_OPTIONS=() GPG_FORWARD=0 # config RIOT_CONFIG_FILES=( "$DOTFILES/riot-config.sh" "$HOME/.config/riot-config.sh" "riot-config.sh" ) load_riot_config() { local file for file in "${RIOT_CONFIG_FILES[@]}"; do if [[ -f "$file" ]]; then source "$file" fi done } # check if port number valid check_port() { [[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1 [[ $1 -lt 65536 && $1 -gt 0 ]] || return 1 return 0 } # check if username valid check_username() { [[ "$1" =~ ^[a-z][-a-z0-9_]*$ ]] || return 1 return 0 } # get single server setting # may be called more than once get_server_meta() { # returns: RET_HOSTNAME="" RET_TRUST_SERVER=0 RET_PORT="" # optional RET_USERNAME="" # optional RET_JUMP_SERVER="" # optional # body local remote="$1" # extract username from user@... if [[ "$remote" == *@* ]]; then RET_USERNAME=${remote%%@*} remote=${remote#*@} check_username $RET_USERNAME || fmt_warning \"$RET_USERNAME\" is not a valid unix username fi # extract port from ...:port if [[ "$remote" =~ ^[^:]+:[1-9][0-9]*$ # contains only one colon || "$remote" =~ ^\[.+\]:[1-9][0-9]*$ # in the form of [host]:port || "$remote" =~ :::[1-9][0-9]*$ # in the form of :::port || "$remote" =~ ^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}:[1-9][0-9]*$ # full ipv6 address with port || "$remote" =~ ^[0-9A-Fa-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port ]]; then RET_PORT=${remote##*:} remote=${remote%:*} check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\" fi # remove square brackets if [[ "$remote" =~ ^\[.*\]$ ]]; then remote=${remote:1:-1} fi # presets -- match remote local remote_func="$remote.remote" if is_function "$remote_func"; then "$remote_func" fi # presets -- match domain RET_HOSTNAME=${remote} local domain=${remote##*.} local host=${remote%.*} # if it contains no dot and is not ipv6 if [[ "$remote" != *.* && "$remote" != *:* ]]; then domain="default" fi local domain_func="$domain.domain" if is_function "$domain_func"; then "$domain_func" elif is_function ".domain"; then ".domain" fi } parse_remote() { # remote setting, including jump servers # called for every remote # provides: SERVER="" TRUST_SERVER=1 PORT="" # optional USERNAME="" # optional SSH_OPTIONS=("-o" "ServerAliveInterval=60") if [[ -t 1 ]]; then SSH_OPTIONS+=("-o" "RequestTTY=yes") fi if [[ "$RIOT_TRUST_CLIENT" == "1" ]]; then SSH_OPTIONS+=("-o" "PermitLocalCommand=yes") if [[ "$(get_os_type)" != "msys" ]]; then test "$DFS_DRY_RUN" = "1" || mkdir -p ~/.ssh/master-socket SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPersist=5s" "-o" "ControlPath=~/.ssh/master-socket/%C") fi fi # handle input local remote="$1" local jump_servers="" # loop for jump servers while [[ -n $remote ]]; do local server=${remote%%/*} remote=${remote#*/} get_server_meta "$server" if [[ -n "$RET_JUMP_SERVER" ]]; then jump_servers="$jump_servers${jump_servers:+,}$RET_JUMP_SERVER" fi # only if all servers are trusted TRUST_SERVER=$((TRUST_SERVER*RET_TRUST_SERVER)) if [[ "$server" == "$remote" || -z "$remote" ]]; then SERVER="$RET_HOSTNAME" PORT="$RET_PORT" USERNAME="$RET_USERNAME" remote="" else jump_servers="$jump_servers${jump_servers:+,}$RET_USERNAME${RET_USERNAME:+@}$RET_HOSTNAME${RET_PORT:+:}$RET_PORT" fi done # construct cmd if [[ "$RIOT_TRUST_SERVER" == "1" || "$TRUST_SERVER" == "1" ]]; then SSH_OPTIONS+=("-o" "ForwardX11=yes" "-o" "ForwardAgent=yes") fi if [[ -n "$jump_servers" ]]; then SSH_OPTIONS+=("-o" "ProxyJump=$jump_servers") fi } check_local_gpg_agent() { LOCAL_GPG_EXTRA_SOCKET="" GPG_FORWARD_ERROR="" if ! command -v gpgconf > /dev/null 2>&1 || ! command -v gpg-connect-agent > /dev/null 2>&1; then GPG_FORWARD_ERROR="gpgconf or gpg-connect-agent is not available" return 1 fi local agent_info agent_info=$(gpg-connect-agent --no-autostart 'GETINFO pid' /bye 2>/dev/null || true) if ! grep -qE '^D [1-9][0-9]*$' <<< "$agent_info"; then GPG_FORWARD_ERROR="local gpg-agent is not running" return 1 fi LOCAL_GPG_EXTRA_SOCKET=$(gpgconf --list-dirs agent-extra-socket 2>/dev/null || true) if [[ -z "$LOCAL_GPG_EXTRA_SOCKET" || "$LOCAL_GPG_EXTRA_SOCKET" != /* \ || "$LOCAL_GPG_EXTRA_SOCKET" == *:* || "$LOCAL_GPG_EXTRA_SOCKET" == *$'\r'* \ || "$LOCAL_GPG_EXTRA_SOCKET" == *$'\n'* || ! -S "$LOCAL_GPG_EXTRA_SOCKET" ]]; then GPG_FORWARD_ERROR="local gpg-agent extra socket is unavailable" return 1 fi local extra_info extra_info=$(gpg-connect-agent --raw-socket "$LOCAL_GPG_EXTRA_SOCKET" 'GETINFO version' /bye 2>/dev/null || true) if ! grep -qE '^D [^[:space:]]+' <<< "$extra_info"; then GPG_FORWARD_ERROR="local gpg-agent extra socket exists but is not accepting connections" return 1 fi } probe_remote_gpg_socket() { REMOTE_GPG_SOCKET="" # The probe must be an independent connection: reusing or creating a # multiplex master here races with the immediately following login. local query_cmd=(ssh "-S" "none" "-o" "ClearAllForwardings=yes") if [[ -n "$PORT" ]]; then query_cmd+=("-p" "$PORT") fi query_cmd+=( "${SSH_OPTIONS[@]}" "${EXTRA_SSH_OPTIONS[@]}" "-T" "$USERNAME${USERNAME:+@}$SERVER" 'socket=$(gpgconf --list-dirs agent-socket 2>/dev/null) || exit 10 case "$socket" in /*/S.gpg-agent) ;; *) exit 11;; esac case "$socket" in *:*) exit 11;; esac case "$socket" in *" "*) exit 11;; esac carriage_return=$(printf "\r") case "$socket" in *"$carriage_return"*) exit 11;; esac if [ -e "$socket" ] && [ ! -S "$socket" ]; then exit 12; fi if [ -S "$socket" ] && command -v gpg-connect-agent >/dev/null 2>&1; then agent_mode=$(gpg-connect-agent --raw-socket "$socket" "GETINFO restricted" /bye 2>/dev/null || true) case "$agent_mode" in *"D 1"*) exit 13;; esac fi systemd_socket=0 if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active --quiet gpg-agent.socket >/dev/null 2>&1; then systemd_socket=1 fi gpgconf --kill gpg-agent >/dev/null 2>&1 || exit 14 rm -f "$socket" || exit 15 printf "%s\n%s\n" "$socket" "$systemd_socket"' ) local output status if output=$("${query_cmd[@]}"); then status=0 else status=$? fi case "$status" in 0) ;; 10) GPG_FORWARD_ERROR="gpgconf is unavailable on the remote host" ;; 11) GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket" ;; 12) GPG_FORWARD_ERROR="refusing to remove the non-socket remote gpg-agent path" ;; 13) GPG_FORWARD_ERROR="another forwarded gpg-agent is already using the remote socket; close that session first" ;; 14) GPG_FORWARD_ERROR="failed to stop the remote gpg-agent (the socket may belong to another forwarding session)" ;; 15) GPG_FORWARD_ERROR="failed to remove the stale remote gpg-agent socket" ;; *) GPG_FORWARD_ERROR="failed to query or clean the remote gpg-agent socket (ssh status $status)" ;; esac if [[ "$status" != "0" ]]; then return 1 fi local remote_systemd_socket REMOTE_GPG_SOCKET=${output%%$'\n'*} remote_systemd_socket=${output#*$'\n'} if [[ -z "$REMOTE_GPG_SOCKET" || "$REMOTE_GPG_SOCKET" == *$'\r'* || "$REMOTE_GPG_SOCKET" == *$'\n'* \ || "$REMOTE_GPG_SOCKET" != /*/S.gpg-agent || "$REMOTE_GPG_SOCKET" == *:* \ || "$remote_systemd_socket" != "0" && "$remote_systemd_socket" != "1" ]]; then GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket: $output" return 1 fi if [[ "$remote_systemd_socket" == "1" ]]; then fmt_warning "remote gpg-agent.socket is active and may race with GPG forwarding; consider disabling its socket activation" fi } prepare_gpg_forwarding() { if [[ "$DFS_DRY_RUN" == "1" ]]; then REMOTE_GPG_SOCKET="" LOCAL_GPG_EXTRA_SOCKET="" else if ! check_local_gpg_agent; then fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR" fi if ! probe_remote_gpg_socket; then fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR" fi fi SSH_OPTIONS+=( # The probe already removed the old socket. Do not let the main SSH # connection unlink a path recreated during the gap between them. "-o" "StreamLocalBindUnlink=no" "-o" "ExitOnForwardFailure=yes" "-R" "$REMOTE_GPG_SOCKET:$LOCAL_GPG_EXTRA_SOCKET" ) } print_cmd() { local output="" for s in "${CMD[@]}"; do if [[ "$s" =~ [\ \\\'\"] ]]; then # needs to be escaped s="${s@Q}" fi output+="$s " done fmt_note "--> ${output% }" } eval_or_echo() { local DO="" tmux_win=${tmux_win:-0} if [[ "$DFS_DRY_RUN" == "1" ]]; then DO=echo fi if [[ "$USE_TMUX" == "1" ]]; then if [[ -z "$TMUX_SESS" ]]; then TMUX_SESS=riot-$(date +%s) $DO tmux new-session -d -s $TMUX_SESS bash -l else tmux_win=$((tmux_win+1)) $DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l fi local command printf -v command '%q ' "${CMD[@]}" $DO tmux send-keys -l -t "$TMUX_SESS:$tmux_win" "${command% }" $DO tmux send-keys -t "$TMUX_SESS:$tmux_win" Enter else $DO "${CMD[@]}" fi } # ssh series prepare_ssh_cmd() { local ssh_bin="${1:-ssh}" if [[ "$ssh_bin" == "scp" || "$ssh_bin" == "sftp" ]]; then local port_param='-P' else local port_param='-p' fi CMD=( "$ssh_bin" "${PORT:+$port_param}" "$PORT" "${SSH_OPTIONS[@]}" "${EXTRA_SSH_OPTIONS[@]}" "$SCP_SRC" "$USERNAME${USERNAME:+@}$SERVER" "$SCP_DST" "${@:2}" ) for i in ${!CMD[@]}; do if [[ -z "${CMD[i]}" ]]; then unset CMD[i]; fi; done } # ssh run_ssh() { prepare_ssh_cmd "$@" print_cmd eval_or_echo } # sshl run_sshl() { local arg left right localsock access local res="${1//[^:]}" local lorr="-L" if [[ "${FUNCNAME[1]}" == "run_sshr" ]]; then lorr="-R" fi if [[ ${#res} -eq 2 ]]; then arg="$1" elif [[ ${#res} -eq 0 ]]; then if [[ "$lorr" == "-R" ]]; then arg="$1" else arg="$(get_free_port):localhost:$1" fi else left=${1%%:*} right=${1##*:} if check_port "$left"; then arg="$1" elif check_port "$right"; then arg="$(get_free_port):$1" else arg="$1" localsock=1 fi fi if [[ "$localsock" == "1" ]]; then access="unix://${arg%%:*}" else access="localhost:${arg%%:*}" fi SSH_OPTIONS+=("-NC" "$lorr" "$arg") prepare_ssh_cmd ssh print_cmd fmt_note " > please access $access" eval_or_echo } # sshr run_sshr() { run_sshl "$1" } # sshd run_sshd() { local port=${1:-$(get_free_port)} SSH_OPTIONS+=("-NC" "-D" "$port") prepare_ssh_cmd ssh print_cmd fmt_note " > please access localhost:$port" eval_or_echo } # scp run_scp() { local src="$1" local dst="$2" local dst_is_remote=1 # whoever is ./*, it can't be the remote; whoever not exists on local, it's possible the remote. # it is suggested to use ./* for local files. if [[ "$src" != "./"* && ( "$dst" == "./"* || ( ! -e "$src" && -e "$dst" ) ) ]]; then dst_is_remote=0 fi if [[ "$dst_is_remote" == "1" ]]; then SCP_SRC="$src" SERVER="$SERVER":"$dst" else SERVER="$SERVER":"$src" SCP_DST="$dst" fi SSH_OPTIONS+=("-r") prepare_ssh_cmd scp print_cmd eval_or_echo } # ping run_ping() { CMD=(ping) if [[ "$1" == "ping4" ]]; then CMD+=(-4) elif [[ "$1" == "ping6" ]]; then CMD+=(-6) fi CMD+=(-c 4 "$SERVER") print_cmd eval_or_echo } # remove host keys remove_hostkey() { local key if [[ -z "$PORT" || "$PORT" == "22" ]]; then key=$SERVER else key="[$SERVER]:$PORT" fi ssh-keygen -R "$key" } # main print_help() { local pager=(cat) if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then pager=(less -R) fi cat <