aboutsummaryrefslogtreecommitdiff
path: root/ci/testsshd/main.go
diff options
context:
space:
mode:
authorLena <lena@omega>2026-01-01 00:00:00 +0000
committerLena <lena@omega>2026-01-01 00:00:00 +0000
commit7e04941bccb2683f8a6e3ee38a99c50129234dd1 (patch)
tree471227fa437291e7a6b499e3de6c106c54eaf311 /ci/testsshd/main.go
downloadrsend-7e04941bccb2683f8a6e3ee38a99c50129234dd1.tar.gz
rsend: push phone folders to a home SSH host over rsync
A small Android app for one-way folder backup, a KISS alternative to Syncthing. It bundles rsync (built from pinned source via the NDK) and a pure-Go SSH transport, both shipped in the APK as lib*.so and run from the native library directory. rsend pins the host key, stores the ed25519 identity Keystore-encrypted, pushes each folder additively or as a mirror, and runs on demand or on a WiFi-only schedule. The build is self-contained and reproducible: make setup provisions the toolchain, make builds rsync, rsh, and the APK.
Diffstat (limited to 'ci/testsshd/main.go')
-rw-r--r--ci/testsshd/main.go149
1 files changed, 149 insertions, 0 deletions
diff --git a/ci/testsshd/main.go b/ci/testsshd/main.go
new file mode 100644
index 0000000..19a53ac
--- /dev/null
+++ b/ci/testsshd/main.go
@@ -0,0 +1,149 @@
+// testsshd is a minimal SSH server for rsend integration tests: one host key,
+// an authorized_keys allowlist, and command execution via /bin/sh (so it runs
+// the remote rsync). It is not a product component; it stands in for a real
+// sshd so the full push path can be exercised on a device or in CI.
+package main
+
+import (
+ "flag"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+
+ "golang.org/x/crypto/ssh"
+)
+
+func main() {
+ addr := flag.String("addr", "127.0.0.1:2222", "listen address")
+ hostKeyPath := flag.String("hostkey", "", "OpenSSH private host key (required)")
+ authPath := flag.String("authorized", "", "authorized_keys file (required)")
+ flag.Parse()
+
+ if *hostKeyPath == "" || *authPath == "" {
+ log.Fatal("testsshd: -hostkey and -authorized are required")
+ }
+
+ hostKey := mustSigner(*hostKeyPath)
+ allowed := mustAuthorized(*authPath)
+
+ cfg := &ssh.ServerConfig{
+ PublicKeyCallback: func(_ ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+ if allowed[string(key.Marshal())] {
+ return &ssh.Permissions{}, nil
+ }
+ return nil, io.EOF
+ },
+ }
+ cfg.AddHostKey(hostKey)
+
+ ln, err := net.Listen("tcp", *addr)
+ if err != nil {
+ log.Fatalf("testsshd: listen: %v", err)
+ }
+ log.Printf("testsshd: listening on %s", ln.Addr())
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ log.Fatalf("testsshd: accept: %v", err)
+ }
+ go handle(c, cfg)
+ }
+}
+
+func mustSigner(path string) ssh.Signer {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ log.Fatalf("testsshd: read host key: %v", err)
+ }
+ s, err := ssh.ParsePrivateKey(b)
+ if err != nil {
+ log.Fatalf("testsshd: parse host key: %v", err)
+ }
+ return s
+}
+
+func mustAuthorized(path string) map[string]bool {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ log.Fatalf("testsshd: read authorized_keys: %v", err)
+ }
+ m := map[string]bool{}
+ for len(b) > 0 {
+ key, _, _, rest, err := ssh.ParseAuthorizedKey(b)
+ if err != nil {
+ break
+ }
+ m[string(key.Marshal())] = true
+ b = rest
+ }
+ if len(m) == 0 {
+ log.Fatal("testsshd: no authorized keys")
+ }
+ return m
+}
+
+func handle(nConn net.Conn, cfg *ssh.ServerConfig) {
+ conn, chans, reqs, err := ssh.NewServerConn(nConn, cfg)
+ if err != nil {
+ return
+ }
+ defer conn.Close()
+ go ssh.DiscardRequests(reqs)
+ for nc := range chans {
+ if nc.ChannelType() != "session" {
+ nc.Reject(ssh.UnknownChannelType, "only session")
+ continue
+ }
+ ch, requests, err := nc.Accept()
+ if err != nil {
+ return
+ }
+ go session(ch, requests)
+ }
+}
+
+func session(ch ssh.Channel, requests <-chan *ssh.Request) {
+ for req := range requests {
+ if req.Type != "exec" {
+ if req.WantReply {
+ req.Reply(false, nil)
+ }
+ continue
+ }
+ var payload struct{ Command string }
+ ssh.Unmarshal(req.Payload, &payload)
+ if req.WantReply {
+ req.Reply(true, nil)
+ }
+ code := run(payload.Command, ch)
+ ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{uint32(code)}))
+ ch.Close()
+ return
+ }
+}
+
+func run(cmd string, ch ssh.Channel) int {
+ c := exec.Command("/bin/sh", "-c", cmd)
+ c.Stdout = ch
+ c.Stderr = ch.Stderr()
+ in, err := c.StdinPipe()
+ if err != nil {
+ return 1
+ }
+ if err := c.Start(); err != nil {
+ return 1
+ }
+ go func() {
+ io.Copy(in, ch)
+ in.Close()
+ }()
+ if err := c.Wait(); err != nil {
+ if ee, ok := err.(*exec.ExitError); ok {
+ return ee.ExitCode()
+ }
+ return 1
+ }
+ return 0
+}