Compare commits

..

No commits in common. "41cb0fe549b327d2ecf9983a64cc5c0fb8aae79e" and "6fa601c458f1ce443ad87139e6846a0432584983" have entirely different histories.

12 changed files with 113 additions and 726 deletions

View File

@ -28,34 +28,3 @@ SNt5JZCLBT4nLt0uQp9O/xctdHElZw+/W8OfnP5vxnPdccIeVOxpGIyzErwjD0+E
ZJc6v5e+iqbE1ECZLco5LjWqqfvFfYCrkqeOhCRsRkVPsXnGPo2QDDYdTm4EGCmg ZJc6v5e+iqbE1ECZLco5LjWqqfvFfYCrkqeOhCRsRkVPsXnGPo2QDDYdTm4EGCmg
dVZ6R452SZsrE4V+3LR011BxzEg= dVZ6R452SZsrE4V+3LR011BxzEg=
-----END CERTIFICATE----- -----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFQDCCAyigAwIBAgIRAJmQ852eXApHBvrQujWaFAEwDQYJKoZIhvcNAQELBQAw
ODESMBAGA1UEChMJRGljdCBUZWNoMSIwIAYDVQQDExlEaWN0IFRlY2ggSXNzdWlu
ZyBDQSAyMDI2MB4XDTI2MDgxMjE1NTQyM1oXDTI5MDgyMTExMDAwMFowKzESMBAG
A1UEChMJRGljdCBUZWNoMRUwEwYDVQQDEwxzazEuZHhuZy5uZXQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGl
Eme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8V
r6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5
v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmx
T+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X
7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCArAgMBAAGjggFQ
MIIBTDAOBgNVHQ8BAf8EBAMCBaAwKwYDVR0lBCQwIgYIKwYBBQUHAwIGCisGAQQB
gjcUAgIGCisGAQQBgjdDAQEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUOyThWF2A
BkoQv9xljyo3fFOclq0wHwYDVR0jBBgwFoAU9qXPjCjcil59akpPVISS8Yu/Jc0w
SgYIKwYBBQUHAQEEPjA8MDoGCCsGAQUFBzAChi5odHRwOi8vdG9vbHMuZHhuZy5j
bi9jZXJ0cy9pc3N1aW5nLWNhLTIwMjYuY2VyMCoGA1UdHwQjMCEwH6AdoBuGGWh0
dHA6Ly9jYS5pYmQuaW5rLzEuMC9jcmwwRwYMKwYBBAGCpGTGKEABBDcwNQIBAQQD
cGl2BCtaMnFWY05lMzlUaHZyc3RIbFVFWE5laXg1QXd2aFU3MjBlWEFFZWRlUG4w
MA0GCSqGSIb3DQEBCwUAA4ICAQCKSLjnMOlQiZ381IaBEl4iT7pCvY0QQxsVEMDD
RWYhIImx3JJDt7kiZXcJwlH70VG0opSz9eKrT3cw7OY6DnNm7Bm0JT2fJgyCSnWo
nG7J1soE74Rl2MiNnP1JTqhFlZKLrmIb6eP4U0eM+ZaJ7luyxizqiJAeRyJB+YCe
DM9uZ7rC33DYZeZ46V4f7DfJ23TjVJvaX/NFu0Ab/7ylF6X7frT8xf11XLlE2IyR
MmnLtQiREXXETaw9l5suMuH8fmVg01QrqnOHmz4r9Nuzh7vHxogEsJtgBLhv2Min
X6IfaKABhFXk1cVZGNgaC0Pao6cYbaWJwF2zcVHGq5EDKXhkKmCLeDo9em0rOJ2y
fU23yP3ByMGJ3Xi7nI8E5dfSw6Q39b3t1/kxj1y8T3PGdTN/3vFLVqf5A7XXdQL9
/uy2lYh3Fud/aZuthZyoL2rkC4O8N4seqfNIOBEHAmfbk5j6LoFFwSTrRGqzCAf4
4r6ckHjL3KBnR8EeMwwit324iRYd3ppUL3TINkeiHRNFVKU0flUvcG6T0v+hC2sX
yupYy0wylkonub6L0gUIINqx38nsDCR07eAfsDXJshIwTxUBFrVpKjPK7jVQOZ6+
9/yPu72BhyGMZs4bgYMEPwqKTbzUNitO2WfBIgXhzGe8YzMA1U2BMTRT0nIQ6oa9
8YbnQw==
-----END CERTIFICATE-----

View File

@ -1,7 +1,6 @@
[user] [user]
email = me@dxng.cn email = me@beardic.cn
name = Dict Xiong name = Dict Xiong
signingkey = 3E241558655D7FE06C6711A5A5D6250D1806CAA8
[core] [core]
editor = vim editor = vim
# test this by `git update-index --test-untracked-cache` # test this by `git update-index --test-untracked-cache`

View File

@ -44,6 +44,47 @@ jobs:
shell: /bin/zsh -ileo PIPE_FAIL {0} shell: /bin/zsh -ileo PIPE_FAIL {0}
run: source tools/test.zsh run: source tools/test.zsh
test-macos:
name: test on macos
runs-on: macos-latest
steps:
- name: checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: install dfs
run: |
rev=`git rev-parse HEAD`
pwd
set -x
DFS_NO_WALL=0 ./install.sh -adl
test `git rev-parse HEAD` = "$rev"
- name: antigen build
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
echo $SHELL
antigen list
- name: run tests and reset
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
source tools/test.zsh
antigen reset
rm -rf $ANTIGEN $HOME/.antigen $HOME/.config/dotfiles
./install.sh -dl
- name: antigen build with DFS_NO_WALL
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
echo $SHELL
antigen list
- name: run tests
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: source tools/test.zsh
test-autodep: test-autodep:
name: test of auto-install dependencies name: test of auto-install dependencies
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -1,105 +0,0 @@
#compdef riot
_riot() {
local context state state_descr line
typeset -A opt_args
_arguments -C -s \
'(-4 -6)-4[Force ssh to use IPv4 addresses only]' \
'(-4 -6)-6[Force ssh to use IPv6 addresses only]' \
'(-D --dry-run)'{-D,--dry-run}'[Print commands without executing them]' \
'(-d --dev)'{-d,--dev}'[Enable shell execution tracing]' \
'(-g --gpg)'{-g,--gpg}'[Forward the local GPG agent for an interactive login]' \
'(-h --help)'{-h,--help}'[Display help and exit]' \
'(-l --lite)'{-l,--lite}'[Enable dotfiles lite mode]' \
'*-o[Pass an option to ssh]:SSH option:' \
'(-p --password)'{-p,--password}'[Use password authentication]' \
'(-q --quiet)'{-q,--quiet}'[Enable dotfiles quiet mode]' \
'(-t --trust)'{-t,--trust}'[Trust the remote and enable forwarding]' \
'*-v[Enable verbose ssh output]' \
'--color[Force colored output]' \
'--tmux[Open SSH sessions in tmux windows]' \
'1:remote:_riot_remotes' \
'2:command:_riot_commands' \
'*::command argument:->command-args'
case $state in
command-args)
case $line[2] in
ssh|tmux|zssh)
_riot_complete_with _ssh ssh "$line[1]" "${line[@]:2}"
;;
git)
_riot_complete_with _git git "${line[@]:2}"
;;
sftp)
_riot_complete_with _sftp sftp "$line[1]" "${line[@]:2}"
;;
scp)
_riot_complete_with _scp scp "${line[@]:2}"
;;
sshl|sshr)
_message 'forwarding specification'
;;
sshd)
_guard '[0-9]#' 'local port'
;;
rm|ping|ping4|ping6)
_message 'no more arguments'
;;
*)
_default
;;
esac
;;
esac
}
_riot_commands() {
local -a commands=(
'ssh:Open an SSH session (default)'
'tmux:Open SSH sessions in tmux windows'
'git:Run git on the remote host'
'sshl:Create local port forwarding with ssh -L'
'sshr:Create remote port forwarding with ssh -R'
'sshd:Create dynamic port forwarding with ssh -D'
'zssh:Open a zssh session'
'sftp:Open an SFTP session'
'scp:Copy files to or from the remote host'
'rm:Remove the remote host key from known_hosts'
'ping:Ping the remote host'
'ping4:Ping the remote host over IPv4'
'ping6:Ping the remote host over IPv6'
)
_describe -t commands 'riot command' commands
}
_riot_presets() {
local -a config_files presets
local file
[[ -z $DOTFILES ]] || config_files+=("$DOTFILES/riot-config.sh")
config_files+=("$HOME/.config/riot-config.sh" "$PWD/riot-config.sh")
for file in "${config_files[@]}"; do
[[ -r $file ]] || continue
presets+=("${(@f)$(sed -nE 's/^([[:alnum:]_.-]+)\.(remote|batch)\(\).*/\1/p' "$file" 2>/dev/null)}")
done
presets=(${(u)presets})
_describe -t presets 'riot preset' presets
}
_riot_remotes() {
_alternative \
'presets:riot preset:_riot_presets' \
'hosts:host:_hosts'
}
_riot_complete_with() {
local completer=$1
shift
local -a words=("$@")
local CURRENT=$#words
"$completer"
}
_riot "$@"

View File

@ -1,3 +1,3 @@
#compdef sagt #compdef sagt
compadd -- gpg gpg-pin kill ls op piv reset compadd -- kill ls op piv reset

View File

@ -1,13 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# connect to iot services # connect to iot services
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
DFS_SKIP_ARG_PARSE=1
source "$THIS_DIR/../tools/common.sh" source "$THIS_DIR/../tools/common.sh"
unset DFS_SKIP_ARG_PARSE
RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}} RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}}
RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0} RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0}
EXTRA_SSH_OPTIONS=() EXTRA_SSH_OPTIONS=()
GPG_FORWARD=0
# config # config
RIOT_CONFIG_FILES=( RIOT_CONFIG_FILES=(
@ -15,19 +12,16 @@ RIOT_CONFIG_FILES=(
"$HOME/.config/riot-config.sh" "$HOME/.config/riot-config.sh"
"riot-config.sh" "riot-config.sh"
) )
load_riot_config() { for file in "${RIOT_CONFIG_FILES[@]}"; do
local file
for file in "${RIOT_CONFIG_FILES[@]}"; do
if [[ -f "$file" ]]; then if [[ -f "$file" ]]; then
source "$file" source "$file"
fi fi
done done
}
# check if port number valid # check if port number valid
check_port() { check_port() {
[[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1 [[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1
[[ $1 -lt 65536 && $1 -gt 0 ]] || return 1 [[ $1 < 65536 && $1 > 0 ]] || return 1
return 0 return 0
} }
@ -59,8 +53,8 @@ get_server_meta() {
"$remote" =~ ^[^:]+:[1-9][0-9]*$ # contains only one colon "$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 [host]:port
|| "$remote" =~ :::[1-9][0-9]*$ # in the form of :::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-f]{1,4}:){7}[0-9a-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 || "$remote" =~ ^[0-9a-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port
]]; then ]]; then
RET_PORT=${remote##*:} RET_PORT=${remote##*:}
remote=${remote%:*} remote=${remote%:*}
@ -141,129 +135,6 @@ parse_remote() {
fi 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="<remote-gpg-agent-socket>"
LOCAL_GPG_EXTRA_SOCKET="<local-gpg-agent-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() { print_cmd() {
local output="" local output=""
for s in "${CMD[@]}"; do for s in "${CMD[@]}"; do
@ -289,10 +160,7 @@ eval_or_echo() {
tmux_win=$((tmux_win+1)) tmux_win=$((tmux_win+1))
$DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l $DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l
fi fi
local command $DO tmux send-keys -t $TMUX_SESS:$tmux_win $(printf '%s Space ' "${CMD[@]}") Enter
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 else
$DO "${CMD[@]}" $DO "${CMD[@]}"
fi fi
@ -435,147 +303,26 @@ remove_hostkey() {
# main # main
print_help() print_help()
{ {
local pager=(cat) fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [command-args]"
if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then cat <<EOF
pager=(less -R) available commands:
fi - ssh [ssh-command-args] (default)
- tmux [ssh-command-args] (run ssh in multiple tmux windows)
cat <<EOF | "${pager[@]}" - git [git-command-args] (run git on remote machine)
NAME - sshl [local-port:remote-host:]remote-port (ssh -L)
riot - connect to remote hosts using SSH presets - sshd [local-port] (ssh -D)
- zssh [ssh-command-args]
SYNOPSIS - sftp
${0##*/} [OPTION]... REMOTE [OPTION]... [COMMAND [COMMAND-ARG]...] - scp source destination
- rm (remove host keys)
DESCRIPTION - ping/ping4/ping6 (ping the remote servers)
Connect to REMOTE using the matching configuration from riot-config.sh.
COMMAND defaults to ssh. Separate multiple remotes with commas and jump
hosts with slashes. OPTIONs may appear before COMMAND. Once COMMAND is
found, all remaining arguments are passed to it without further parsing.
OPTIONS
-4
Force ssh to use IPv4 addresses only.
-6
Force ssh to use IPv6 addresses only.
-D, --dry-run
Print commands without executing them.
-d, --dev
Enable shell execution tracing.
-g, --gpg
Forward the local GPG agent during an interactive SSH login. Use only
with trusted remote hosts.
-h, --help
Display this help and exit.
-l, --lite
Enable dotfiles lite mode for loaded configuration.
-o SSH-OPTION
Pass an option to ssh. This option may be specified multiple times.
-p, --password
Use password authentication instead of public-key authentication.
-q, --quiet
Enable dotfiles quiet mode for loaded configuration.
-t, --trust
Trust the remote and enable X11 and SSH agent forwarding.
-v
Enable verbose ssh output.
--color
Force colored output.
--tmux
Open SSH sessions in tmux windows.
--
End option parsing.
COMMANDS
ssh [SSH-ARG]...
Open an SSH session. This is the default command.
tmux [SSH-ARG]...
Open SSH sessions in multiple tmux windows.
git [GIT-ARG]...
Run git on the remote host.
sshl [LOCAL-PORT:REMOTE-HOST:]REMOTE-PORT
Create local port forwarding with ssh -L.
sshr [REMOTE-HOST:]REMOTE-PORT
Create remote port forwarding with ssh -R.
sshd [LOCAL-PORT]
Create dynamic port forwarding with ssh -D.
zssh [SSH-ARG]...
Open a zssh session.
sftp [SFTP-ARG]...
Open an SFTP session.
scp SOURCE DESTINATION
Copy files to or from the remote host.
rm
Remove the remote host key from known_hosts.
ping, ping4, ping6
Ping the remote host.
EOF EOF
} }
router() { router() {
local positional=() local positional=()
local arg="" while [[ $# > 0 ]]; do
local option="" case "$1" in
local remaining=""
local option_value=""
while [[ $# -gt 0 || -n "$arg" ]]; do
if [[ -z "$arg" ]]; then
arg=$1
shift
fi
# Normalize a long option or one item from a short-option group.
remaining=""
case "$arg" in
-- )
positional+=("$@")
break
;;
--* )
option=$arg
arg=""
;;
-?* )
option=${arg:0:2}
remaining=${arg:2}
arg=${remaining:+-$remaining}
;;
* )
positional+=("$arg")
arg=""
if [[ "${#positional[@]}" -ge 2 ]]; then
positional+=("$@")
break
fi
continue
;;
esac
case "$option" in
-h|--help ) -h|--help )
print_help print_help
exit 0 exit 0
@ -583,68 +330,45 @@ router() {
-t|--trust ) -t|--trust )
RIOT_TRUST_SERVER=1 RIOT_TRUST_SERVER=1
;; ;;
-g|--gpg )
GPG_FORWARD=1
;;
--tmux ) --tmux )
USE_TMUX=1 USE_TMUX=1
;; ;;
-p|--password ) --password )
EXTRA_SSH_OPTIONS+=("-o" "PasswordAuthentication=yes" "-o" "PubkeyAuthentication=no") EXTRA_SSH_OPTIONS+=("-o" "PasswordAuthentication=yes" "-o" "PubkeyAuthentication=no")
;; ;;
-D|--dry-run )
export DFS_DRY_RUN=1
;;
-d|--dev )
export DFS_DEV=1
set -x
;;
-l|--lite )
export DFS_LITE=1
;;
-q|--quiet )
export DFS_QUIET=1
;;
--color )
export DFS_COLOR=1
setup_color
;;
-4|-6|-v )
EXTRA_SSH_OPTIONS+=("$option")
;;
-o ) -o )
if [[ -n "$remaining" ]]; then EXTRA_SSH_OPTIONS+=("-o" "$2")
option_value=$remaining
arg=""
else
[[ $# -gt 0 ]] || fmt_fatal "option '-o' requires an argument"
option_value=$1
shift shift
fi ;;
EXTRA_SSH_OPTIONS+=("-o" "$option_value") -4 )
EXTRA_SSH_OPTIONS+=("-4")
;;
-6 )
EXTRA_SSH_OPTIONS+=("-6")
;;
-v )
EXTRA_SSH_OPTIONS+=("-v")
;;
-- )
shift
positional+=("$@")
break
;;
-* )
fmt_fatal "unknown option: '$1'. if this option is for the remote command, add '--' before."
;; ;;
* ) * )
fmt_fatal "unknown option: '$option'" positional+=("$1")
;; ;;
esac esac
shift
done done
if [[ "${positional[2]}" == "--" ]]; then
positional=("${positional[@]:0:2}" "${positional[@]:3}")
fi
IFS=',' read -ra remotes <<< "${positional[0]}" IFS=',' read -ra remotes <<< "${positional[0]}"
for i in ${!remotes[@]}; do if [[ -z "${remotes[i]}" ]]; then unset remotes[i]; fi; done for i in ${!remotes[@]}; do if [[ -z "${remotes[i]}" ]]; then unset remotes[i]; fi; done
if [[ "${#positional[@]}" == "0" || "${#remotes[@]}" == "0" ]]; then if [[ "${#positional[@]}" == "0" || "${#remotes[@]}" == "0" ]]; then
print_help print_help
exit 1 exit 1
fi fi
load_riot_config
if [[ "$GPG_FORWARD" == "1" && ( \
( "${positional[1]}" != "" && "${positional[1]}" != "ssh" && "${positional[1]}" != "tmux" ) \
|| "${#positional[@]}" -gt 2 ) ]]; then
fmt_fatal "gpg-agent forwarding is only supported for interactive SSH login"
fi
for i in ${!remotes[@]}; do for i in ${!remotes[@]}; do
remote="${remotes[i]}" remote="${remotes[i]}"
local batch_func="${remote}.batch" local batch_func="${remote}.batch"
@ -656,9 +380,6 @@ router() {
case "${positional[1]}" in case "${positional[1]}" in
ssh|tmux|"" ) ssh|tmux|"" )
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1 [[ "${positional[1]}" == tmux ]] && USE_TMUX=1
if [[ "$GPG_FORWARD" == "1" ]]; then
prepare_gpg_forwarding
fi
run_ssh ssh "${positional[@]:2}" run_ssh ssh "${positional[@]:2}"
;; ;;
git ) git )
@ -719,4 +440,4 @@ router() {
fi fi
} }
router "$@" router "${GOT_OPTS[@]}"

View File

@ -6,19 +6,19 @@ if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env;
if [[ "$DFS_DEV" == "1" ]]; then set -x; fi if [[ "$DFS_DEV" == "1" ]]; then set -x; fi
DFS_CURL_OPTIONS="--retry 2 --max-time 20" DFS_CURL_OPTIONS="--retry 2 --max-time 20"
# Parse args and set env when sourced, unless the caller handles its own # parse args and set env, when it is sourced
# option boundary. # todo: make this skipable
if [[ "${BASH_SOURCE[0]}" != "${0}" && "$DFS_SKIP_ARG_PARSE" != "1" ]]; then if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
ORIGIN_ARGS=("$@") ORIGIN_ARGS=("$@")
ARG="" ARG=""
GOT_OPTS=() GOT_OPTS=()
while [[ $# -gt 0 || -n "$ARG" ]]; do while [[ $# > 0 || -n "$ARG" ]]; do
if [[ -z "$ARG" ]]; then if [[ -z "$ARG" ]]; then
if [[ "$1" == "--" ]]; then GOT_OPTS+=("$@"); break; fi if [[ "$1" == "--" ]]; then GOT_OPTS+=("$@"); break; fi
ARG="$1"; shift; ARG="$1"; shift;
fi fi
case $ARG in case $ARG in
-q*|--quiet ) export DFS_QUIET=1 ;; -q*|--quite ) export DFS_QUIET=1 ;;
-l*|--lite ) export DFS_LITE=1 ;; -l*|--lite ) export DFS_LITE=1 ;;
-d*|--dev ) export DFS_DEV=1; set -x ;; -d*|--dev ) export DFS_DEV=1; set -x ;;
-D*|--dry-run ) export DFS_DRY_RUN=1 ;; -D*|--dry-run ) export DFS_DRY_RUN=1 ;;
@ -41,9 +41,15 @@ fi
# Color settings # Color settings
# Source: https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh # Source: https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
is_tty() { if [[ -t 1 || "$DFS_COLOR" == "1" ]]; then
[[ -t 1 || "$DFS_COLOR" == "1" ]] is_tty() {
} true
}
else
is_tty() {
false
}
fi
supports_truecolor() { supports_truecolor() {
case "$COLORTERM" in case "$COLORTERM" in

View File

@ -154,7 +154,7 @@ check_pinentry()
fi fi
} }
prepare_gpg_agent() use_gpg_agent()
{ {
command -v gpgconf > /dev/null 2>&1 || fmt_fatal "gpgconf not found" command -v gpgconf > /dev/null 2>&1 || fmt_fatal "gpgconf not found"
command -v gpg-connect-agent > /dev/null 2>&1 || fmt_fatal "gpg-connect-agent not found" command -v gpg-connect-agent > /dev/null 2>&1 || fmt_fatal "gpg-connect-agent not found"
@ -167,11 +167,6 @@ prepare_gpg_agent()
gpgconf --launch gpg-agent gpgconf --launch gpg-agent
gpg-connect-agent updatestartuptty /bye > /dev/null gpg-connect-agent updatestartuptty /bye > /dev/null
}
use_gpg_agent()
{
prepare_gpg_agent
local agent_socket local agent_socket
agent_socket=$(gpgconf --list-dirs agent-ssh-socket) agent_socket=$(gpgconf --list-dirs agent-ssh-socket)
@ -181,37 +176,10 @@ use_gpg_agent()
fmt_note "using gpg-agent: $agent_socket" fmt_note "using gpg-agent: $agent_socket"
echo unset SSH_AGENT_PID echo unset SSH_AGENT_PID
printf 'export GPG_TTY=%q\n' "$GPG_TTY" printf 'export GPG_TTY=%q\n' "$current_tty"
printf 'export SSH_AUTH_SOCK=%q\n' "$agent_socket" printf 'export SSH_AUTH_SOCK=%q\n' "$agent_socket"
} }
cache_gpg_pin()
{
[[ $# -le 1 ]] || fmt_fatal "usage: sagt gpg-pin [KEY]"
command -v gpg > /dev/null 2>&1 || fmt_fatal "gpg not found"
prepare_gpg_agent
local signing_key="${1:-}"
local temp_dir
local signature_file
local status=0
local gpg_args=(--detach-sign)
temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/sagent-gpg-pin.XXXXXXXXXX") || \
fmt_fatal "failed to create a temporary directory"
signature_file="$temp_dir/signature.gpg"
gpg_args+=(--output "$signature_file")
[[ -z "$signing_key" ]] || gpg_args+=(--local-user "$signing_key")
fmt_note "performing a test signature; enter the GPG PIN and touch the token if prompted"
printf 'sagt gpg-pin\n' | gpg "${gpg_args[@]}" || status=$?
rm -f -- "$signature_file"
rmdir -- "$temp_dir"
[[ $status -eq 0 ]] || fmt_fatal "test signature failed (gpg exit $status)"
fmt_note "test signature completed; the GPG PIN should remain cached until the card or agent session is reset"
}
read_agent_file() read_agent_file()
{ {
local agent_file="$1" local agent_file="$1"
@ -322,9 +290,6 @@ route()
gpg) gpg)
use_gpg_agent use_gpg_agent
;; ;;
gpg-pin)
cache_gpg_pin "${@:2}"
;;
reset) reset)
reset reset
;; ;;

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
OPTS='-a -bcl --color --quiet --arg1=1 --arg2 2 " 1 2" yes' OPTS='-a -bcl --color --arg1=1 --arg2 2 " 1 2" yes'
TARGET_OPTS='-a -b -c --arg1 1 --arg2 2 1 2 yes' TARGET_OPTS='-a -b -c --arg1 1 --arg2 2 1 2 yes'
eval set -- $OPTS eval set -- $OPTS
@ -8,11 +8,11 @@ THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"
test "${GOT_OPTS[*]}" = "$TARGET_OPTS" test "${GOT_OPTS[*]}" = "$TARGET_OPTS"
test $# -eq 9 test $# -eq 8
test "$*" = "${OPTS//\"/}" test "$*" = "${OPTS//\"/}"
test "$DFS_LITE" = "1" test "$DFS_LITE" = "1"
is_tty is_tty
test "$DFS_QUIET" = "1" test -z "$DFS_QUIET"
set +x set +x
echo "test passed, args:" echo "test passed, args:"

View File

@ -1,132 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
RIOT="$THIS_DIR/../scripts/riot"
TEST_DIR=$(mktemp -d /tmp/riot-gpg.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
MOCK_HOME="$TEST_DIR/home"
MOCK_LOCAL_SOCKET="$TEST_DIR/local/S.gpg-agent.extra"
MOCK_REMOTE_SOCKET="$TEST_DIR/remote/S.gpg-agent"
MOCK_GPG_LOG="$TEST_DIR/gpg.log"
MOCK_SSH_LOG="$TEST_DIR/ssh.log"
mkdir -p "$MOCK_BIN" "$MOCK_HOME" "${MOCK_LOCAL_SOCKET%/*}" "${MOCK_REMOTE_SOCKET%/*}"
export MOCK_LOCAL_SOCKET MOCK_REMOTE_SOCKET MOCK_GPG_LOG MOCK_SSH_LOG
make_stale_socket() {
rm -f "$1"
python3 - "$1" <<'PY'
import socket
import sys
sock = socket.socket(socket.AF_UNIX)
sock.bind(sys.argv[1])
sock.close()
PY
}
cat > "$MOCK_BIN/gpgconf" <<'EOF'
#!/usr/bin/env bash
printf 'gpgconf %s\n' "$*" >> "$MOCK_GPG_LOG"
case "$*" in
'--list-dirs agent-extra-socket') printf '%s\n' "$MOCK_LOCAL_SOCKET" ;;
'--list-dirs agent-socket') printf '%s\n' "$MOCK_REMOTE_SOCKET" ;;
'--kill gpg-agent') exit "${MOCK_KILL_STATUS:-0}" ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF'
#!/usr/bin/env bash
printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_GPG_LOG"
case "$*" in
*'GETINFO pid'*) printf 'D 123\nOK\n' ;;
*'GETINFO version'*)
[[ "${MOCK_LOCAL_LIVE:-1}" == 1 ]] || exit 1
printf 'D 2.4.0\nOK\n'
;;
*'GETINFO restricted'*) printf 'D %s\nOK\n' "${MOCK_REMOTE_RESTRICTED:-0}" ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/systemctl" <<'EOF'
#!/usr/bin/env bash
[[ "${MOCK_SYSTEMD_ACTIVE:-0}" == 1 ]]
EOF
cat > "$MOCK_BIN/ssh" <<'EOF'
#!/usr/bin/env bash
{
printf 'CALL\n'
printf 'ARG=%s\n' "$@"
} >> "$MOCK_SSH_LOG"
is_probe=0
last_arg=''
for arg in "$@"; do
[[ "$arg" == '-T' ]] && is_probe=1
last_arg=$arg
done
if [[ "$is_probe" == 1 ]]; then
sh -c "$last_arg"
fi
EOF
chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/systemctl" "$MOCK_BIN/ssh"
run_riot() {
HOME="$MOCK_HOME" RIOT_TRUST_CLIENT=0 PATH="$MOCK_BIN:$PATH" "$RIOT" "$@"
}
expect_failure() {
local expected=$1
shift
if run_riot "$@" > "$TEST_DIR/out" 2> "$TEST_DIR/err"; then
echo "expected riot to fail: $*" >&2
exit 1
fi
grep -Fq "$expected" "$TEST_DIR/err"
}
# Dry-run must not inspect or mutate either host.
: > "$MOCK_GPG_LOG"
: > "$MOCK_SSH_LOG"
DFS_DRY_RUN=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
grep -Fq '<remote-gpg-agent-socket>:<local-gpg-agent-extra-socket>' "$TEST_DIR/out"
[[ ! -s "$MOCK_GPG_LOG" && ! -s "$MOCK_SSH_LOG" ]]
# A socket inode without a listener must be rejected locally.
make_stale_socket "$MOCK_LOCAL_SOCKET"
MOCK_LOCAL_LIVE=0 expect_failure 'extra socket exists but is not accepting connections' -g example.test
# -g is valid only for an interactive SSH login.
expect_failure 'only supported for interactive SSH login' -g example.test scp ./a ./b
expect_failure 'only supported for interactive SSH login' -g example.test ssh -- true
# Never remove a regular file merely because it has the expected basename.
MOCK_LOCAL_LIVE=1
printf 'keep me\n' > "$MOCK_REMOTE_SOCKET"
expect_failure 'refusing to remove the non-socket remote gpg-agent path' -g example.test
grep -Fqx 'keep me' "$MOCK_REMOTE_SOCKET"
# A restricted agent at the remote socket represents another forwarding session.
make_stale_socket "$MOCK_REMOTE_SOCKET"
MOCK_REMOTE_RESTRICTED=1 expect_failure 'another forwarded gpg-agent is already using the remote socket' -g example.test
# A normal remote agent can be cleaned up; systemd activation is reported.
make_stale_socket "$MOCK_REMOTE_SOCKET"
: > "$MOCK_SSH_LOG"
MOCK_REMOTE_RESTRICTED=0 MOCK_SYSTEMD_ACTIVE=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
grep -Fq 'remote gpg-agent.socket is active' "$TEST_DIR/err"
grep -Fq 'ARG=none' "$MOCK_SSH_LOG"
grep -Fq 'ARG=ClearAllForwardings=yes' "$MOCK_SSH_LOG"
grep -Fq 'ARG=-T' "$MOCK_SSH_LOG"
! grep -Fq 'ARG=RequestTTY=' "$MOCK_SSH_LOG"
grep -Fq 'ARG=StreamLocalBindUnlink=no' "$MOCK_SSH_LOG"
grep -Fq "ARG=$MOCK_REMOTE_SOCKET:$MOCK_LOCAL_SOCKET" "$MOCK_SSH_LOG"
[[ ! -e "$MOCK_REMOTE_SOCKET" ]]
echo 'riot gpg forwarding tests passed'

View File

@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SAGENT="$THIS_DIR/sagent.sh"
TEST_DIR=$(mktemp -d /tmp/sagent-gpg-pin.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
MOCK_HOME="$TEST_DIR/home"
MOCK_TMP="$TEST_DIR/tmp"
MOCK_GPG_ARGS="$TEST_DIR/gpg-args"
MOCK_GPG_INPUT="$TEST_DIR/gpg-input"
MOCK_AGENT_LOG="$TEST_DIR/agent-log"
mkdir -p "$MOCK_BIN" "$MOCK_HOME/gnupg" "$MOCK_HOME/sysconf" "$MOCK_TMP"
export MOCK_BIN MOCK_HOME MOCK_GPG_ARGS MOCK_GPG_INPUT MOCK_AGENT_LOG
cat > "$MOCK_BIN/gpgconf" <<'EOF'
#!/usr/bin/env bash
printf 'gpgconf %s\n' "$*" >> "$MOCK_AGENT_LOG"
case "$*" in
'--list-dirs homedir') printf '%s\n' "$MOCK_HOME/gnupg" ;;
'--list-dirs sysconfdir') printf '%s\n' "$MOCK_HOME/sysconf" ;;
'--list-dirs bindir') printf '%s\n' "$MOCK_BIN" ;;
'--launch gpg-agent') ;;
*) exit 1 ;;
esac
EOF
cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF'
#!/usr/bin/env bash
printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_AGENT_LOG"
EOF
cat > "$MOCK_BIN/tty" <<'EOF'
#!/usr/bin/env bash
printf '/dev/pts/mock\n'
EOF
cat > "$MOCK_BIN/gpg" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$@" > "$MOCK_GPG_ARGS"
output=''
while [[ $# -gt 0 ]]; do
if [[ "$1" == '--output' ]]; then
output=$2
shift 2
else
shift
fi
done
cat > "$MOCK_GPG_INPUT"
printf 'mock signature\n' > "$output"
EOF
printf '#!/usr/bin/env bash\n' > "$MOCK_BIN/pinentry"
chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/tty" \
"$MOCK_BIN/gpg" "$MOCK_BIN/pinentry"
output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin TEST-KEY 2> "$TEST_DIR/stderr")
[[ -z "$output" ]]
grep -Fxq -- '--detach-sign' "$MOCK_GPG_ARGS"
grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS"
grep -Fxq -- 'TEST-KEY' "$MOCK_GPG_ARGS"
grep -Fxq -- 'sagt gpg-pin' "$MOCK_GPG_INPUT"
grep -Fq -- 'gpgconf --launch gpg-agent' "$MOCK_AGENT_LOG"
grep -Fq -- 'gpg-connect-agent updatestartuptty /bye' "$MOCK_AGENT_LOG"
grep -Fq -- 'test signature completed' "$TEST_DIR/stderr"
output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin 2> "$TEST_DIR/stderr")
[[ -z "$output" ]]
! grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS"
[[ -z $(find "$MOCK_TMP" -mindepth 1 -print -quit) ]]
echo "sagent gpg-pin tests passed"

View File

@ -34,17 +34,15 @@ dogo
doll doll
dfs cd dfs cd
tools/test-getopts.sh tools/test-getopts.sh
tools/test-riot-gpg.sh
tools/test-sagent-gpg-pin.sh
tools/common.sh get_os_name tools/common.sh get_os_name
test $(echo y | tools/common.sh ask_for_yN "test") = "1" test $(echo y | tools/common.sh ask_for_yN "test") = "1"
test $(echo n | tools/common.sh ask_for_yN "test") = "0" test $(echo n | tools/common.sh ask_for_yN "test") = "0"
test $(echo | tools/common.sh ask_for_yN "test") = "0" test $(echo | tools/common.sh ask_for_yN "test") = "0"
test $(echo | tools/common.sh ask_for_Yn "test") = "1" test $(echo | tools/common.sh ask_for_Yn "test") = "1"
test $(DFS_QUIET=1 tools/common.sh ask_for_Yn "test") = "1" test $(DFS_QUIET=1 tools/common.sh ask_for_Yn "test") = "1"
test "$(DFS_TRUST=1 riot time@is.impt:2222/yes@you-r.right/you@are.really.recht./ibd./try@it,another@host -D scp /tmp/ ./tmp 2>/dev/null)" = 'scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ProxyJump=time@is.impt:2222,yes@you-r.right:12022,you@are.really.recht:12022,root@ibd:12022 -r try@it.dxng.net:/tmp/ ./tmp test "$(DFS_TRUST=1 riot time@is.impt:2222/yes@you-r.right/you@are.really.recht./ibd./try@it,another@host scp /tmp/ ./tmp -D 2>/dev/null)" = 'scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ProxyJump=time@is.impt:2222,yes@you-r.right:12022,you@are.really.recht:12022,root@ibd:12022 -r try@it.dxng.net:/tmp/ ./tmp
scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ForwardX11=yes -o ForwardAgent=yes -r another@host.dxng.net:/tmp/ ./tmp' scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ForwardX11=yes -o ForwardAgent=yes -r another@host.dxng.net:/tmp/ ./tmp'
test "$(riot you@example.com:55 -tD --password ssh ping -c 1 2>/dev/null)" = 'ssh -p 55 -o ServerAliveInterval=60 -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1' test "$(riot you@example.com:55 -tD ssh --password -- ping -c 1 2>/dev/null)" = 'ssh -p 55 -o ServerAliveInterval=60 -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1'
# check alias # check alias
alias p114 alias p114