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
15 changed files with 525 additions and 60 deletions

View File

@@ -2,11 +2,15 @@
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-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGlEme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8Vr6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmxT+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCAr sk1/piv/9a
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBN24pvN1RMN+iSZyHPdyExA1Rvt8pdr3e6ih6iX+KrQ ltp2
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA71ukY9nzzeAGp9WYstPYiIwexzIANlPwMQ68QNQwqxAAAABHNzaDo= sk1/fido2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0
@@ -16,5 +20,4 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF52mPmbLEriZ+DfmrMHFtvg2kiO2JpRpitvs7PbJowh
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC5gu+U4picxVvCNYjLlmxGLpJl7svj3LklO7jSpwqdrNxlE+/xkx07PJx76AbA77dDM1Dxmm/VOvVQCKTRLTgk= ip14/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMXu7UND6N1EKcbeSeAj2CwzGA3sM5/bRtLwoY0euWdR4vzB0nwcwd6nct5JCEyozjujJfMt8d7oCzhSt1w/1cU= ip17/sep

View File

@@ -1,4 +1,5 @@
set -g prefix ^a
bind ^a send-prefix
set -g mouse on
set -g set-clipboard on
set -g default-terminal "xterm-256color"
@@ -24,9 +25,19 @@ bind - split-window -v -c "#{pane_current_path}"
bind | split-window -h -c "#{pane_current_path}"
# other key bindings
## prefix r: reload conf
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'
## prefix C-x: pane sync
bind C-x set-window-option synchronize-panes\; display-message "synchronize-panes is now #{?pane_synchronized,on,off}"
## 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
## ESC: exit copy mode
bind -T copy-mode-vi Escape send-keys -X cancel
# styles
# status bar

31
.zshrc2
View File

@@ -9,6 +9,7 @@ else
fi
export DOTFILES=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
export PATH="$PATH:$DOTFILES/scripts"
fpath=($DOTFILES/functions $fpath)
export EDITOR='vim'
# env for apps
export PYTHONIOENCODING='UTF-8'
@@ -130,9 +131,33 @@ alias "jcf"='jc -f'
alias "jcfu"='jc -fu'
alias "sc"='systemctl'
alias "t"='tmux'
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 }
sagt() { eval "$($DOTFILES/tools/sagent.sh $@)" }
use() { nix --experimental-features nix-command --extra-experimental-features flakes shell "${(*)@/#%(#b)([^#]#)/nixpkgs#$match}" }
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 ; }
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
bindkey "^b" beginning-of-line # ctrl+b

99
functions/_beam Normal file
View File

@@ -0,0 +1,99 @@
#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 "$@"

35
functions/_beaml Normal file
View File

@@ -0,0 +1,35 @@
#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

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

3
functions/_sagt Normal file
View File

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

3
functions/_use Normal file
View File

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

View File

@@ -25,7 +25,7 @@ nasps.batch() {
# remotes
nasp.remote() {
remote=nasp.fit
remote=jump.nasp.fit
RET_PORT=${RET_PORT:-36022}
RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1
@@ -60,7 +60,7 @@ nasp.domain() {
RET_HOSTNAME=$host
RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root}
RET_JUMP_SERVER="ssh@nasp.fit:36022"
RET_JUMP_SERVER="ssh@jump.nasp.fit:36022"
RET_TRUST_SERVER=1
}

View File

@@ -20,14 +20,14 @@ done
# check if port number valid
check_port() {
( echo $1 | grep -qxE "[1-9][0-9]{0,4}" ) || return 1
test $1 -lt 65536 -a $1 -gt 0 || return 1
[[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1
[[ $1 < 65536 && $1 > 0 ]] || return 1
return 0
}
# check if username valid
check_username() {
( echo $1 | grep -qxE "^[a-z][-a-z0-9_]*\$" ) || return 1
[[ "$1" =~ ^[a-z][-a-z0-9_]*$ ]] || return 1
return 0
}
@@ -42,18 +42,28 @@ get_server_meta() {
RET_JUMP_SERVER="" # optional
# body
local remote="$1"
# if in the form user@...
# extract username from user@...
if [[ "$remote" == *@* ]]; then
RET_USERNAME=${remote%%@*}
remote=${remote#*@}
check_username $RET_USERNAME || fmt_warning \"$RET_USERNAME\" is not a valid unix username
fi
# if in the form ...:22
if [[ "$remote" == "["*"]":* || ( "$remote" != "["*"]" && "$remote" == *:* ) ]]; then
# extract port from ...:port
if [[
"$remote" =~ ^[^:]+:[1-9][0-9]*$ # contains only one colon
|| "$remote" =~ ^\[.+\]:[1-9][0-9]*$ # in the form of [host]:port
|| "$remote" =~ :::[1-9][0-9]*$ # in the form of :::port
|| "$remote" =~ ^([0-9a-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%:*}
check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\"
fi
# remove square brackets
if [[ "$remote" =~ ^\[.*\]$ ]]; then
remote=${remote:1:-1}
fi
# presets -- match remote
local remote_func="$remote.remote"
if is_function "$remote_func"; then
@@ -63,8 +73,8 @@ get_server_meta() {
RET_HOSTNAME=${remote}
local domain=${remote##*.}
local host=${remote%.*}
# if there's no dot
if [[ "$host" == "$domain" && "$host" != "["*"]" ]]; then
# if it contains no dot and is not ipv6
if [[ "$remote" != *.* && "$remote" != *:* ]]; then
domain="default"
fi
local domain_func="$domain.domain"
@@ -83,12 +93,15 @@ parse_remote() {
TRUST_SERVER=1
PORT="" # optional
USERNAME="" # optional
SSH_OPTIONS=("-o" "RequestTTY=yes")
SSH_OPTIONS=("-o" "ServerAliveInterval=60")
if [[ -t 1 ]]; then
SSH_OPTIONS+=("-o" "RequestTTY=yes")
fi
if [[ "$RIOT_TRUST_CLIENT" == "1" ]]; then
SSH_OPTIONS+=("-o" "PermitLocalCommand=yes")
if [[ "$(get_os_type)" != "msys" ]]; then
test "$DFS_DRY_RUN" = "1" || mkdir -p ~/.ssh/master-socket
SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPath=~/.ssh/master-socket/%C")
SSH_OPTIONS+=("-o" "ControlMaster=auto" "-o" "ControlPersist=5s" "-o" "ControlPath=~/.ssh/master-socket/%C")
fi
fi
# handle input
@@ -125,7 +138,7 @@ parse_remote() {
print_cmd() {
local output=""
for s in "${CMD[@]}"; do
if [[ "$s" =~ [\ \\\'\"] ]]; then
if [[ "$s" =~ [\ \\\'\"] ]]; then # needs to be escaped
s="${s@Q}"
fi
output+="$s "
@@ -135,7 +148,7 @@ print_cmd() {
eval_or_echo() {
local DO=""
local tmux_win=0
tmux_win=${tmux_win:-0}
if [[ "$DFS_DRY_RUN" == "1" ]]; then
DO=echo
fi
@@ -147,7 +160,7 @@ eval_or_echo() {
tmux_win=$((tmux_win+1))
$DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l
fi
$DO tmux send-keys -t $TMUX_SESS:$tmux_win "${CMD[@]}" Enter
$DO tmux send-keys -t $TMUX_SESS:$tmux_win $(printf '%s Space ' "${CMD[@]}") Enter
else
$DO "${CMD[@]}"
fi
@@ -185,19 +198,50 @@ run_ssh()
# sshl
run_sshl()
{
local arg="$1"
if [[ "$arg" != *":"* ]]; then
# treat as a port number
arg=localhost:$arg
local arg left right localsock access
local res="${1//[^:]}"
local lorr="-L"
if [[ "${FUNCNAME[1]}" == "run_sshr" ]]; then
lorr="-R"
fi
local port=$(get_free_port)
SSH_OPTIONS+=("-NC" "-L" "$port:$arg")
if [[ ${#res} -eq 2 ]]; then
arg="$1"
elif [[ ${#res} -eq 0 ]]; then
if [[ "$lorr" == "-R" ]]; then
arg="$1"
else
arg="$(get_free_port):localhost:$1"
fi
else
left=${1%%:*}
right=${1##*:}
if check_port "$left"; then
arg="$1"
elif check_port "$right"; then
arg="$(get_free_port):$1"
else
arg="$1"
localsock=1
fi
fi
if [[ "$localsock" == "1" ]]; then
access="unix://${arg%%:*}"
else
access="localhost:${arg%%:*}"
fi
SSH_OPTIONS+=("-NC" "$lorr" "$arg")
prepare_ssh_cmd ssh
print_cmd
fmt_note " > please access localhost:$port"
fmt_note " > please access $access"
eval_or_echo
}
# sshr
run_sshr()
{
run_sshl "$1"
}
# sshd
run_sshd()
{
@@ -259,11 +303,12 @@ remove_hostkey() {
# main
print_help()
{
fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [ssh-command-args]"
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]
@@ -295,13 +340,22 @@ router() {
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"
fmt_fatal "unknown option: '$1'. if this option is for the remote command, add '--' before."
;;
* )
positional+=("$1")
@@ -328,6 +382,13 @@ router() {
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
run_ssh ssh "${positional[@]:2}"
;;
git )
if printf '%s\0' "${positional[@]:2}" | grep -Fxqz -- '-C'; then
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]}"
@@ -343,6 +404,11 @@ router() {
test "${#positional[@]}" -eq 3 || fmt_fatal "sshl requires exactly one argument"
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 )
test "${#positional[@]}" -le 3 || fmt_fatal "sshd requires one or no arguments"
if [[ "${#positional[@]}" -eq 3 ]]; then

31
scripts/sne Executable file
View File

@@ -0,0 +1,31 @@
#!/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

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

View File

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

View File

@@ -20,6 +20,7 @@ find_so_file()
return
fi
done
return 1
}
create_agent()
@@ -30,27 +31,50 @@ create_agent()
kill_agent()
{
if pgrep -x ssh-agent > /dev/null; then
fmt_note "killing existing agent"
pkill -9 -x ssh-agent
local status
if pgrep -u "$EUID" -x ssh-agent > /dev/null; then
fmt_note "stopping existing ssh-agent"
if pkill -TERM -u "$EUID" -x ssh-agent; then
:
else
status=$?
[[ $status -eq 1 ]] || return "$status"
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()
{
local SO_FILE=$(find_so_file)
if [[ -n "$SO_FILE" ]]; then
echo ssh-add -s \"$SO_FILE\"
else
local SO_FILE
if ! SO_FILE=$(find_so_file); then
fmt_error "opensc-pkcs11.so not found"
return 1
fi
printf 'ssh-add -s %q\n' "$SO_FILE"
list
}
add_id25519_with_op()
{
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"
local status
if SSH_ASKPASS_REQUIRE=force SSH_ASKPASS="$THIS_DIR/sagent-op.sh" timeout 60s ssh-add "$HOME/.ssh/id_ed25519"; then
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()
@@ -59,30 +83,188 @@ list()
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
}
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"
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
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' "$current_tty"
printf 'export SSH_AUTH_SOCK=%q\n' "$agent_socket"
}
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()
{
kill_agent
all
all already-killed
}
all()
{
test -d ~/.ssh || mkdir ~/.ssh
local agent_file=~/.ssh/agent-$(whoami)
if [[ -f $agent_file ]]; then
source $agent_file > /dev/null
local mode="${1:-}"
mkdir -p "$HOME/.ssh"
local agent_file="$HOME/.ssh/agent-$(whoami)"
[[ ! -L "$agent_file" ]] || fmt_fatal "refusing to use symlink as agent file: $agent_file"
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
touch $agent_file
chmod 600 $agent_file
touch "$agent_file"
chmod 600 "$agent_file"
fi
if ! ps -p "$SSH_AGENT_PID" 1>/dev/null 2>&1; then
if ! agent_is_usable; then
if [[ "$mode" != "already-killed" ]]; then
kill_agent
fi
fmt_note "launching a new agent"
create_agent | tee $agent_file
local agent_output
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
fmt_note "using existing agent: $SSH_AGENT_PID"
cat $agent_file
fi
print_agent_env
}
route()
@@ -105,6 +287,9 @@ route()
op)
add_id25519_with_op
;;
gpg)
use_gpg_agent
;;
reset)
reset
;;
@@ -113,6 +298,7 @@ route()
;;
*)
fmt_error "unknown command: $1"
return 1
;;
esac
}

View File

@@ -40,9 +40,9 @@ 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 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 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 ssh --password -- ping -c 1 2>/dev/null)" = 'ssh -p 55 -o RequestTTY=yes -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1'
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 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