26 Commits

Author SHA1 Message Date
6fa601c458 feat(sagent): check pinentry and give notes 2026-08-13 02:20:36 +08:00
b891e4ed07 feat(sagent): sagt gpg; improve code 2026-08-13 01:51:03 +08:00
7e33f92e51 feat(ssh): add key openpgp:0x8774BF70 and remove key ip14/sep and ltp2 (asu127) 2026-08-12 15:07:30 +08:00
3e425f45e0 feat(ssh): add key sk1/fido2 2026-08-11 02:29:01 +08:00
e9e8a5fede feat(ssh): add key sk1/piv/9a 2026-08-11 00:36:31 +08:00
74bffde6cf feat(tmux): press ESC to exit copy mode 2026-08-05 00:48:09 +08:00
b170d06d9a feat(ssh): add key ip17/sep 2026-06-28 00:27:06 +08:00
cb70dc7cdc feat: sne: search and edit 2026-05-06 12:19:26 +08:00
9cf6d65692 fix(riot): ControlPersist 60s -> 5s 2026-05-06 12:15:07 +08:00
a7074fc940 feat(tmux): F12 to passthru 2026-04-20 23:04:50 +08:00
4e3ce4f9d9 feat(tmux): prevent tmux from exiting copy mode after selection with mouse
see:
- https://www.reddit.com/r/tmux/comments/v73005/how_to_prevent_tmux_from_exiting_copy_mode_after/
- https://www.reddit.com/r/tmux/comments/1ltms6a/how_to_select_text_in_tmux_without_having_it_jump/
2026-04-12 01:18:13 +08:00
ed98a56e05 fix(ci): riot 2026-04-03 17:10:29 +08:00
e79b092054 feat(riot): ControlPersist=60s 2026-04-03 15:54:17 +08:00
f45f7e415c fix(riot): tmux error with window number and spaces
The fix to spaces is just a workaround. Issue may still exist when
command contains spaces.
2026-01-19 16:27:56 +08:00
9f1bc8f0fb fix(frigg): ddns get ip 2025-12-20 12:40:19 +08:00
ee44717891 feat: zsh completion for beam and beaml 2025-12-19 21:56:09 +08:00
d283671f25 feat(riot): conditionally RequestTTY 2025-12-19 21:28:16 +08:00
bdef3b52d1 feat(riot): -o ServerAliveInterval=60 2025-12-19 21:23:52 +08:00
c86cc85e24 feat(riot): sshr for ssh -R 2025-12-19 21:21:22 +08:00
426e97c10e feat(riot): sshl supports specifying local port and unix sock 2025-12-19 17:34:37 +08:00
e1f0ec0ebc fix(tmux): press ^a twice to send ^a 2025-11-20 20:22:17 +08:00
8dbb310e02 fix(zshrc): bnd exit when build error 2025-11-17 01:45:53 +08:00
8865dee1e9 feat: add bnd and nor and completions 2025-11-15 14:48:19 +08:00
65d690f47c build(riot-config): nasp.fit -> jump.nasp.fit 2025-10-28 17:18:56 +08:00
ecf652ad7b feat(riot): command git 2025-07-24 16:28:37 +08:00
86670306af feat(riot): support literal ipv6 addresses (with or without ports given)
feat(riot): -v, -4, and -6

fix(riot): do not call external binaries if possible
2025-07-16 21:09:04 +08:00
18 changed files with 117 additions and 1284 deletions

View File

@@ -28,34 +28,3 @@ SNt5JZCLBT4nLt0uQp9O/xctdHElZw+/W8OfnP5vxnPdccIeVOxpGIyzErwjD0+E
ZJc6v5e+iqbE1ECZLco5LjWqqfvFfYCrkqeOhCRsRkVPsXnGPo2QDDYdTm4EGCmg
dVZ6R452SZsrE4V+3LR011BxzEg=
-----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]
email = me@dxng.cn
email = me@beardic.cn
name = Dict Xiong
signingkey = 3E241558655D7FE06C6711A5A5D6250D1806CAA8
[core]
editor = vim
# test this by `git update-index --test-untracked-cache`

View File

@@ -44,6 +44,47 @@ jobs:
shell: /bin/zsh -ileo PIPE_FAIL {0}
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:
name: test of auto-install dependencies
runs-on: ubuntu-latest
@@ -87,4 +128,4 @@ jobs:
shell: /bin/zsh -ileo PIPE_FAIL {0}
run: |
dfs cd
source tools/test.zsh
source tools/test.zsh

View File

@@ -31,9 +31,8 @@ bind r source-file ~/.tmux.conf \; display-message "tmux.conf reloaded"
bind m run 'old=$(tmux show -gv mouse); new="off"; if [ "$old" = "off" ]; then new="on"; fi; tmux set -g mouse $new && tmux display-message toggle\ mouse\ mode\ $new'
## prefix C-x: pane sync
bind C-x set-window-option synchronize-panes\; display-message "synchronize-panes is now #{?pane_synchronized,on,off}"
## tmux 3.6+: copy selection without jumping to the end
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection
bind -T copy-mode MouseDragEnd1Pane send-keys -X copy-selection
## prevent jumping to end after mouse selection
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection -x
## F12: passthru
bind -T root F12 set prefix None \; set key-table off \; set status-bg red \; refresh-client -S
bind -T off F12 set -u prefix \; set -u key-table \; set -u status-bg \; refresh-client -S

View File

@@ -149,14 +149,7 @@ bnd() {
}
nor() {
test -n "$1" || { echo fatal: no verb given; return 1; }
local verb="$1"
local flake=".${2:+#$2}"
if command -v nh >/dev/null 2>&1 && [[ "$verb" != dry-build && "$verb" != dry-activate ]]; then
nh os "$verb" --bypass-root-check "$flake"
else
nixos-rebuild "$verb" --flake "$flake"
fi
nixos-rebuild "$1" --flake .${2:+#}$2
}
alias 'nors'='nor switch'
alias 'norbt'='nor boot'

View File

@@ -1,58 +1 @@
# dotfiles
My personal shell environment and command-line tools, mainly for Linux and
NixOS systems.
It includes configurations for zsh, tmux, Vim, Git, and SSH, together with a
few utility scripts used across my machines.
## Highlights
- `riot`: an SSH wrapper with host presets, jump hosts, port forwarding, tmux
sessions, and optional GPG agent forwarding.
- `sagt`: an SSH/GPG agent manager with support for PIV, 1Password, and
OpenPGP smart cards.
- zsh aliases and completions for the bundled tools.
- tmux and Vim configurations with plugin setup.
- automatic updates with selectable `main`, `dev`, and `latest` channels.
## Installation
Quick install:
```sh
curl -fsSL https://dotfiles.cn | bash
```
Alternatively, clone the repository so the installer can be reviewed first:
```sh
git clone https://github.com/DictXiong/dotfiles.git ~/dotfiles
~/dotfiles/install.sh -a
```
The installer adds small source/include entries to the existing zsh, tmux,
Vim, and Git configuration files instead of replacing them entirely.
## Useful commands
```sh
riot --help # show the SSH wrapper manual
sagt # start or reuse an ssh-agent
sagt gpg # use gpg-agent as SSH agent
sagt gpg-pin # cache the smart-card signature PIN
dfs update # update dotfiles from the selected channel
dfs config # edit machine-specific settings
```
## Configuration
Machine-specific settings can be placed in:
```text
~/.config/dotfiles/env
~/.config/riot-config.sh
```
This is a personal dotfiles repository. Review the configuration, SSH keys,
and installation script before using it on another machine.
# dotfiles

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
compadd -- gpg gpg-pin kill ls op piv reset
compadd -- kill ls op piv reset

View File

@@ -1,27 +0,0 @@
#compdef scf
_scf() {
_arguments -s \
'(-r)-r[Recursively inspect referenced files and directories]' \
'(-n)-n[Open candidate number directly]:candidate number:' \
'--[End of options]' \
'1:systemd service:_scf_services'
}
_scf_services() {
local -a services
if (( $+commands[systemctl] )); then
services=("${(@f)$(systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null \
| awk '$1 ~ /\.service$/ {print $1}' \
| sort -u)}")
fi
if (( ${#services} > 0 )); then
_describe -t services 'systemd service' services
else
_message 'systemd service'
fi
}
_scf "$@"

View File

@@ -1,13 +1,10 @@
#!/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=(
@@ -15,19 +12,16 @@ RIOT_CONFIG_FILES=(
"$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
}
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
[[ $1 < 65536 && $1 > 0 ]] || return 1
return 0
}
@@ -59,8 +53,8 @@ get_server_meta() {
"$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
|| "$remote" =~ ^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}:[1-9][0-9]*$ # full ipv6 address with port
|| "$remote" =~ ^[0-9a-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port
]]; then
RET_PORT=${remote##*:}
remote=${remote%:*}
@@ -141,129 +135,6 @@ parse_remote() {
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() {
local output=""
for s in "${CMD[@]}"; do
@@ -289,10 +160,7 @@ eval_or_echo() {
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
$DO tmux send-keys -t $TMUX_SESS:$tmux_win $(printf '%s Space ' "${CMD[@]}") Enter
else
$DO "${CMD[@]}"
fi
@@ -435,147 +303,26 @@ remove_hostkey() {
# main
print_help()
{
local pager=(cat)
if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then
pager=(less -R)
fi
cat <<EOF | "${pager[@]}"
NAME
riot - connect to remote hosts using SSH presets
SYNOPSIS
${0##*/} [OPTION]... REMOTE [OPTION]... [COMMAND [COMMAND-ARG]...]
DESCRIPTION
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.
fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [command-args]"
cat <<EOF
available commands:
- ssh [ssh-command-args] (default)
- tmux [ssh-command-args] (run ssh in multiple tmux windows)
- git [git-command-args] (run git on remote machine)
- sshl [local-port:remote-host:]remote-port (ssh -L)
- sshd [local-port] (ssh -D)
- zssh [ssh-command-args]
- sftp
- scp source destination
- rm (remove host keys)
- ping/ping4/ping6 (ping the remote servers)
EOF
}
router() {
local positional=()
local arg=""
local option=""
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
while [[ $# > 0 ]]; do
case "$1" in
-h|--help )
print_help
exit 0
@@ -583,68 +330,45 @@ router() {
-t|--trust )
RIOT_TRUST_SERVER=1
;;
-g|--gpg )
GPG_FORWARD=1
;;
--tmux )
USE_TMUX=1
;;
-p|--password )
--password )
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 )
if [[ -n "$remaining" ]]; then
option_value=$remaining
arg=""
else
[[ $# -gt 0 ]] || fmt_fatal "option '-o' requires an argument"
option_value=$1
shift
fi
EXTRA_SSH_OPTIONS+=("-o" "$option_value")
EXTRA_SSH_OPTIONS+=("-o" "$2")
shift
;;
-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
shift
done
if [[ "${positional[2]}" == "--" ]]; then
positional=("${positional[@]:0:2}" "${positional[@]:3}")
fi
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
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
remote="${remotes[i]}"
local batch_func="${remote}.batch"
@@ -656,9 +380,6 @@ router() {
case "${positional[1]}" in
ssh|tmux|"" )
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
if [[ "$GPG_FORWARD" == "1" ]]; then
prepare_gpg_forwarding
fi
run_ssh ssh "${positional[@]:2}"
;;
git )
@@ -719,4 +440,4 @@ router() {
fi
}
router "$@"
router "${GOT_OPTS[@]}"

View File

@@ -1,378 +0,0 @@
#!/usr/bin/env bash
# Author: OpenAI Codex
# AI-assisted implementation, generated and reviewed with OpenAI Codex.
set -euo pipefail
# Keep glob expansion and sorting stable across locales, so -n remains
# repeatable when the service and filesystem contents have not changed.
scf_lc_all_was_set=false
scf_original_lc_all=""
if [[ -n ${LC_ALL+x} ]]; then
scf_lc_all_was_set=true
scf_original_lc_all=$LC_ALL
fi
export LC_ALL=C
restore_locale() {
if [[ $scf_lc_all_was_set == true ]]; then
export LC_ALL=$scf_original_lc_all
else
unset LC_ALL
fi
}
usage() {
cat >&2 <<'EOF'
Usage: scf [-r] [-n NUM] SERVICE
Open the configuration file referenced by a systemd service's ExecStart.
SERVICE may be given with or without the .service suffix.
-r recursively inspect referenced files and directories (maximum reference depth: 3)
-n open candidate NUM directly, without displaying the selection list
Environment:
SCF_MAX_CANDIDATES maximum number of candidates (default: 20)
SCF_MAX_DEPTH maximum reference depth (default: 3)
EOF
exit 2
}
recursive=false
selection_number=""
service=""
while (( $# > 0 )); do
argument=$1
shift
if [[ $argument == -- ]]; then
[[ -z $service && $# -eq 1 ]] || usage
service=$1
shift
break
fi
if [[ $argument == -* ]]; then
options=${argument#-}
[[ -n $options ]] || usage
while [[ -n $options ]]; do
option=${options:0:1}
options=${options:1}
case $option in
r) recursive=true ;;
n)
if [[ -n $options ]]; then
selection_number=$options
options=""
else
(( $# > 0 )) || usage
selection_number=$1
shift
fi
;;
*) usage ;;
esac
done
else
[[ -z $service ]] || usage
service=$argument
fi
done
if [[ -z $service ]]; then
usage
fi
interactive=false
if [[ -t 0 && -t 1 && -t 2 ]]; then
interactive=true
fi
# Prompts and candidate details are written to stderr. Keep pipelines and
# redirected output free of escape sequences.
if [[ $interactive == true ]]; then
color_item=$'\033[1;36m'
color_detail=$'\033[2;37m'
color_prompt=$'\033[1;33m'
color_reset=$'\033[0m'
else
color_item=""
color_detail=""
color_prompt=""
color_reset=""
fi
if [[ -n $selection_number && ! $selection_number =~ ^[0-9]+$ ]]; then
echo "scf: invalid candidate number: $selection_number" >&2
exit 2
fi
if [[ -n $selection_number ]]; then
# Bash arithmetic treats numbers with a leading zero as octal.
selection_number=$((10#$selection_number))
if (( selection_number < 1 )); then
echo "scf: invalid candidate number: 0" >&2
exit 2
fi
fi
service_argument=$service
if [[ $service != *.service ]]; then
service+=.service
fi
# Keep the argument a unit name, rather than allowing it to become a
# systemctl option. This also makes accidental shell syntax harmless.
if [[ ! $service =~ ^[A-Za-z0-9_.@:%+-]+\.service$ ]]; then
echo "scf: invalid service name: $service_argument" >&2
exit 2
fi
if ! exec_start=$(systemctl show --no-pager --property=ExecStart --value "$service"); then
echo "scf: cannot inspect $service" >&2
exit 1
fi
if [[ -z $exec_start || $exec_start == "{}" ]]; then
echo "scf: $service has no ExecStart" >&2
exit 1
fi
# Keep recursion bounded even when configuration files refer to each other.
max_candidates=${SCF_MAX_CANDIDATES:-20}
max_reference_depth=${SCF_MAX_DEPTH:-3}
if [[ ! $max_candidates =~ ^[0-9]+$ ]]; then
echo "scf: SCF_MAX_CANDIDATES must be a positive integer" >&2
exit 2
fi
if [[ ! $max_reference_depth =~ ^[0-9]+$ ]]; then
echo "scf: SCF_MAX_DEPTH must be a non-negative integer" >&2
exit 2
fi
# Normalize user-provided decimal values before using Bash arithmetic.
max_candidates=$((10#$max_candidates))
max_reference_depth=$((10#$max_reference_depth))
if (( max_candidates < 1 )); then
echo "scf: SCF_MAX_CANDIDATES must be a positive integer" >&2
exit 2
fi
declare -A candidate_seen=()
declare -A candidate_depth=()
declare -A candidate_source=()
declare -A candidate_source_detail=()
config_paths=()
other_paths=()
recursive_paths=()
recursive_depths=()
declare -A recursive_seen=()
path_pattern="/[^[:space:];,'\"})]+"
extract_paths() {
grep -a -oE "$path_pattern" || true
}
has_config_extension() {
local path=$1
[[ $path =~ \.(conf|cfg|cnf|ini|json|toml|ya?ml|xml|properties)$ ]]
}
is_config_path() {
local path=$1
has_config_extension "$path" \
|| [[ $path == *config* || $path == *configuration* ]]
}
add_candidate() {
local path=$1
local depth=$2
local source=$3
local source_detail=${4:-}
[[ -f $path ]] || return 1
[[ -n ${candidate_seen[$path]+seen} ]] && return 0
(( ${#candidate_seen[@]} < max_candidates )) || return 1
candidate_seen[$path]=1
candidate_depth[$path]=$depth
candidate_source[$path]=$source
candidate_source_detail[$path]=$source_detail
if is_config_path "$path"; then
config_paths+=("$path")
else
other_paths+=("$path")
fi
}
enqueue_recursive() {
local path=$1
local depth=$2
[[ $recursive == true ]] || return 0
(( depth <= max_reference_depth )) || return 0
[[ -f $path ]] || return 0
[[ -n ${recursive_seen[$path]+seen} ]] && return 0
recursive_seen[$path]=1
recursive_paths+=("$path")
recursive_depths+=("$depth")
}
expand_directory() {
local directory=$1
local depth=$2
local source=$3
local path
local entries
[[ $recursive == true && -d $directory ]] || return 0
# dotglob also catches hidden configuration files. nullglob prevents the
# literal pattern from being treated as a candidate for an empty directory.
shopt -s dotglob nullglob
entries=("$directory"/*)
shopt -u dotglob nullglob
for path in "${entries[@]}"; do
# A service's configuration often contains runtime directories such as
# Python's bin directory. Only configuration-looking files from a
# directory should consume the candidate budget.
has_config_extension "$path" || continue
add_candidate "$path" "$depth" "$source" "directory $directory" || continue
enqueue_recursive "$path" "$depth"
done
}
consider_initial_path() {
local path=$1
if [[ -f $path ]]; then
add_candidate "$path" 0 "ExecStart" || true
elif [[ -d $path ]]; then
expand_directory "$path" 0 "ExecStart"
fi
}
# systemctl's ExecStart value contains both `path=` and `argv[]=`. Pull out
# absolute path tokens, then drop the first one (the executable itself).
# NixOS-generated configuration files are normally immutable regular files in
# /nix/store, but the same also works for /etc and /run paths.
mapfile -t initial_paths < <(
printf '%s\n' "$exec_start" \
| extract_paths \
| awk '!seen[$0]++'
)
if (( ${#initial_paths[@]} > 0 )); then
initial_paths=("${initial_paths[@]:1}")
fi
for path in "${initial_paths[@]}"; do
consider_initial_path "$path"
done
paths=("${config_paths[@]}")
if (( ${#paths[@]} == 0 )); then
paths=("${other_paths[@]}")
fi
if [[ $recursive == true ]]; then
for path in "${paths[@]}"; do
enqueue_recursive "$path" 0
done
recursive_index=0
while (( recursive_index < ${#recursive_paths[@]} )); do
path=${recursive_paths[recursive_index]}
depth=${recursive_depths[recursive_index]}
recursive_index=$((recursive_index + 1))
(( depth < max_reference_depth )) || continue
next_depth=$((depth + 1))
mapfile -t referenced_paths < <(extract_paths < "$path")
for referenced_path in "${referenced_paths[@]}"; do
if [[ -f $referenced_path ]]; then
add_candidate "$referenced_path" "$next_depth" "$path" || continue
enqueue_recursive "$referenced_path" "$next_depth"
elif [[ -d $referenced_path ]]; then
expand_directory "$referenced_path" "$next_depth" "$path"
fi
done
done
paths=("${config_paths[@]}")
if (( ${#paths[@]} == 0 )); then
paths=("${other_paths[@]}")
fi
fi
if (( ${#paths[@]} == 0 )); then
echo "scf: no existing configuration path found in ExecStart of $service" >&2
echo "ExecStart: $exec_start" >&2
exit 1
fi
if [[ $interactive == false && -z $selection_number ]]; then
selection_number=1
fi
if [[ -n $selection_number ]]; then
if (( selection_number > ${#paths[@]} )); then
echo "scf: candidate number $selection_number is out of range (1-${#paths[@]})" >&2
exit 2
fi
config_path=${paths[selection_number - 1]}
elif (( ${#paths[@]} > 1 )); then
declare -A displayed_indices=()
for i in "${!paths[@]}"; do
displayed_indices["${paths[i]}"]=$((i + 1))
done
printf '%sscf: multiple possible configuration files for %s:%s\n' \
"$color_prompt" "$service" "$color_reset" >&2
for i in "${!paths[@]}"; do
path=${paths[i]}
source=${candidate_source[$path]}
if [[ $source == ExecStart ]]; then
source_label=ExecStart
elif [[ -n ${displayed_indices[$source]+seen} ]]; then
source_label="#${displayed_indices[$source]}"
else
source_label=$source
fi
source_detail=${candidate_source_detail[$path]}
if [[ -n $source_detail ]]; then
source_label+=" ($source_detail)"
fi
printf '%s %d) %s%s\n' \
"$color_item" "$((i + 1))" "$path" "$color_reset" >&2
printf '%s depth: %d; derived from: %s%s\n' \
"$color_detail" "${candidate_depth[$path]}" "$source_label" "$color_reset" >&2
done
read -r -p "${color_prompt}Select a file [1]: ${color_reset}" selection
selection=${selection:-1}
if [[ ! $selection =~ ^[0-9]+$ ]]; then
echo "scf: invalid selection" >&2
exit 2
fi
selection=$((10#$selection))
if (( selection < 1 || selection > ${#paths[@]} )); then
echo "scf: invalid selection" >&2
exit 2
fi
config_path=${paths[selection - 1]}
else
config_path=${paths[0]}
fi
if [[ $interactive == false ]]; then
restore_locale
exec cat -- "$config_path"
fi
editor=${VISUAL:-${EDITOR:-vim}}
read -r -a editor_command <<< "$editor"
if (( ${#editor_command[@]} == 0 )); then
editor_command=(vim)
fi
restore_locale
exec "${editor_command[@]}" "$config_path"

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

View File

@@ -154,7 +154,7 @@ check_pinentry()
fi
}
prepare_gpg_agent()
use_gpg_agent()
{
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"
@@ -167,11 +167,6 @@ prepare_gpg_agent()
gpgconf --launch gpg-agent
gpg-connect-agent updatestartuptty /bye > /dev/null
}
use_gpg_agent()
{
prepare_gpg_agent
local agent_socket
agent_socket=$(gpgconf --list-dirs agent-ssh-socket)
@@ -181,37 +176,10 @@ use_gpg_agent()
fmt_note "using gpg-agent: $agent_socket"
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"
}
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()
{
local agent_file="$1"
@@ -322,9 +290,6 @@ route()
gpg)
use_gpg_agent
;;
gpg-pin)
cache_gpg_pin "${@:2}"
;;
reset)
reset
;;

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
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'
eval set -- $OPTS
@@ -8,14 +8,14 @@ THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh"
test "${GOT_OPTS[*]}" = "$TARGET_OPTS"
test $# -eq 9
test $# -eq 8
test "$*" = "${OPTS//\"/}"
test "$DFS_LITE" = "1"
is_tty
test "$DFS_QUIET" = "1"
test -z "$DFS_QUIET"
set +x
echo "test passed, args:"
for i in "${GOT_OPTS[@]}"; do
echo "$i"
done
done

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

@@ -1,83 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SCF="$THIS_DIR/../scripts/scf"
TEST_DIR=$(mktemp -d /tmp/scf.XXXXXX)
trap 'rm -rf "$TEST_DIR"' EXIT
MOCK_BIN="$TEST_DIR/bin"
DATA_DIR="$TEST_DIR/data"
MOCK_SYSTEMCTL_LOG="$TEST_DIR/systemctl.log"
MOCK_MAIN="$DATA_DIR/main.yaml"
MOCK_CHILD="$DATA_DIR/child.conf"
MOCK_NOTE="$DATA_DIR/notes.txt"
mkdir -p "$MOCK_BIN" "$DATA_DIR"
export MOCK_SYSTEMCTL_LOG MOCK_MAIN MOCK_CHILD MOCK_NOTE
printf 'include %s\n' "$MOCK_CHILD" > "$MOCK_MAIN"
printf 'child configuration\n' > "$MOCK_CHILD"
printf 'not a configuration candidate\n' > "$MOCK_NOTE"
cat > "$MOCK_BIN/systemctl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$MOCK_SYSTEMCTL_LOG"
service=${@: -1}
case "$service" in
demo.service)
printf 'path=/usr/bin/demo ; argv[0]=demo ; argv[1]=%s ; argv[2]=%s ;\n' \
"$MOCK_MAIN" "$MOCK_NOTE"
;;
empty.service)
printf 'path=/usr/bin/empty ; argv[0]=empty ;\n'
;;
*)
exit 1
;;
esac
EOF
chmod +x "$MOCK_BIN/systemctl"
run_scf() {
PATH="$MOCK_BIN:$PATH" "$SCF" "$@"
}
# A service name without .service is accepted, and non-interactive mode prints
# the only configuration candidate instead of opening an editor.
output=$(run_scf demo 2> "$TEST_DIR/stderr")
grep -Fxq 'include '"$MOCK_CHILD" <<< "$output"
grep -Fq -- '--property=ExecStart --value demo.service' "$MOCK_SYSTEMCTL_LOG"
# Recursive mode discovers configuration files referenced by the first one.
output=$(run_scf -r -n 2 demo)
grep -Fxq 'child configuration' <<< "$output"
# Decimal values with a leading zero must not be treated as invalid octal.
output=$(SCF_MAX_DEPTH=08 SCF_MAX_CANDIDATES=08 run_scf demo)
grep -Fxq 'include '"$MOCK_CHILD" <<< "$output"
if run_scf -n 08 demo > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected scf -n 08 to fail because only one candidate exists' >&2
exit 1
fi
grep -Fq 'candidate number 8 is out of range' "$TEST_DIR/stderr"
# Invalid service names are rejected before systemctl is invoked.
before=$(wc -l < "$MOCK_SYSTEMCTL_LOG")
if run_scf 'demo; touch /tmp/unexpected' > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected an invalid service name to fail' >&2
exit 1
fi
grep -Fq 'invalid service name' "$TEST_DIR/stderr"
after=$(wc -l < "$MOCK_SYSTEMCTL_LOG")
test "$before" -eq "$after"
# A service without a referenced file reports a useful failure.
if run_scf empty > "$TEST_DIR/out" 2> "$TEST_DIR/stderr"; then
echo 'expected a service without a config path to fail' >&2
exit 1
fi
grep -Fq 'no existing configuration path found' "$TEST_DIR/stderr"
echo 'scf tests passed'

View File

@@ -34,18 +34,15 @@ dogo
doll
dfs cd
tools/test-getopts.sh
tools/test-scf.sh
tools/test-riot-gpg.sh
tools/test-sagent-gpg-pin.sh
tools/common.sh get_os_name
test $(echo y | tools/common.sh ask_for_yN "test") = "1"
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") = "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'
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
alias p114