#!/usr/bin/env bash # connect to iot services THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) source "$THIS_DIR/../tools/common.sh" RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}} RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0} EXTRA_SSH_OPTIONS=() # config RIOT_CONFIG_FILES=( "$DOTFILES/riot-config.sh" "$HOME/.config/riot-config.sh" "riot-config.sh" ) for file in "${RIOT_CONFIG_FILES[@]}"; do if [[ -f "$file" ]]; then source "$file" fi done # check if port number valid check_port() { ( echo $1 | grep -qxE "[1-9][0-9]{0,4}" ) || return 1 test $1 -lt 65536 -a $1 -gt 0 || return 1 return 0 } # check if username valid check_username() { ( echo $1 | grep -qxE "^[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" # if in the form user@... if [[ "$remote" == *@* ]]; then RET_USERNAME=${remote%%@*} remote=${remote#*@} check_username $RET_USERNAME || fmt_warning \"$RET_USERNAME\" is not a valid unix username fi # if in the form ...:22 if [[ "$remote" == "["*"]":* || ( "$remote" != "["*"]" && "$remote" == *:* ) ]]; then RET_PORT=${remote##*:} remote=${remote%:*} check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\" 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 there's no dot if [[ "$host" == "$domain" && "$host" != "["*"]" ]]; 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" "RequestTTY=yes") 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" "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 } print_cmd() { local output="" for s in "${CMD[@]}"; do if [[ "$s" =~ [\ \\\'\"] ]]; then s="${s@Q}" fi output+="$s " done fmt_note "--> ${output% }" } eval_or_echo() { local DO="" local 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 $DO tmux send-keys -t $TMUX_SESS:$tmux_win "${CMD[@]}" 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="$1" if [[ "$arg" != *":"* ]]; then # treat as a port number arg=localhost:$arg fi local port=$(get_free_port) SSH_OPTIONS+=("-NC" "-L" "$port:$arg") prepare_ssh_cmd ssh print_cmd fmt_note " > please access localhost:$port" eval_or_echo } # 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() { fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [ssh-command-args]" cat < 0 ]]; do case "$1" in -h|--help ) print_help exit 0 ;; -t|--trust ) RIOT_TRUST_SERVER=1 ;; --tmux ) USE_TMUX=1 ;; --password ) EXTRA_SSH_OPTIONS+=("-o" "PasswordAuthentication=yes" "-o" "PubkeyAuthentication=no") ;; -o ) EXTRA_SSH_OPTIONS+=("-o" "$2") shift ;; -- ) shift positional+=("$@") break ;; -* ) fmt_fatal "unknown option: $1" ;; * ) positional+=("$1") ;; esac shift done IFS=',' read -ra remotes <<< "${positional[0]}" for i in ${!remotes[@]}; do if [[ -z "${remotes[i]}" ]]; then unset remotes[i]; fi; done if [[ "${#positional[@]}" == "0" || "${#remotes[@]}" == "0" ]]; then print_help exit 1 fi for i in ${!remotes[@]}; do remote="${remotes[i]}" local batch_func="${remote}.batch" if is_function "$batch_func"; then "$batch_func" continue fi parse_remote "$remote" case "${positional[1]}" in ssh|tmux|"" ) [[ "${positional[1]}" == tmux ]] && USE_TMUX=1 run_ssh ssh "${positional[@]:2}" ;; ping|ping4|ping6 ) test "${#positional[@]}" -eq 2 || fmt_fatal "ping requires no arguments" run_ping "${positional[1]}" ;; zssh ) run_ssh zssh "${positional[@]:2}" ;; sftp ) run_ssh sftp "${positional[@]:2}" ;; sshl ) test -n "${positional[2]}" || fmt_fatal "no target address provided" test "${#positional[@]}" -eq 3 || fmt_fatal "sshl requires exactly one argument" run_sshl "${positional[2]}" ;; sshd ) test "${#positional[@]}" -le 3 || fmt_fatal "sshd requires one or no arguments" if [[ "${#positional[@]}" -eq 3 ]]; then check_port "${positional[2]}" || fmt_fatal "invalid port number: ${positional[2]}" run_sshd "${positional[2]}" else run_sshd fi ;; scp ) test "${#positional[@]}" -eq 4 || fmt_fatal "scp requires exactly two arguments: source and destination" test -n "${positional[2]}" || fmt_fatal "no source path specified" test -n "${positional[3]}" || fmt_fatal "no destination path specified" run_scp "${positional[2]}" "${positional[3]}" ;; rm ) test "${#positional[@]}" -eq 2 || fmt_fatal "rm requires no arguments" remove_hostkey ;; * ) print_help fmt_fatal "unknown command: ${positional[1]}" ;; esac done if [[ -n "$TMUX_SESS" && "$DFS_DRY_RUN" != "1" ]]; then tmux attach-session -t $TMUX_SESS fi } router "${GOT_OPTS[@]}"