Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fafa14ad | ||
|
|
63575ce827 | ||
|
|
8aeed7b901 |
@@ -17,7 +17,7 @@ setup:
|
||||
@script/setup
|
||||
|
||||
build:
|
||||
go build -trimpath -ldflags "$(LDFLAGS)" -o keyfunc .
|
||||
go build -trimpath -ldflags "$(LDFLAGS)" -o keyfunc ./cmd/keyfunc
|
||||
|
||||
test:
|
||||
@script/test
|
||||
|
||||
@@ -88,17 +88,49 @@ Prints the unencrypted private key in OpenSSH format (the
|
||||
and nothing else, so it can be redirected into a file. The key's comment is the
|
||||
same as for `pub`.
|
||||
|
||||
### `keyfunc ssh install <[user@]host> [-- ssh options...]`
|
||||
### `keyfunc ssh install <[user@]host> [-- sftp options...]`
|
||||
|
||||
Runs the system `ssh` to the host and, on the host:
|
||||
Adds the `pub` line to `~/.ssh/authorized_keys` on the host. No command is run
|
||||
on the host: the file is fetched, changed here, and written back with the
|
||||
system `sftp` client in batch mode.
|
||||
|
||||
- creates `~/.ssh` with mode `0700` if it is missing;
|
||||
- creates `~/.ssh/authorized_keys` with mode `0600` if it is missing;
|
||||
- appends the `pub` line only if an identical line is not already there.
|
||||
The first connection fetches `~/.ssh/authorized_keys`. The file reads as empty
|
||||
only when `sftp` reported that file as not being there — the one line naming
|
||||
that path. The same wording anywhere else in the session does not count: `ssh`
|
||||
writes `No such file or directory` about an `-i` it cannot find, on a session
|
||||
that then authenticates through the agent. When `sftp` failed for any other
|
||||
reason — the file is there and cannot be read, the connection did not come up —
|
||||
the tool prints what `sftp` said and exits with status 1 without writing
|
||||
anything, rather than put a file back holding the new key alone. What `sftp`
|
||||
cannot tell apart is a missing file and one in a directory it cannot enter, so a
|
||||
`~/.ssh` whose mode shuts the user out reads as a host with no file; the second
|
||||
connection sets that mode to `0700` and writes, as on a host that has none. If
|
||||
an identical line is already in the file, the tool prints
|
||||
`already present` and connects no further. Otherwise the line is added (after a
|
||||
newline, if the file did not end with one) and a second connection:
|
||||
|
||||
It then prints `added` or `already present`. How this `ssh` connection
|
||||
authenticates is up to the user's normal `ssh` setup (existing keys, agent,
|
||||
password). Anything after `--` is passed to `ssh` unchanged.
|
||||
- creates `~/.ssh` and sets it to mode `0700`;
|
||||
- uploads the new file as `~/.ssh/authorized_keys.keyfunc-<random>` and sets it
|
||||
to mode `0600`;
|
||||
- renames that file over `~/.ssh/authorized_keys`.
|
||||
|
||||
The tool then prints `added`. So a run that adds a line connects twice. The
|
||||
rename is the step that either happens or does not: the file on the host is
|
||||
never half-written. `sftp` does it in one step against servers that offer
|
||||
OpenSSH's POSIX rename extension, as OpenSSH's own server does; a server
|
||||
without it may refuse to rename onto a file that is already there.
|
||||
|
||||
If a step fails, the tool prints what `sftp` said, removes nothing, and exits
|
||||
with status 1. It names the uploaded file only when the step that failed was
|
||||
the upload or one after it, which is where a file of that name can be on the
|
||||
host; a failure before the upload names none. Everything `sftp`
|
||||
writes goes to standard error, so the tool's own standard output is only
|
||||
`added` or `already present`.
|
||||
|
||||
Anything after `--` is passed to `sftp` unchanged, which is where the port goes
|
||||
(`-P 2222`, not `-p`). How the connection authenticates is up to the user's
|
||||
normal `ssh` setup, except that batch mode does not prompt: a key or an agent
|
||||
has to do it, not a typed password.
|
||||
|
||||
### `keyfunc ssh to <host> [ssh arguments...]`
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
module git.eeqj.de/sneak/keyfunc
|
||||
|
||||
go 1.26
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
filippo.io/age v1.2.1
|
||||
filippo.io/age v1.3.2
|
||||
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd
|
||||
github.com/btcsuite/btcd v0.24.2
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/btcsuite/btcd v0.25.0
|
||||
github.com/btcsuite/btcd/btcutil v1.2.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/term v0.46.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,136 +1,50 @@
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
|
||||
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
|
||||
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
|
||||
filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd h1:6YFV6horz2wDFPWWhour8qx8gLGyO0qoplwEeOuQ2J4=
|
||||
git.eeqj.de/sneak/secret v0.0.0-20260810132333-41cea400a7fd/go.mod h1:gKCcMZvlBOqusn/BxR8IyFmSJQr6R4vvjJ926iNpOSI=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM=
|
||||
github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk=
|
||||
github.com/btcsuite/btcd/btcutil v1.2.0 h1:p3+S2g3Q+7G5NOh4Ji+2UrBOrg5Z0Q4ykzShWG1Dhgs=
|
||||
github.com/btcsuite/btcd/btcutil v1.2.0/go.mod h1:/Taflm113pYjUpbWKKQEfa6XOtI/+WS8awxeMZpY75k=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 h1:yMIg99+4aBvqfl/HzJRKfxTX9rGfikoI9uvFzterhc8=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0/go.mod h1:Y72Ren9gfhlEvnwnT78BGcSNO2UMphTKLn9AorF+5rg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk=
|
||||
github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+227
-53
@@ -1,57 +1,48 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// script is what runs on the host. It reads the key line from its own
|
||||
// standard input, so the line never appears on a command line, where
|
||||
// anyone else on the host could read it out of the process list. It
|
||||
// contains no single quote, so the whole of it travels through ssh
|
||||
// inside one pair of them. The umask keeps anything it makes to the
|
||||
// owner from the start; the modes are then set outright, whatever the
|
||||
// umask on the host turns out to be. A file whose last line has no
|
||||
// newline at its end gets one before the key line goes on, so that the
|
||||
// two do not run into each other.
|
||||
const script = `
|
||||
set -e
|
||||
umask 077
|
||||
directory="$HOME/.ssh"
|
||||
file="$directory/authorized_keys"
|
||||
if [ ! -d "$directory" ]; then
|
||||
mkdir -p "$directory"
|
||||
chmod 700 "$directory"
|
||||
fi
|
||||
if [ ! -f "$file" ]; then
|
||||
: > "$file"
|
||||
chmod 600 "$file"
|
||||
fi
|
||||
IFS= read -r line
|
||||
if grep -q -x -F -e "$line" "$file"; then
|
||||
echo "already present"
|
||||
else
|
||||
if [ -s "$file" ] && [ -n "$(tail -c 1 "$file")" ]; then
|
||||
printf "\n" >> "$file"
|
||||
fi
|
||||
printf "%s\n" "$line" >> "$file"
|
||||
echo "added"
|
||||
fi
|
||||
`
|
||||
// Where the key goes on the host and what the file it arrives in is
|
||||
// called before it is renamed into place. The random end of that name
|
||||
// keeps two runs at once from writing to the same file.
|
||||
const (
|
||||
directory = ".ssh"
|
||||
authorized = ".ssh/authorized_keys"
|
||||
sidecarPrefix = ".ssh/authorized_keys.keyfunc-"
|
||||
sidecarBytes = 8
|
||||
)
|
||||
|
||||
// The modes the host is left with, as sftp's chmod spells them, and
|
||||
// the mode of the copy made here on the way.
|
||||
const (
|
||||
directoryMode = "700"
|
||||
fileMode = "600"
|
||||
localMode = 0o600
|
||||
)
|
||||
|
||||
// install returns the command that adds the public key to a host.
|
||||
func install() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "install <[user@]host> [-- ssh options...]",
|
||||
Use: "install <[user@]host> [-- sftp options...]",
|
||||
Short: "add the public key to a host's authorized_keys",
|
||||
Long: "Runs the system ssh to the host, which makes ~/.ssh and " +
|
||||
"~/.ssh/authorized_keys there if they are missing and adds " +
|
||||
"the public key unless the same line is already in the " +
|
||||
"file. Anything after -- is given to ssh unchanged.",
|
||||
Long: "Downloads the host's authorized_keys with the system " +
|
||||
"sftp, adds the public key to it here unless the same " +
|
||||
"line is already there, and uploads the result as a file " +
|
||||
"beside it which is then renamed over it. Nothing is run " +
|
||||
"on the host. Anything after -- is given to sftp " +
|
||||
"unchanged, which is where the port goes (-P).",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key, comment, err := derived(cmd)
|
||||
@@ -64,7 +55,7 @@ func install() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
return send(cmd, args[0], args[1:], line)
|
||||
return add(cmd, args[0], args[1:], line)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,24 +64,207 @@ func install() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// send runs ssh to the host with the user's options, gives it the
|
||||
// script to run there, and writes the key line to its standard input.
|
||||
// What the host says, added or already present, is passed straight on.
|
||||
func send(cmd *cobra.Command, host string, options []string, line string) error {
|
||||
argv := slices.Concat(options, []string{
|
||||
host, "/bin/sh -c '" + script + "'",
|
||||
})
|
||||
// add puts the key line in the host's authorized_keys. The file is
|
||||
// fetched in one sftp session and written back in another, so a run
|
||||
// that adds a line connects twice; a run that finds the line already
|
||||
// there connects once and stops.
|
||||
func add(cmd *cobra.Command, host string, options []string, line string) error {
|
||||
work, err := os.MkdirTemp("", "keyfunc-install-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("making a temporary directory: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gosec // the options are the user's own, meant for ssh
|
||||
command := exec.CommandContext(cmd.Context(), "ssh", argv...)
|
||||
command.Stdin = strings.NewReader(line + "\n")
|
||||
command.Stdout = cmd.OutOrStdout()
|
||||
command.Stderr = cmd.ErrOrStderr()
|
||||
defer func() { _ = os.RemoveAll(work) }()
|
||||
|
||||
content, err := fetch(cmd, host, options,
|
||||
filepath.Join(work, "authorized_keys"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, added := merge(content, line)
|
||||
if !added {
|
||||
return write(cmd, "already present\n")
|
||||
}
|
||||
|
||||
return upload(cmd, host, options, work, merged)
|
||||
}
|
||||
|
||||
// upload writes the new file to the host and renames it over
|
||||
// authorized_keys, which is the step that either happens or does not.
|
||||
// Nothing is removed when a step fails: the file left behind is named
|
||||
// so that it can be looked at and cleared away by hand.
|
||||
func upload(
|
||||
cmd *cobra.Command, host string, options []string,
|
||||
work, merged string,
|
||||
) error {
|
||||
local := filepath.Join(work, "authorized_keys.merged")
|
||||
|
||||
err := os.WriteFile(local, []byte(merged), localMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing the new file: %w", err)
|
||||
}
|
||||
|
||||
sidecar, err := sidecarName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The mkdir may fail: the directory is usually there already.
|
||||
said, err := session(cmd, host, options, []string{
|
||||
"-mkdir " + directory,
|
||||
"chmod " + directoryMode + " " + directory,
|
||||
"put " + quoted(local) + " " + sidecar,
|
||||
"chmod " + fileMode + " " + sidecar,
|
||||
"rename " + sidecar + " " + authorized,
|
||||
})
|
||||
if err != nil {
|
||||
// sftp echoes each command as it runs it and stops at the
|
||||
// first that fails, so the name is in what it said only once
|
||||
// the put was reached, which is where a file of that name
|
||||
// can be on the host. Before that there is none to name.
|
||||
if strings.Contains(said, sidecar) {
|
||||
return fmt.Errorf(
|
||||
"%w; %s may be left on the host", err, sidecar,
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return write(cmd, "added\n")
|
||||
}
|
||||
|
||||
// session runs one sftp session with the user's own options and the
|
||||
// batch of commands, which sftp reads from its standard input and
|
||||
// stops at the first of which that fails, unless it begins with a
|
||||
// dash. sftp echoes the commands as it runs them, so everything it
|
||||
// says goes to the error output and the tool's own output stays the
|
||||
// one word it prints. What it said is also given back: a session that
|
||||
// failed says there what went wrong, and the status alone does not.
|
||||
func session(
|
||||
cmd *cobra.Command, host string, options []string, batch []string,
|
||||
) (string, error) {
|
||||
argv := slices.Concat(
|
||||
[]string{"-b", "-"}, options, []string{host},
|
||||
)
|
||||
|
||||
var said bytes.Buffer
|
||||
|
||||
//nolint:gosec // the options are the user's own, meant for sftp
|
||||
command := exec.CommandContext(cmd.Context(), "sftp", argv...)
|
||||
command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n")
|
||||
command.Stdout = &said
|
||||
command.Stderr = &said
|
||||
|
||||
err := command.Run()
|
||||
|
||||
_, _ = cmd.ErrOrStderr().Write(said.Bytes())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("running ssh: %w", err)
|
||||
return said.String(), fmt.Errorf("running sftp: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return said.String(), nil
|
||||
}
|
||||
|
||||
// merge returns the file with the key line on the end, and whether it
|
||||
// had to be added. A file whose last line has no newline at its end
|
||||
// gets one first, so that the two lines do not run into each other.
|
||||
func merge(content, line string) (string, bool) {
|
||||
if slices.Contains(strings.Split(content, "\n"), line) {
|
||||
return content, false
|
||||
}
|
||||
|
||||
if content != "" && !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
|
||||
return content + line + "\n", true
|
||||
}
|
||||
|
||||
// fetch brings the host's authorized_keys into the given path and
|
||||
// returns what is in it. A host that has no such file reads as empty,
|
||||
// but only when that is what sftp said about it: a file that is there
|
||||
// and cannot be read fails the run, because writing back over it
|
||||
// would leave the host with the new key and nothing else.
|
||||
func fetch(
|
||||
cmd *cobra.Command, host string, options []string, into string,
|
||||
) (string, error) {
|
||||
said, err := session(cmd, host, options, []string{
|
||||
"get " + authorized + " " + quoted(into),
|
||||
})
|
||||
if err != nil {
|
||||
if absent(said) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
//nolint:gosec // the path is a temporary file of the tool's own
|
||||
content, err := os.ReadFile(into)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading the fetched file: %w", err)
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// absent says whether sftp reported the file that was asked for as
|
||||
// not being there, which is the one failure of the fetch that is read
|
||||
// as an empty authorized_keys. The reading is taken only from the
|
||||
// line in which sftp reports on that file, because ssh writes "no
|
||||
// such file" into the same output for reasons of its own — a missing
|
||||
// -i identity file draws that warning on a session that then
|
||||
// authenticates through the agent — and a real read failure on such a
|
||||
// session must not pass for an empty file.
|
||||
func absent(said string) bool {
|
||||
for line := range strings.Lines(said) {
|
||||
named, is := reportedNotFound(strings.TrimSpace(line))
|
||||
if is && (named == authorized ||
|
||||
strings.HasSuffix(named, "/"+authorized)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// reportedNotFound returns the path an sftp line reports as not being
|
||||
// there, and whether the line is such a report. The client writes one
|
||||
// wording for a remote file it cannot find, naming the path the
|
||||
// server expanded, which is the absolute one.
|
||||
func reportedNotFound(line string) (string, bool) {
|
||||
const (
|
||||
before = `File "`
|
||||
after = `" not found.`
|
||||
)
|
||||
|
||||
if !strings.HasPrefix(line, before) ||
|
||||
!strings.HasSuffix(line, after) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(strings.TrimPrefix(line, before), after), true
|
||||
}
|
||||
|
||||
// sidecarName returns the name the new file is uploaded under.
|
||||
func sidecarName() (string, error) {
|
||||
random := make([]byte, sidecarBytes)
|
||||
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("making a name for the new file: %w", err)
|
||||
}
|
||||
|
||||
return sidecarPrefix + hex.EncodeToString(random), nil
|
||||
}
|
||||
|
||||
// quoted puts the double quotes around a path that sftp needs when the
|
||||
// path has a space in it. Only paths of the tool's own making are
|
||||
// given to it, and they hold no quote of their own.
|
||||
func quoted(path string) string {
|
||||
return `"` + path + `"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//nolint:testpackage // absent is what these wordings are read by
|
||||
package ssh
|
||||
|
||||
import "testing"
|
||||
|
||||
// What a session says besides its report on the file that was asked
|
||||
// for: sftp echoes the command it is running, and ssh warns about an
|
||||
// identity file it cannot find in the words of a missing file even
|
||||
// though the session goes on to authenticate.
|
||||
const (
|
||||
echoed = `sftp> get .ssh/authorized_keys "/tmp/keyfunc/authorized_keys"
|
||||
`
|
||||
warning = `Warning: Identity file /gone not accessible: ` +
|
||||
"No such file or directory.\n"
|
||||
)
|
||||
|
||||
// TestAbsenceIsReadOnlyFromWhatSFTPSaidAboutAuthorizedKeys holds the
|
||||
// wordings the OpenSSH client was seen to use against a real server:
|
||||
// a file it cannot find is reported one way, naming the path the
|
||||
// server expanded, and everything else it says is a failure.
|
||||
func TestAbsenceIsReadOnlyFromWhatSFTPSaidAboutAuthorizedKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sessions := map[string]struct {
|
||||
said string
|
||||
want bool
|
||||
}{
|
||||
"the file is not there": {
|
||||
said: echoed +
|
||||
`File "/home/someone/.ssh/authorized_keys" not found.` + "\n",
|
||||
want: true,
|
||||
},
|
||||
"the file is not there, named as it was asked for": {
|
||||
said: echoed + `File ".ssh/authorized_keys" not found.` + "\n",
|
||||
want: true,
|
||||
},
|
||||
"the file is not there and an identity file is not either": {
|
||||
said: warning + echoed +
|
||||
`File "/home/someone/.ssh/authorized_keys" not found.` + "\n",
|
||||
want: true,
|
||||
},
|
||||
"the file is there and cannot be read": {
|
||||
said: echoed +
|
||||
`remote open "/home/someone/.ssh/authorized_keys": ` +
|
||||
"Permission denied\n",
|
||||
want: false,
|
||||
},
|
||||
"only an identity file is not there": {
|
||||
said: warning + echoed +
|
||||
`remote open "/home/someone/.ssh/authorized_keys": ` +
|
||||
"Permission denied\n",
|
||||
want: false,
|
||||
},
|
||||
"some other file is not there": {
|
||||
said: echoed + `File "/home/someone/.ssh/known_hosts" not found.` +
|
||||
"\n",
|
||||
want: false,
|
||||
},
|
||||
"the connection did not come up": {
|
||||
said: "ssh: connect to host example.com port 22: " +
|
||||
"Connection refused\nConnection closed\n",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, session := range sessions {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if absent(session.said) != session.want {
|
||||
t.Errorf(
|
||||
"read as absent: %t, wanted %t, from:\n%s",
|
||||
!session.want, session.want, session.said,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+311
-56
@@ -1,8 +1,10 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// The modes the host is supposed to end up with, and the mode the
|
||||
// stand-in ssh needs so that it can be run at all.
|
||||
// stand-ins need so that they can be run at all.
|
||||
const (
|
||||
directoryMode = 0o700
|
||||
fileMode = 0o600
|
||||
@@ -22,8 +24,21 @@ const (
|
||||
)
|
||||
|
||||
// failingStatus is the status the stand-in ssh ends with when a test
|
||||
// wants to see a status handed on.
|
||||
const failingStatus = 7
|
||||
// wants to see a status handed on, and failedStatus is the status the
|
||||
// tool itself ends with when something went wrong.
|
||||
const (
|
||||
failingStatus = 7
|
||||
failedStatus = 1
|
||||
)
|
||||
|
||||
// notADirectory is what a test puts where the .ssh directory belongs
|
||||
// to make a step of the write session fail.
|
||||
const notADirectory = "a file where the directory belongs\n"
|
||||
|
||||
// missingIdentity is a path with no file at it, handed to sftp after
|
||||
// the dashes so that ssh warns about it in the words of a missing
|
||||
// file.
|
||||
const missingIdentity = "/nonexistent/keyfunc-test-identity"
|
||||
|
||||
// The host, and where on it the key ends up.
|
||||
const (
|
||||
@@ -32,20 +47,77 @@ const (
|
||||
keptIn = "authorized_keys"
|
||||
)
|
||||
|
||||
// installer is a stand-in for the system ssh for the install command.
|
||||
// It writes down what it was given and then runs the command meant for
|
||||
// the host right here, with the home directory pointed at a directory
|
||||
// standing in for the host's, so that what keyfunc sends can be
|
||||
// watched doing its work.
|
||||
// The tool's own name, as it stands in the arguments a test hands to
|
||||
// Main, the ssh subcommand both commands the tests here drive live
|
||||
// under, and the one of those two these tests name most.
|
||||
const (
|
||||
tool = "keyfunc"
|
||||
subcommand = "ssh"
|
||||
installing = "install"
|
||||
)
|
||||
|
||||
// The key line the example mnemonic gives at index 0, as it stands in
|
||||
// an authorized_keys file.
|
||||
const keyLine = vectorZero + " keyfunc/ssh/0\n"
|
||||
|
||||
// installer is a stand-in for the system sftp for the install
|
||||
// command. It writes down the arguments and every command of the
|
||||
// batch it is given, echoes each command as sftp does, and carries
|
||||
// the commands out against a directory standing in for the host's
|
||||
// home directory, so that what keyfunc sends can be watched doing its
|
||||
// work. A command that begins with a dash may fail; any other failure
|
||||
// ends the session, as it does in sftp's own batch mode.
|
||||
//
|
||||
// The two ways a get can fail are worded as the OpenSSH client words
|
||||
// them, both naming the path the server expanded: a file that is not
|
||||
// there, which is the one failure the tool reads as an empty file, and
|
||||
// a file that is there and cannot be read, which is not. An -i naming
|
||||
// a file that is not here draws the warning ssh writes for it, which
|
||||
// carries the wording of a missing file into a session that goes on to
|
||||
// authenticate.
|
||||
const installer = `
|
||||
while [ $# -gt 1 ]; do
|
||||
printf '%s\n' "$1" >> "$KEYFUNC_TEST_ARGUMENTS"
|
||||
shift
|
||||
previous=
|
||||
for argument in "$@"; do
|
||||
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
|
||||
if [ "$previous" = -i ] && [ ! -e "$argument" ]; then
|
||||
printf 'Warning: Identity file %s not accessible: %s.\n' \
|
||||
"$argument" "No such file or directory" >&2
|
||||
fi
|
||||
previous=$argument
|
||||
done
|
||||
home="$KEYFUNC_TEST_HOME"
|
||||
while IFS= read -r line; do
|
||||
printf 'sftp> %s\n' "$line"
|
||||
printf '%s\n' "$line" >> "$KEYFUNC_TEST_BATCH"
|
||||
allowed=no
|
||||
case "$line" in
|
||||
-*)
|
||||
line=${line#-}
|
||||
allowed=yes
|
||||
;;
|
||||
esac
|
||||
eval "set -- $line"
|
||||
worked=yes
|
||||
case "$1" in
|
||||
get)
|
||||
if [ ! -e "$home/$2" ]; then
|
||||
worked=no
|
||||
printf 'File "%s" not found.\n' "$home/$2" >&2
|
||||
elif ! cp "$home/$2" "$3" 2>/dev/null; then
|
||||
worked=no
|
||||
printf 'remote open "%s": Permission denied\n' "$home/$2" >&2
|
||||
fi
|
||||
;;
|
||||
put) cp "$2" "$home/$3" 2>/dev/null || worked=no ;;
|
||||
mkdir) mkdir "$home/$2" 2>/dev/null || worked=no ;;
|
||||
chmod) chmod "$2" "$home/$3" 2>/dev/null || worked=no ;;
|
||||
rename) mv "$home/$2" "$home/$3" 2>/dev/null || worked=no ;;
|
||||
esac
|
||||
if [ "$worked" = no ] && [ "$allowed" = no ]; then
|
||||
printf 'sftp: %s failed\n' "$1" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
printf '%s' "$1" > "$KEYFUNC_TEST_COMMAND"
|
||||
HOME="$KEYFUNC_TEST_HOME"
|
||||
export HOME
|
||||
eval "$1"
|
||||
`
|
||||
|
||||
// caller is a stand-in for the system ssh for the to command. It
|
||||
@@ -63,24 +135,22 @@ fi
|
||||
exit "$KEYFUNC_TEST_STATUS"
|
||||
`
|
||||
|
||||
// pretended is where a stand-in ssh writes down what it was asked to
|
||||
// do.
|
||||
// pretended is where a stand-in writes down what it was asked to do.
|
||||
type pretended struct {
|
||||
// home stands in for the home directory on the host.
|
||||
home string
|
||||
// arguments holds what ssh was given before the command, one per
|
||||
// line.
|
||||
// arguments holds the arguments of every session, one per line.
|
||||
arguments string
|
||||
// command holds what ssh was told to run on the host.
|
||||
command string
|
||||
// batch holds the commands of every session, one per line.
|
||||
batch string
|
||||
}
|
||||
|
||||
func TestTheKeyIsAddedToTheHostAndThenLeftAlone(t *testing.T) {
|
||||
func TestTheKeyIsAddedToAHostThatHasNoFileYet(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
|
||||
require.Equal(t, "added\n", run(t, "ssh", "install", host))
|
||||
require.Equal(t, "added\n", install(t, host))
|
||||
|
||||
directory, err := os.Stat(filepath.Join(pretend.home, keptUnder))
|
||||
require.NoError(t, err)
|
||||
@@ -94,11 +164,30 @@ func TestTheKeyIsAddedToTheHostAndThenLeftAlone(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, os.FileMode(fileMode), file.Mode().Perm())
|
||||
|
||||
added := read(t, path)
|
||||
require.Equal(t, vectorZero+" keyfunc/ssh/0\n", added)
|
||||
require.Equal(t, keyLine, read(t, path))
|
||||
}
|
||||
|
||||
require.Equal(t, "already present\n", run(t, "ssh", "install", host))
|
||||
require.Equal(t, added, read(t, path))
|
||||
func TestAKeyThatIsAlreadyThereIsLeftAlone(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
path := seed(t, pretend, "somebody else\n"+keyLine)
|
||||
|
||||
require.Equal(t, "already present\n", install(t, host))
|
||||
require.Equal(t, "somebody else\n"+keyLine, read(t, path))
|
||||
|
||||
// The fetch and nothing after it: the tool did not connect again.
|
||||
require.Len(t, recorded(t, pretend.batch), 1)
|
||||
}
|
||||
|
||||
func TestAnEmptyFileGetsTheKeyAndNoBlankLineBeforeIt(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
path := seed(t, pretend, "")
|
||||
|
||||
require.Equal(t, "added\n", install(t, host))
|
||||
require.Equal(t, keyLine, read(t, path))
|
||||
}
|
||||
|
||||
func TestTheKeyDoesNotRunIntoALineWithNoNewlineAtItsEnd(t *testing.T) {
|
||||
@@ -106,41 +195,145 @@ func TestTheKeyDoesNotRunIntoALineWithNoNewlineAtItsEnd(t *testing.T) {
|
||||
|
||||
pretend := pretendHost(t)
|
||||
already := "ssh-ed25519 AAAAsomebodyelse somebody@else"
|
||||
path := seed(t, pretend, already)
|
||||
|
||||
require.NoError(t,
|
||||
os.Mkdir(filepath.Join(pretend.home, keptUnder), directoryMode),
|
||||
)
|
||||
|
||||
path := filepath.Join(pretend.home, keptUnder, keptIn)
|
||||
require.NoError(t, os.WriteFile(path, []byte(already), fileMode))
|
||||
|
||||
require.Equal(t, "added\n", run(t, "ssh", "install", host))
|
||||
require.Equal(t,
|
||||
already+"\n"+vectorZero+" keyfunc/ssh/0\n",
|
||||
read(t, path),
|
||||
)
|
||||
require.Equal(t, "added\n", install(t, host))
|
||||
require.Equal(t, already+"\n"+keyLine, read(t, path))
|
||||
}
|
||||
|
||||
func TestTheKeyLineIsNotOnTheCommandLine(t *testing.T) {
|
||||
func TestTheFileIsUploadedBesideTheOldOneAndThenRenamedOverIt(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
|
||||
run(t, "ssh", "install", host)
|
||||
require.Equal(t, "added\n", install(t, host))
|
||||
|
||||
sent := recorded(t, pretend.batch)
|
||||
require.Len(t, sent, 6)
|
||||
|
||||
// The name of the uploaded file is random, so it is read off the
|
||||
// put and then looked for in the two commands that follow.
|
||||
beside := strings.Fields(sent[3])[2]
|
||||
require.True(t,
|
||||
strings.HasPrefix(beside, ".ssh/authorized_keys.keyfunc-"),
|
||||
)
|
||||
|
||||
require.True(t, strings.HasPrefix(sent[0], "get .ssh/authorized_keys "))
|
||||
require.Equal(t, "-mkdir .ssh", sent[1])
|
||||
require.Equal(t, "chmod 700 .ssh", sent[2])
|
||||
require.Equal(t, "put", strings.Fields(sent[3])[0])
|
||||
require.Equal(t, "chmod 600 "+beside, sent[4])
|
||||
require.Equal(t, "rename "+beside+" .ssh/authorized_keys", sent[5])
|
||||
}
|
||||
|
||||
func TestAFileThatCannotBeReadIsNotWrittenOver(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
unreadable := unfetchable(t, pretend)
|
||||
|
||||
printed, said, err := attempt(t, host)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, printed)
|
||||
require.Contains(t, said, "Permission denied")
|
||||
|
||||
// The fetch and nothing after it, and what was on the host is
|
||||
// still what is on the host.
|
||||
require.Len(t, recorded(t, pretend.batch), 1)
|
||||
require.DirExists(t, unreadable)
|
||||
}
|
||||
|
||||
func TestAWarningAboutAnotherFileIsNotTakenForTheOneAskedFor(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
unreadable := unfetchable(t, pretend)
|
||||
|
||||
// ssh warns about an -i it cannot find in the words of a missing
|
||||
// file, on a session that then authenticates perfectly well. That
|
||||
// warning is not sftp reporting on authorized_keys, so the fetch
|
||||
// failure is still a failure.
|
||||
printed, said, err := attempt(t, host, "--", "-i", missingIdentity)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, printed)
|
||||
require.Contains(t, said, "No such file or directory")
|
||||
require.Contains(t, said, "Permission denied")
|
||||
|
||||
require.Len(t, recorded(t, pretend.batch), 1)
|
||||
require.DirExists(t, unreadable)
|
||||
|
||||
// The same run again, this way for the status it ends with.
|
||||
given := os.Args
|
||||
|
||||
t.Cleanup(func() { os.Args = given })
|
||||
|
||||
os.Args = []string{
|
||||
tool, subcommand, installing, host, "--", "-i", missingIdentity,
|
||||
}
|
||||
|
||||
require.Equal(t, failedStatus, cli.Main())
|
||||
}
|
||||
|
||||
func TestAFailedStepNamesTheUploadedFileAndChangesNothing(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
|
||||
// A file where the .ssh directory belongs: nothing is there to
|
||||
// fetch, and then the put has nowhere to put anything, so the
|
||||
// write session ends at the put.
|
||||
inTheWay := filepath.Join(pretend.home, keptUnder)
|
||||
require.NoError(t,
|
||||
os.WriteFile(inTheWay, []byte(notADirectory), fileMode),
|
||||
)
|
||||
|
||||
printed, said, err := attempt(t, host)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, printed)
|
||||
require.Contains(t, said, "put failed")
|
||||
|
||||
// The put is the last command the session got to, and the file it
|
||||
// was uploading is the one the message names.
|
||||
sent := recorded(t, pretend.batch)
|
||||
require.Len(t, sent, 4)
|
||||
require.Equal(t, "put", strings.Fields(sent[3])[0])
|
||||
require.Contains(t, err.Error(), strings.Fields(sent[3])[2])
|
||||
|
||||
require.Equal(t, notADirectory, read(t, inTheWay))
|
||||
|
||||
// The same run again, this way for the status it ends with.
|
||||
given := os.Args
|
||||
|
||||
t.Cleanup(func() { os.Args = given })
|
||||
|
||||
os.Args = []string{tool, subcommand, installing, host}
|
||||
|
||||
require.Equal(t, failedStatus, cli.Main())
|
||||
}
|
||||
|
||||
func TestTheKeyLineIsNotSentAsACommand(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
|
||||
install(t, host)
|
||||
|
||||
require.NotContains(t, read(t, pretend.arguments), "ssh-ed25519")
|
||||
require.NotContains(t, read(t, pretend.command), "ssh-ed25519")
|
||||
require.NotContains(t, read(t, pretend.batch), "ssh-ed25519")
|
||||
}
|
||||
|
||||
func TestWhatComesAfterTheDashesIsGivenToSSH(t *testing.T) {
|
||||
func TestWhatComesAfterTheDashesIsGivenToSFTP(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
pretend := pretendHost(t)
|
||||
|
||||
run(t, "ssh", "install", host, "--", "-p", "2222")
|
||||
install(t, host, "--", "-P", "2222")
|
||||
|
||||
// The same arguments twice over: adding a line takes two
|
||||
// connections, one to fetch the file and one to write it back.
|
||||
session := []string{"-b", "-", "-P", "2222", host}
|
||||
require.Equal(t,
|
||||
[]string{"-p", "2222", host},
|
||||
slices.Concat(session, session),
|
||||
recorded(t, pretend.arguments),
|
||||
)
|
||||
}
|
||||
@@ -150,7 +343,7 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
|
||||
|
||||
arguments, noted := pretendCall(t)
|
||||
|
||||
_, err := execute(t, "ssh", "to", host, "uptime")
|
||||
_, err := execute(t, subcommand, "to", host, "uptime")
|
||||
|
||||
var passed ssh.StatusError
|
||||
|
||||
@@ -177,7 +370,7 @@ func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) {
|
||||
|
||||
t.Cleanup(func() { os.Args = given })
|
||||
|
||||
os.Args = []string{"keyfunc", "ssh", "to", host, "uptime"}
|
||||
os.Args = []string{tool, subcommand, "to", host, "uptime"}
|
||||
|
||||
require.Equal(t, failingStatus, cli.Main())
|
||||
}
|
||||
@@ -190,17 +383,78 @@ func pretendHost(t *testing.T) pretended {
|
||||
pretend := pretended{
|
||||
home: t.TempDir(),
|
||||
arguments: filepath.Join(t.TempDir(), "arguments"),
|
||||
command: filepath.Join(t.TempDir(), "command"),
|
||||
batch: filepath.Join(t.TempDir(), "batch"),
|
||||
}
|
||||
|
||||
t.Setenv("KEYFUNC_TEST_HOME", pretend.home)
|
||||
t.Setenv("KEYFUNC_TEST_ARGUMENTS", pretend.arguments)
|
||||
t.Setenv("KEYFUNC_TEST_COMMAND", pretend.command)
|
||||
standIn(t, installer)
|
||||
t.Setenv("KEYFUNC_TEST_BATCH", pretend.batch)
|
||||
standIn(t, "sftp", installer)
|
||||
|
||||
return pretend
|
||||
}
|
||||
|
||||
// install runs the install command, requires it to have worked, and
|
||||
// gives back what the tool itself printed.
|
||||
func install(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
|
||||
printed, _, err := attempt(t, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
return printed
|
||||
}
|
||||
|
||||
// attempt runs the install command with the tool's own output kept
|
||||
// apart from what the stand-in said, since the stand-in echoes its
|
||||
// batch as sftp does. It gives back what the tool printed, what the
|
||||
// stand-in said, and how the run ended.
|
||||
func attempt(t *testing.T, args ...string) (string, string, error) {
|
||||
t.Helper()
|
||||
|
||||
var printed, said bytes.Buffer
|
||||
|
||||
root := cli.Root()
|
||||
root.SetOut(&printed)
|
||||
root.SetErr(&said)
|
||||
root.SetArgs(slices.Concat([]string{subcommand, installing}, args))
|
||||
|
||||
err := root.ExecuteContext(t.Context())
|
||||
|
||||
return printed.String(), said.String(), err
|
||||
}
|
||||
|
||||
// seed puts an authorized_keys file on the stand-in host before the
|
||||
// tool runs and gives back its path.
|
||||
func seed(t *testing.T, pretend pretended, content string) string {
|
||||
t.Helper()
|
||||
|
||||
directory := filepath.Join(pretend.home, keptUnder)
|
||||
require.NoError(t, os.Mkdir(directory, directoryMode))
|
||||
|
||||
path := filepath.Join(directory, keptIn)
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), fileMode))
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// unfetchable puts a directory where authorized_keys belongs on the
|
||||
// stand-in host, which the stand-in can see but cannot fetch: that is
|
||||
// how a file that is there and cannot be read looks from here. It
|
||||
// gives back the path.
|
||||
func unfetchable(t *testing.T, pretend pretended) string {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t,
|
||||
os.Mkdir(filepath.Join(pretend.home, keptUnder), directoryMode),
|
||||
)
|
||||
|
||||
path := filepath.Join(pretend.home, keptUnder, keptIn)
|
||||
require.NoError(t, os.Mkdir(path, directoryMode))
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// pretendCall puts the to stand-in on the path and gives back the file
|
||||
// the arguments are written down in and the file the agent socket is
|
||||
// noted in.
|
||||
@@ -213,20 +467,21 @@ func pretendCall(t *testing.T) (string, string) {
|
||||
t.Setenv("KEYFUNC_TEST_ARGUMENTS", arguments)
|
||||
t.Setenv("KEYFUNC_TEST_SOCKET", noted)
|
||||
t.Setenv("KEYFUNC_TEST_STATUS", strconv.Itoa(failingStatus))
|
||||
standIn(t, caller)
|
||||
standIn(t, "ssh", caller)
|
||||
|
||||
return arguments, noted
|
||||
}
|
||||
|
||||
// standIn writes a stand-in for the system ssh and puts it first on
|
||||
// the path, so that the tool finds it instead of the real one.
|
||||
func standIn(t *testing.T, body string) {
|
||||
// standIn writes a stand-in for one of the system programs and puts it
|
||||
// first on the path, so that the tool finds it instead of the real
|
||||
// one.
|
||||
func standIn(t *testing.T, name, body string) {
|
||||
t.Helper()
|
||||
|
||||
directory := t.TempDir()
|
||||
|
||||
err := os.WriteFile(
|
||||
filepath.Join(directory, "ssh"),
|
||||
filepath.Join(directory, name),
|
||||
[]byte("#!/bin/sh\n"+body), standInMode,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -247,7 +502,7 @@ func read(t *testing.T, path string) string {
|
||||
return string(content)
|
||||
}
|
||||
|
||||
// recorded returns the arguments a stand-in wrote down, one per line.
|
||||
// recorded returns the lines a stand-in wrote down.
|
||||
func recorded(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user