diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8fb452c --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,32 @@ +# https://docs.github.com/en/actions/learn-github-actions/contexts +name: release +permissions: + contents: write +on: + push: + branches: [ "main" ] +jobs: + build-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: version + run: | + VERSION=$( date '+%y%m%d.%H%M.0' ) + echo "VERSION:$VERSION" + echo "VERSION=$VERSION" >> $GITHUB_ENV + - uses: actions/setup-go@v4 + with: + go-version: '^1.21.0' + - name: go build + run: GOOS=linux GOARCH=amd64 go build -o hs.linux.amd64 -trimpath -ldflags '-X main.Version='$VERSION hs.go + - name: gzip + run: gzip -k hs.linux.amd64 + - name: list files + run: ls -l -a + - name: gh release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: gh release create $VERSION hs.linux.amd64.gz + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f750044 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +.DS_Store + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8af46ff --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/shoce/hs + +go 1.21.0 + +require ( + github.com/ipfs/go-cid v0.4.1 + github.com/multiformats/go-multihash v0.2.3 + golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 +) + +require ( + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + golang.org/x/sys v0.11.0 // indirect + lukechampine.com/blake3 v1.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..229b558 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/hs.go b/hs.go new file mode 100644 index 0000000..28bcc54 --- /dev/null +++ b/hs.go @@ -0,0 +1,891 @@ +/* +Hs + +history: +020/0605 v1 +020/1016 repl +020/302 2020/10/28 stdin reading support +020/302 interrupt signal (ctrl+c) catching so only child processes get it +still not working with root sessions: +Oct 28 21:37:28 ci sshd[3685911]: error: session_signal_req: session signalling requires privilege separation +020/357 UserKeyFile support +021/0502 InReaderBufferSize +021/1117 Silent +023/0827 Verbose +023/0827 keepalive + +go mod init github.com/shoce/hs +go get -a -u -v +go mod tidy +GoFmt +GoBuildNull +GoBuild +GoRelease + +GoRun put a '<' >/etc/ssh/sshd_config && systemctl reload sshd + log("Session.Setenv %s: %v", s, err) + return "", err + } + } + */ + + /* + if err := session.Setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"); err != nil { + // ( echo ; echo AcceptEnv PATH ) >>/etc/ssh/sshd_config && systemctl reload sshd + log("Session.Setenv PATH: %v", err) + return "", err + } + */ + + if stdin != nil { + stdinpipe, err := session.StdinPipe() + if err != nil { + return "", fmt.Errorf("stdin pipe for session: %v", err) + } + + go func() { + _, err := io.Copy(stdinpipe, stdin) + if err != nil { + log("copy to stdin pipe: %v", err) + } + + err = stdinpipe.Close() + if err != nil { + log("close stdin pipe: %v", err) + } + }() + } + + stdoutpipe, err := session.StdoutPipe() + if err != nil { + return "", fmt.Errorf("stdout pipe for session: %v", err) + } + + stderrpipe, err := session.StderrPipe() + if err != nil { + return "", fmt.Errorf("stderr pipe for session: %v", err) + } + + if !Silent { + log(fmt.Sprintf("%s: ", cmds)) + } + + copyoutnotify := make(chan error) + go copynotify(os.Stdout, stdoutpipe, copyoutnotify) + copyerrnotify := make(chan error) + go copynotify(os.Stderr, stderrpipe, copyerrnotify) + + err = session.Start(cmds) + + if err != nil { + log("Start command: %v", err) + return "", err + } + + keepalivedonechan := make(chan bool) + go keepalive(SshClient, ProxyConn, keepalivedonechan) + + InterruptChan = make(chan bool) + + go func() { + interrupt := <-InterruptChan + if !interrupt { + return + } + err := session.Signal(ssh.SIGINT) + if err != nil { + log("Signal to session: %v", err) + } + }() + + err = session.Wait() + + keepalivedonechan <- true + close(keepalivedonechan) + keepalivedonechan = nil + + close(InterruptChan) + InterruptChan = nil + + if err != nil { + switch err.(type) { + case *ssh.ExitMissingError: + status = "missing" + case *ssh.ExitError: + exiterr := err.(*ssh.ExitError) + status = fmt.Sprintf("%d", exiterr.ExitStatus()) + if sig := exiterr.Signal(); sig != "" { + status += "-" + sig + } + default: + log("Wait: %v", err) + return "", err + } + } + + err = <-copyoutnotify + if err != nil { + log(fmt.Sprintf("%s: copy out: %v", cmds, err)) + } + + err = <-copyerrnotify + if err != nil { + log(fmt.Sprintf("%s: copy err: %v", cmds, err)) + } + + return status, nil +} + +func runlocal(cmds string, cmd []string, stdin io.Reader) (status string, err error) { + var cmdargs []string + if len(cmd) > 1 { + cmdargs = cmd[1:] + } + + command := exec.Command(cmd[0], cmdargs...) + + var stdinpipe io.WriteCloser + var stdoutpipe, stderrpipe io.ReadCloser + + if stdin != nil { + stdinpipe, err = command.StdinPipe() + if err != nil { + return "", fmt.Errorf("stdin pipe for command: %v", err) + } + + go func() { + _, err := io.Copy(stdinpipe, stdin) + if err != nil { + log("write to stdin pipe: %v", err) + } + + err = stdinpipe.Close() + if err != nil { + log("close stdin pipe: %v", err) + } + }() + } + + stdoutpipe, err = command.StdoutPipe() + if err != nil { + return "", fmt.Errorf("stdout pipe for command: %v", err) + } + copyoutnotify := make(chan error) + go copynotify(os.Stdout, stdoutpipe, copyoutnotify) + + stderrpipe, err = command.StderrPipe() + if err != nil { + return "", fmt.Errorf("stderr pipe for command: %v", err) + } + copyerrnotify := make(chan error) + go copynotify(os.Stderr, stderrpipe, copyerrnotify) + + if !Silent { + log(fmt.Sprintf("%s: ", cmds)) + } + + err = command.Start() + if err != nil { + return "", fmt.Errorf("Start command: %v", err) + } + + err = command.Wait() + + if err != nil { + switch err.(type) { + case *exec.ExitError: + exiterr := err.(*exec.ExitError) + status = fmt.Sprintf("%d", exiterr.ExitCode()) + default: + return "", fmt.Errorf("Wait: %v", err) + } + } + + return status, nil +} + +func run(cmds string, cmd []string, stdin io.Reader) (status string, err error) { + if cmds == "" && len(cmd) == 0 { + return "", errors.New("empty cmd") + } + if Host == "" { + return runlocal(cmds, cmd, stdin) + } else { + return runssh(cmds, cmd, stdin) + } +} + +func printpathinfo(fpath string, finfo os.FileInfo, err error) error { + if err != nil { + return err + } + + s := fmt.Sprintf("%s", strings.ReplaceAll(fpath, "\t", "\\\t")) + if (finfo.Mode() & os.ModeSymlink) != 0 { + if linkpath, err := os.Readlink(fpath); err != nil { + return err + } else { + s += "@" + linkpath + "@" + } + } + if finfo.IsDir() { + s += string(os.PathSeparator) + } + + s += fmt.Sprintf("\tmode:%04o", finfo.Mode()&os.ModePerm) + + if !finfo.IsDir() && (finfo.Mode()&os.ModeSymlink == 0) { + s += fmt.Sprintf("\tsize:%s", seps(int(finfo.Size()), 3)) + } + + if !finfo.IsDir() && (finfo.Mode()&os.ModeSymlink == 0) { + f, err := os.Open(fpath) + if err != nil { + log("%v", err) + return err + } + defer f.Close() + fmh, err := mh.SumStream(f, mh.SHA2_256, -1) + if err != nil { + log("%v", err) + return err + } + c := cid.NewCidV1(cid.Raw, fmh) + s += fmt.Sprintf("\tcid:%s", c) + } + fmt.Println(s) + return nil +} + +func listpath(fpath string) error { + if fpath, err := filepath.Abs(fpath); err != nil { + return err + } else { + fpath = filepath.Clean(fpath) + if err := filepath.Walk(fpath, printpathinfo); err != nil { + return err + } + } + return nil +} + +func hlist() { + flag.Parse() + args := flag.Args() + if len(args) == 0 { + args = []string{"."} + } + for _, p := range args { + if err := listpath(p); err != nil { + log("%v", err) + os.Exit(1) + } + } +} + +func hlink() { + flag.Parse() + args := flag.Args() + if len(args) != 2 { + log("usage: hlink src dst") + os.Exit(1) + } + src, dst := args[0], args[1] + log("%s@%s@", src, dst) +} + +func hhash() { + var err error + var limit int + flag.IntVar(&limit, "limit", 0, "max bytes to read from the file") + flag.Parse() + args := flag.Args() + + if len(args) != 1 { + log("usage: hhash [-limit=123123] path") + os.Exit(1) + } + if limit < 0 { + log("hshash needs limit flag set to a positive integer") + os.Exit(1) + } + fpath := args[0] + if fpath, err = filepath.Abs(fpath); err != nil { + log("%v", err) + os.Exit(1) + } else { + fpath = filepath.Clean(fpath) + } + finfo, err := os.Stat(fpath) + if err != nil { + log("%v", err) + os.Exit(1) + } + if (finfo.Mode() & os.ModeSymlink) != 0 { + log("provided path is a symlink") + os.Exit(1) + } + if finfo.IsDir() { + log("provided path is a directory") + os.Exit(1) + } + + s := fmt.Sprintf("%s", strings.ReplaceAll(fpath, "\t", "\\\t")) + + s += fmt.Sprintf("\tsize:%s", seps(int(finfo.Size()), 3)) + + s += fmt.Sprintf("\tlimit:%s", seps(limit, 3)) + + f, err := os.Open(fpath) + if err != nil { + log("%v", err) + os.Exit(1) + } + defer f.Close() + + var fr io.Reader + if limit > 0 { + fr = io.LimitReader(f, int64(limit)) + } else { + fr = f + } + fmh, err := mh.SumStream(fr, mh.SHA2_256, -1) + if err != nil { + log("%v", err) + os.Exit(1) + } + + c := cid.NewCidV1(cid.Raw, fmh) + s += fmt.Sprintf("\tcid:%s", c) + + fmt.Println(s) +} + +func hget() { +} + +func hput() { +} + +func hs() { + var err error + + flag.Parse() + args := flag.Args() + + signalchan := make(chan os.Signal, 1) + signal.Notify(signalchan, os.Interrupt) + go func() { + for { + s := <-signalchan + switch s { + case os.Interrupt: + lognl() + log("interrupt signal") + if InterruptChan != nil { + InterruptChan <- true + } + } + } + }() + + if Host == "" { + + Hostname, err = os.Hostname() + if err != nil { + log("Hostname: %v", err) + os.Exit(1) + } + Hostname = strings.TrimSuffix(Hostname, ".local") + //log("Hostname:%s", Hostname) + + u, err := user.Current() + if err != nil { + log("user.Current: %v", err) + } + User = u.Username + + } else { + + if len(ProxyChain) > 0 { + for _, p := range ProxyChain { + proxyurl, err := url.Parse(p) + if err != nil { + log("Proxy url `%s`: %v", p, err) + os.Exit(1) + } + pd, err := proxy.FromURL(proxyurl, ProxyDialer) + if err != nil { + log("Proxy from url: %v", err) + os.Exit(1) + } + ProxyDialer = pd + } + } + + if len(strings.Split(Host, ":")) < 2 { + Host = fmt.Sprintf("%s:22", Host) + //log("Host:%s", Host) + } + + err = connectssh() + if err != nil { + //log("connect ssh: %v", err) + } + if SshClient != nil { + defer SshClient.Close() + } + } + + inreader := bufio.NewReaderSize(os.Stdin, InReaderBufferSize) + + if len(args) > 0 { + cmd := args[:] + cmds := strings.Join(cmd, " ") + + if cmd[len(cmd)-1] == "<" { + cmd = cmd[:len(cmd)-1] + cmds = strings.Join(cmd, " ") + if !Silent { + log("%s stdin: ", cmds) + } + } + + Status, err = run(cmds, cmd, inreader) + if err != nil { + os.Exit(1) + } + if Status != "" { + log("%s status: %s", cmds, Status) + } + os.Exit(0) + } + + var stdinbb []byte + for { + logstatus() + + cmds, err := inreader.ReadString('\n') + if err != nil { + if err == io.EOF { + log("EOF") + break + } + log("ReadString: %v", err) + continue + } + + cmds = strings.TrimSpace(cmds) + if cmds == "" { + continue + } + + cmd := strings.Split(cmds, " ") + + stdinbb = nil + if cmd[len(cmd)-1] == "<" { + cmd = cmd[:len(cmd)-1] + cmds = cmds[:len(cmds)-1] + if !Silent { + log("%s stdin: ", cmds) + } + stdinbb, err = ioutil.ReadAll(os.Stdin) + if err != nil { + log("read stdin: %v", err) + continue + } + } + + Status, err = run(cmds, cmd, bytes.NewBuffer(stdinbb)) + if err != nil { + continue + } + } +} diff --git a/readme.text b/readme.text new file mode 100644 index 0000000..1ceb77a --- /dev/null +++ b/readme.text @@ -0,0 +1 @@ +Ssh client with no annoying input delays (no tty)