Compare commits

..

105 commits

Author SHA1 Message Date
dependabot[bot]
9ad810900d
build(deps): bump github.com/pkg/sftp from 1.13.7 to 1.13.9 (#53)
Bumps [github.com/pkg/sftp](https://github.com/pkg/sftp) from 1.13.7 to 1.13.9.
- [Release notes](https://github.com/pkg/sftp/releases)
- [Commits](https://github.com/pkg/sftp/compare/v1.13.7...v1.13.9)

---
updated-dependencies:
- dependency-name: github.com/pkg/sftp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 20:28:28 +09:00
dependabot[bot]
9fdeeb1a84
build(deps): bump github.com/spf13/cobra from 1.8.1 to 1.9.1 (#48)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-20 21:39:01 +09:00
dependabot[bot]
f4814644ec
build(deps): bump golang.org/x/crypto from 0.32.0 to 0.33.0 (#47)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-15 23:25:49 +09:00
dependabot[bot]
5951dd5bdc
build(deps): bump golang.org/x/crypto from 0.31.0 to 0.32.0 (#46)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/crypto/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 22:21:21 +09:00
dependabot[bot]
11f7602ab8
build(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#43)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 12:53:25 +09:00
dependabot[bot]
fadb7e3233
build(deps): bump golang.org/x/crypto from 0.30.0 to 0.31.0 (#45)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 12:48:58 +09:00
dependabot[bot]
7beaab3f22
build(deps): bump golang.org/x/crypto from 0.29.0 to 0.30.0 (#44)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-08 22:32:17 +09:00
dependabot[bot]
cc8c8e7916
build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0 (#42)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 21:01:31 +09:00
dependabot[bot]
0a867b860d
build(deps): bump github.com/pkg/sftp from 1.13.6 to 1.13.7 (#38)
Bumps [github.com/pkg/sftp](https://github.com/pkg/sftp) from 1.13.6 to 1.13.7.
- [Release notes](https://github.com/pkg/sftp/releases)
- [Commits](https://github.com/pkg/sftp/compare/v1.13.6...v1.13.7)

---
updated-dependencies:
- dependency-name: github.com/pkg/sftp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 21:12:23 +09:00
dependabot[bot]
98d68900ba
build(deps): bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#36)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 00:01:00 +09:00
dependabot[bot]
e4acc725d5
build(deps): bump golang.org/x/crypto from 0.26.0 to 0.27.0 (#35)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/crypto/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 21:33:30 +09:00
dependabot[bot]
71493a7895
build(deps): bump golang.org/x/crypto from 0.25.0 to 0.26.0 (#32)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-10 17:12:19 +09:00
dependabot[bot]
c1ec261ed6
build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#31)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-08 19:02:28 +09:00
dependabot[bot]
42c23a9b69
build(deps): bump github.com/spf13/cobra from 1.8.0 to 1.8.1 (#30)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 22:56:20 +09:00
dependabot[bot]
fed11ed08a
build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#29)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 20:10:12 +09:00
Ryo Ota
739f825f40 Merge tag 'v0.4.3' into develop
no message
2024-05-27 17:19:20 +09:00
Ryo Ota
45257031fa Merge branch 'release/0.4.3' 2024-05-27 17:19:20 +09:00
Ryo Ota
2977f90966 bump: 0.4.3 2024-05-27 17:19:13 +09:00
Ryo Ota
f3231aec39 deps: update 2024-05-27 17:14:02 +09:00
Ryo Ota
f6d96d1a78 fix: wait non-nil process 2024-05-27 17:11:21 +09:00
dependabot[bot]
f41ff25604
build(deps): bump golang.org/x/crypto from 0.22.0 to 0.23.0 (#26)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/crypto/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 22:39:44 +09:00
Ryo Ota
fc55b1a7f1 docs: 0.4.2 2024-04-06 07:49:53 +09:00
Ryo Ota
df7755ecc3 Merge branch 'release/0.4.2' 2024-04-06 07:46:28 +09:00
Ryo Ota
2b6594cea3 Merge tag 'v0.4.2' into develop
no message
2024-04-06 07:46:28 +09:00
Ryo Ota
b4f174cf61 bump: 0.4.2 2024-04-06 07:46:21 +09:00
Ryo Ota
a72a61a47f deps: update 2024-04-06 07:41:15 +09:00
dependabot[bot]
9a54ed3dd3
build(deps): bump golang.org/x/crypto from 0.21.0 to 0.22.0 (#25)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 07:37:58 +09:00
Ryo Ota
7d29c35f97 deps: update 2024-03-06 13:59:42 +09:00
dependabot[bot]
6baaf4ac0e
build(deps): bump golang.org/x/crypto from 0.20.0 to 0.21.0 (#24)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/crypto/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 13:54:44 +09:00
dependabot[bot]
1f51c1d3af
build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#23)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 20:10:05 +09:00
dependabot[bot]
e5eb9593ad
build(deps): bump golang.org/x/crypto from 0.19.0 to 0.20.0 (#22)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/crypto/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-01 07:36:22 +09:00
dependabot[bot]
4a4c6c7602
build(deps): bump golang.org/x/crypto from 0.18.0 to 0.19.0 (#21)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/crypto/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-08 18:58:04 +09:00
dependabot[bot]
6d1f48c6cf
build(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 (#20)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 19:48:08 +09:00
dependabot[bot]
5639819bd9
build(deps): bump github.com/creack/pty from 1.1.18 to 1.1.21 (#14)
Bumps [github.com/creack/pty](https://github.com/creack/pty) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/creack/pty/releases)
- [Commits](https://github.com/creack/pty/compare/v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: github.com/creack/pty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-10 22:56:14 +09:00
dependabot[bot]
c8121690aa
build(deps): bump golang.org/x/crypto from 0.17.0 to 0.18.0 (#19)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/crypto/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-10 04:14:36 +09:00
dependabot[bot]
7cc32f3e30
build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#18)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 10:43:26 +09:00
dependabot[bot]
5bad4baad4
build(deps): bump actions/setup-go from 4 to 5 (#16)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 01:06:24 +09:00
dependabot[bot]
6e09a6fdb7
build(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#17)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 01:05:55 +09:00
dependabot[bot]
28bd116388
build(deps): bump golang.org/x/crypto from 0.15.0 to 0.16.0 (#15)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-09 20:05:40 +09:00
dependabot[bot]
3cdb3fe048
build(deps): bump github.com/spf13/cobra from 1.7.0 to 1.8.0 (#12)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 22:07:52 +09:00
dependabot[bot]
29b7c6f24c
build(deps): bump golang.org/x/crypto from 0.14.0 to 0.15.0 (#13)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 22:07:17 +09:00
dependabot[bot]
5a44e6cdaa
build(deps): bump github.com/google/uuid from 1.3.1 to 1.4.0 (#10)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.1 to 1.4.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.3.1...v1.4.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-29 20:56:47 +09:00
dependabot[bot]
b59d8c9b68
build(deps): bump golang.org/x/crypto from 0.13.0 to 0.14.0 (#9)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 03:09:42 +09:00
Ryo Ota
4a2df34cfc Merge branch 'release/0.4.1' 2023-09-13 21:23:57 +09:00
Ryo Ota
04d5f34d73 Merge tag 'v0.4.1' into develop
no message
2023-09-13 21:23:57 +09:00
Ryo Ota
fb77c1c5a2 bump: 0.4.1 2023-09-13 21:23:40 +09:00
Ryo Ota
c9ab431395 deps: update 2023-09-13 21:17:44 +09:00
dependabot[bot]
4479e20482
build(deps): bump goreleaser/goreleaser-action from 4 to 5 (#8)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 4 to 5.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 21:14:29 +09:00
dependabot[bot]
11bdaeb354
build(deps): bump golang.org/x/crypto from 0.12.0 to 0.13.0 (#7)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/crypto/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 21:28:51 +09:00
dependabot[bot]
f508fb2ddb
build(deps): bump actions/checkout from 3 to 4 (#6)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 08:42:44 +09:00
dependabot[bot]
7fc00e16e3
build(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1 (#5)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 21:07:37 +09:00
dependabot[bot]
d7d3d615aa
build(deps): bump github.com/pkg/sftp from 1.13.5 to 1.13.6 (#4)
Bumps [github.com/pkg/sftp](https://github.com/pkg/sftp) from 1.13.5 to 1.13.6.
- [Release notes](https://github.com/pkg/sftp/releases)
- [Commits](https://github.com/pkg/sftp/compare/v1.13.5...v1.13.6)

---
updated-dependencies:
- dependency-name: github.com/pkg/sftp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 19:38:19 +09:00
Ryo Ota
a2739a8c1e docs: 0.4.0 2023-08-11 23:56:26 +09:00
Ryo Ota
19a0a8a7c5 Merge branch 'release/0.4.0' 2023-08-11 21:52:06 +09:00
Ryo Ota
886d93ba25 Merge tag 'v0.4.0' into develop
no message
2023-08-11 21:52:06 +09:00
Ryo Ota
2d5c82a7bd bump: 0.4.0 2023-08-11 21:51:35 +09:00
Ryo Ota
e5fbf86f8d mypassword → mypass 2023-08-11 21:43:58 +09:00
Ryo Ota
3ef24a9954 support "cancel-tcpip-forward" and "cancel-streamlocal-forward@openssh.com" 2023-08-11 21:40:14 +09:00
Ryo Ota
445676f901 deps: update 2023-08-11 16:42:00 +09:00
Ryo Ota
edfd9533f3 update help and log 2023-08-11 16:24:59 +09:00
Ryo Ota
d3a0a420f4 docs: 0.3.0 2023-08-11 12:48:31 +09:00
Ryo Ota
ef2fa8f26d Merge branch 'release/0.3.0' 2023-08-11 12:36:04 +09:00
Ryo Ota
79ffadacd0 Merge tag 'v0.3.0' into develop
no message
2023-08-11 12:36:04 +09:00
Ryo Ota
5068de6f1f bump: 0.3.0 2023-08-11 12:35:57 +09:00
Ryo Ota
8aa0cf8b37 Merge branch 'feature/streamlocal-forward' into develop 2023-08-11 11:48:07 +09:00
Ryo Ota
6937695b52 docs: --allow-streamlocal-forward 2023-08-11 11:44:23 +09:00
Ryo Ota
9703d25277 test: streamlocal-forward 2023-08-11 11:37:12 +09:00
Ryo Ota
b1973ce4e8 fix: handle multiple global requests simultaneously 2023-08-11 11:31:35 +09:00
Ryo Ota
5cc6ad44c6 support Unix domain socket remote forwarding 2023-08-11 10:48:51 +09:00
Ryo Ota
4b7793e445 Merge branch 'feature/direct-streamlocal' into develop 2023-08-11 09:50:06 +09:00
Ryo Ota
7b324d52e6 docs: --allow-direct-streamlocal 2023-08-11 09:44:31 +09:00
Ryo Ota
36b1ade1c0 test: direct-streamlocal 2023-08-11 09:33:24 +09:00
Ryo Ota
6c183ffac4 support Unix domain socket local forwarding 2023-08-11 09:02:22 +09:00
Ryo Ota
663c9fae81 send originator address in "tcpip-forward" 2023-08-11 02:08:32 +09:00
Ryo Ota
f36e6f8702 test: use HostKeyCallback: ssh.InsecureIgnoreHostKey() 2023-08-11 01:37:40 +09:00
Ryo Ota
2295abacdd test SFTP 2023-08-11 00:40:59 +09:00
Ryo Ota
3b0b650318 test pty 2023-08-11 00:30:26 +09:00
Ryo Ota
695d0603b3 Merge branch 'feature/test' into develop 2023-08-10 23:35:57 +09:00
Ryo Ota
9e10ac7dea test permissions 2023-08-10 23:17:03 +09:00
Ryo Ota
cad0e3fa23 add more tests 2023-08-10 22:53:02 +09:00
Ryo Ota
35c29a6de2 ci: test 2023-08-10 22:34:08 +09:00
Ryo Ota
aac75f81a6 test RootCmd 2023-08-10 22:16:35 +09:00
Ryo Ota
b5f2030e3d Merge tag 'v0.2.1' into develop
no message
2023-08-10 07:25:17 +09:00
Ryo Ota
5184ebed95 Merge branch 'release/0.2.1' 2023-08-10 07:25:16 +09:00
Ryo Ota
4ba028c8a1 bump: 0.2.1 2023-08-10 07:25:09 +09:00
Ryo Ota
54f0a6069e deps: update 2023-08-10 07:23:03 +09:00
Ryo Ota
bfe1e922ec goreleaser 1.19.2 2023-08-10 07:19:40 +09:00
Ryo Ota
bd0d0aae99 bump: 0.2.0 2023-08-10 03:54:10 +09:00
Ryo Ota
58d3afaaee Merge branch 'release/0.2.0' 2023-08-10 03:49:36 +09:00
Ryo Ota
faf81be83c Merge tag 'v0.2.0' into develop
no message
2023-08-10 03:49:36 +09:00
Ryo Ota
d41dabd212 bump: 0.2.0 2023-08-10 03:49:30 +09:00
Ryo Ota
9af7dd047f docs: update "Permissions" 2023-08-10 03:35:35 +09:00
Ryo Ota
54b502d3ae require one user at least 2023-08-10 03:31:30 +09:00
Ryo Ota
0b3b8d0f96 deps: update 2023-08-09 20:57:13 +09:00
Ryo Ota
9e54b43be9 Merge branch 'feature/permissions' into develop 2023-08-09 20:54:10 +09:00
Ryo Ota
ba76a4552f docs: permissions 2023-08-09 20:52:35 +09:00
Ryo Ota
1143b17071 implement permissions 2023-08-09 20:45:46 +09:00
Ryo Ota
0796e5e2d9 docs: add badge and chore 2023-08-09 20:36:19 +09:00
Ryo Ota
c4cc2752d3 docs: add install sections 2023-08-09 09:28:04 +09:00
Ryo Ota
26baea535c docs: "--help" 2023-08-09 09:19:27 +09:00
dependabot[bot]
24a4ed6871
build(deps): bump goreleaser/goreleaser-action from 2 to 4 (#1)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 2 to 4.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v2...v4)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 09:17:41 +09:00
dependabot[bot]
86554a2289
build(deps): bump actions/setup-go from 2 to 4 (#3)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 09:03:51 +09:00
dependabot[bot]
ece9017869
build(deps): bump actions/checkout from 2 to 3 (#2)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 09:03:34 +09:00
Ryo Ota
746cf06e13
dependabot: package-ecosystem: github-actions 2023-08-09 09:01:34 +09:00
Ryo Ota
e717be788c Merge tag 'v0.1.0' into develop
no message
2023-08-09 08:59:13 +09:00
15 changed files with 1360 additions and 147 deletions

View file

@ -10,3 +10,11 @@ updates:
- nwtgck - nwtgck
assignees: assignees:
- nwtgck - nwtgck
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
timezone: Asia/Tokyo
open-pull-requests-limit: 99
reviewers: [ nwtgck ]
assignees: [ nwtgck ]

View file

@ -3,12 +3,11 @@ name: CI
on: [push] on: [push]
jobs: jobs:
build_multi_platform: test:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up Go 1.x - uses: actions/setup-go@v5
uses: actions/setup-go@v2
with: with:
go-version: "1.20" go-version: "1.20"
- name: Build for multi-platform - name: Build for multi-platform
@ -29,3 +28,5 @@ jobs:
# Build # Build
CGO_ENABLED=0 go build -o "${BUILD_PATH}/handy-sshd${EXTENSION}" main/main.go CGO_ENABLED=0 go build -o "${BUILD_PATH}/handy-sshd${EXTENSION}" main/main.go
done done
- name: Test
run: go test -v ./...

View file

@ -9,17 +9,17 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: "1.20" go-version: "1.20"
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v5
with: with:
version: v1.3.1 version: v1.19.2
args: release --rm-dist args: release --rm-dist
env: env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_TOKEN: ${{ secrets.GH_PAT }}

View file

@ -5,8 +5,57 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
## [Unreleased] ## [Unreleased]
## [0.4.3] - 2024-05-27
### Changed
* Update dependencies
### Fixed
* Wait non-nil process when pty open failed
## [0.4.2] - 2024-04-05
### Changed
* Update dependencies
## [0.4.1] - 2023-09-13
### Changed
* Update dependencies
## [0.4.0] - 2023-08-11
### Added
* Add `-u` short flag for `--user`
* Support "cancel-tcpip-forward" and "cancel-streamlocal-forward@openssh.com"
### Changed
* Add more examples to help message
## [0.3.0] - 2023-08-11
### Added
* Support Unix domain socket local port forwarding
* Support Unix domain socket remote port forwarding
### Fixed
* Handle multiple global requests simultaneously
## [0.2.1] - 2023-08-09
### Changed
* Update dependencies
## [0.2.0] - 2023-08-09
### Added
* Add permissions
### Changed
* Require one user at least
## 0.1.0 - 2023-08-08 ## 0.1.0 - 2023-08-08
### Added ### Added
* Initial release * Initial release
[Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...HEAD [Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.4.2...HEAD
[0.4.3]: https://github.com/nwtgck/handy-sshd/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/nwtgck/handy-sshd/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/nwtgck/handy-sshd/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/nwtgck/handy-sshd/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/nwtgck/handy-sshd/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/nwtgck/handy-sshd/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...v0.2.0

101
README.md
View file

@ -1,24 +1,115 @@
# handy-sshd # handy-sshd
[![CI](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml/badge.svg)](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml)
Portable SSH Server Portable SSH Server
## Install on Ubuntu/Debian
```bash
wget https://github.com/nwtgck/handy-sshd/releases/download/v0.4.2/handy-sshd-0.4.2-linux-amd64.deb
sudo dpkg -i handy-sshd-0.4.2-linux-amd64.deb
```
## Install on Mac
```bash
brew install nwtgck/handy-sshd/handy-sshd
```
Get more executables in [the releases](https://github.com/nwtgck/handy-sshd/releases).
## Examples ## Examples
```bash ```bash
# Listen on 2222 and accept user name "john" with password "mypassword" # Listen on 2222 and accept user name "john" with password "mypass"
handy-sshd -p 2222 --user "john:mypassword" handy-sshd -p 2222 -u john:mypass
``` ```
```bash ```bash
# Listen on 2222 and accept user name "john" without password # Listen on 2222 and accept user name "john" without password
handy-sshd -p 2222 --user "john:" handy-sshd -p 2222 -u john:
``` ```
```bash ```bash
# Listen on 2222 and accept users "john" and "alice" without password # Listen on 2222 and accept users "john" and "alice" without password
handy-sshd -p 2222 --user "john:" --user "alice:" handy-sshd -p 2222 -u john: -u alice:
``` ```
```bash ```bash
# Listen on unix domain socket # Listen on unix domain socket
handy-sshd --unix-socket /tmp/my-unix-socket --user "john:" handy-sshd --unix-socket /tmp/my-unix-socket -u john:
```
## Features
An SSH client can use
* Shell/Interactive shell
* Local port forwarding (ssh -L)
* Remote port forwarding (ssh -R)
* [SOCKS proxy](https://wikipedia.org/wiki/SOCKS) (dynamic port forwarding)
* SFTP
* [SSHFS](https://wikipedia.org/wiki/SSHFS)
* Unix domain socket (local/remote port forwarding)
All features are enabled by default. You can allow only some of them using permission flags.
## Permissions
There are several permissions:
* --allow-direct-streamlocal
* --allow-direct-tcpip
* --allow-execute
* --allow-sftp
* --allow-streamlocal-forward
* --allow-tcpip-forward
**All permissions are allowed when nothing is specified.** The log shows "allowed: " and "NOT allowed: " permissions as follows:
```console
$ handy-sshd -u "john:"
2023/08/11 11:40:44 INFO listening on :2222...
2023/08/11 11:40:44 INFO allowed: "tcpip-forward", "direct-tcpip", "execute", "sftp", "streamlocal-forward", "direct-streamlocal"
2023/08/11 11:40:44 INFO NOT allowed: none
```
For example, specifying `--allow-direct-tcpip` and `--allow-execute` allows only them:
```console
$ handy-sshd -u "john:" --allow-direct-tcpip --allow-execute
2023/08/11 11:41:03 INFO listening on :2222...
2023/08/11 11:41:03 INFO allowed: "direct-tcpip", "execute"
2023/08/11 11:41:03 INFO NOT allowed: "tcpip-forward", "sftp", "streamlocal-forward", "direct-streamlocal"
```
## --help
```
Portable SSH server
Usage:
handy-sshd [flags]
Examples:
# Listen on 2222 and accept user name "john" with password "mypass"
handy-sshd -u john:mypass
# Listen on 22 and accept the user without password
handy-sshd -p 22 -u john:
Permissions:
All permissions are allowed by default.
For example, specifying --allow-direct-tcpip and --allow-execute allows only them.
Flags:
--allow-direct-streamlocal client can use Unix domain socket local forwarding (ssh -L)
--allow-direct-tcpip client can use local forwarding (ssh -L) and SOCKS proxy (ssh -D)
--allow-execute client can use shell/interactive shell
--allow-sftp client can use SFTP and SSHFS
--allow-streamlocal-forward client can use Unix domain socket remote forwarding (ssh -R)
--allow-tcpip-forward client can use remote forwarding (ssh -R)
-h, --help help for handy-sshd
--host string SSH server host to listen (e.g. 127.0.0.1)
-p, --port uint16 port to listen (default 2222)
--shell string Shell
--unix-socket string Unix domain socket to listen
-u, --user stringArray SSH user name (e.g. "john:mypass")
-v, --version show version
``` ```

View file

@ -13,7 +13,7 @@ import (
"strings" "strings"
) )
var flag struct { type flagType struct {
//dnsServer string //dnsServer string
showsVersion bool showsVersion bool
sshHost string sshHost string
@ -21,6 +21,18 @@ var flag struct {
sshUnixSocket string sshUnixSocket string
sshShell string sshShell string
sshUsers []string sshUsers []string
allowTcpipForward bool
allowDirectTcpip bool
allowExecute bool
allowSftp bool
allowStreamlocalForward bool
allowDirectStreamlocal bool
}
type permissionFlagType = struct {
name string
flagPtr *bool
} }
type sshUser struct { type sshUser struct {
@ -30,29 +42,85 @@ type sshUser struct {
func init() { func init() {
cobra.OnInitialize() cobra.OnInitialize()
RootCmd.PersistentFlags().BoolVarP(&flag.showsVersion, "version", "v", false, "show version")
RootCmd.PersistentFlags().StringVarP(&flag.sshHost, "host", "", "", "SSH server host (e.g. 127.0.0.1)")
RootCmd.PersistentFlags().Uint16VarP(&flag.sshPort, "port", "p", 2222, "SSH server port")
// NOTE: long name 'unix-socket' is from curl (ref: https://curl.se/docs/manpage.html)
RootCmd.PersistentFlags().StringVarP(&flag.sshUnixSocket, "unix-socket", "", "", "Unix-domain socket")
RootCmd.PersistentFlags().StringVarP(&flag.sshShell, "shell", "", "", "Shell")
//RootCmd.PersistentFlags().StringVar(&flag.dnsServer, "dns-server", "", "DNS server (e.g. 1.1.1.1:53)")
RootCmd.PersistentFlags().StringArrayVarP(&flag.sshUsers, "user", "", nil, `SSH user name (e.g. "john:mypassword")`)
} }
var RootCmd = &cobra.Command{ func RootCmd() *cobra.Command {
var flag flagType
allPermissionFlags := []permissionFlagType{
{name: "tcpip-forward", flagPtr: &flag.allowTcpipForward},
{name: "direct-tcpip", flagPtr: &flag.allowDirectTcpip},
{name: "execute", flagPtr: &flag.allowExecute},
{name: "sftp", flagPtr: &flag.allowSftp},
{name: "streamlocal-forward", flagPtr: &flag.allowStreamlocalForward},
{name: "direct-streamlocal", flagPtr: &flag.allowDirectStreamlocal},
}
rootCmd := cobra.Command{
Use: os.Args[0], Use: os.Args[0],
Short: "handy-sshd", Short: "handy-sshd",
Long: "Portable SSH server", Long: "Portable SSH server",
SilenceUsage: true, SilenceUsage: true,
Example: `# Listen on 2222 and accept user name "john" with password "mypass"
handy-sshd -u john:mypass
# Listen on 22 and accept the user without password
handy-sshd -p 22 -u john:
Permissions:
All permissions are allowed by default.
For example, specifying --allow-direct-tcpip and --allow-execute allows only them.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return rootRunEWithExtra(cmd, args, &flag, allPermissionFlags)
},
}
rootCmd.PersistentFlags().BoolVarP(&flag.showsVersion, "version", "v", false, "show version")
rootCmd.PersistentFlags().StringVarP(&flag.sshHost, "host", "", "", "SSH server host to listen (e.g. 127.0.0.1)")
rootCmd.PersistentFlags().Uint16VarP(&flag.sshPort, "port", "p", 2222, "port to listen")
// NOTE: long name 'unix-socket' is from curl (ref: https://curl.se/docs/manpage.html)
rootCmd.PersistentFlags().StringVarP(&flag.sshUnixSocket, "unix-socket", "", "", "Unix domain socket to listen")
rootCmd.PersistentFlags().StringVarP(&flag.sshShell, "shell", "", "", "Shell")
//rootCmd.PersistentFlags().StringVar(&flag.dnsServer, "dns-server", "", "DNS server (e.g. 1.1.1.1:53)")
rootCmd.PersistentFlags().StringArrayVarP(&flag.sshUsers, "user", "u", nil, `SSH user name (e.g. "john:mypass")`)
// Permission flags
rootCmd.PersistentFlags().BoolVarP(&flag.allowTcpipForward, "allow-tcpip-forward", "", false, "client can use remote forwarding (ssh -R)")
rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectTcpip, "allow-direct-tcpip", "", false, "client can use local forwarding (ssh -L) and SOCKS proxy (ssh -D)")
rootCmd.PersistentFlags().BoolVarP(&flag.allowExecute, "allow-execute", "", false, "client can use shell/interactive shell")
rootCmd.PersistentFlags().BoolVarP(&flag.allowSftp, "allow-sftp", "", false, "client can use SFTP and SSHFS")
rootCmd.PersistentFlags().BoolVarP(&flag.allowStreamlocalForward, "allow-streamlocal-forward", "", false, "client can use Unix domain socket remote forwarding (ssh -R)")
rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectStreamlocal, "allow-direct-streamlocal", "", false, "client can use Unix domain socket local forwarding (ssh -L)")
return &rootCmd
}
func rootRunEWithExtra(cmd *cobra.Command, args []string, flag *flagType, allPermissionFlags []permissionFlagType) error {
if flag.showsVersion { if flag.showsVersion {
fmt.Println(version.Version) fmt.Fprintln(cmd.OutOrStdout(), version.Version)
return nil return nil
} }
logger := slog.Default() logger := slog.Default()
// Allow all permissions if all permission is not set
{
allPermissionFalse := true
for _, permissionFlag := range allPermissionFlags {
allPermissionFalse = allPermissionFalse && !*permissionFlag.flagPtr
}
if allPermissionFalse {
for _, permissionFlag := range allPermissionFlags {
*permissionFlag.flagPtr = true
}
}
}
sshServer := &handy_sshd.Server{ sshServer := &handy_sshd.Server{
Logger: logger, Logger: logger,
AllowTcpipForward: flag.allowTcpipForward,
AllowDirectTcpip: flag.allowDirectTcpip,
AllowExecute: flag.allowExecute,
AllowSftp: flag.allowSftp,
AllowStreamlocalForward: flag.allowStreamlocalForward,
AllowDirectStreamlocal: flag.allowDirectStreamlocal,
} }
var sshUsers []sshUser var sshUsers []sshUser
for _, u := range flag.sshUsers { for _, u := range flag.sshUsers {
@ -62,7 +130,11 @@ var RootCmd = &cobra.Command{
} }
sshUsers = append(sshUsers, sshUser{name: splits[0], password: splits[1]}) sshUsers = append(sshUsers, sshUser{name: splits[0], password: splits[1]})
} }
if len(sshUsers) == 0 {
return fmt.Errorf(`No user specified
e.g. --user "john:mypass"
e.g. --user "john:"`)
}
// (base: https://gist.github.com/jpillora/b480fde82bff51a06238) // (base: https://gist.github.com/jpillora/b480fde82bff51a06238)
sshConfig := &ssh.ServerConfig{ sshConfig := &ssh.ServerConfig{
//Define a function to run when a client attempts a password login //Define a function to run when a client attempts a password login
@ -109,6 +181,9 @@ var RootCmd = &cobra.Command{
logger.Info(fmt.Sprintf("listening on %s...", flag.sshUnixSocket)) logger.Info(fmt.Sprintf("listening on %s...", flag.sshUnixSocket))
} }
defer ln.Close() defer ln.Close()
showPermissions(logger, allPermissionFlags)
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
@ -121,9 +196,28 @@ var RootCmd = &cobra.Command{
conn.Close() conn.Close()
continue continue
} }
logger.Info("new SSH connection", "client_version", string(sshConn.ClientVersion())) logger.Info("new SSH connection", "remote_address", sshConn.RemoteAddr(), "client_version", string(sshConn.ClientVersion()))
go sshServer.HandleGlobalRequests(sshConn, reqs) go sshServer.HandleGlobalRequests(sshConn, reqs)
go sshServer.HandleChannels(flag.sshShell, chans) go sshServer.HandleChannels(flag.sshShell, chans)
} }
}, }
func showPermissions(logger *slog.Logger, allPermissionFlags []permissionFlagType) {
var allowedList []string
var notAllowedList []string
for _, permissionFlag := range allPermissionFlags {
if *permissionFlag.flagPtr {
allowedList = append(allowedList, `"`+permissionFlag.name+`"`)
} else {
notAllowedList = append(notAllowedList, `"`+permissionFlag.name+`"`)
}
}
showList := func(l []string) string {
if len(l) == 0 {
return "none"
}
return strings.Join(l, ", ")
}
logger.Info(fmt.Sprintf("allowed: %s", showList(allowedList)))
logger.Info(fmt.Sprintf("NOT allowed: %s", showList(notAllowedList)))
} }

327
cmd/root_test.go Normal file
View file

@ -0,0 +1,327 @@
package cmd
import (
"bytes"
"context"
"github.com/nwtgck/handy-sshd/version"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
"net"
"strconv"
"testing"
)
func TestVersion(t *testing.T) {
rootCmd := RootCmd()
rootCmd.SetArgs([]string{"--version"})
var stdoutBuf bytes.Buffer
rootCmd.SetOut(&stdoutBuf)
assert.NoError(t, rootCmd.Execute())
assert.Equal(t, version.Version+"\n", stdoutBuf.String())
}
func TestZeroUsers(t *testing.T) {
rootCmd := RootCmd()
rootCmd.SetArgs([]string{})
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
assert.Error(t, rootCmd.Execute())
assert.Equal(t, `Error: No user specified
e.g. --user "john:mypass"
e.g. --user "john:"
`, stderrBuf.String())
}
func TestAllPermissionsAllowed(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertRemotePortForwarding(t, client)
assertLocalPortForwarding(t, client)
assertExec(t, client)
assertPtyTerminal(t, client)
assertSftp(t, client)
assertUnixRemotePortForwarding(t, client)
assertUnixLocalPortForwarding(t, client)
}
func TestEmptyPassword(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
}
func TestMultipleUsers(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass1", "--user", "alex:mypass2"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
for _, user := range []struct {
name string
password string
}{{name: "john", password: "mypass1"}, {name: "alex", password: "mypass2"}} {
sshClientConfig := &ssh.ClientConfig{
User: user.name,
Auth: []ssh.AuthMethod{ssh.Password(user.password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
}
}
func TestWrongPassword(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mywrongpassword")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
_, err := ssh.Dial("tcp", address, sshClientConfig)
assert.Error(t, err)
assert.Equal(t, `ssh: handshake failed: ssh: unable to authenticate, attempted methods [none password], no supported methods remain`, err.Error())
}
func TestAllowExecute(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-execute"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertExec(t, client)
assertPtyTerminal(t, client)
assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client)
}
func TestAllowTcpipForward(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-tcpip-forward"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client)
}
func TestAllowStreamlocalForward(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-streamlocal-forward"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoSftp(t, client)
assertUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client)
}
func TestAllowDirectTcpip(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-direct-tcpip"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client)
}
func TestAllowDirectStreamlocal(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-direct-streamlocal"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertUnixLocalPortForwarding(t, client)
}
func TestAllowSftp(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypass", "--allow-sftp"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertSftp(t, client)
}

283
cmd/test_util_test.go Normal file
View file

@ -0,0 +1,283 @@
package cmd
import (
"bytes"
"github.com/google/uuid"
"github.com/pkg/sftp"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
"io"
"net"
"os"
"os/exec"
"path"
"strconv"
"testing"
"time"
)
func getAvailableTcpPort() int {
ln, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
defer ln.Close()
return ln.Addr().(*net.TCPAddr).Port
}
func waitTCPServer(port int) {
for {
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
if err == nil {
conn.Close()
break
}
}
}
func assertExec(t *testing.T, client *ssh.Client) {
session, err := client.NewSession()
assert.NoError(t, err)
defer session.Close()
whoamiBytes, err := session.Output("whoami")
assert.NoError(t, err)
expectedWhoamiBytes, err := exec.Command("whoami").Output()
assert.NoError(t, err)
assert.Equal(t, string(expectedWhoamiBytes), string(whoamiBytes))
}
func assertNoExec(t *testing.T, client *ssh.Client) {
session, err := client.NewSession()
assert.NoError(t, err)
defer session.Close()
_, err = session.Output("whoami")
assert.Error(t, err)
assert.Equal(t, "ssh: command whoami failed", err.Error())
}
func assertPtyTerminal(t *testing.T, client *ssh.Client) {
session, err := client.NewSession()
assert.NoError(t, err)
defer session.Close()
err = session.RequestPty("xterm", 100, 200, ssh.TerminalModes{})
assert.NoError(t, err)
stdin, err := session.StdinPipe()
assert.NoError(t, err)
_, err = stdin.Write([]byte("echo helloworldviapty\r"))
assert.NoError(t, err)
stdout, err := session.StdoutPipe()
assert.NoError(t, err)
stdoutBytesChan := make(chan []byte)
go func() {
var buff bytes.Buffer
_, err := io.Copy(&buff, stdout)
assert.NoError(t, err)
stdoutBytesChan <- buff.Bytes()
}()
err = session.Shell()
assert.NoError(t, err)
time.Sleep(1 * time.Second)
session.Close()
stdoutBytes := <-stdoutBytesChan
assert.Contains(t, string(stdoutBytes), "helloworldviapty")
}
func assertNoPtyTerminal(t *testing.T, client *ssh.Client) {
session, err := client.NewSession()
assert.NoError(t, err)
defer session.Close()
err = session.RequestPty("xterm", 100, 200, ssh.TerminalModes{})
assert.Error(t, err)
assert.Equal(t, "ssh: pty-req failed", err.Error())
}
func assertLocalPortForwarding(t *testing.T, client *ssh.Client) {
var remoteTcpPort int
acceptedConnChan := make(chan net.Conn)
{
ln, err := net.Listen("tcp", ":0")
assert.NoError(t, err)
remoteTcpPort = ln.Addr().(*net.TCPAddr).Port
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
acceptedConnChan <- conn
}()
}
raddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: remoteTcpPort}
conn, err := client.DialTCP("tcp", nil, raddr)
assert.NoError(t, err)
defer conn.Close()
acceptedConn := <-acceptedConnChan
defer acceptedConn.Close()
{
localToRemote := [3]byte{1, 2, 3}
_, err = conn.Write(localToRemote[:])
assert.NoError(t, err)
var buf [len(localToRemote)]byte
_, err = io.ReadFull(acceptedConn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, localToRemote)
}
{
remoteToLocal := [4]byte{10, 20, 30, 40}
_, err = acceptedConn.Write(remoteToLocal[:])
assert.NoError(t, err)
var buf [len(remoteToLocal)]byte
_, err = io.ReadFull(conn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, remoteToLocal)
}
}
func assertNoLocalPortForwarding(t *testing.T, client *ssh.Client) {
raddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}
_, err := client.DialTCP("tcp", nil, raddr)
assert.Error(t, err)
assert.Equal(t, "ssh: rejected: administratively prohibited (direct-tcpip not allowed)", err.Error())
}
func assertUnixLocalPortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
acceptedConnChan := make(chan net.Conn)
{
ln, err := net.Listen("unix", remoteUnixSocket)
assert.NoError(t, err)
defer os.Remove(remoteUnixSocket)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
acceptedConnChan <- conn
}()
}
conn, err := client.Dial("unix", remoteUnixSocket)
assert.NoError(t, err)
defer conn.Close()
acceptedConn := <-acceptedConnChan
defer acceptedConn.Close()
{
localToRemote := [3]byte{1, 2, 3}
_, err = conn.Write(localToRemote[:])
assert.NoError(t, err)
var buf [len(localToRemote)]byte
_, err = io.ReadFull(acceptedConn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, localToRemote)
}
{
remoteToLocal := [4]byte{10, 20, 30, 40}
_, err = acceptedConn.Write(remoteToLocal[:])
assert.NoError(t, err)
var buf [len(remoteToLocal)]byte
_, err = io.ReadFull(conn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, remoteToLocal)
}
}
func assertNoUnixLocalPortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
_, err := client.Dial("unix", remoteUnixSocket)
assert.Error(t, err)
assert.Equal(t, "ssh: rejected: administratively prohibited (direct-streamlocal (Unix domain socket) not allowed)", err.Error())
}
func assertRemotePortForwarding(t *testing.T, client *ssh.Client) {
remotePort := getAvailableTcpPort()
ln, err := client.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(remotePort)))
assert.NoError(t, err)
defer ln.Close()
acceptedConnChan := make(chan net.Conn)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
acceptedConnChan <- conn
}()
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(remotePort)))
assert.NoError(t, err)
defer conn.Close()
acceptedConn := <-acceptedConnChan
defer acceptedConn.Close()
{
localToRemote := [3]byte{1, 2, 3}
_, err = conn.Write(localToRemote[:])
assert.NoError(t, err)
var buf [len(localToRemote)]byte
_, err = io.ReadFull(acceptedConn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, localToRemote)
}
{
remoteToLocal := [4]byte{10, 20, 30, 40}
_, err = acceptedConn.Write(remoteToLocal[:])
assert.NoError(t, err)
var buf [len(remoteToLocal)]byte
_, err = io.ReadFull(conn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, remoteToLocal)
}
}
func assertNoRemotePortForwarding(t *testing.T, client *ssh.Client) {
_, err := client.Listen("tcp", "127.0.0.1:5678")
assert.Error(t, err)
assert.Equal(t, "ssh: tcpip-forward request denied by peer", err.Error())
}
func assertUnixRemotePortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
ln, err := client.ListenUnix(remoteUnixSocket)
assert.NoError(t, err)
defer os.Remove(remoteUnixSocket)
defer ln.Close()
acceptedConnChan := make(chan net.Conn)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
acceptedConnChan <- conn
}()
conn, err := net.Dial("unix", remoteUnixSocket)
assert.NoError(t, err)
defer conn.Close()
acceptedConn := <-acceptedConnChan
defer acceptedConn.Close()
{
localToRemote := [3]byte{1, 2, 3}
_, err = conn.Write(localToRemote[:])
assert.NoError(t, err)
var buf [len(localToRemote)]byte
_, err = io.ReadFull(acceptedConn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, localToRemote)
}
{
remoteToLocal := [4]byte{10, 20, 30, 40}
_, err = acceptedConn.Write(remoteToLocal[:])
assert.NoError(t, err)
var buf [len(remoteToLocal)]byte
_, err = io.ReadFull(conn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, remoteToLocal)
}
}
func assertNoUnixRemotePortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
_, err := client.ListenUnix(remoteUnixSocket)
assert.Error(t, err)
assert.Equal(t, "ssh: streamlocal-forward@openssh.com request denied by peer", err.Error())
}
func assertSftp(t *testing.T, client *ssh.Client) {
sftpClient, err := sftp.NewClient(client)
assert.NoError(t, err)
_, err = sftpClient.Getwd()
assert.NoError(t, err)
}
func assertNoSftp(t *testing.T, client *ssh.Client) {
_, err := sftp.NewClient(client)
assert.Error(t, err)
assert.Equal(t, "ssh: subsystem request failed", err.Error())
}

23
go.mod
View file

@ -3,19 +3,26 @@ module github.com/nwtgck/handy-sshd
go 1.20 go 1.20
require ( require (
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.21
github.com/google/uuid v1.6.0
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.5 github.com/pkg/sftp v1.13.9
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.9.1
golang.org/x/crypto v0.12.0 github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.11.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

122
go.sum
View file

@ -1,46 +1,120 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de h1:l5Za6utMv/HsBWWqzt4S8X17j+kt1uVETUX5UFhn2rE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,14 +1,12 @@
package main package main
import ( import (
"fmt"
"github.com/nwtgck/handy-sshd/cmd" "github.com/nwtgck/handy-sshd/cmd"
"os" "os"
) )
func main() { func main() {
if err := cmd.RootCmd.Execute(); err != nil { if err := cmd.RootCmd().Execute(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, err.Error())
os.Exit(-1) os.Exit(-1)
} }
} }

View file

@ -31,10 +31,12 @@ func (s *Server) createPty(shell string, connection ssh.Channel) (*os.File, erro
Status: 0, Status: 0,
})) }))
connection.Close() connection.Close()
if sh.Process != nil {
_, err := sh.Process.Wait() _, err := sh.Process.Wait()
if err != nil { if err != nil {
s.Logger.Info("failed to exit shell", err) s.Logger.Info("failed to exit shell", err)
} }
}
s.Logger.Info("session closed") s.Logger.Info("session closed")
} }

225
server.go
View file

@ -15,6 +15,7 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/nwtgck/handy-sshd/sync_generics"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
@ -28,6 +29,16 @@ import (
type Server struct { type Server struct {
Logger *slog.Logger Logger *slog.Logger
bindAddressToListener sync_generics.Map[string, net.Listener]
// Permissions
AllowTcpipForward bool
AllowDirectTcpip bool
AllowExecute bool // this should not be split into "allow-exec" and "allow-pty-req" for now because "pty-req" can be used not for shell execution.
AllowSftp bool
AllowStreamlocalForward bool
AllowDirectStreamlocal bool
// TODO: DNS server ? // TODO: DNS server ?
} }
@ -47,7 +58,17 @@ func (s *Server) handleChannel(shell string, newChannel ssh.NewChannel) {
case "session": case "session":
s.handleSession(shell, newChannel) s.handleSession(shell, newChannel)
case "direct-tcpip": case "direct-tcpip":
if !s.AllowDirectTcpip {
newChannel.Reject(ssh.Prohibited, "direct-tcpip not allowed")
break
}
s.handleDirectTcpip(newChannel) s.handleDirectTcpip(newChannel)
case "direct-streamlocal@openssh.com":
if !s.AllowDirectStreamlocal {
newChannel.Reject(ssh.Prohibited, "direct-streamlocal (Unix domain socket) not allowed")
break
}
s.handleDirectStreamlocal(newChannel)
default: default:
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", newChannel.ChannelType())) newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", newChannel.ChannelType()))
} }
@ -67,6 +88,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
for req := range requests { for req := range requests {
switch req.Type { switch req.Type {
case "exec": case "exec":
if !s.AllowExecute {
s.Logger.Info("execution not allowed (exec)")
req.Reply(false, nil)
break
}
s.handleExecRequest(req, connection) s.handleExecRequest(req, connection)
case "shell": case "shell":
// We only accept the default shell // We only accept the default shell
@ -75,6 +101,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
req.Reply(true, nil) req.Reply(true, nil)
} }
case "pty-req": case "pty-req":
if !s.AllowExecute {
s.Logger.Info("execution not allowed (pty-req)")
req.Reply(false, nil)
break
}
termLen := req.Payload[3] termLen := req.Payload[3]
w, h := parseDims(req.Payload[termLen+4:]) w, h := parseDims(req.Payload[termLen+4:])
shf, err = s.createPty(shell, connection) shf, err = s.createPty(shell, connection)
@ -93,6 +124,8 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
} }
case "subsystem": case "subsystem":
s.handleSessionSubSystem(req, connection) s.handleSessionSubSystem(req, connection)
default:
s.Logger.Info("unsupported request", "req_type", req.Type)
} }
} }
} }
@ -140,9 +173,17 @@ func (s *Server) handleExecRequest(req *ssh.Request, connection ssh.Channel) {
func (s *Server) handleSessionSubSystem(req *ssh.Request, connection ssh.Channel) { func (s *Server) handleSessionSubSystem(req *ssh.Request, connection ssh.Channel) {
// https://github.com/pkg/sftp/blob/42e9800606febe03f9cdf1d1283719af4a5e6456/examples/go-sftp-server/main.go#L111 // https://github.com/pkg/sftp/blob/42e9800606febe03f9cdf1d1283719af4a5e6456/examples/go-sftp-server/main.go#L111
ok := string(req.Payload[4:]) == "sftp" if string(req.Payload[4:]) != "sftp" {
req.Reply(ok, nil) req.Reply(false, nil)
return
}
if !s.AllowSftp {
s.Logger.Info("sftp not allowed")
req.Reply(false, nil)
return
}
req.Reply(true, nil)
serverOptions := []sftp.ServerOption{ serverOptions := []sftp.ServerOption{
sftp.WithDebug(os.Stderr), sftp.WithDebug(os.Stderr),
} }
@ -168,7 +209,7 @@ func (s *Server) handleDirectTcpip(newChannel ssh.NewChannel) {
SourcePort uint32 SourcePort uint32
} }
if err := ssh.Unmarshal(newChannel.ExtraData(), &msg); err != nil { if err := ssh.Unmarshal(newChannel.ExtraData(), &msg); err != nil {
s.Logger.Info("failed to parse message", "err", err) s.Logger.Info("failed to parse direct-tcpip message", "err", err)
return return
} }
channel, reqs, err := newChannel.Accept() channel, reqs, err := newChannel.Accept()
@ -198,6 +239,44 @@ func (s *Server) handleDirectTcpip(newChannel ssh.NewChannel) {
return return
} }
// client side: https://github.com/golang/crypto/blob/b4ddeeda5bc71549846db71ba23e83ecb26f36ed/ssh/streamlocal.go#L52
func (s *Server) handleDirectStreamlocal(newChannel ssh.NewChannel) {
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L237
var msg struct {
SocketPath string
Reserved0 string
Reserved1 uint32
}
if err := ssh.Unmarshal(newChannel.ExtraData(), &msg); err != nil {
s.Logger.Info("failed to parse direct-streamlocal message", "err", err)
return
}
channel, reqs, err := newChannel.Accept()
if err != nil {
s.Logger.Info("failed to accept", "err", err)
return
}
go ssh.DiscardRequests(reqs)
conn, err := net.Dial("unix", msg.SocketPath)
if err != nil {
s.Logger.Info("failed to dial", "err", err)
channel.Close()
return
}
var closeOnce sync.Once
closer := func() {
channel.Close()
conn.Close()
}
go func() {
io.Copy(channel, conn)
closeOnce.Do(closer)
}()
io.Copy(conn, channel)
closeOnce.Do(closer)
return
}
// ======================= // =======================
// parseDims extracts terminal dimensions (width x height) from the provided buffer. // parseDims extracts terminal dimensions (width x height) from the provided buffer.
@ -232,7 +311,31 @@ func (s *Server) HandleGlobalRequests(sshConn *ssh.ServerConn, reqs <-chan *ssh.
for req := range reqs { for req := range reqs {
switch req.Type { switch req.Type {
case "tcpip-forward": case "tcpip-forward":
if !s.AllowTcpipForward {
s.Logger.Info("tcpip-forward not allowed")
req.Reply(false, nil)
break
}
go func() {
s.handleTcpipForward(sshConn, req) s.handleTcpipForward(sshConn, req)
}()
case "cancel-tcpip-forward":
go func() {
s.cancelTcpipForward(req)
}()
case "streamlocal-forward@openssh.com":
if !s.AllowStreamlocalForward {
s.Logger.Info("streamlocal-forward not allowed")
req.Reply(false, nil)
break
}
go func() {
s.handleStreamlocalForward(sshConn, req)
}()
case "cancel-streamlocal-forward@openssh.com":
go func() {
s.cancelStreamlocalForward(req)
}()
default: default:
// discard // discard
if req.WantReply { if req.WantReply {
@ -253,11 +356,14 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
req.Reply(false, nil) req.Reply(false, nil)
return return
} }
ln, err := net.Listen("tcp", net.JoinHostPort(msg.Addr, strconv.Itoa(int(msg.Port)))) address := net.JoinHostPort(msg.Addr, strconv.Itoa(int(msg.Port)))
ln, err := net.Listen("tcp", address)
if err != nil { if err != nil {
req.Reply(false, nil) req.Reply(false, nil)
return return
} }
s.bindAddressToListener.Store(address, ln)
req.Reply(true, nil)
go func() { go func() {
sshConn.Wait() sshConn.Wait()
ln.Close() ln.Close()
@ -266,6 +372,7 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
s.Logger.Info("failed to accept", "err", err)
return return
} }
var replyMsg struct { var replyMsg struct {
@ -276,6 +383,14 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
} }
replyMsg.Addr = msg.Addr replyMsg.Addr = msg.Addr
replyMsg.Port = msg.Port replyMsg.Port = msg.Port
originatorAddr, originatorPortStr, err := net.SplitHostPort(conn.RemoteAddr().String())
if err == nil {
originatorPort, _ := strconv.Atoi(originatorPortStr)
replyMsg.OriginatorAddr = originatorAddr
replyMsg.OriginatorPort = uint32(originatorPort)
} else {
s.Logger.Error("failed to split remote address", "remote_address", conn.RemoteAddr())
}
go func() { go func() {
channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(&replyMsg)) channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(&replyMsg))
@ -298,3 +413,105 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
}() }()
} }
} }
// https://datatracker.ietf.org/doc/html/rfc4254#section-7.1
func (s *Server) cancelTcpipForward(req *ssh.Request) {
var msg struct {
Addr string
Port uint32
}
if err := ssh.Unmarshal(req.Payload, &msg); err != nil {
req.Reply(false, nil)
return
}
address := net.JoinHostPort(msg.Addr, strconv.Itoa(int(msg.Port)))
ln, loaded := s.bindAddressToListener.LoadAndDelete(address)
if !loaded {
req.Reply(false, nil)
s.Logger.Info("failed to find listener", "address", address)
}
if err := ln.Close(); err != nil {
req.Reply(false, nil)
s.Logger.Info("failed to close", "err", err)
}
req.Reply(true, nil)
}
// client side: https://github.com/golang/crypto/blob/b4ddeeda5bc71549846db71ba23e83ecb26f36ed/ssh/streamlocal.go#L34
func (s *Server) handleStreamlocalForward(sshConn *ssh.ServerConn, req *ssh.Request) {
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L272
var msg struct {
SocketPath string
}
if err := ssh.Unmarshal(req.Payload, &msg); err != nil {
req.Reply(false, nil)
return
}
ln, err := net.Listen("unix", msg.SocketPath)
if err != nil {
req.Reply(false, nil)
return
}
s.bindAddressToListener.Store(msg.SocketPath, ln)
req.Reply(true, nil)
go func() {
sshConn.Wait()
ln.Close()
s.Logger.Info("connection closed", "address", ln.Addr().String())
}()
for {
conn, err := ln.Accept()
if err != nil {
s.Logger.Info("failed to accept", "err", err)
return
}
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L255
var replyMsg struct {
SocketPath string
Reserved string
}
replyMsg.SocketPath = msg.SocketPath
go func() {
channel, reqs, err := sshConn.OpenChannel("forwarded-streamlocal@openssh.com", ssh.Marshal(&replyMsg))
if err != nil {
req.Reply(false, nil)
conn.Close()
return
}
go ssh.DiscardRequests(reqs)
go func() {
io.Copy(channel, conn)
conn.Close()
channel.Close()
}()
go func() {
io.Copy(conn, channel)
conn.Close()
channel.Close()
}()
}()
}
}
func (s *Server) cancelStreamlocalForward(req *ssh.Request) {
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L280
var msg struct {
SocketPath string
}
if err := ssh.Unmarshal(req.Payload, &msg); err != nil {
req.Reply(false, nil)
return
}
ln, loaded := s.bindAddressToListener.LoadAndDelete(msg.SocketPath)
if !loaded {
s.Logger.Info("failed to find listener", "address", msg.SocketPath)
req.Reply(false, nil)
return
}
if err := ln.Close(); err != nil {
req.Reply(false, nil)
s.Logger.Info("failed to close", "err", err)
}
req.Reply(true, nil)
}

62
sync_generics/map.go Normal file
View file

@ -0,0 +1,62 @@
package sync_generics
import "sync"
type Map[K any, V any] struct {
inner sync.Map
}
func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool) {
deleted = m.inner.CompareAndDelete(key, old)
return
}
func (m *Map[K, V]) CompareAndSwap(key K, old V, new V) bool {
return m.inner.CompareAndSwap(key, old, new)
}
func (m *Map[K, V]) Delete(key K) {
m.inner.Delete(key)
}
func (m *Map[K, V]) Load(key K) (value V, ok bool) {
_value, ok := m.inner.Load(key)
value = nilSafeTypeAssertion[V](_value)
return
}
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
_value, loaded := m.inner.LoadAndDelete(key)
value = nilSafeTypeAssertion[V](_value)
return
}
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
_actual, loaded := m.inner.LoadOrStore(key, value)
actual = nilSafeTypeAssertion[V](_actual)
return
}
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
m.inner.Range(func(key, value any) bool {
return f(nilSafeTypeAssertion[K](key), nilSafeTypeAssertion[V](value))
})
}
func (m *Map[K, V]) Store(key K, value V) {
m.inner.Store(key, value)
}
func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool) {
_previous, loaded := m.inner.Swap(key, value)
previous = nilSafeTypeAssertion[V](_previous)
return
}
func nilSafeTypeAssertion[T any](value any) T {
var zero T
if value == nil {
return zero
}
return value.(T)
}

View file

@ -1,3 +1,3 @@
package version package version
const Version = "0.1.0" const Version = "0.4.3"