Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ dist/
bin/
debug.test
snap.login
.gocache/
.gopath/
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Repository Guidelines

## Project Structure & Module Organization
- `cmd/gopssh/main.go` provides the CLI entrypoint, with supporting helpers under `cmd/gopssh/gopssh/`.
- Core parallel SSH logic, worker pools, and agent wrappers live in `pkg/pssh/`, which also houses unit tests and fixtures in `pkg/pssh/test/`.
- Packaging utilities for Homebrew, RPM, and DEB builds are grouped under `pack/`, while release metadata lives in `releasenote.template.md`.
- The top-level `Makefile` orchestrates builds, tests, linting, and coverage aggregation; keep changes aligned with its targets.

## Build, Test, and Development Commands
- `make build` compiles `cmd/gopssh` into a local `gopssh` binary; it is the default target and cleans stale artifacts.
- `make test` runs race-enabled tests for both the CLI and library packages and merges coverage into `coverage.txt`.
- `make lint` invokes the pinned `golangci-lint` binary from `./bin`; run `make setup` once to install it (requires network access).
- For release builds, mirror the README example: `go build -ldflags "-X main.version=$(TAG) ..."` so version metadata stays accurate.

## Coding Style & Naming Conventions
- Follow Go defaults: tabs for indentation, `gofmt` + `goimports` formatting, exported symbols in PascalCase, internals in camelCase.
- Keep files self-contained and small; place shared concurrency helpers in `pkg/pssh` rather than duplicating logic in `cmd/`.
- Run `make fmt` (or its underlying `gofmt`/`goimports` loop) before committing to avoid lint noise.

## Testing Guidelines
- Name tests `*_test.go` with functions `TestXxx`; table-driven cases fit well for SSH session permutations.
- Use `go test ./cmd/gopssh ./pkg/pssh -race` for quick local runs; rely on `make test` when you need coverage artifacts.
- Place reusable fixtures under `pkg/pssh/test` and keep them deterministic to prevent race test flakiness.

## Commit & Pull Request Guidelines
- Recent history mixes concise imperative commits (`fix worker hang`) with Conventional Commits (`fix: update dependency`); prefer the latter style for clarity.
- Ensure commits stay focused and include context for packaging or release changes (e.g., note RPM spec updates).
- Before opening a PR, run `make build test lint`, attach the resulting summary, link related issues, and describe any manual SSH verification performed.

## Release & Packaging Notes
- Build artifacts land in `.bin/`; confirm the binary version with `./gopssh -version` before running `pack/debpack` or `pack/rpmpack`.
- Update `releasenote.template.md` alongside packaging changes so CI-driven releases stay coherent.
47 changes: 37 additions & 10 deletions cmd/gopssh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import (
"io"
"log"
"os"
"strings"
"time"

"github.com/masahide/gopssh/pkg/pssh"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/crypto/ssh"
"golang.org/x/term"
)

const (
defaultKexFlags = "diffie-hellman-group1-sha1,diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,curve25519-sha256@libssh.org"
defaultCiphersFlags = "arcfour256,aes128-gcm@openssh.com,chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr"
defaultMacsFlags = "hmac-sha1-96,hmac-sha1,hmac-sha2-256,hmac-sha2-256-etm@openssh.com"
// https://man.openbsd.org/ssh_config#IdentityFile
defaultIdentityFiles = "~/.ssh/id_dsa,~/.ssh/id_ecdsa,~/.ssh/id_ed25519,~/.ssh/id_rsa"
defaultMaxAgent = 50
Expand All @@ -25,16 +24,44 @@ const (

// nolint: gochecknoglobals
var (
defaultMacsFlags = []string{
ssh.InsecureHMACSHA196,
ssh.HMACSHA1,
ssh.HMACSHA256ETM,
ssh.HMACSHA512ETM,
ssh.HMACSHA256,
ssh.HMACSHA512,
}
defaultKexAlgos = []string{
ssh.InsecureKeyExchangeDH1SHA1,
ssh.InsecureKeyExchangeDH14SHA1,
ssh.InsecureKeyExchangeDHGEXSHA1,
ssh.KeyExchangeMLKEM768X25519,
ssh.KeyExchangeCurve25519,
ssh.KeyExchangeECDHP256,
ssh.KeyExchangeECDHP384,
ssh.KeyExchangeECDHP521,
ssh.KeyExchangeDH14SHA256,
}
defaultCiphersFlags = []string{
ssh.InsecureCipherRC4256,
ssh.CipherAES128GCM,
ssh.CipherAES256GCM,
ssh.CipherChaCha20Poly1305,
ssh.CipherAES128CTR,
ssh.CipherAES192CTR,
ssh.CipherAES256CTR,
}
version = "dev"
commit = "none"
date = "unknown"
showVer = flag.Bool("version", false, "Show version")
)

func newConfig() *pssh.Config {
kexFlag := defaultKexFlags
ciphersFlag := defaultCiphersFlags
macsFlag := defaultMacsFlags
kexAlgos := strings.Join(defaultKexAlgos, ",")
ciphersFlag := strings.Join(defaultCiphersFlags, ",")
macsFlag := strings.Join(defaultMacsFlags, ",")
identityFiles := defaultIdentityFiles
c := pssh.Config{
Concurrency: 0,
Expand All @@ -59,19 +86,19 @@ func newConfig() *pssh.Config {
flag.BoolVar(&c.IgnoreHostKey, "k", c.IgnoreHostKey, "Do not check the host key")
flag.BoolVar(&c.Debug, "debug", c.Debug, "debug outputs")
flag.DurationVar(&c.Timeout, "timeout", c.Timeout, "maximum amount of time for the TCP connection to establish.")
flag.StringVar(&kexFlag, "kex", kexFlag, "allowed key exchanges algorithms")
flag.StringVar(&kexAlgos, "kex", kexAlgos, "allowed key exchanges algorithms")
flag.StringVar(&ciphersFlag, "ciphers", ciphersFlag, "allowed cipher algorithms")
flag.StringVar(&macsFlag, "macs", macsFlag, "allowed MAC algorithms")
flag.StringVar(&identityFiles, "i", identityFiles, "identity files")
flag.Parse()
c.Kex = pssh.ToSlice(kexFlag)
c.Kex = pssh.ToSlice(kexAlgos)
c.Ciphers = pssh.ToSlice(ciphersFlag)
c.Macs = pssh.ToSlice(macsFlag)
c.IdentityFileOnly = identityFiles != defaultIdentityFiles
c.IdentFiles = pssh.ToSlice(identityFiles)

// see: https://qiita.com/tanksuzuki/items/e712717675faf4efb07a#パイプで渡された時だけ処理する
c.StdinFlag = !terminal.IsTerminal(0)
c.StdinFlag = !term.IsTerminal(0)
return &c
}

Expand Down
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
module github.com/masahide/gopssh

go 1.23.0
go 1.25

toolchain go1.24.2
toolchain go1.25.3

require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/dzeromsk/debpack v0.0.0-20190912160929-4b3d7b5dd69b
github.com/fatih/color v1.18.0
github.com/google/rpmpack v0.6.0
github.com/google/rpmpack v0.7.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.37.0
golang.org/x/crypto v0.43.0
golang.org/x/term v0.36.0
)

require (
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
golang.org/x/sys v0.37.0 // indirect
)
44 changes: 14 additions & 30 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,30 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/dzeromsk/debpack v0.0.0-20190912160929-4b3d7b5dd69b h1:dPL3hXMmS1fDUcnOmD7OYIr3J5P3TeIlEPYixIhe8u4=
github.com/dzeromsk/debpack v0.0.0-20190912160929-4b3d7b5dd69b/go.mod h1:uFUKzp8opkz50jcHuTEG8ZrivmQ++kmTUJs7MkKvxvA=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/rpmpack v0.6.0 h1:LoQuqlw6kHRwg25n3M0xtYrW+z2pTkR0ae1xx11hRw8=
github.com/google/rpmpack v0.6.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/rpmpack v0.7.1 h1:YdWh1IpzOjBz60Wvdw0TU0A5NWP+JTVHA5poDqwMO2o=
github.com/google/rpmpack v0.7.1/go.mod h1:h1JL16sUTWCLI/c39ox1rDaTBo3BXUQGjczVJyK4toU=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
2 changes: 1 addition & 1 deletion pkg/pssh/agent_wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (cp *connPools) newKeyAgent() (*keyAgent, error) {
var err error
ka.authConn, err = cp.dialSocket()
if err != nil {
return nil, fmt.Errorf("Failed dial sockFile: %s,err:%w", cp.sockFile, err)
return nil, fmt.Errorf("failed dial sockFile: %s, err:%w", cp.sockFile, err)
}
if ka.authConn == nil {
return nil, fmt.Errorf("dial sockFile: %s authConn==null", cp.sockFile)
Expand Down
7 changes: 3 additions & 4 deletions pkg/pssh/pssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
Expand Down Expand Up @@ -185,7 +184,7 @@ var re = regexp.MustCompile(":.+")

func readHosts(fileName string) ([]string, error) {
// nolint: gosec
data, err := ioutil.ReadFile(fileName)
data, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -265,7 +264,7 @@ func (p *Pssh) Run() int {

stdin := []byte{}
if p.StdinFlag {
if stdin, err = ioutil.ReadAll(os.Stdin); err != nil {
if stdin, err = io.ReadAll(os.Stdin); err != nil {
log.Fatal(err)
}
}
Expand Down Expand Up @@ -449,7 +448,7 @@ func (p *Pssh) readIdentFiles() [][]byte {
for _, filePath := range p.IdentFiles {
// nolint: gosec
filePath = strings.Replace(filePath, "~", home, one)
buffer, err := ioutil.ReadFile(filePath)
buffer, err := os.ReadFile(filePath)
if err != nil {
continue
}
Expand Down