14 Commits

Author SHA1 Message Date
Dict Xiong
86f74b0a84 feat(scf): Open the configuration file referenced by a systemd service's ExecStart
Signed-off-by: Dict Xiong <me@dxng.cn>
2026-08-26 18:01:07 +08:00
Dict Xiong
e88d6a4621 feat(zshrc): nor use nh if available
Signed-off-by: Dict Xiong <me@dxng.cn>
2026-08-22 01:56:06 +08:00
Dict Xiong
913032b59d [debug] fix: tmux 3.6 selection (#48)
* fix(tmux): tmux 3.6 selection

Signed-off-by: Dict Xiong <me@dxng.cn>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Dict Xiong <me@dxng.cn>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-20 16:37:43 +08:00
Dict Xiong
aa1df92783 [dev] feat: README.md (#47)
feat: README.md
2026-08-17 18:33:33 +08:00
Dict Xiong
3bd0121786 [dev] GPG agent forwarding; improve riot, sagt, tmux and zsh (#46)
* 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

* feat(riot): command git

* build(riot-config): nasp.fit -> jump.nasp.fit

* feat: add bnd and nor and completions

* fix(zshrc): bnd exit when build error

* fix(tmux): press ^a twice to send ^a

* feat(riot): sshl supports specifying local port and unix sock

* feat(riot): sshr for ssh -R

* feat(riot): -o ServerAliveInterval=60

* feat(riot): conditionally RequestTTY

* feat: zsh completion for beam and beaml

* fix(frigg): ddns get ip

* 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.

* feat(riot): ControlPersist=60s

* fix(ci): riot

* 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/

* feat(tmux): F12 to passthru

* fix(riot): ControlPersist 60s -> 5s

* feat: sne: search and edit

* feat(ssh): add key ip17/sep

* feat(tmux): press ESC to exit copy mode

* feat(ssh): add key sk1/piv/9a

* feat(ssh): add key sk1/fido2

* feat(ssh): add key openpgp:0x8774BF70 and remove key ip14/sep and ltp2 (asu127)

* feat(sagent): sagt gpg; improve code

* feat(sagent): check pinentry and give notes

* feat(riot): add GPG agent forwarding

Signed-off-by: Dict Xiong <me@beardic.cn>

* feat: gitconf add gpg signature; remove macos from ci

* feat(eid): add sk1 9a cert

* feat(riot): better help and fix option quite->quiet

* feat(riot): separate command options from riot options

* feat(sagt): gpg-pin and associated tests

* feat(riot): zsh completions

* fix: Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(riot): simplify and secure

* fix: syntax

---------

Signed-off-by: Dict Xiong <me@beardic.cn>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-13 20:10:55 +08:00
891bbcacff [dev] refactor riot; use command; remove log; sagt op (#45)
* feat(riot-config): default port 12022

* fix(ci)

* feat(riot-config): remove tailing dot from .domain; remove j.remote and x.domain

* feat(riot): *.batch

* feat(riot): run command in tmux window(s)

note that spaces in ssh commands are still not supported

* feat(riot): -o RequestTTY=yes

* fix(riot): SSH_OPTIONS

* fix(riot): tmux use bash

* feat(zshrc): use()

* fix(riot): scp, and ci

note that riot still has problems with spaces. we should use array to
handle parameters.

* fix(ci): update macos image

* feat: update email

* feat: revert git email

* feat(ssh): update keys

* feat(sagent): sagt op

* fix(sagent): error note

* fix(ci): test of auto-dep

* feat(ssh): remove keys ltp1-bd and ltp1

* feat(riot): rm - remove host keys

* build(ci): update to ubuntu-latest and Yikun/hub-mirror-action@v1.5

* feat: remove frigg-client.log

* feat(riot): -t or --trust to set RIOT_TRUST_SERVER

* feat(zshrc/alias): add sc and t, remove cps and mvs

feat(zshrc/plugins): add man and web-search, remove ufw

* feat(riot): refactor argparse

feat(riot): add `--password` and `--`

feat(common.sh): argparse supports `--`

feat(riot): refactor ping to ping remote

* feat(riot): sshd can specify the local port

* feat(riot): print help when no argument or no remote

* fix(riot): ci

* feat(riot): better print_cmd with escape

* feat(riot-config): support sed*

* fix(frigg): hostname converted into lower case
2025-06-24 19:56:04 +08:00
055d44886b [debug] vim syntax highlighting; sagt file location; dxng.net (#44)
* feat(tmux): prefix + C-x to toggle synchronize-panes

* feat(riot-cfg): nasp domain username to root

* feat(vimrc): ^N to switch between nu/rnu/null

* feat(zshrc): VIRTUAL_ENV_DISABLE_PROMPT set

* fix(sagent): secure file location

* ci: remove schedule run for gitee_sync

* fix(vimrc): syntax highlighting

* feat(zshrc): move time style from l to ls

* fix(riot-config): ibd.ink -> dxng.net

* fix: ci

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2024-09-11 01:14:13 +08:00
92aa5fa920 [dev] refactor riot; improve tmux and vim; more plugins; more aliases (#43)
* riot: remove unknown domain warn

* install.sh: --no-ssh -> --no-auth-info

* doll: --restart=unless-stopped

* zshrc: alias cps and mvs

* riot: proxy -> ssh

* zshrc: not alias rm to trash

* (trial) riot devel: separate preset to config dir riot.d

* riot: support extra options and extra -o options

* (experimental) riot config in a single file

* riot config: add nasp remote and null domain

* gitconf: pull.ff = only

* fix ci

* riot: dynamic port forwarding

* riot: only one domain func will be exec

* to-install: update lemonbench

* to-install: alist

* [exp] riot inferred ssh: ping ping6

* sagt: fix nixos

* riot config: domain 42

* ubuntu.sh: DEBIAN_FRONTEND=noninteractive

* zshrc: ping -n

* zshrc: alias ping -n

* riot-config: jumpserver from sir0 to ssh.beardic.cn

* zshrc: alias ping before checking os type

* frigg: support api4.beardic.cn

* fix(install.sh): crontab fails on a new server

* fix(riot-conf): nasp.ob.ac.cn -> nasp.fit

* fix(install.sh): install crontab (exp)

* feat(test.zsh): test crontab

* fix(riot): secure control master

* fix(ci): riot control master

* fix(riot): not mkdir if dry-run

* feat(vimrc): set shiftwidth=4

* feat(ci): sync tmux-yank

* feat(tmux): set-clipboard on and mouse on (experimental)

* feat(zshrc): alias ping6

* build(ci): hub mirror 1.3->1.4

* fix(zshrc): tmux on msys; feat(common): better perf getting os type and linux dist

* fix(common.sh): get_os_type and get_linux_dist

* feat(zshrc): add plugin {magic-enter,per-directory-history,pip,podman,python,rsync,systemd,timer}

* feat(zshrc): journalctl alias

* feat(vimrc): tab=2 for c,cpp,nix,yaml

* build(ci): checkout v3 -> v4

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2024-05-05 12:02:55 +08:00
483f7fd7f3 [dev] nixos; riot multiple targets; install without ssh keys (#42)
* auth: add .eid/authorized_certificates for pam pkcs11 auth

* .zshrc: alias sl for sudo zsh -l

* to-install: nix

* zshrc: use gnu ls on mac

* zshrc: try to use gnu-ls

* try to fix ci for macos

* riot: add domain box[0-9]

* riot: shortcuts i,x,j

* .zshrc: warn if not in main channel

* sagt: reset agent so paths

* sagt: import ssh-agent -P paths

* common.sh: is_port_free and get_free_port

* riot use get_free_port to fix issue on windows

* riot: ssh support instant command

* riot: proxy delimiter from comma (,) to slash (/)

* riot: support multiple remotes, delimiter=comma (,)

* riot: fix ci; install.sh: --no-ssh

* riot: improve ci

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2023-11-06 19:13:22 +08:00
5fb7ff0833 [debug] support nixos; fix ci (#41)
* support for nixos

* fix ci
2023-07-26 21:18:24 +08:00
39b8dfce82 [debug/dev] loosen riot username check; autoload zcalc (#40)
* riot: loosen username check as sometimes it's not a unix username

* Revert "riot: loosen username check as sometimes it's not a unix username"

This reverts commit bdb64ce1f2.

* riot: loosen username check as sometimes it's not a unix username

* zshrc: autoload zcalc

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2023-07-03 16:52:06 +08:00
1a62ffe74b [debug+dev] riot checks and fix ipv6 support, fix crontab installation; antigen use F-Sy-H (#39)
* format: remove space

* sry but..., riot: 'i' for 'ibd', and remove support for ebd

* fix ipv6 support

* validate port and username

* install.sh: failed to add to crontab when there's no crontab

* sync zdharma-continuum/fast-syntax-highlighting

* use fast-syntax-highlighting instead

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2023-06-20 15:10:59 +08:00
2cc2ab48b8 [dev] enhance riot; ddns avoid burst (#38)
* riot: add . for .ibd and non for proxied

* riot: 'user@..' to overwrite username, '...:22' to overwrite port, treat the arg as server name if it doesn't match any preset, sshl can accept only a port number and default to use localhost

* update.sh: ddns sleep for random seconds to avoid burst

* riot: support zssh and sftp, and set ssh options according to trust settings

* riot: add scp

* riot: support jump servers, sep by commas

* common.sh: argparser supports -d/--dev and -D/--dry-run

* install.sh: remove -s/--secure because of no use

* riot: if remote ends with dot, treat it as a full-hostname; add tests

* sagent.sh: refuse to work on windows

---------

Co-authored-by: xiongdian.me <xiongdian.me@bytedance.com>
2023-05-30 13:43:25 +08:00
37bf1fa1f0 [keys] remove pc{0,1}/old,sk0/fido,ph0/sep, update pc1/windows (#37)
keys: remove pc{0,1}/old,sk0/fido,ph0/sep, update pc1/windows
2023-04-29 17:46:01 +08:00
42 changed files with 2477 additions and 377 deletions

View File

@@ -0,0 +1,61 @@
-----BEGIN CERTIFICATE-----
MIIFIDCCAwigAwIBAgIUK1zXH5UosBim7i+kIZyPGpowU9cwDQYJKoZIhvcNAQEL
BQAwgZUxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCRUlKSU5HMRAwDgYDVQQHDAdC
RUlKSU5HMRIwEAYDVQQKDAlEaWN0IFRlY2gxEDAOBgNVBAsMB1Jvb3QgQ0ExHjAc
BgNVBAMMFURpY3QgVGVjaCBSb290IENBIC0gMDEcMBoGCSqGSIb3DQEJARYNbWVA
YmVhcmRpYy5jbjAeFw0yMjA3MjIxNzU5MjNaFw0yNTA3MjExNzU5MjNaMEkxEjAQ
BgNVBAoMCURpY3QgVGVjaDEQMA4GA1UECAwHQkVJSklORzELMAkGA1UEBhMCQ04x
FDASBgNVBAMMC3NrMC5pYmQuaW5rMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAlDeyFxeJ5RXFX4B3rIIpNyyAIl5VTGCT5t8g9dxFWq0MeY6nQYJPA8IJ
IH+KzzujBabyKYpzshDvMzkuhx0Kwi2VL+ckxWg5FLge3kNBJqYnBm5pPWx6o9R5
DmcWqG5KRH0FJhSmL5KeBHsVcOurDtKn174x2eB+sggCSyuWlJyn4KxPLTg8z/Nr
FP5qSY1vj6t7mpzau+eIGh8IcQiFxteN/86fKswQHint/MVaqg6vX05BuIb8xb5E
XUvPIupdc/vtD959BxnTo7UoOz+3zu/u2cj2K9IDubwe9tDpv88aj6xnfXInFouD
/mlSmvtLo1h/6arn1hurgoylh5wbOwIDAQABo4GyMIGvMAkGA1UdEwQCMAAwCwYD
VR0PBAQDAgTwMB0GA1UdDgQWBBR9DxLqtR0+zUWoPyFoDRrnyJyDBTAfBgNVHSME
GDAWgBRsprmVI/Tjey+sw4qoZpLd7PT0gTBVBgNVHSUETjBMBggrBgEFBQcDAgYI
KwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCN0MBAQYKKwYBBAGC
N0MBAgYKKwYBBAGCNxQCAjANBgkqhkiG9w0BAQsFAAOCAgEA7BJXiRg32shmZWyY
gPjq52ffN9RDNe58KadajJ1PnVP1yNJySgsOG3eSxONQX3ITr36Ihsfb3fZneBfX
/IcXgjyvy1CDcvBBAyee91hdR8orMCePqMpeOSNP9CUp9Ctd4V+an0DE8Z1yrezA
ieaYgBZCDxuZdloYSmj9Z/Rqn2dy4dcUAxKrEwW/6VPqQNrJETTtadS1SF0EsDxV
XZpDqNYaS/N7/ciSm7TaQJud62A4m+FWWHNnttTp9IjLESr3U80+qtRY8yDNpulb
/eNL8KK9aORiFUz9789l6M62HPi6vCsMflI0iHUeoi69GKX8mKf0WFR1KxoDsJiN
DG9/1gCfj6whFCrT6fJTKAGl8Hp5toWMOeDprbsE5gf4Ik3p+sKMV9t43zNm3gvA
1zNPtgQUffPU0kONlavIxUNfCmD8qVwK5ECT7wh0hrB8fRYfxTZHgN6Yvu7M9HNz
SNt5JZCLBT4nLt0uQp9O/xctdHElZw+/W8OfnP5vxnPdccIeVOxpGIyzErwjD0+E
9mNiqOa5JV9OMAJ+0I9cbOnuMaXm4mvHZadnetUzq2ZUw1Jba7z62zIJTciNRq9i
ZJc6v5e+iqbE1ECZLco5LjWqqfvFfYCrkqeOhCRsRkVPsXnGPo2QDDYdTm4EGCmg
dVZ6R452SZsrE4V+3LR011BxzEg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFQDCCAyigAwIBAgIRAJmQ852eXApHBvrQujWaFAEwDQYJKoZIhvcNAQELBQAw
ODESMBAGA1UEChMJRGljdCBUZWNoMSIwIAYDVQQDExlEaWN0IFRlY2ggSXNzdWlu
ZyBDQSAyMDI2MB4XDTI2MDgxMjE1NTQyM1oXDTI5MDgyMTExMDAwMFowKzESMBAG
A1UEChMJRGljdCBUZWNoMRUwEwYDVQQDEwxzazEuZHhuZy5uZXQwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGl
Eme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8V
r6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5
v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmx
T+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X
7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCArAgMBAAGjggFQ
MIIBTDAOBgNVHQ8BAf8EBAMCBaAwKwYDVR0lBCQwIgYIKwYBBQUHAwIGCisGAQQB
gjcUAgIGCisGAQQBgjdDAQEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUOyThWF2A
BkoQv9xljyo3fFOclq0wHwYDVR0jBBgwFoAU9qXPjCjcil59akpPVISS8Yu/Jc0w
SgYIKwYBBQUHAQEEPjA8MDoGCCsGAQUFBzAChi5odHRwOi8vdG9vbHMuZHhuZy5j
bi9jZXJ0cy9pc3N1aW5nLWNhLTIwMjYuY2VyMCoGA1UdHwQjMCEwH6AdoBuGGWh0
dHA6Ly9jYS5pYmQuaW5rLzEuMC9jcmwwRwYMKwYBBAGCpGTGKEABBDcwNQIBAQQD
cGl2BCtaMnFWY05lMzlUaHZyc3RIbFVFWE5laXg1QXd2aFU3MjBlWEFFZWRlUG4w
MA0GCSqGSIb3DQEBCwUAA4ICAQCKSLjnMOlQiZ381IaBEl4iT7pCvY0QQxsVEMDD
RWYhIImx3JJDt7kiZXcJwlH70VG0opSz9eKrT3cw7OY6DnNm7Bm0JT2fJgyCSnWo
nG7J1soE74Rl2MiNnP1JTqhFlZKLrmIb6eP4U0eM+ZaJ7luyxizqiJAeRyJB+YCe
DM9uZ7rC33DYZeZ46V4f7DfJ23TjVJvaX/NFu0Ab/7ylF6X7frT8xf11XLlE2IyR
MmnLtQiREXXETaw9l5suMuH8fmVg01QrqnOHmz4r9Nuzh7vHxogEsJtgBLhv2Min
X6IfaKABhFXk1cVZGNgaC0Pao6cYbaWJwF2zcVHGq5EDKXhkKmCLeDo9em0rOJ2y
fU23yP3ByMGJ3Xi7nI8E5dfSw6Q39b3t1/kxj1y8T3PGdTN/3vFLVqf5A7XXdQL9
/uy2lYh3Fud/aZuthZyoL2rkC4O8N4seqfNIOBEHAmfbk5j6LoFFwSTrRGqzCAf4
4r6ckHjL3KBnR8EeMwwit324iRYd3ppUL3TINkeiHRNFVKU0flUvcG6T0v+hC2sX
yupYy0wylkonub6L0gUIINqx38nsDCR07eAfsDXJshIwTxUBFrVpKjPK7jVQOZ6+
9/yPu72BhyGMZs4bgYMEPwqKTbzUNitO2WfBIgXhzGe8YzMA1U2BMTRT0nIQ6oa9
8YbnQw==
-----END CERTIFICATE-----

View File

@@ -1,6 +1,7 @@
[user] [user]
email = me@beardic.cn email = me@dxng.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`
@@ -9,6 +10,8 @@
quotepath = false # chinese chars quotepath = false # chinese chars
[push] [push]
autoSetupRemote = true autoSetupRemote = true
[pull]
ff = only
[branch] [branch]
# Show most recently changed branches first. # Show most recently changed branches first.
sort = -committerdate sort = -committerdate

View File

@@ -1,24 +1,22 @@
name: sync repos to gitee name: sync repos to gitee
on: on:
push: ~ push: ~
schedule:
- cron: 10 4 * * *
workflow_dispatch: ~ workflow_dispatch: ~
jobs: jobs:
dotfiles: dotfiles:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: DictXiong/dotfiles # 名字随便起 - name: DictXiong/dotfiles
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/DictXiong # 源端账户名(github) src: github/DictXiong
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: user src_account_type: user
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "dotfiles" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "dotfiles"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'

View File

@@ -5,121 +5,138 @@ on:
workflow_dispatch: ~ workflow_dispatch: ~
jobs: jobs:
ohmyzsh: ohmyzsh:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: ohmyzsh repo # 名字随便起 - name: ohmyzsh repo
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/ohmyzsh # 源端账户名(github) src: github/ohmyzsh
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: org src_account_type: org
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https" # https/ssh
debug: true # 启用后会显示所有执行命令 debug: true # print all commands
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "ohmyzsh" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "ohmyzsh"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
zsh-users: zsh-users:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: zsh-users repos # 名字随便起 - name: zsh-users repos
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/zsh-users # 源端账户名(github) src: github/zsh-users
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: org src_account_type: org
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "antigen,zsh-syntax-highlighting,zsh-autosuggestions,zsh-completions" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "antigen,zsh-syntax-highlighting,zsh-autosuggestions,zsh-completions"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
zdharma-continuum:
runs-on: ubuntu-latest
steps:
- name: zdharma-continuum repos
uses: Yikun/hub-mirror-action@v1.5
with:
src: github/zdharma-continuum
dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: org
dst_account_type: user
clone_style: "https"
debug: true
force_update: true
static_list: "fast-syntax-highlighting"
timeout: '600s'
tmux-mem-cpu-load: tmux-mem-cpu-load:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: thewtex/tmux-mem-cpu-load # 名字随便起 - name: thewtex/tmux-mem-cpu-load
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/thewtex # 源端账户名(github) src: github/thewtex
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: user src_account_type: user
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "tmux-mem-cpu-load" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "tmux-mem-cpu-load"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
tmux-plugins: tmux-plugins:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: tmux-plugins # 名字随便起 - name: tmux-plugins
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/tmux-plugins # 源端账户名(github) src: github/tmux-plugins
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: org src_account_type: org
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "tpm,tmux-resurrect" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "tpm,tmux-resurrect,tmux-yank"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
vundle-vim: vundle-vim:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: VundleVim/Vundle-vim # 名字随便起 - name: VundleVim/Vundle-vim
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/VundleVim # 源端账户名(github) src: github/VundleVim
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: org src_account_type: org
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "Vundle.vim" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "Vundle.vim"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
onedark-vim: onedark-vim:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: joshdick/onedark-vim # 名字随便起 - name: joshdick/onedark-vim
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/joshdick # 源端账户名(github) src: github/joshdick
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: user src_account_type: user
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "onedark.vim" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "onedark.vim"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'
fzf: fzf:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: junegunn/fzf # 名字随便起 - name: junegunn/fzf
uses: Yikun/hub-mirror-action@v1.2 # 使用Yikun/hub-mirror-action uses: Yikun/hub-mirror-action@v1.5
with: with:
src: github/junegunn # 源端账户名(github) src: github/junegunn
dst: gitee/dictxiong # 目的端账户名(gitee) dst: gitee/dictxiong
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} # SSH密钥对中的私钥 dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }} # Gitee账户的私人令牌 dst_token: ${{ secrets.GITEE_TOKEN }}
src_account_type: user src_account_type: user
dst_account_type: user # 账户类型 dst_account_type: user
clone_style: "https" # 使用https方式进行clone,也可以使用ssh clone_style: "https"
debug: true # 启用后会显示所有执行命令 debug: true
force_update: true # 启用后,强制同步,即强制覆盖目的端仓库 force_update: true
static_list: "fzf" # 静态同步列表,在此填写需要同步的仓库名称,可填写多个 static_list: "fzf"
timeout: '600s' # git超时设置,超时后会自动重试git操作 timeout: '600s'

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -44,57 +44,16 @@ 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-12
steps:
- name: checkout repo
uses: actions/checkout@v3
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
steps: steps:
- name: remove dependencies - name: remove dependencies
run: | run: |
sudo apt-get -y remove curl vim python3 sudo apt-get -y remove curl vim
- name: checkout repo - name: checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -116,7 +75,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout repo - name: checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -128,4 +87,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

@@ -1,34 +1,23 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPmMImRSAhksHUfCwNpcj61izGn0Xkv1KRIuxb0OSdJl pc0/old
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO4K5S3zDYbxlw7B45zOdmKud95nXU1V6kSxEvbTOsy2 pc1/old
# keys below are the new security system starting from 2022-07 # keys below are the new security system starting from 2022-07
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIGdLJYOrBhWERmkQ+sNM8tGzJ37UBEKkEuhbtE9B65mOAAAABHNzaDo= sk0/fido
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGakUcSuNhuH7tk+Nj9gDfRtxYfGPL9yb8toQ/EpSJAM openpgp:0xD9271B01 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGakUcSuNhuH7tk+Nj9gDfRtxYfGPL9yb8toQ/EpSJAM openpgp:0xD9271B01
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUN7IXF4nlFcVfgHesgik3LIAiXlVMYJPm3yD13EVarQx5jqdBgk8Dwgkgf4rPO6MFpvIpinOyEO8zOS6HHQrCLZUv5yTFaDkUuB7eQ0EmpicGbmk9bHqj1HkOZxaobkpEfQUmFKYvkp4EexVw66sO0qfXvjHZ4H6yCAJLK5aUnKfgrE8tODzP82sU/mpJjW+Pq3uanNq754gaHwhxCIXG143/zp8qzBAeKe38xVqqDq9fTkG4hvzFvkRdS88i6l1z++0P3n0HGdOjtSg7P7fO7+7ZyPYr0gO5vB720Om/zxqPrGd9cicWi4P+aVKa+0ujWH/pqufWG6uCjKWHnBs7 sk0.ibd.ink/piv/9a ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJ4FeeBwPcUe25JsZQ7xDmGtFVeO1TQ9332IPjFY0Xm openpgp:0x8774BF70
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGV/6cLUGA5YWVI6uPhIBOBZvlNBlZLkehHQN0Gxnc+B ltp0/ubuntu ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCUN7IXF4nlFcVfgHesgik3LIAiXlVMYJPm3yD13EVarQx5jqdBgk8Dwgkgf4rPO6MFpvIpinOyEO8zOS6HHQrCLZUv5yTFaDkUuB7eQ0EmpicGbmk9bHqj1HkOZxaobkpEfQUmFKYvkp4EexVw66sO0qfXvjHZ4H6yCAJLK5aUnKfgrE8tODzP82sU/mpJjW+Pq3uanNq754gaHwhxCIXG143/zp8qzBAeKe38xVqqDq9fTkG4hvzFvkRdS88i6l1z++0P3n0HGdOjtSg7P7fO7+7ZyPYr0gO5vB720Om/zxqPrGd9cicWi4P+aVKa+0ujWH/pqufWG6uCjKWHnBs7 sk0/piv/9a
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILqRfdez8IAOf6m9Ysi3uSNKEG0Xi6GPuhA6R2fci2yB ltp0/windows ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAkkF0Qc1YhhgmgZcujjfyn3liCxAAJSGlEme0eD6s908zm95mGd64fQJSoYVaFt5y9wrQLdWX16c+gvRyNaQdPfs5jxaP+G8Vr6PkkE79hwauEOQOTbr+q3bQJmfLe56cMV7oHrKVFffkJQTvp1mcK4+CYytsxJP5v0m5TDyv4op6VcZ2m0gdrFEBV3DMMgsqF6/tSPkqbTTj9fMMSOFzXPq7qEQWgYmxT+M0iQdgEiG7Z+INkofM56tBmQ7jSeO65/FTytB6ZMiI23NGmTmYvHPyxSDWXL0X7tX42S0/A7QhxZHN0GUD0NGaSExLfgTawRLhARVHbUXC9iu5aCAr sk1/piv/9a
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFO4k0tJ+Bfu95Uavg/5P3EXMKNcq+bMqaTqzkvRZ7ji ltp1/ubuntu sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA71ukY9nzzeAGp9WYstPYiIwexzIANlPwMQ68QNQwqxAAAABHNzaDo= sk1/fido2
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8g34WHLEix6Qt0J/ClYbZeb9wh+p1IOcZkz/vNbZrv ltp1/windows ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMyZILj+GxTUhdCgz2w1TxQ+aTcggnOJIb84qA4u271S ltp0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBN24pvN1RMN+iSZyHPdyExA1Rvt8pdr3e6ih6iX+KrQ ltp2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZFTyR+R86fUpZBDkDR9yYzJpmeNsIv2CMHTh6EuEeq pc0/windows ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLYgVj+NPino6sOmahULN7SbAMaVAgzqPfDjz2S8zDv pc1
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID8WHIpwdZl7+HIZfQpwIAhpxT1huI9sd6Ydeokilg5l pc1/windows
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ/+BWSTv3w1Ru9ksfse3EUbu4ukJruUU94fCG7Sk+5g3+F80KqJGykP9tHueU07iZfAuaYj7Zcy+IWx9Ih+eYA= ph0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC5gu+U4picxVvCNYjLlmxGLpJl7svj3LklO7jSpwqdrNxlE+/xkx07PJx76AbA77dDM1Dxmm/VOvVQCKTRLTgk= ip14/sep
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF52mPmbLEriZ+DfmrMHFtvg2kiO2JpRpitvs7PbJowh home0 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF52mPmbLEriZ+DfmrMHFtvg2kiO2JpRpitvs7PbJowh home0
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKhS4voo3K/Dvzqckr0bouO1WkCI5XxswstHWnuuyKBz ltp1-bd ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvHya5i3GNniGww9Yx5ikos4Z5jevu8zz8hCQi05++SgejFPnbw9UjbHFjODK2M+ZHuhRF5mdf996r4/BxWyP8= pad0/sep
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMXu7UND6N1EKcbeSeAj2CwzGA3sM5/bRtLwoY0euWdR4vzB0nwcwd6nct5JCEyozjujJfMt8d7oCzhSt1w/1cU= ip17/sep

View File

@@ -1,5 +1,7 @@
set -g prefix ^a set -g prefix ^a
set -g mouse off bind ^a send-prefix
set -g mouse on
set -g set-clipboard on
set -g default-terminal "xterm-256color" set -g default-terminal "xterm-256color"
set -g history-limit 10000 set -g history-limit 10000
@@ -23,8 +25,20 @@ 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}"
## 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
@@ -50,4 +64,4 @@ set -g window-style fg=colour248
set -g window-active-style fg=white set -g window-active-style fg=white
# better mouse scrolling. see: https://superuser.com/questions/1622812/mouse-scrolling-in-mobaxterm-tmux # better mouse scrolling. see: https://superuser.com/questions/1622812/mouse-scrolling-in-mobaxterm-tmux
set -g terminal-overrides 'xterm*:smcup@:rmcup@' set -g terminal-overrides 'xterm*:smcup@:rmcup@'

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
THIS_FILE=$(basename "${BASH_SOURCE}") THIS_FILE=$(basename "${BASH_SOURCE}")
@@ -13,6 +13,11 @@ apost_beacon "sys.online"
# update dns # update dns
if [[ "$DFS_DDNS_ENABLE" == "1" ]]; then if [[ "$DFS_DDNS_ENABLE" == "1" ]]; then
fmt_info "updating dns ..." fmt_info "updating dns ..."
if ! is_tty; then
time_to_sleep=$((RANDOM%600))
fmt_note "sleep for $time_to_sleep seconds"
sleep $time_to_sleep
fi
"$THIS_DIR/tools/frigg-client.sh" ddns || (fmt_error "failed to update dns" && apost_beacon "dfs.ddns.fail") "$THIS_DIR/tools/frigg-client.sh" ddns || (fmt_error "failed to update dns" && apost_beacon "dfs.ddns.fail")
fi fi
@@ -34,8 +39,7 @@ case $DFS_UPDATE_CHANNEL in
esac esac
if [[ ${#DFS_COMMIT} != 40 ]]; then if [[ ${#DFS_COMMIT} != 40 ]]; then
fmt_error "invalid commit id" fmt_error "invalid commit id"
apost_beacon "dfs.invalid-commit" apost_beacon "dfs.invalid-commit" "invalid commit id: ${DFS_COMMIT}"
apost_log "ERROR" "$THIS_FILE" "invalid commit id: ${DFS_COMMIT}"
exit exit
fi fi

View File

@@ -10,12 +10,13 @@ filetype plugin indent on
" end vundle " end vundle
colorscheme onedark colorscheme onedark
syntax on
set cursorline set cursorline
set syntax=on
set autoindent set autoindent
set smartindent set smartindent
set tabstop=4
set expandtab "spaces instead of tabs set expandtab "spaces instead of tabs
set tabstop=4 softtabstop=4 shiftwidth=4
autocmd FileType c,cpp,nix,yaml setlocal tabstop=2 softtabstop=2 shiftwidth=2
set backspace=indent,eol,start set backspace=indent,eol,start
set number "line numbers set number "line numbers
set history=1000 set history=1000
@@ -31,6 +32,7 @@ hi User3 ctermfg=15 ctermbg=2
au InsertEnter * hi User3 ctermfg=15 ctermbg=5 au InsertEnter * hi User3 ctermfg=15 ctermbg=5
au InsertLeave * hi User3 ctermfg=15 ctermbg=2 au InsertLeave * hi User3 ctermfg=15 ctermbg=2
set statusline=%3*\ %{mode()}\ %*[%n][%F](%{\"\".(&fenc==\"\"?&enc:&fenc).((exists(\"+bomb\")\ &&\ &bomb)?\"+\":\"\").\"\"},%{&ff},%Y)%2*%r%1*%m%*%<%=\'0x%B\'\ [Line:%l/%L,Col:%c][%p%%] set statusline=%3*\ %{mode()}\ %*[%n][%F](%{\"\".(&fenc==\"\"?&enc:&fenc).((exists(\"+bomb\")\ &&\ &bomb)?\"+\":\"\").\"\"},%{&ff},%Y)%2*%r%1*%m%*%<%=\'0x%B\'\ [Line:%l/%L,Col:%c][%p%%]
nnoremap <silent> <C-n> :let [&nu, &rnu] = [!&rnu, &nu+&rnu==1]<cr>
" When editing a file, always jump to the last known cursor position. " When editing a file, always jump to the last known cursor position.
" Don't do it when the position is invalid, when inside an event handler " Don't do it when the position is invalid, when inside an event handler

84
.zshrc2
View File

@@ -9,20 +9,23 @@ 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'
export GPG_TTY=$(tty) export GPG_TTY=$(tty)
export LESS_TERMCAP_md=$'\E[01;33m' export LESS_TERMCAP_md=$'\E[01;33m'
export VIRTUAL_ENV_DISABLE_PROMPT=1
# env for dfs # env for dfs
if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env; set +a; fi if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env; set +a; fi
export DFS_OS_TYPE="$("$DOTFILES/tools/common.sh" get_os_type)"
# antigen # antigen
if [[ "$DFS_NO_WALL" == "1" ]]; then if [[ "$DFS_NO_WALL" == "1" ]]; then
ANTIGEN_URL="https://raw.githubusercontent.com/zsh-users/antigen/develop/bin/antigen.zsh" ANTIGEN_URL="https://raw.githubusercontent.com/zsh-users/antigen/develop/bin/antigen.zsh"
ANTIGEN_OMZ_REPO_URL="https://github.com/ohmyzsh/ohmyzsh.git" ANTIGEN_OMZ_REPO_URL="https://github.com/ohmyzsh/ohmyzsh.git"
ANTIGEN_PLUGINS=( ANTIGEN_PLUGINS=(
"https://github.com/zsh-users/zsh-syntax-highlighting" "https://github.com/zdharma-continuum/fast-syntax-highlighting"
"https://github.com/zsh-users/zsh-completions" "https://github.com/zsh-users/zsh-completions"
"https://github.com/zsh-users/zsh-autosuggestions" "https://github.com/zsh-users/zsh-autosuggestions"
) )
@@ -30,7 +33,7 @@ else
ANTIGEN_URL="https://gitee.com/dictxiong/antigen/raw/develop/bin/antigen.zsh" ANTIGEN_URL="https://gitee.com/dictxiong/antigen/raw/develop/bin/antigen.zsh"
ANTIGEN_OMZ_REPO_URL="https://gitee.com/dictxiong/ohmyzsh.git" ANTIGEN_OMZ_REPO_URL="https://gitee.com/dictxiong/ohmyzsh.git"
ANTIGEN_PLUGINS=( ANTIGEN_PLUGINS=(
"https://gitee.com/dictxiong/zsh-syntax-highlighting" "https://gitee.com/dictxiong/fast-syntax-highlighting"
"https://gitee.com/dictxiong/zsh-completions" "https://gitee.com/dictxiong/zsh-completions"
"https://gitee.com/dictxiong/zsh-autosuggestions" "https://gitee.com/dictxiong/zsh-autosuggestions"
) )
@@ -66,9 +69,18 @@ ANTIGEN_PLUGINS+=(
"extract" "extract"
"fzf" "fzf"
"git" "git"
"magic-enter"
"man"
"per-directory-history"
"pip"
"podman"
"python"
"ripgrep" "ripgrep"
"rsync"
"systemd"
"timer"
"tmux" "tmux"
"ufw" "web-search"
"z" "z"
) )
for i in ${ANTIGEN_PLUGINS[*]}; do for i in ${ANTIGEN_PLUGINS[*]}; do
@@ -101,21 +113,58 @@ fi
# alias # alias
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'
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
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 2001:da8::666' alias "p666"='ping6 240c::6666'
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))"'
case $(bash "$DOTFILES/tools/common.sh" get_os_type) in alias "ls"='ls --color=tty --time-style="+%y-%m-%d %H:%M"'
macos ) alias l='ls -lAGh -D "%y-%m-%d %H:%M"' ;; alias "l"='ls -lAGh'
* ) alias l='ls -lAGh --time-style="+%y-%m-%d %H:%M"' ;; alias "jc"='journalctl'
esac alias "jce"='jc -e'
if [[ -x $(command -v trash) ]]; then alias "jceu"='jc -eu'
alias "rm"="echo use the full path i.e. '/bin/rm'\; consider using trash" alias "jcf"='jc -f'
fi alias "jcfu"='jc -fu'
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 "sc"='systemctl'
sagt() { eval "$($DOTFILES/tools/sagent.sh $@)" } 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() {
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; }
local verb="$1"
local flake=".${2:+#$2}"
if command -v nh >/dev/null 2>&1 && [[ "$verb" != dry-build && "$verb" != dry-activate ]]; then
nh os "$verb" --bypass-root-check "$flake"
else
nixos-rebuild "$verb" --flake "$flake"
fi
}
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
@@ -140,6 +189,9 @@ bindkey "\e[1;5D" backward-word # alt+left_arrow; termius
bindkey "\ed" delete-char # alt+d bindkey "\ed" delete-char # alt+d
# mods
autoload zcalc
# dfs # dfs
dfs() dfs()
{ {
@@ -168,12 +220,16 @@ dfs()
"$DOTFILES/install.sh" -i "$DOTFILES/install.sh" -i
echo 'Done. Please open a new shell to see the changes.' echo 'Done. Please open a new shell to see the changes.'
;; ;;
log ) "$DOTFILES/tools/common.sh" "post_log" "INFO" "dfs" "$2" ;;
beacon ) "$DOTFILES/tools/common.sh" "post_beacon" "$2" "$3" ;; beacon ) "$DOTFILES/tools/common.sh" "post_beacon" "$2" "$3" ;;
* ) echo "unknown command \"$1\". available: update, force-update, version, reset, cd, log" ;; * ) echo "unknown command \"$1\". available: update, force-update, version, reset, cd, log" ;;
esac esac
} }
# motd
if [[ "$DFS_INITED" != "1" && -n "$DFS_UPDATE_CHANNEL" && "$DFS_UPDATE_CHANNEL" != "main" ]]; then
echo dotfiles not in the main channel. use with caution.
fi
# clean # clean
unset i unset i
export DFS_INITED=1 export DFS_INITED=1

View File

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

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)

105
functions/_riot Normal file
View File

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

3
functions/_sagt Normal file
View File

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

27
functions/_scf Normal file
View File

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

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

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/tools/common.sh" source "$THIS_DIR/tools/common.sh"
@@ -26,6 +26,8 @@ declare -a HOME_SYMLINKS_SRC
declare -a HOME_SYMLINKS_DST declare -a HOME_SYMLINKS_DST
HOME_SYMLINKS_SRC[0]=".ssh/authorized_keys2" HOME_SYMLINKS_SRC[0]=".ssh/authorized_keys2"
HOME_SYMLINKS_DST[0]=".ssh/authorized_keys2" HOME_SYMLINKS_DST[0]=".ssh/authorized_keys2"
HOME_SYMLINKS_SRC[1]=".eid/authorized_certificates"
HOME_SYMLINKS_DST[1]=".eid/authorized_certificates"
install_dependencies() install_dependencies()
{ {
@@ -147,6 +149,9 @@ install_symlink()
{ {
fmt_note "installing symlinks ..." fmt_note "installing symlinks ..."
for ((i=0; i<${#HOME_SYMLINKS_SRC[@]}; i++)); do for ((i=0; i<${#HOME_SYMLINKS_SRC[@]}; i++)); do
if [[ -z "${HOME_SYMLINKS_SRC[$i]}" ]]; then
continue
fi
local src="$DOTFILES/${HOME_SYMLINKS_SRC[$i]}" local src="$DOTFILES/${HOME_SYMLINKS_SRC[$i]}"
local dst="$HOME/${HOME_SYMLINKS_DST[$i]}" local dst="$HOME/${HOME_SYMLINKS_DST[$i]}"
fmt_info "creating symlink \"$dst\" --> \"$src\" ..." fmt_info "creating symlink \"$dst\" --> \"$src\" ..."
@@ -197,7 +202,11 @@ install_crontab()
{ {
if [[ -x $(command -v crontab) ]]; then if [[ -x $(command -v crontab) ]]; then
fmt_note "installing \"$CRON_JOB\" to crontab ..." fmt_note "installing \"$CRON_JOB\" to crontab ..."
( crontab -l | grep -vxF "${CRON_JOB}" | grep -v "no crontab for"; echo "$CRON_JOB" ) | crontab - if [[ -z "$(crontab -l 2>/dev/null || true)" ]]; then
echo "$CRON_JOB" | crontab -
elif !( crontab -l | grep -qxF "${CRON_JOB}"); then
( crontab -l; echo "$CRON_JOB" ) | crontab -
fi
else else
fmt_warning "crontab does not exist. skipping ..." fmt_warning "crontab does not exist. skipping ..."
fi fi
@@ -319,11 +328,10 @@ for i in ${GOT_OPTS[@]}; do
case $i in case $i in
-i ) FUNC=install ;; -i ) FUNC=install ;;
-r ) FUNC=uninstall ;; -r ) FUNC=uninstall ;;
-d|--dev ) export DFS_DEV=1; set -x ;;
-a|--auto ) INSTALL_DEP=1 ;; -a|--auto ) INSTALL_DEP=1 ;;
-s|--secure ) export DFS_DEV=0 ;;
-H|--hist|--history ) store_hist=1 ;; -H|--hist|--history ) store_hist=1 ;;
-x ) store_config=1 ;; -x ) store_config=1 ;;
--no-auth-info ) HOME_SYMLINKS_SRC=(); HOME_SYMLINKS_DST=() ;;
* ) fmt_fatal "unknown option \"$i\"" ;; * ) fmt_fatal "unknown option \"$i\"" ;;
esac esac
done done

69
riot-config.sh Normal file
View File

@@ -0,0 +1,69 @@
#!/bin/false
# batches
nasps.batch() {
remotes+=(
g1.nasp
g2.nasp
g3.nasp
g4.nasp
g5.nasp
g6.nasp
g7.nasp
g8.nasp
g9.nasp
g10.nasp
g11.nasp
g12.nasp
g13.nasp
g14.nasp
dictxiong@g15.nasp
dictxiong@g16.nasp
g17.nasp
)
}
# remotes
nasp.remote() {
remote=jump.nasp.fit
RET_PORT=${RET_PORT:-36022}
RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1
}
# domains
.domain() {
RET_USERNAME=${RET_USERNAME:-root}
RET_PORT=${RET_PORT:-12022}
RET_HOSTNAME=${remote%.}
}
dxng.domain() {
if [[ "$host" =~ ^sed([0-9]{1,2})$ ]]; then
RET_HOSTNAME=192.168.98.$((100+BASH_REMATCH[1]))
else
RET_HOSTNAME=$host.dxng.net
fi
RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1
}
42.domain() {
RET_HOSTNAME=$host.i.bd.dn42
RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root}
RET_TRUST_SERVER=1
}
nasp.domain() {
RET_HOSTNAME=$host
RET_PORT=${RET_PORT:-12022}
RET_USERNAME=${RET_USERNAME:-root}
RET_JUMP_SERVER="ssh@jump.nasp.fit:36022"
RET_TRUST_SERVER=1
}
default.domain() {
dxng.domain
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/../tools/common.sh" source "$THIS_DIR/../tools/common.sh"

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/../tools/common.sh" source "$THIS_DIR/../tools/common.sh"
@@ -21,5 +21,5 @@ if [[ -z "$IMAGE" ]]; then
fmt_fatal "image not found" fmt_fatal "image not found"
else else
fmt_note "--> ${IMAGE_META[@]}" fmt_note "--> ${IMAGE_META[@]}"
$SUDO docker run ${2:+"--name"} $2 -itd $IMAGE sh $SUDO docker run ${2:+"--name"} $2 -itd --restart=unless-stopped $IMAGE sh
fi fi

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/../tools/common.sh" source "$THIS_DIR/../tools/common.sh"

View File

@@ -1,101 +1,722 @@
#!/bin/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_SERVER=${RIOT_TRUST_SERVER:-0}
EXTRA_SSH_OPTIONS=()
GPG_FORWARD=0
# get target settings # config
# provides: RIOT_CONFIG_FILES=(
SERVER="" "$DOTFILES/riot-config.sh"
PORT="" "$HOME/.config/riot-config.sh"
SSH_USERNAME="" "riot-config.sh"
SSH_OPTIONS="" )
get_server_meta() load_riot_config() {
{ local file
local domain=${1##*.} for file in "${RIOT_CONFIG_FILES[@]}"; do
local host=${1%.*} if [[ -f "$file" ]]; then
if [[ "$host" == "$domain" ]]; then source "$file"
domain="" fi
done
}
# check if port number valid
check_port() {
[[ "$1" =~ ^[1-9][0-9]{0,4}$ ]] || return 1
[[ $1 -lt 65536 && $1 -gt 0 ]] || return 1
return 0
}
# check if username valid
check_username() {
[[ "$1" =~ ^[a-z][-a-z0-9_]*$ ]] || return 1
return 0
}
# get single server setting
# may be called more than once
get_server_meta() {
# returns:
RET_HOSTNAME=""
RET_TRUST_SERVER=0
RET_PORT="" # optional
RET_USERNAME="" # optional
RET_JUMP_SERVER="" # optional
# body
local remote="$1"
# 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 fi
case $domain in # extract port from ...:port
ibd|ebd ) if [[
SERVER=$host.$domain.ink "$remote" =~ ^[^:]+:[1-9][0-9]*$ # contains only one colon
PORT=12022 || "$remote" =~ ^\[.+\]:[1-9][0-9]*$ # in the form of [host]:port
SSH_USERNAME=root || "$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
nasp ) || "$remote" =~ ^[0-9A-Fa-f:]+%.+:[1-9][0-9]*$ # ipv6 address with scope and port
SERVER=$host ]]; then
PORT=12022 RET_PORT=${remote##*:}
SSH_USERNAME=dictxiong remote=${remote%:*}
SSH_OPTIONS='-o ProxyJump="ssh@nasp.ob.ac.cn:36022"' check_port $RET_PORT || fmt_fatal invalid port number \"$RET_PORT\"
;; fi
"" ) # remove square brackets
SERVER=proxy.beardic.cn if [[ "$remote" =~ ^\[.*\]$ ]]; then
local tmp=$(sha256sum <<< "$host" | tr -cd "[:digit:]") remote=${remote:1:-1}
tmp=${tmp:0:4} fi
PORT=$((10#$tmp+36000)) # presets -- match remote
SSH_USERNAME=root local remote_func="$remote.remote"
;; if is_function "$remote_func"; then
* ) "$remote_func"
fmt_fatal "unknown domain: $domain" fi
# presets -- match domain
RET_HOSTNAME=${remote}
local domain=${remote##*.}
local host=${remote%.*}
# if it contains no dot and is not ipv6
if [[ "$remote" != *.* && "$remote" != *:* ]]; then
domain="default"
fi
local domain_func="$domain.domain"
if is_function "$domain_func"; then
"$domain_func"
elif is_function ".domain"; then
".domain"
fi
}
parse_remote() {
# remote setting, including jump servers
# called for every remote
# provides:
SERVER=""
TRUST_SERVER=1
PORT="" # optional
USERNAME="" # optional
SSH_OPTIONS=("-o" "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" "ControlPersist=5s" "-o" "ControlPath=~/.ssh/master-socket/%C")
fi
fi
# handle input
local remote="$1"
local jump_servers=""
# loop for jump servers
while [[ -n $remote ]]; do
local server=${remote%%/*}
remote=${remote#*/}
get_server_meta "$server"
if [[ -n "$RET_JUMP_SERVER" ]]; then
jump_servers="$jump_servers${jump_servers:+,}$RET_JUMP_SERVER"
fi
# only if all servers are trusted
TRUST_SERVER=$((TRUST_SERVER*RET_TRUST_SERVER))
if [[ "$server" == "$remote" || -z "$remote" ]]; then
SERVER="$RET_HOSTNAME"
PORT="$RET_PORT"
USERNAME="$RET_USERNAME"
remote=""
else
jump_servers="$jump_servers${jump_servers:+,}$RET_USERNAME${RET_USERNAME:+@}$RET_HOSTNAME${RET_PORT:+:}$RET_PORT"
fi
done
# construct cmd
if [[ "$RIOT_TRUST_SERVER" == "1" || "$TRUST_SERVER" == "1" ]]; then
SSH_OPTIONS+=("-o" "ForwardX11=yes" "-o" "ForwardAgent=yes")
fi
if [[ -n "$jump_servers" ]]; then
SSH_OPTIONS+=("-o" "ProxyJump=$jump_servers")
fi
}
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 esac
if [[ "$status" != "0" ]]; then
return 1
fi
local remote_systemd_socket
REMOTE_GPG_SOCKET=${output%%$'\n'*}
remote_systemd_socket=${output#*$'\n'}
if [[ -z "$REMOTE_GPG_SOCKET" || "$REMOTE_GPG_SOCKET" == *$'\r'* || "$REMOTE_GPG_SOCKET" == *$'\n'* \
|| "$REMOTE_GPG_SOCKET" != /*/S.gpg-agent || "$REMOTE_GPG_SOCKET" == *:* \
|| "$remote_systemd_socket" != "0" && "$remote_systemd_socket" != "1" ]]; then
GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket: $output"
return 1
fi
if [[ "$remote_systemd_socket" == "1" ]]; then
fmt_warning "remote gpg-agent.socket is active and may race with GPG forwarding; consider disabling its socket activation"
fi
}
prepare_gpg_forwarding() {
if [[ "$DFS_DRY_RUN" == "1" ]]; then
REMOTE_GPG_SOCKET="<remote-gpg-agent-socket>"
LOCAL_GPG_EXTRA_SOCKET="<local-gpg-agent-extra-socket>"
else
if ! check_local_gpg_agent; then
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
fi
if ! probe_remote_gpg_socket; then
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
fi
fi
SSH_OPTIONS+=(
# The probe already removed the old socket. Do not let the main SSH
# connection unlink a path recreated during the gap between them.
"-o" "StreamLocalBindUnlink=no"
"-o" "ExitOnForwardFailure=yes"
"-R" "$REMOTE_GPG_SOCKET:$LOCAL_GPG_EXTRA_SOCKET"
)
}
print_cmd() {
local output=""
for s in "${CMD[@]}"; do
if [[ "$s" =~ [\ \\\'\"] ]]; then # needs to be escaped
s="${s@Q}"
fi
output+="$s "
done
fmt_note "--> ${output% }"
}
eval_or_echo() {
local DO=""
tmux_win=${tmux_win:-0}
if [[ "$DFS_DRY_RUN" == "1" ]]; then
DO=echo
fi
if [[ "$USE_TMUX" == "1" ]]; then
if [[ -z "$TMUX_SESS" ]]; then
TMUX_SESS=riot-$(date +%s)
$DO tmux new-session -d -s $TMUX_SESS bash -l
else
tmux_win=$((tmux_win+1))
$DO tmux new-window -t $TMUX_SESS:$tmux_win -d bash -l
fi
local command
printf -v command '%q ' "${CMD[@]}"
$DO tmux send-keys -l -t "$TMUX_SESS:$tmux_win" "${command% }"
$DO tmux send-keys -t "$TMUX_SESS:$tmux_win" Enter
else
$DO "${CMD[@]}"
fi
}
# ssh series
prepare_ssh_cmd() {
local ssh_bin="${1:-ssh}"
if [[ "$ssh_bin" == "scp" || "$ssh_bin" == "sftp" ]]; then
local port_param='-P'
else
local port_param='-p'
fi
CMD=(
"$ssh_bin"
"${PORT:+$port_param}" "$PORT"
"${SSH_OPTIONS[@]}"
"${EXTRA_SSH_OPTIONS[@]}"
"$SCP_SRC"
"$USERNAME${USERNAME:+@}$SERVER"
"$SCP_DST"
"${@:2}"
)
for i in ${!CMD[@]}; do if [[ -z "${CMD[i]}" ]]; then unset CMD[i]; fi; done
} }
# ssh # ssh
run_ssh() run_ssh()
{ {
CMD="ssh -p $PORT $SSH_OPTIONS $SSH_USERNAME@$SERVER" prepare_ssh_cmd "$@"
fmt_note "-->" $CMD print_cmd
eval $CMD eval_or_echo
} }
# sshl # sshl
run_sshl() run_sshl()
{ {
if [[ -z "$1" || "$1" != *":"* ]]; then local arg left right localsock access
fmt_fatal "invalid remote address: $1" local res="${1//[^:]}"
local lorr="-L"
if [[ "${FUNCNAME[1]}" == "run_sshr" ]]; then
lorr="-R"
fi fi
while if [[ ${#res} -eq 2 ]]; then
local port=$(shuf -n 1 -i 49152-65535) arg="$1"
netstat -atun | grep -q "$port" elif [[ ${#res} -eq 0 ]]; then
do if [[ "$lorr" == "-R" ]]; then
continue arg="$1"
done else
CMD="ssh -p $PORT $SSH_OPTIONS -NC -L $port:$1 $SSH_USERNAME@$SERVER" arg="$(get_free_port):localhost:$1"
fmt_note "-->" $CMD 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
run_sshr()
{
run_sshl "$1"
}
# sshd
run_sshd()
{
local port=${1:-$(get_free_port)}
SSH_OPTIONS+=("-NC" "-D" "$port")
prepare_ssh_cmd ssh
print_cmd
fmt_note " > please access localhost:$port" fmt_note " > please access localhost:$port"
eval $CMD eval_or_echo
}
# scp
run_scp() {
local src="$1"
local dst="$2"
local dst_is_remote=1
# whoever is ./*, it can't be the remote; whoever not exists on local, it's possible the remote.
# it is suggested to use ./* for local files.
if [[ "$src" != "./"* && ( "$dst" == "./"* || ( ! -e "$src" && -e "$dst" ) ) ]]; then
dst_is_remote=0
fi
if [[ "$dst_is_remote" == "1" ]]; then
SCP_SRC="$src"
SERVER="$SERVER":"$dst"
else
SERVER="$SERVER":"$src"
SCP_DST="$dst"
fi
SSH_OPTIONS+=("-r")
prepare_ssh_cmd scp
print_cmd
eval_or_echo
}
# ping
run_ping() {
CMD=(ping)
if [[ "$1" == "ping4" ]]; then
CMD+=(-4)
elif [[ "$1" == "ping6" ]]; then
CMD+=(-6)
fi
CMD+=(-c 4 "$SERVER")
print_cmd
eval_or_echo
}
# remove host keys
remove_hostkey() {
local key
if [[ -z "$PORT" || "$PORT" == "22" ]]; then
key=$SERVER
else
key="[$SERVER]:$PORT"
fi
ssh-keygen -R "$key"
} }
# main # main
print_help() print_help()
{ {
fmt_info "usage: $0 <service> [command] [options]" local pager=(cat)
echo "available commands: ssh (default), sshl (ssh -L)" if [[ -t 1 ]] && command -v less > /dev/null 2>&1; then
} pager=(less -R)
router()
{
if [[ -z "$1" || "$1" == "-h" || "$1" == "--help" ]]; then
print_help
exit
fi fi
get_server_meta "$1"
case $2 in cat <<EOF | "${pager[@]}"
-h|--help) NAME
print_help riot - connect to remote hosts using SSH presets
exit
;; SYNOPSIS
ssh|"" ) ${0##*/} [OPTION]... REMOTE [OPTION]... [COMMAND [COMMAND-ARG]...]
run_ssh
;; DESCRIPTION
sshl ) Connect to REMOTE using the matching configuration from riot-config.sh.
run_sshl "$3" 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.
print_help
fmt_fatal "unknown command: $2" OPTIONS
;; -4
esac 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
} }
router "${GOT_OPTS[@]}" router() {
local positional=()
local arg=""
local option=""
local remaining=""
local option_value=""
while [[ $# -gt 0 || -n "$arg" ]]; do
if [[ -z "$arg" ]]; then
arg=$1
shift
fi
# Normalize a long option or one item from a short-option group.
remaining=""
case "$arg" in
-- )
positional+=("$@")
break
;;
--* )
option=$arg
arg=""
;;
-?* )
option=${arg:0:2}
remaining=${arg:2}
arg=${remaining:+-$remaining}
;;
* )
positional+=("$arg")
arg=""
if [[ "${#positional[@]}" -ge 2 ]]; then
positional+=("$@")
break
fi
continue
;;
esac
case "$option" in
-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
if [[ "${positional[2]}" == "--" ]]; then
positional=("${positional[@]:0:2}" "${positional[@]:3}")
fi
IFS=',' read -ra remotes <<< "${positional[0]}"
for i in ${!remotes[@]}; do if [[ -z "${remotes[i]}" ]]; then unset remotes[i]; fi; done
if [[ "${#positional[@]}" == "0" || "${#remotes[@]}" == "0" ]]; then
print_help
exit 1
fi
load_riot_config
if [[ "$GPG_FORWARD" == "1" && ( \
( "${positional[1]}" != "" && "${positional[1]}" != "ssh" && "${positional[1]}" != "tmux" ) \
|| "${#positional[@]}" -gt 2 ) ]]; then
fmt_fatal "gpg-agent forwarding is only supported for interactive SSH login"
fi
for i in ${!remotes[@]}; do
remote="${remotes[i]}"
local batch_func="${remote}.batch"
if is_function "$batch_func"; then
"$batch_func"
continue
fi
parse_remote "$remote"
case "${positional[1]}" in
ssh|tmux|"" )
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
if [[ "$GPG_FORWARD" == "1" ]]; then
prepare_gpg_forwarding
fi
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]}"
;;
zssh )
run_ssh zssh "${positional[@]:2}"
;;
sftp )
run_ssh sftp "${positional[@]:2}"
;;
sshl )
test -n "${positional[2]}" || fmt_fatal "no target address provided"
test "${#positional[@]}" -eq 3 || fmt_fatal "sshl requires exactly one argument"
run_sshl "${positional[2]}"
;;
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
check_port "${positional[2]}" || fmt_fatal "invalid port number: ${positional[2]}"
run_sshd "${positional[2]}"
else
run_sshd
fi
;;
scp )
test "${#positional[@]}" -eq 4 || fmt_fatal "scp requires exactly two arguments: source and destination"
test -n "${positional[2]}" || fmt_fatal "no source path specified"
test -n "${positional[3]}" || fmt_fatal "no destination path specified"
run_scp "${positional[2]}" "${positional[3]}"
;;
rm )
test "${#positional[@]}" -eq 2 || fmt_fatal "rm requires no arguments"
remove_hostkey
;;
* )
print_help
fmt_fatal "unknown command: ${positional[1]}"
;;
esac
done
if [[ -n "$TMUX_SESS" && "$DFS_DRY_RUN" != "1" ]]; then
tmux attach-session -t $TMUX_SESS
fi
}
router "$@"

378
scripts/scf Executable file
View File

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

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

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"

View File

@@ -1,24 +1,28 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR_COMMON_SH=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR_COMMON_SH=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
export DOTFILES=$( cd "$THIS_DIR_COMMON_SH/.." && pwd ) export DOTFILES=$( cd "$THIS_DIR_COMMON_SH/.." && pwd )
if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env; set +a; fi if [[ -f ~/.config/dotfiles/env ]]; then set -a; source ~/.config/dotfiles/env; set +a; fi
if [[ "$DFS_DEBUG" == "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 it is sourced # Parse args and set env when sourced, unless the caller handles its own
# todo: make this skipable # option boundary.
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then if [[ "${BASH_SOURCE[0]}" != "${0}" && "$DFS_SKIP_ARG_PARSE" != "1" ]]; then
ORIGIN_ARGS=("$@") ORIGIN_ARGS=("$@")
ARG="" ARG=""
GOT_OPTS=() GOT_OPTS=()
while [[ $# > 0 || -n "$ARG" ]]; do while [[ $# -gt 0 || -n "$ARG" ]]; do
if [[ -z "$ARG" ]]; then ARG=$1; shift; fi if [[ -z "$ARG" ]]; then
if [[ "$1" == "--" ]]; then GOT_OPTS+=("$@"); break; fi
ARG="$1"; shift;
fi
case $ARG in case $ARG in
-q*|--quite ) export DFS_QUIET=1 ;; -q*|--quiet ) 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*|--dry-run ) export DFS_DRY_RUN=1 ;;
--color ) export DFS_COLOR=1 ;; --color ) export DFS_COLOR=1 ;;
--dry-run ) export DFS_DRY_RUN=1 ;; # TODO!!!
--*=* ) GOT_OPTS+=("${ARG%%=*}" "${ARG#*=}") ;; --*=* ) GOT_OPTS+=("${ARG%%=*}" "${ARG#*=}") ;;
--* ) GOT_OPTS+=("$ARG") ;; --* ) GOT_OPTS+=("$ARG") ;;
-* ) GOT_OPTS+=("${ARG:0:2}") ;; -* ) GOT_OPTS+=("${ARG:0:2}") ;;
@@ -37,15 +41,9 @@ 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
if [[ -t 1 || "$DFS_COLOR" == "1" ]]; then is_tty() {
is_tty() { [[ -t 1 || "$DFS_COLOR" == "1" ]]
true }
}
else
is_tty() {
false
}
fi
supports_truecolor() { supports_truecolor() {
case "$COLORTERM" in case "$COLORTERM" in
@@ -161,19 +159,6 @@ ask_for_Yn()
fi fi
} }
post_log()
{
if [[ $# != 3 || -z "$1" || -z "$2" || -z "$3" ]]; then
fmt_fatal "usage: post_log <level> <section> <content>"
fi
"${DOTFILES}/tools/frigg-client.sh" "log" "[$1][$2] $3"
}
apost_log()
{
post_log "$@" 1>/dev/null &
}
post_beacon() post_beacon()
{ {
if [[ $# < 1 || -z "$1" ]]; then if [[ $# < 1 || -z "$1" ]]; then
@@ -189,6 +174,7 @@ apost_beacon()
get_os_type() get_os_type()
{ {
test -z "$DFS_OS_TYPE" || { echo "$DFS_OS_TYPE"; return; }
local ans="unknown" local ans="unknown"
case "$(uname -s)" in case "$(uname -s)" in
Darwin*) ans="MacOS";; Darwin*) ans="MacOS";;
@@ -197,11 +183,13 @@ get_os_type()
Linux* ) ans="Linux";; Linux* ) ans="Linux";;
*) ans="unknown";; *) ans="unknown";;
esac esac
export DFS_OS_TYPE="$ans"
echo $ans | tr '[:upper:]' '[:lower:]' echo $ans | tr '[:upper:]' '[:lower:]'
} }
get_linux_dist() get_linux_dist()
{ {
test -z "$DFS_LINUX_DIST" || { echo "$DFS_LINUX_DIST"; return; }
local ans="unknown" local ans="unknown"
if [ -f /etc/os-release ]; then if [ -f /etc/os-release ]; then
. /etc/os-release . /etc/os-release
@@ -220,6 +208,7 @@ get_linux_dist()
else else
ans="unknown" ans="unknown"
fi fi
export DFS_LINUX_DIST="$ans"
echo $ans | tr '[:upper:]' '[:lower:]' echo $ans | tr '[:upper:]' '[:lower:]'
} }
@@ -232,6 +221,35 @@ get_os_name()
echo $ans echo $ans
} }
is_port_free() {
( echo $1 | grep -qxE "[1-9][0-9]{0,4}" ) || false
local cmd
case $(get_os_type) in
macos ) cmd="netstat -van | grep -q \".$1\"" ;;
cygwin|msys ) cmd="netstat -ano | grep -q \":$1\"" ;;
*) cmd="netstat -tuanp | grep -q \":$1\"" ;;
esac
if eval $cmd; then
return 2
else
return 0
fi
}
get_free_port() {
while
local port=$(shuf -n 1 -i 49152-65535)
! is_port_free $port
do
continue
done
echo $port
}
is_function() {
test "$(type -t "$1")" = "function"
}
# if bash-ed, else source-d # if bash-ed, else source-d
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
$1 "${@:2}" $1 "${@:2}"

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
@@ -25,6 +25,7 @@ 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()
{ {
@@ -63,8 +64,8 @@ handle_resp()
post_beacon() post_beacon()
{ {
local beacon_type=$1 local beacon_type="$1"
local meta=$2 local meta="$2"
if [[ -n "$CI" && "$beacon_type" != "gh.ci" && "$beacon_type" != "dfs.invalid-commit" && "$beacon_type" != "dfs.dirty" ]]; then if [[ -n "$CI" && "$beacon_type" != "gh.ci" && "$beacon_type" != "dfs.invalid-commit" && "$beacon_type" != "dfs.dirty" ]]; then
return return
fi fi
@@ -75,17 +76,6 @@ post_beacon()
handle_resp "$resp" handle_resp "$resp"
} }
post_log()
{
local log_content=$1
if [[ -z "$log_content" ]]; then
fmt_fatal "log content is required"
fi
init_uuid
resp=$(curl $DFS_CURL_OPTIONS -sSL -X POST -H "Content-Type: text/plain" -d "$log_content" "https://api.beardic.cn/post-log?hostname=$hostname&uuid=$uuid")
handle_resp "$resp"
}
update_dns() update_dns()
{ {
if [[ -z "$DFS_DDNS_IP4$DFS_DDNS_IP6" ]]; then if [[ -z "$DFS_DDNS_IP4$DFS_DDNS_IP6" ]]; then
@@ -103,12 +93,13 @@ update_dns()
ip4="" ip4=""
elif [[ "$DFS_DDNS_IP4" == "auto" ]]; then elif [[ "$DFS_DDNS_IP4" == "auto" ]]; then
ip4="auto" ip4="auto"
api_url="https://api4.beardic.cn"
elif [[ "$DFS_DDNS_IP4" == "api" ]]; then elif [[ "$DFS_DDNS_IP4" == "api" ]]; then
ip4=$(curl $DFS_CURL_OPTIONS -sSL "https://api.ipify.org") ip4=$(curl $DFS_CURL_OPTIONS -sSL "https://api.ipify.org")
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 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 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"
@@ -124,7 +115,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 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 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"
@@ -137,7 +128,7 @@ update_dns()
print_help() print_help()
{ {
fmt_info "usage: $0 <beacon|log|ddns> [beacon_type|log_content]" fmt_info "usage: $0 <beacon|ddns> [beacon_type] [beacon_meta]"
} }
router() router()
@@ -149,9 +140,6 @@ router()
beacon) beacon)
post_beacon "$2" "$3" post_beacon "$2" "$3"
;; ;;
log)
post_log "$2"
;;
ddns) ddns)
update_dns update_dns
;; ;;
@@ -162,4 +150,4 @@ router()
esac esac
} }
router "${GOT_OPTS[@]}" router "${GOT_OPTS[@]}"

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
ARG="" ARG=""
GOT_OPTS=() GOT_OPTS=()

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"
@@ -6,7 +6,7 @@ source "$THIS_DIR/common.sh"
brew_install() brew_install()
{ {
# brew update # brew update
brew install git zsh curl tmux vim util-linux brew install git zsh curl tmux vim util-linux coreutils
} }
router() router()

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"

9
tools/sagent-op.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
op=$(command -v op || command -v op.exe || true)
if [[ -z "$op" || ! -x "$op" ]]; then
echo "1Password CLI not found" >&2
exit 1
fi
exec "$op" read "op://Personal/id25519-passphrase/$(hostname)"

View File

@@ -1,13 +1,18 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
export DFS_COLOR=1 export DFS_COLOR=1
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"
SO_PATHS=(
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so" # ubuntu 22.04
"/run/current-system/sw/lib/opensc-pkcs11.so" # nixos 23.05
"/Library/OpenSC/lib/opensc-pkcs11.so" # macos 13.4
)
find_so_file() find_so_file()
{ {
local SO_PATHS=( "/usr/lib64/opensc-pkcs11.so" "/usr/local/lib/opensc-pkcs11.so" )
local SO_FILE local SO_FILE
for SO_FILE in ${SO_PATHS[*]}; do for SO_FILE in ${SO_PATHS[*]}; do
if [[ -f "$SO_FILE" ]]; then if [[ -f "$SO_FILE" ]]; then
@@ -15,70 +20,291 @@ find_so_file()
return return
fi fi
done done
return 1
} }
create_agent() create_agent()
{ {
local SO_FILE=$(find_so_file) local IFS=","
if [[ -n "$SO_FILE" ]]; then ssh-agent -P "${SO_PATHS[*]},/nix/store/*"
fmt_note "opensc-pkcs11.so found"
SO_FILE="-P $SO_FILE"
fi
ssh-agent $SO_FILE
} }
kill_agent() kill_agent()
{ {
if pgrep -x ssh-agent > /dev/null; then local status
fmt_note "killing existing agent" if pgrep -u "$EUID" -x ssh-agent > /dev/null; then
pkill -9 -x ssh-agent fmt_note "stopping existing 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=$(find_so_file) local SO_FILE
if [[ -n "$SO_FILE" ]]; then if ! SO_FILE=$(find_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()
{
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() list()
{ {
echo echo "available keys:" echo echo "available keys:"
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 all already-killed
} }
all() all()
{ {
local agent_file="/tmp/piv-agent-$(whoami)" local mode="${1:-}"
if [[ -f $agent_file ]]; then mkdir -p "$HOME/.ssh"
source $agent_file > /dev/null 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 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
kill_agent if ! agent_is_usable; then
if [[ "$mode" != "already-killed" ]]; then
kill_agent
fi
fmt_note "launching a new agent" 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 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()
{ {
os_type="$(get_os_type)"
if [[ "$os_type" == "msys" || "$os_type" == "cygwin" ]]; then
fmt_fatal "unsupported platform: $os_type. you may use WinCryptSSHAgent."
fi
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
all all
return return
@@ -90,6 +316,15 @@ route()
piv) piv)
add_piv add_piv
;; ;;
op)
add_id25519_with_op
;;
gpg)
use_gpg_agent
;;
gpg-pin)
cache_gpg_pin "${@:2}"
;;
reset) reset)
reset reset
;; ;;
@@ -98,8 +333,9 @@ route()
;; ;;
*) *)
fmt_error "unknown command: $1" fmt_error "unknown command: $1"
return 1
;; ;;
esac esac
} }
route "$@" route "$@"

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/usr/bin/env bash
set -ex set -ex
OPTS='-a -bcl --color --arg1=1 --arg2 2 " 1 2" yes' OPTS='-a -bcl --color --quiet --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 8 test $# -eq 9
test "$*" = "${OPTS//\"/}" test "$*" = "${OPTS//\"/}"
test "$DFS_LITE" = "1" test "$DFS_LITE" = "1"
is_tty is_tty
test -z "$DFS_QUIET" test "$DFS_QUIET" = "1"
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

132
tools/test-riot-gpg.sh Executable file
View File

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

75
tools/test-sagent-gpg-pin.sh Executable file
View File

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

83
tools/test-scf.sh Executable file
View File

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

View File

@@ -3,6 +3,12 @@
set -ex set -ex
trap "dfs beacon gh.ci.fail" ERR trap "dfs beacon gh.ci.fail" ERR
# fix for macos
dfs cd
if [[ $(./tools/common.sh get_os_type) == "macos" ]]; then
export PATH="/usr/local/opt/coreutils/libexec/gnubin:/opt/homebrew/opt/coreutils/libexec/gnubin:${PATH}"
fi
# check files # check files
cd / cd /
l l
@@ -13,11 +19,14 @@ l
pwd pwd
test -f .zshrc2 test -f .zshrc2
diff -q ./.ssh/authorized_keys2 ~/.ssh/authorized_keys2 diff -q ./.ssh/authorized_keys2 ~/.ssh/authorized_keys2
diff -q ./.eid/authorized_certificates ~/.eid/authorized_certificates
grep -q ".zshrc2" ~/.zshrc grep -q ".zshrc2" ~/.zshrc
if [[ -x $(command -v crontab) ]]; then
crontab -l | grep -qxF "0 * * * * ${DOTFILES}/update.sh"
fi
# check scripts and functions # check scripts and functions
dfs version dfs version
dfs log 1
dfs beacon gh.ci $GITHUB_SHA dfs beacon gh.ci $GITHUB_SHA
z ~ z ~
test ~ -ef "$(pwd)" test ~ -ef "$(pwd)"
@@ -25,19 +34,25 @@ dogo
doll doll
dfs cd dfs cd
tools/test-getopts.sh tools/test-getopts.sh
tools/test-scf.sh
tools/test-riot-gpg.sh
tools/test-sagent-gpg-pin.sh
tools/common.sh get_os_name 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
scp -P 12022 -o ServerAliveInterval=60 -o PermitLocalCommand=yes -o ControlMaster=auto -o ControlPersist=5s -o ControlPath=~/.ssh/master-socket/%C -o ForwardX11=yes -o ForwardAgent=yes -r another@host.dxng.net:/tmp/ ./tmp'
test "$(riot you@example.com:55 -tD --password ssh ping -c 1 2>/dev/null)" = 'ssh -p 55 -o ServerAliveInterval=60 -o ForwardX11=yes -o ForwardAgent=yes -o PasswordAuthentication=yes -o PubkeyAuthentication=no you@example.com ping -c 1'
# check alias # check alias
alias p114 alias p114
alias cbds alias cbds
which riot which riot
sagt sagt
test -f "/tmp/piv-agent-$(whoami)" test -f ~/.ssh/agent-$(whoami)
gbes || which gbes gbes || which gbes
# check update # check update

View File

@@ -1,16 +1,18 @@
#!/bin/bash #!/usr/bin/env bash
declare -A INSTALL_COMMANDS declare -A INSTALL_COMMANDS
INSTALL_COMMANDS=(\ INSTALL_COMMANDS=(\
[git]="apt update && apt install git" \ [git]="apt update && apt install git" \
[fzf]="git clone --depth 1 https://gitee.com/dictxiong/fzf.git ~/.fzf && ~/.fzf/install" \ [fzf]="git clone --depth 1 https://gitee.com/dictxiong/fzf.git ~/.fzf && ~/.fzf/install" \
[acme.sh]="curl https://get.acme.sh | sh -s email=${EMAIL:-me@beardic.cn}" \ [acme.sh]="curl https://get.acme.sh | sh -s email=${EMAIL:-acme@dxng.net}" \
[oh-my-zsh]='sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"' \ [oh-my-zsh]='sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"' \
[oh-my-tuna]='wget https://tuna.moe/oh-my-tuna/oh-my-tuna.py && sudo python oh-my-tuna.py --global' \ [oh-my-tuna]='wget https://tuna.moe/oh-my-tuna/oh-my-tuna.py && sudo python oh-my-tuna.py --global' \
[v2fly]="bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh) #--remove" \ [v2fly]="bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh) #--remove" \
[zerotier-one]='curl -s https://install.zerotier.com | sudo bash' \ [zerotier-one]='curl -s https://install.zerotier.com | sudo bash' \
[docker-ce]='curl -fsSL https://get.docker.com | sudo bash -s - --mirror Aliyun #--dry-run' \ [docker-ce]='curl -fsSL https://get.docker.com | sudo bash -s - --mirror Aliyun #--dry-run' \
[lemonbench]='curl -fsSL https://ilemonra.in/LemonBenchIntl | bash -s fast # or full' \ [lemonbench]='curl -fsSL https://raw.githubusercontent.com/LemonBench/LemonBench/main/LemonBench.sh | bash -s fast # or full' \
[nix]='sh <(curl -L https://nixos.org/nix/install) #--daemon' \
[alist]='curl -fsSL "https://alist.nn.ci/v3.sh" | bash -s install' \
) )
install() install()

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e
THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd ) THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" && pwd )
source "$THIS_DIR/common.sh" source "$THIS_DIR/common.sh"
@@ -14,11 +14,11 @@ apt_install()
{ {
apt-get update -y apt-get update -y
# lite # lite
apt-get install -y git zsh bash tmux vim curl inetutils-ping less bsdmainutils DEBIAN_FRONTEND=noninteractive apt-get install -y git zsh bash tmux vim curl iputils-ping less bsdmainutils
# full # full
if [[ -z "$DFS_LITE" || "$DFS_LITE" == "0" ]]; then if [[ -z "$DFS_LITE" || "$DFS_LITE" == "0" ]]; then
apt-get install -y wget dialog net-tools dnsutils netcat traceroute sudo python3 python3-pip cron openssh-client openssh-server htop gcc g++ cmake make zip DEBIAN_FRONTEND=noninteractive apt-get install -y wget dialog net-tools dnsutils netcat traceroute sudo python3 python3-pip cron openssh-client openssh-server htop gcc g++ cmake make zip
for i in {fzf,ripgrep,man-db}; do apt-get install -y $i; done for i in {fzf,ripgrep,man-db}; do DEBIAN_FRONTEND=noninteractive apt-get install -y $i; done
fi fi
} }