21 Commits

Author SHA1 Message Date
70090242be feat: remove frigg-client.log 2025-04-22 17:12:26 +08:00
86088ecfb1 build(ci): update to ubuntu-latest and Yikun/hub-mirror-action@v1.5 2025-04-22 17:05:06 +08:00
b02f249f68 feat(riot): rm - remove host keys 2025-04-22 17:00:14 +08:00
b005624be2 feat(ssh): remove keys ltp1-bd and ltp1 2025-02-28 13:55:43 +08:00
f276f0719f fix(ci): test of auto-dep 2025-02-22 19:20:54 +08:00
270da9ca42 fix(sagent): error note 2025-02-22 19:09:22 +08:00
4af1aa4a4d feat(sagent): sagt op 2025-02-22 18:51:36 +08:00
38468291fc feat(ssh): update keys 2025-02-11 03:15:07 +08:00
0c02851c7b feat: revert git email 2025-01-20 22:06:50 +08:00
ed745c4ea2 feat: update email 2025-01-17 21:29:31 +08:00
21e9b7854c fix(ci): update macos image 2024-12-22 13:42:57 +08:00
5318740ed4 fix(riot): scp, and ci
note that riot still has problems with spaces. we should use array to
handle parameters.
2024-12-22 13:38:44 +08:00
fb5804faef feat(zshrc): use() 2024-12-22 12:18:28 +08:00
4c9cb23fb6 fix(riot): tmux use bash 2024-11-12 18:33:26 +08:00
12d77a1e01 fix(riot): SSH_OPTIONS 2024-11-12 18:21:46 +08:00
9d51db1c5b feat(riot): -o RequestTTY=yes 2024-11-12 18:17:53 +08:00
1215d4c352 feat(riot): run command in tmux window(s)
note that spaces in ssh commands are still not supported
2024-11-12 17:06:49 +08:00
7946656a7e feat(riot): *.batch 2024-11-12 15:03:56 +08:00
1b32deeea1 feat(riot-config): remove tailing dot from .domain; remove j.remote and x.domain 2024-11-12 11:45:23 +08:00
ba82ac68db fix(ci) 2024-09-21 10:32:26 +08:00
f1a77adb0c feat(riot-config): default port 12022 2024-09-21 10:15:37 +08:00
24 changed files with 215 additions and 1417 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
@@ -87,4 +128,4 @@ jobs:
shell: /bin/zsh -ileo PIPE_FAIL {0} shell: /bin/zsh -ileo PIPE_FAIL {0}
run: | run: |
dfs cd dfs cd
source tools/test.zsh source tools/test.zsh

View File

@@ -2,16 +2,12 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGakUcSuNhuH7tk+Nj9gDfRtxYfGPL9yb8toQ/EpSJAM openpgp:0xD9271B01 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGakUcSuNhuH7tk+Nj9gDfRtxYfGPL9yb8toQ/EpSJAM openpgp:0xD9271B01
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJ4FeeBwPcUe25JsZQ7xDmGtFVeO1TQ9332IPjFY0Xm openpgp:0x8774BF70
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUN7IXF4nlFcVfgHesgik3LIAiXlVMYJPm3yD13EVarQx5jqdBgk8Dwgkgf4rPO6MFpvIpinOyEO8zOS6HHQrCLZUv5yTFaDkUuB7eQ0EmpicGbmk9bHqj1HkOZxaobkpEfQUmFKYvkp4EexVw66sO0qfXvjHZ4H6yCAJLK5aUnKfgrE8tODzP82sU/mpJjW+Pq3uanNq754gaHwhxCIXG143/zp8qzBAeKe38xVqqDq9fTkG4hvzFvkRdS88i6l1z++0P3n0HGdOjtSg7P7fO7+7ZyPYr0gO5vB720Om/zxqPrGd9cicWi4P+aVKa+0ujWH/pqufWG6uCjKWHnBs7 sk0/piv/9a ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUN7IXF4nlFcVfgHesgik3LIAiXlVMYJPm3yD13EVarQx5jqdBgk8Dwgkgf4rPO6MFpvIpinOyEO8zOS6HHQrCLZUv5yTFaDkUuB7eQ0EmpicGbmk9bHqj1HkOZxaobkpEfQUmFKYvkp4EexVw66sO0qfXvjHZ4H6yCAJLK5aUnKfgrE8tODzP82sU/mpJjW+Pq3uanNq754gaHwhxCIXG143/zp8qzBAeKe38xVqqDq9fTkG4hvzFvkRdS88i6l1z++0P3n0HGdOjtSg7P7fO7+7ZyPYr0gO5vB720Om/zxqPrGd9cicWi4P+aVKa+0ujWH/pqufWG6uCjKWHnBs7 sk0/piv/9a
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGlEme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8Vr6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmxT+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCAr sk1/piv/9a
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA71ukY9nzzeAGp9WYstPYiIwexzIANlPwMQ68QNQwqxAAAABHNzaDo= sk1/fido2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBN24pvN1RMN+iSZyHPdyExA1Rvt8pdr3e6ih6iX+KrQ ltp2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLYgVj+NPino6sOmahULN7SbAMaVAgzqPfDjz2S8zDv pc1 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLYgVj+NPino6sOmahULN7SbAMaVAgzqPfDjz2S8zDv pc1
@@ -20,4 +16,5 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF52mPmbLEriZ+DfmrMHFtvg2kiO2JpRpitvs7PbJowh
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMXu7UND6N1EKcbeSeAj2CwzGA3sM5/bRtLwoY0euWdR4vzB0nwcwd6nct5JCEyozjujJfMt8d7oCzhSt1w/1cU= ip17/sep ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC5gu+U4picxVvCNYjLlmxGLpJl7svj3LklO7jSpwqdrNxlE+/xkx07PJx76AbA77dDM1Dxmm/VOvVQCKTRLTgk= ip14/sep

View File

@@ -1,5 +1,4 @@
set -g prefix ^a set -g prefix ^a
bind ^a send-prefix
set -g mouse on set -g mouse on
set -g set-clipboard on set -g set-clipboard on
set -g default-terminal "xterm-256color" set -g default-terminal "xterm-256color"
@@ -25,20 +24,9 @@ bind - split-window -v -c "#{pane_current_path}"
bind | split-window -h -c "#{pane_current_path}" bind | split-window -h -c "#{pane_current_path}"
# other key bindings # other key bindings
## prefix r: reload conf
bind r source-file ~/.tmux.conf \; display-message "tmux.conf reloaded" bind r source-file ~/.tmux.conf \; display-message "tmux.conf reloaded"
## prefix m: toggle mouse mode
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' 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}" 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
## 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
## ESC: exit copy mode
bind -T copy-mode-vi Escape send-keys -X cancel
# styles # styles
# status bar # status bar

40
.zshrc2
View File

@@ -9,7 +9,6 @@ else
fi fi
export DOTFILES=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) export DOTFILES=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
export PATH="$PATH:$DOTFILES/scripts" export PATH="$PATH:$DOTFILES/scripts"
fpath=($DOTFILES/functions $fpath)
export EDITOR='vim' export EDITOR='vim'
# env for apps # env for apps
export PYTHONIOENCODING='UTF-8' export PYTHONIOENCODING='UTF-8'
@@ -70,7 +69,6 @@ ANTIGEN_PLUGINS+=(
"fzf" "fzf"
"git" "git"
"magic-enter" "magic-enter"
"man"
"per-directory-history" "per-directory-history"
"pip" "pip"
"podman" "podman"
@@ -80,7 +78,7 @@ ANTIGEN_PLUGINS+=(
"systemd" "systemd"
"timer" "timer"
"tmux" "tmux"
"web-search" "ufw"
"z" "z"
) )
for i in ${ANTIGEN_PLUGINS[*]}; do for i in ${ANTIGEN_PLUGINS[*]}; do
@@ -114,11 +112,13 @@ fi
alias "pls"='sudo $(fc -ln -1)' alias "pls"='sudo $(fc -ln -1)'
alias "se"='sudo -sE' alias "se"='sudo -sE'
alias "sl"='sudo zsh -l' alias "sl"='sudo zsh -l'
alias "cps"='rsync -avh --info=progress2'
alias "mvs"='rsync -avh --info=progress2 --remove-source-files'
if [[ "$DFS_OS_TYPE" == "linux" ]]; then alias "ping"='ping -n'; alias "ping6"='ping6 -n'; fi if [[ "$DFS_OS_TYPE" == "linux" ]]; then alias "ping"='ping -n'; alias "ping6"='ping6 -n'; fi
if [[ "$DFS_OS_TYPE" == "msys" ]]; then alias "tmux"='script -qO /dev/null -c "tmux -u"'; fi if [[ "$DFS_OS_TYPE" == "msys" ]]; then alias "tmux"='script -qO /dev/null -c "tmux -u"'; fi
alias "pbd"='ping baidu.com' alias "pbd"='ping baidu.com'
alias "p114"='ping 114.114.114.114' alias "p114"='ping 114.114.114.114'
alias "p666"='ping6 240c::6666' alias "p666"='ping6 2001:da8::666'
alias "cbd"='curl http://www.baidu.com' alias "cbd"='curl http://www.baidu.com'
alias "cbds"='curl https://www.baidu.com' alias "cbds"='curl https://www.baidu.com'
alias "gdebug"='git add -A; git commit --allow-empty -m "bug fix ($(date))"' alias "gdebug"='git add -A; git commit --allow-empty -m "bug fix ($(date))"'
@@ -129,35 +129,9 @@ alias "jce"='jc -e'
alias "jceu"='jc -eu' alias "jceu"='jc -eu'
alias "jcf"='jc -f' alias "jcf"='jc -f'
alias "jcfu"='jc -fu' alias "jcfu"='jc -fu'
alias "sc"='systemctl' gbes() { git for-each-ref --sort=-committerdate refs/heads refs/remotes --format="%(authordate:format:%y-%m-%d.%a %H:%M %z)|%(color:red)%(objectname:short)|%(color:yellow)%(refname:short)%(color:reset)|%(color:reset)%(authorname): %(color:green)%(subject)" --color=always | column -ts"|" | less -FX }
alias "t"='tmux' sagt() { eval "$($DOTFILES/tools/sagent.sh $@)" }
gbes() { git for-each-ref --sort=-committerdate refs/heads refs/remotes --format="%(authordate:format:%y-%m-%d.%a %H:%M %z)|%(color:red)%(objectname:short)|%(color:yellow)%(refname:short)%(color:reset)|%(color:reset)%(authorname): %(color:green)%(subject)" --color=always | column -ts"|" | less -FX ; } use() { nix --experimental-features nix-command --extra-experimental-features flakes shell "${(*)@/#%(#b)([^#]#)/nixpkgs#$match}" }
sagt() {
local output
output="$("$DOTFILES/tools/sagent.sh" "$@")" || return $?
eval "$output"
}
## nixos
use() { nix --experimental-features nix-command --extra-experimental-features flakes shell "${(*)@/#%(#b)([^#]#)/nixpkgs#$match}" ; }
bnd() {
if [[ -n "$1" && -d "./hosts/$1" ]]; then
nixos-rebuild --flake .#$1 build || return $?
nix-diff /opt/NixOS-Config/results/hosts/$1 ./result --color=always | less
else
echo host $1 does not exist
fi
}
nor() {
test -n "$1" || { echo fatal: no verb given; return 1; }
nixos-rebuild "$1" --flake .${2:+#}$2
}
alias 'nors'='nor switch'
alias 'norbt'='nor boot'
alias 'nort'='nor test'
alias 'norb'='nor build'
alias 'nordb'='nor dry-build'
alias 'norda'='nor dry-activate'
# key bindings # key bindings
bindkey "^b" beginning-of-line # ctrl+b bindkey "^b" beginning-of-line # ctrl+b

View File

@@ -1,58 +1 @@
# dotfiles # 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.

View File

@@ -1,99 +0,0 @@
#compdef beam
# =============================================================================
# Beam-Go Zsh Completion Script
# =============================================================================
_beam() {
local context state state_descr line
typeset -A opt_args
_arguments -C \
'1: :_beam_commands' \
'*:: :->args'
case $state in
(args)
case $line[1] in
(serve) _beam_serve ;;
(add) _beam_add ;;
(list) _beam_list ;;
(del) _beam_del ;;
esac
;;
esac
}
# --- Subcommand Definitions ---
(( $+functions[_beam_commands] )) ||
_beam_commands() {
local -a commands
commands=(
'serve:Start the beam server daemon'
'add:Share a file or directory'
'list:List active shares'
'del:Delete a share by path or code'
)
_describe -t commands 'beam command' commands
}
_beam_serve() {
_arguments \
'(-d --database)'{-d,--database}'=[Database directory (Required)]:directory:_path_files -/' \
'(-p --port)'{-p,--port}'=[Port to listen on (default :8280)]:port:' \
'(-s --socket)'{-s,--socket}'=[Socket path]:socket path:_files' \
'--subpath=[Enable subpath sharing]'
}
_beam_add() {
_arguments \
'(-d --days)'{-d,--days}'=[Expiration days (default 7)]:days:' \
'(-s --socket)'{-s,--socket}'=[Socket path]:socket path:_files' \
'1:file to share:_files'
}
_beam_list() {
_arguments \
'(-s --socket)'{-s,--socket}'=[Socket path]:socket path:_files'
}
_beam_del() {
_arguments \
'(-s --socket)'{-s,--socket}'=[Socket path]:socket path:_files' \
'(-c --code)'{-c,--code}'=[Share code]:share code:_beam_active_codes' \
'1:shared path:_files'
}
# --- Helpers ---
# 动态获取当前的 Share Code
# 它会尝试读取用户当前输入的 -s 参数,去连接正确的 socket
_beam_active_codes() {
local socket_path=""
# 从 Zsh 解析的参数中提取 socket 路径
if [[ -n ${opt_args[-s]} ]]; then
socket_path=${opt_args[-s]}
elif [[ -n ${opt_args[--socket]} ]]; then
socket_path=${opt_args[--socket]}
fi
# 构造查询命令
local cmd_args=()
cmd_args+=("list")
[[ -n $socket_path ]] && cmd_args+=("-s" "$socket_path")
# 调用 beam list,跳过前两行表头,提取 Code($1) 和 Path($3)
# 2>/dev/null 防止服务未启动时报错打印到终端
local -a shares
shares=("${(@f)$(beam "${cmd_args[@]}" 2>/dev/null | awk 'NR>2 {print $1":"$2" "$3" "$4}')}")
if [[ ${#shares} -gt 0 ]]; then
_describe -t shares 'active share' shares
else
_message 'no active shares or server not running'
fi
}
_beam "$@"

View File

@@ -1,35 +0,0 @@
#compdef beaml
_beaml() {
local context state line
typeset -A opt_args
_arguments -C \
'1: :->cmds' \
'2: :->args'
case $state in
cmds)
local commands; commands=(
'add:Add a new item'
'del:Delete an item'
'list:List all items'
)
_describe -t commands 'beaml commands' commands
;;
args)
case $line[1] in
add)
_files
;;
del)
local -a items
items=( /pss/lite/*(N:t) )
_describe -t items 'item to delete' items
;;
esac
;;
esac
}
_beaml "$@"

View File

@@ -1,3 +0,0 @@
#compdef bnd nor
compadd -- /root/NixOS-Config/hosts/*(/:t)

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 +0,0 @@
#compdef sagt
compadd -- gpg gpg-pin kill ls op piv reset

View File

@@ -1,3 +0,0 @@
#compdef use
compadd -- $(nix eval --impure --expr 'builtins.attrNames ( (import <nixpkgs>) {} )' 2>/dev/null | tr '[]"' ' ')

View File

@@ -25,7 +25,7 @@ nasps.batch() {
# remotes # remotes
nasp.remote() { nasp.remote() {
remote=jump.nasp.fit remote=nasp.fit
RET_PORT=${RET_PORT:-36022} RET_PORT=${RET_PORT:-36022}
RET_USERNAME=${RET_USERNAME:-root} RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1 RET_TRUST_SERVER=1
@@ -39,11 +39,14 @@ nasp.remote() {
} }
dxng.domain() { dxng.domain() {
if [[ "$host" =~ ^sed([0-9]{1,2})$ ]]; then RET_HOSTNAME=$host.dxng.net
RET_HOSTNAME=192.168.98.$((100+BASH_REMATCH[1])) RET_PORT=${RET_PORT:-12022}
else RET_USERNAME=${RET_USERNAME:-root}
RET_HOSTNAME=$host.dxng.net RET_TRUST_SERVER=1
fi }
i.domain() {
RET_HOSTNAME=$host.ibd.ink
RET_PORT=${RET_PORT:-12022} RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root} RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1 RET_TRUST_SERVER=1
@@ -60,7 +63,7 @@ nasp.domain() {
RET_HOSTNAME=$host RET_HOSTNAME=$host
RET_PORT=${RET_PORT:-12022} RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root} RET_USERNAME=${RET_USERNAME:-root}
RET_JUMP_SERVER="ssh@jump.nasp.fit:36022" RET_JUMP_SERVER="ssh@nasp.fit:36022"
RET_TRUST_SERVER=1 RET_TRUST_SERVER=1
} }

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=() RIOT_EXTRA_OPTIONS=""
GPG_FORWARD=0
# config # config
RIOT_CONFIG_FILES=( RIOT_CONFIG_FILES=(
@@ -15,25 +12,22 @@ 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 if [[ -f "$file" ]]; then
for file in "${RIOT_CONFIG_FILES[@]}"; do source "$file"
if [[ -f "$file" ]]; then fi
source "$file" done
fi
done
}
# check if port number valid # check if port number valid
check_port() { check_port() {
[[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1 ( echo $1 | grep -qxE "[1-9][0-9]{0,4}" ) || return 1
[[ $1 -lt 65536 && $1 -gt 0 ]] || return 1 test $1 -lt 65536 -a $1 -gt 0 || return 1
return 0 return 0
} }
# check if username valid # check if username valid
check_username() { check_username() {
[[ "$1" =~ ^[a-z][-a-z0-9_]*$ ]] || return 1 ( echo $1 | grep -qxE "^[a-z][-a-z0-9_]*\$" ) || return 1
return 0 return 0
} }
@@ -48,28 +42,18 @@ get_server_meta() {
RET_JUMP_SERVER="" # optional RET_JUMP_SERVER="" # optional
# body # body
local remote="$1" local remote="$1"
# extract username from user@... # if in the form user@...
if [[ "$remote" == *@* ]]; then if [[ "$remote" == *@* ]]; then
RET_USERNAME=${remote%%@*} RET_USERNAME=${remote%%@*}
remote=${remote#*@} remote=${remote#*@}
check_username $RET_USERNAME || fmt_warning \"$RET_USERNAME\" is not a valid unix username check_username $RET_USERNAME || fmt_warning \"$RET_USERNAME\" is not a valid unix username
fi fi
# extract port from ...:port # if in the form ...:22
if [[ if [[ "$remote" == "["*"]":* || ( "$remote" != "["*"]" && "$remote" == *:* ) ]]; then
"$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##*:} RET_PORT=${remote##*:}
remote=${remote%:*} remote=${remote%:*}
check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\" check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\"
fi fi
# remove square brackets
if [[ "$remote" =~ ^\[.*\]$ ]]; then
remote=${remote:1:-1}
fi
# presets -- match remote # presets -- match remote
local remote_func="$remote.remote" local remote_func="$remote.remote"
if is_function "$remote_func"; then if is_function "$remote_func"; then
@@ -79,8 +63,8 @@ get_server_meta() {
RET_HOSTNAME=${remote} RET_HOSTNAME=${remote}
local domain=${remote##*.} local domain=${remote##*.}
local host=${remote%.*} local host=${remote%.*}
# if it contains no dot and is not ipv6 # if there's no dot
if [[ "$remote" != *.* && "$remote" != *:* ]]; then if [[ "$host" == "$domain" && "$host" != "["*"]" ]]; then
domain="default" domain="default"
fi fi
local domain_func="$domain.domain" local domain_func="$domain.domain"
@@ -99,15 +83,12 @@ parse_remote() {
TRUST_SERVER=1 TRUST_SERVER=1
PORT="" # optional PORT="" # optional
USERNAME="" # optional USERNAME="" # optional
SSH_OPTIONS=("-o" "ServerAliveInterval=60") SSH_OPTIONS="-o RequestTTY=yes" # optional
if [[ -t 1 ]]; then
SSH_OPTIONS+=("-o" "RequestTTY=yes")
fi
if [[ "$RIOT_TRUST_CLIENT" == "1" ]]; then if [[ "$RIOT_TRUST_CLIENT" == "1" ]]; then
SSH_OPTIONS+=("-o" "PermitLocalCommand=yes") SSH_OPTIONS="$SSH_OPTIONS -o PermitLocalCommand=yes"
if [[ "$(get_os_type)" != "msys" ]]; then if [[ "$(get_os_type)" != "msys" ]]; then
test "$DFS_DRY_RUN" = "1" || mkdir -p ~/.ssh/master-socket test "$DFS_DRY_RUN" = "1" || mkdir -p ~/.ssh/master-socket
SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPersist=5s" "-o" "ControlPath=~/.ssh/master-socket/%C") SSH_OPTIONS="$SSH_OPTIONS -o ControlMaster=auto -o ControlPath=~/.ssh/master-socket/%C"
fi fi
fi fi
# handle input # handle input
@@ -134,171 +115,31 @@ parse_remote() {
done done
# construct cmd # construct cmd
if [[ "$RIOT_TRUST_SERVER" == "1" || "$TRUST_SERVER" == "1" ]]; then if [[ "$RIOT_TRUST_SERVER" == "1" || "$TRUST_SERVER" == "1" ]]; then
SSH_OPTIONS+=("-o" "ForwardX11=yes" "-o" "ForwardAgent=yes") SSH_OPTIONS="$SSH_OPTIONS -o ForwardX11=yes -o ForwardAgent=yes"
fi fi
if [[ -n "$jump_servers" ]]; then if [[ -n "$jump_servers" ]]; then
SSH_OPTIONS+=("-o" "ProxyJump=$jump_servers") SSH_OPTIONS="$SSH_OPTIONS -o ProxyJump=$jump_servers"
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
# A pre-existing multiplex master cannot apply this connection's -R
# forwarding request. Put this first because OpenSSH keeps the first
# value for repeated options, and force a dedicated connection.
SSH_OPTIONS=("-o" "ControlMaster=no" "${SSH_OPTIONS[@]}")
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() { eval_or_echo() {
local cmd="$@"
local DO="" local DO=""
tmux_win=${tmux_win:-0}
if [[ "$DFS_DRY_RUN" == "1" ]]; then if [[ "$DFS_DRY_RUN" == "1" ]]; then
DO=echo DO=echo
fi fi
if [[ "$USE_TMUX" == "1" ]]; then if [[ "$RIOT_USE_TMUX" == "1" ]]; then
if [[ -z "$TMUX_SESS" ]]; then if [[ -z "$RIOT_TMUX_SESS" ]]; then
TMUX_SESS=riot-$(date +%s) RIOT_TMUX_SESS=riot-$(date +%s)
$DO tmux new-session -d -s $TMUX_SESS bash -l RIOT_TMUX_WIN=0
$DO tmux new-session -d -s $RIOT_TMUX_SESS bash -l
else else
tmux_win=$((tmux_win+1)) RIOT_TMUX_WIN=$((RIOT_TMUX_WIN+1))
$DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l $DO tmux new-window -t $RIOT_TMUX_SESS:$RIOT_TMUX_WIN -d bash -l
fi fi
local command $DO tmux send-keys -t $RIOT_TMUX_SESS:$RIOT_TMUX_WIN "$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
} }
@@ -310,83 +151,44 @@ prepare_ssh_cmd() {
else else
local port_param='-p' local port_param='-p'
fi fi
CMD=( echo "$ssh_bin ${PORT:+$port_param} $PORT $SSH_OPTIONS $RIOT_EXTRA_OPTIONS $SCP_SRC $USERNAME${USERNAME:+@}$SERVER $SCP_DST ${@:2}"
"$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 # ssh
run_ssh() run_ssh()
{ {
prepare_ssh_cmd "$@" local cmd="$(prepare_ssh_cmd $@)"
print_cmd fmt_note "-->" $cmd
eval_or_echo eval_or_echo $cmd
} }
# sshl # sshl
run_sshl() run_sshl()
{ {
local arg left right localsock access local arg="$1"
local res="${1//[^:]}" if [[ "$arg" != *":"* ]]; then
local lorr="-L" # treat as a port number
if [[ "${FUNCNAME[1]}" == "run_sshr" ]]; then arg=localhost:$arg
lorr="-R"
fi fi
if [[ ${#res} -eq 2 ]]; then local port=$(get_free_port)
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 SSH_OPTIONS="$SSH_OPTIONS -NC -L $port:$arg"
run_sshr() local cmd="$(prepare_ssh_cmd ssh)"
{ fmt_note "-->" $cmd
run_sshl "$1" fmt_note " > please access localhost:$port"
eval_or_echo $cmd
} }
# sshd # sshd
run_sshd() run_sshd()
{ {
local port=${1:-$(get_free_port)} local port=$(get_free_port)
SSH_OPTIONS+=("-NC" "-D" "$port")
prepare_ssh_cmd ssh SSH_OPTIONS="$SSH_OPTIONS -NC -D $port"
print_cmd local cmd="$(prepare_ssh_cmd ssh)"
fmt_note "-->" $cmd
fmt_note " > please access localhost:$port" fmt_note " > please access localhost:$port"
eval_or_echo eval_or_echo $cmd
} }
# scp # scp
@@ -406,27 +208,15 @@ run_scp() {
SERVER="$SERVER":"$src" SERVER="$SERVER":"$src"
SCP_DST="$dst" SCP_DST="$dst"
fi fi
SSH_OPTIONS+=("-r") SSH_OPTIONS="$SSH_OPTIONS -r"
prepare_ssh_cmd scp local cmd="$(prepare_ssh_cmd scp)"
print_cmd fmt_note "-->" $cmd
eval_or_echo eval_or_echo $cmd
}
# 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 host keys
remove_hostkey() { remove_hostkey()
{
local key local key
if [[ -z "$PORT" || "$PORT" == "22" ]]; then if [[ -z "$PORT" || "$PORT" == "22" ]]; then
key=$SERVER key=$SERVER
@@ -439,288 +229,91 @@ remove_hostkey() {
# main # main
print_help() print_help()
{ {
local pager=(cat) fmt_info "usage: $0 <service> [command] [options]"
if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then cat <<EOF
pager=(less -R) available commands:
fi - ssh (default)
- tmux (run ssh in multiple tmux windows)
cat <<EOF | "${pager[@]}" - sshl (ssh -L)
NAME - sshd (ssh -D)
riot - connect to remote hosts using SSH presets - zssh
- sftp
SYNOPSIS - rm (remove host keys)
${0##*/} [OPTION]... REMOTE [OPTION]... [COMMAND [COMMAND-ARG]...] - ping/ping6 (let the remote to ping the given target)
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.
EOF EOF
} }
router() { router() {
local positional=() if [[ -z "$1" || "$1" == "-h" || "$1" == "--help" ]]; then
local arg="" print_help
local option="" exit
local remaining="" fi
local option_value=""
while [[ $# -gt 0 || -n "$arg" ]]; do while [[ "$1" == -* ]]; do
if [[ -z "$arg" ]]; then if [[ "$1" == "--tmux" ]]; then
arg=$1 RIOT_USE_TMUX=1
shift
continue
fi
RIOT_EXTRA_OPTIONS="$RIOT_EXTRA_OPTIONS $1"
if [[ "$1" == "-o" ]]; then
RIOT_EXTRA_OPTIONS="$RIOT_EXTRA_OPTIONS $2"
shift shift
fi fi
shift
# 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 )
print_help
exit 0
;;
-t|--trust )
RIOT_TRUST_SERVER=1
;;
-g|--gpg )
GPG_FORWARD=1
;;
--tmux )
USE_TMUX=1
;;
-p|--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")
;;
* )
fmt_fatal "unknown option: '$option'"
;;
esac
done done
if [[ "${positional[2]}" == "--" ]]; then IFS=',' read -ra remotes <<< "$1"
positional=("${positional[@]:0:2}" "${positional[@]:3}") for ((i=0; i<${#remotes[@]}; i++)); do
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]}" remote="${remotes[i]}"
if [[ -z "$remote" ]]; then
continue
fi
local batch_func="${remote}.batch" local batch_func="${remote}.batch"
if is_function "$batch_func"; then if is_function "$batch_func"; then
"$batch_func" "$batch_func"
continue continue
fi fi
parse_remote "$remote" parse_remote "$remote"
case "${positional[1]}" in case $2 in
ssh|tmux|"" ) ssh|tmux|"" )
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1 [[ "$2" == tmux ]] && RIOT_USE_TMUX=1
if [[ "$GPG_FORWARD" == "1" ]]; then run_ssh ssh "${@:3}"
prepare_gpg_forwarding
fi
run_ssh ssh "${positional[@]:2}"
;; ;;
git ) ping|ping6 )
if printf '%s\0' "${positional[@]:2}" | grep -Fxqz -- '-C'; then run_ssh ssh "${@:2}"
run_ssh ssh git "${positional[@]:2}"
else
run_ssh ssh git -C "$(pwd)" "${positional[@]:2}"
fi
;;
ping|ping4|ping6 )
test "${#positional[@]}" -eq 2 || fmt_fatal "ping requires no arguments"
run_ping "${positional[1]}"
;; ;;
zssh ) zssh )
run_ssh zssh "${positional[@]:2}" run_ssh zssh
;; ;;
sftp ) sftp )
run_ssh sftp "${positional[@]:2}" run_ssh sftp
;; ;;
sshl ) sshl )
test -n "${positional[2]}" || fmt_fatal "no target address provided" test -n "$3" || fmt_fatal "no target address provided"
test "${#positional[@]}" -eq 3 || fmt_fatal "sshl requires exactly one argument" run_sshl "$3"
run_sshl "${positional[2]}"
;;
sshr )
test -n "${positional[2]}" || fmt_fatal "no target address provided"
test "${#positional[@]}" -eq 3 || fmt_fatal "sshr requires exactly one argument"
run_sshr "${positional[2]}"
;; ;;
sshd ) sshd )
test "${#positional[@]}" -le 3 || fmt_fatal "sshd requires one or no arguments" run_sshd
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 ) scp )
test "${#positional[@]}" -eq 4 || fmt_fatal "scp requires exactly two arguments: source and destination" test -n "$3" || fmt_fatal "no source path specified"
test -n "${positional[2]}" || fmt_fatal "no source path specified" test -n "$4" || fmt_fatal "no destination path specified"
test -n "${positional[3]}" || fmt_fatal "no destination path specified" run_scp "$3" "$4"
run_scp "${positional[2]}" "${positional[3]}"
;; ;;
rm ) rm )
test "${#positional[@]}" -eq 2 || fmt_fatal "rm requires no arguments"
remove_hostkey remove_hostkey
;; ;;
* ) * )
print_help print_help
fmt_fatal "unknown command: ${positional[1]}" fmt_fatal "unknown command: $2"
;; ;;
esac esac
done done
if [[ -n "$TMUX_SESS" && "$DFS_DRY_RUN" != "1" ]]; then if [[ -n "$RIOT_TMUX_SESS" ]]; then
tmux attach-session -t $TMUX_SESS tmux attach-session -t $RIOT_TMUX_SESS
fi fi
} }
router "$@" router "${GOT_OPTS[@]}"

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
if [ "$#" -eq 0 ]; then
echo "usage: sne <filename or regex pattern>"
echo "example: sne config.yaml"
exit 1
fi
SEARCH_PATTERN="$1"
files=()
while IFS= read -r -d $'\0' file; do
files+=("$file")
done < <(fd "$SEARCH_PATTERN" --type f --print0)
file_count=${#files[@]}
if [ "$file_count" -eq 0 ]; then
echo "no file match '$SEARCH_PATTERN'"
exit 0
fi
echo "🔎 found $file_count files:"
for file in "${files[@]}"; do
echo " 📄 $file"
done
echo "----------------------------------------"
read -r -p "edit? [Y/n] " response
response=${response:-Y}
if [[ "$response" =~ ^[Yy]$ ]]; then
vim "${files[@]}"
else
echo "❌ aborted"
fi

View File

@@ -6,19 +6,16 @@ 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 ARG=$1; shift; fi
if [[ "$1" == "--" ]]; then GOT_OPTS+=("$@"); break; fi
ARG="$1"; shift;
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 +38,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

@@ -25,7 +25,6 @@ elif [[ -f /etc/hostname ]]; then
else else
fmt_fatal "unable to get hostname" fmt_fatal "unable to get hostname"
fi fi
hostname=$(echo $hostname | tr '[:upper:]' '[:lower:]')
init_uuid() init_uuid()
{ {
@@ -99,7 +98,7 @@ update_dns()
elif [[ "$DFS_DDNS_IP4" == "http"* ]]; then elif [[ "$DFS_DDNS_IP4" == "http"* ]]; then
ip4=$(curl $DFS_CURL_OPTIONS -sSL "$DFS_DDNS_IP4") ip4=$(curl $DFS_CURL_OPTIONS -sSL "$DFS_DDNS_IP4")
else else
ip4=$(ip -4 --brief addr show $DFS_DDNS_IP4 scope global primary | awk '{split($3,a,"/");print a[1]}' | head -n 1) ip4=$(ip a show $DFS_DDNS_IP4 | grep inet | grep global | awk '/inet / {print $2}' | awk -F'[/]' '{print $1}')
fi fi
if [[ -n "$DFS_DDNS_IP4" && -z "$ip4" ]]; then if [[ -n "$DFS_DDNS_IP4" && -z "$ip4" ]]; then
fmt_fatal "failed getting ip4 address" fmt_fatal "failed getting ip4 address"
@@ -115,7 +114,7 @@ update_dns()
elif [[ "$DFS_DDNS_IP6" == "http"* ]]; then elif [[ "$DFS_DDNS_IP6" == "http"* ]]; then
ip6=$(curl $DFS_CURL_OPTIONS -sSL "$DFS_DDNS_IP6") ip6=$(curl $DFS_CURL_OPTIONS -sSL "$DFS_DDNS_IP6")
else else
ip6=$(ip -6 --brief addr show $DFS_DDNS_IP6 scope global primary | awk '{split($3,a,"/");print a[1]}' | head -n 1) ip6=$(ip a show $DFS_DDNS_IP6 | grep inet6 | grep global | awk '/inet6 / {print $2}' | awk -F'[/]' '{print $1}')
fi fi
if [[ -n "$DFS_DDNS_IP6" && -z "$ip6" ]]; then if [[ -n "$DFS_DDNS_IP6" && -z "$ip6" ]]; then
fmt_fatal "failed getting ip6 address" fmt_fatal "failed getting ip6 address"

View File

@@ -2,8 +2,8 @@
set -e set -e
op=$(command -v op || command -v op.exe || true) op=$(command -v op || command -v op.exe || true)
if [[ -z "$op" || ! -x "$op" ]]; then if [[ ! -x $op ]]; then
echo "1Password CLI not found" >&2 echo "1password cli not found" > /dev/stderr
exit 1 exit -1
fi fi
exec "$op" read "op://Personal/id25519-passphrase/$(hostname)" "$op" read "op://Personal/id25519-passphrase/$(hostname)"

View File

@@ -20,7 +20,6 @@ find_so_file()
return return
fi fi
done done
return 1
} }
create_agent() create_agent()
@@ -31,50 +30,27 @@ create_agent()
kill_agent() kill_agent()
{ {
local status if pgrep -x ssh-agent > /dev/null; then
if pgrep -u "$EUID" -x ssh-agent > /dev/null; then fmt_note "killing existing agent"
fmt_note "stopping existing ssh-agent" pkill -9 -x ssh-agent
if pkill -TERM -u "$EUID" -x ssh-agent; then
:
else
status=$?
[[ $status -eq 1 ]] || return "$status"
fi
fi fi
if command -v gpgconf > /dev/null 2>&1; then
fmt_note "stopping gpg-agent if running"
gpgconf --kill gpg-agent
fi
unset SSH_AUTH_SOCK SSH_AGENT_PID
echo unset SSH_AUTH_SOCK SSH_AGENT_PID
} }
add_piv() add_piv()
{ {
local SO_FILE local SO_FILE=$(find_so_file)
if ! SO_FILE=$(find_so_file); then if [[ -n "$SO_FILE" ]]; then
echo ssh-add -s \"$SO_FILE\"
else
fmt_error "opensc-pkcs11.so not found" fmt_error "opensc-pkcs11.so not found"
return 1
fi fi
printf 'ssh-add -s %q\n' "$SO_FILE"
list list
} }
add_id25519_with_op() add_id25519_with_op()
{ {
local status SSH_ASKPASS_REQUIRE=force SSH_ASKPASS="$THIS_DIR/sagent-op.sh" timeout 60s ssh-add ~/.ssh/id_ed25519 || fmt_fatal "timed out when adding the key. probably the passphrase is wrong or 1password-cli is not working"
if SSH_ASKPASS_REQUIRE=force SSH_ASKPASS="$THIS_DIR/sagent-op.sh" timeout 60s ssh-add "$HOME/.ssh/id_ed25519"; then list
list
return
else
status=$?
fi
if [[ $status -eq 124 ]]; then
fmt_fatal "timed out when adding the key"
else
fmt_fatal "failed to add the key (ssh-add exit $status); check the key, agent, and 1Password CLI"
fi
} }
list() list()
@@ -83,220 +59,30 @@ list()
echo ssh-add -l echo ssh-add -l
} }
configured_pinentry()
{
local agent_conf
local agent_confs=()
for agent_conf in "$@"; do
[[ -f "$agent_conf" ]] && agent_confs+=("$agent_conf")
done
[[ ${#agent_confs[@]} -gt 0 ]] || return 1
awk '
/^[[:space:]]*#/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line ~ /^pinentry-program([[:space:]]|=)/) {
sub(/^pinentry-program[[:space:]=]*/, "", line)
sub(/[[:space:]]*$/, "", line)
pinentry = line
}
}
END {
if (pinentry == "") exit 1
print pinentry
}
' "${agent_confs[@]}"
}
check_pinentry()
{
local gnupg_home
local gpg_sysconfdir
local agent_conf
local system_agent_conf
local pinentry
local gpg_bindir
local candidate
gnupg_home=$(gpgconf --list-dirs homedir)
gpg_sysconfdir=$(gpgconf --list-dirs sysconfdir)
agent_conf="$gnupg_home/gpg-agent.conf"
system_agent_conf="$gpg_sysconfdir/gpg-agent.conf"
if pinentry=$(configured_pinentry "$system_agent_conf" "$agent_conf"); then
if [[ "$pinentry" == "~/"* ]]; then
pinentry="$HOME/${pinentry#\~/}"
fi
if [[ -x "$pinentry" ]]; then
return
fi
fmt_warning "configured pinentry is not executable: $pinentry"
else
gpg_bindir=$(gpgconf --list-dirs bindir)
if [[ -x "$gpg_bindir/pinentry" || -x "$gpg_bindir/pinentry-basic" ]]; then
return
fi
fi
for candidate in \
"$(command -v pinentry-curses 2>/dev/null || true)" \
"$(command -v pinentry 2>/dev/null || true)" \
"$(command -v pinentry-tty 2>/dev/null || true)"; do
[[ -n "$candidate" && -x "$candidate" ]] && break
candidate=""
done
if [[ -n "$candidate" ]]; then
fmt_warning "gpg-agent has no usable pinentry; add 'pinentry-program $candidate' to $agent_conf"
else
fmt_warning "gpg-agent has no usable pinentry; install one and configure pinentry-program in $agent_conf"
fi
}
prepare_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"
check_pinentry
local current_tty
current_tty=$(tty) || fmt_fatal "unable to determine the current TTY"
export GPG_TTY="$current_tty"
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)
if [[ -z "$agent_socket" || ! -S "$agent_socket" ]]; then
fmt_fatal "gpg-agent SSH socket not found; add 'enable-ssh-support' to ~/.gnupg/gpg-agent.conf and restart gpg-agent"
fi
fmt_note "using gpg-agent: $agent_socket"
echo unset SSH_AGENT_PID
printf 'export GPG_TTY=%q\n' "$GPG_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"
local line
local agent_socket=""
local agent_pid=""
while IFS= read -r line; do
case "$line" in
SSH_AUTH_SOCK=*)
agent_socket=${line#SSH_AUTH_SOCK=}
agent_socket=${agent_socket%%;*}
;;
SSH_AGENT_PID=*)
agent_pid=${line#SSH_AGENT_PID=}
agent_pid=${agent_pid%%;*}
;;
esac
done < "$agent_file"
[[ -n "$agent_socket" && "$agent_pid" =~ ^[1-9][0-9]*$ ]] || return 1
export SSH_AUTH_SOCK="$agent_socket"
export SSH_AGENT_PID="$agent_pid"
}
agent_is_usable()
{
[[ -S "$SSH_AUTH_SOCK" ]] || return 1
ps -p "$SSH_AGENT_PID" -o uid= -o comm= 2>/dev/null |
awk -v uid="$EUID" '$1 == uid && $2 ~ /(^|\/)ssh-agent$/ { found=1 } END { exit !found }' || return 1
local status
if ssh-add -l > /dev/null 2>&1; then
status=0
else
status=$?
fi
[[ $status -eq 0 || $status -eq 1 ]]
}
print_agent_env()
{
printf 'export SSH_AUTH_SOCK=%q\n' "$SSH_AUTH_SOCK"
printf 'export SSH_AGENT_PID=%q\n' "$SSH_AGENT_PID"
}
reset() reset()
{ {
kill_agent kill_agent
all already-killed all
} }
all() all()
{ {
local mode="${1:-}" test -d ~/.ssh || mkdir ~/.ssh
mkdir -p "$HOME/.ssh" local agent_file=~/.ssh/agent-$(whoami)
local agent_file="$HOME/.ssh/agent-$(whoami)" if [[ -f $agent_file ]]; then
[[ ! -L "$agent_file" ]] || fmt_fatal "refusing to use symlink as agent file: $agent_file" source $agent_file > /dev/null
unset SSH_AUTH_SOCK SSH_AGENT_PID
if [[ "$mode" != "already-killed" && -f "$agent_file" ]]; then
chmod 600 "$agent_file"
read_agent_file "$agent_file" || true
else else
touch "$agent_file" touch $agent_file
chmod 600 "$agent_file" chmod 600 $agent_file
fi fi
if ! ps -p "$SSH_AGENT_PID" 1>/dev/null 2>&1; then
if ! agent_is_usable; then kill_agent
if [[ "$mode" != "already-killed" ]]; then
kill_agent
fi
fmt_note "launching a new agent" fmt_note "launching a new agent"
local agent_output create_agent | tee $agent_file
if ! agent_output=$(create_agent); then
fmt_fatal "failed to launch ssh-agent"
fi
printf '%s\n' "$agent_output" > "$agent_file"
chmod 600 "$agent_file"
read_agent_file "$agent_file" || fmt_fatal "ssh-agent returned invalid environment data"
agent_is_usable || fmt_fatal "new ssh-agent is not usable"
else else
fmt_note "using existing agent: $SSH_AGENT_PID" fmt_note "using existing agent: $SSH_AGENT_PID"
cat $agent_file
fi fi
print_agent_env
} }
route() route()
@@ -319,12 +105,6 @@ route()
op) op)
add_id25519_with_op add_id25519_with_op
;; ;;
gpg)
use_gpg_agent
;;
gpg-pin)
cache_gpg_pin "${@:2}"
;;
reset) reset)
reset reset
;; ;;
@@ -333,7 +113,6 @@ route()
;; ;;
*) *)
fmt_error "unknown command: $1" fmt_error "unknown command: $1"
return 1
;; ;;
esac esac
} }

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,14 +8,14 @@ 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:"
for i in "${GOT_OPTS[@]}"; do for i in "${GOT_OPTS[@]}"; do
echo "$i" echo "$i"
done done

View File

@@ -1,133 +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"
grep -Fq 'ARG=ControlMaster=no' "$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,14 @@ 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 RequestTTY=yes -o PermitLocalCommand=yes -o ControlMaster=auto -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 RequestTTY=yes -o PermitLocalCommand=yes -o ControlMaster=auto -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'
# check alias # check alias
alias p114 alias p114