diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5db0d8be..17f29ffc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -73,14 +73,9 @@ jobs: cache: true - name: Setup Snap - env: - SNAP_SECRET: ${{ secrets.snap }} run: | set -e - sudo snap install snapcraft --classic - echo $SNAP_SECRET | base64 -d > /tmp/secret - snap run snapcraft login --with /tmp/secret - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 @@ -91,6 +86,7 @@ jobs: env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SKIP_PUSH: ${{ github.event_name != 'release' }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - name: Upload binaries to release env: diff --git a/.gitignore b/.gitignore index b0c6529b..c40cc922 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ vendor/ go.work dist/ +cmd/sshpiperd/snap/launcher/configentry.txt \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8ecee540..8fe73429 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,6 +19,18 @@ builds: binary: sshpiperd ldflags: - -X main.mainver={{.Version}} + - id: snaplauncher + env: + - CGO_ENABLED=0 + goos: + - linux + main: ./cmd/sshpiperd/snap/launcher + binary: snap/launcher + hooks: + pre: go generate ./cmd/sshpiperd/snap/launcher + goarch: + - amd64 + - arm64 - id: plugin_workingdir env: - CGO_ENABLED=0 @@ -221,7 +233,11 @@ release: snapcrafts: - builds: - sshpiperd + - snaplauncher - plugin_workingdir + - plugin_yaml + - plugin_fixed + - plugin_failtoban name: sshpiperd name_template: "sshpiperd_{{ .Version }}_{{ .Os }}_{{ .Arch }}" summary: The missing reverse proxy for ssh scp @@ -236,6 +252,14 @@ snapcrafts: license: MIT apps: sshpiperd: - command: sshpiperd --server-key $SNAP_COMMON/ssh_host_ed25519_key --server-key-generate-mode notexist $SNAP/workingdir --root $SNAP_COMMON/workingdir + command: launcher plugs: [network-bind] - daemon: simple \ No newline at end of file + daemon: simple + extra_files: + - source: cmd/sshpiperd/snap/hooks/configure + destination: meta/hooks/configure + mode: 0755 + hooks: + configure: + # plugin: dump + # source: cmd/sshpiperd/snap/hooks diff --git a/cmd/sshpiperd/snap/configgen/main.go b/cmd/sshpiperd/snap/configgen/main.go new file mode 100644 index 00000000..43fe9566 --- /dev/null +++ b/cmd/sshpiperd/snap/configgen/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "strings" +) + +func main() { + + configs := map[string]string{ + "sshpiperd": "../../main.go", + "workingdir": "../../../../plugin/workingdir/main.go", + "yaml": "../../../../plugin/yaml/main.go", + "fixed": "../../../../plugin/fixed/main.go", + "failtoban": "../../../../plugin/failtoban/main.go", + } + + for k, v := range configs { + extractFlags(k, v) + } +} + +func extractFlags(namespace, filePath string) { + file, err := os.Open(filePath) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + fset := token.NewFileSet() + + node, err := parser.ParseFile(fset, filePath, file, parser.AllErrors) + if err != nil { + log.Fatal(err) + } + + ast.Inspect(node, func(n ast.Node) bool { + + if cl, ok := n.(*ast.CompositeLit); ok { + + if t, ok := cl.Type.(*ast.SelectorExpr); ok { + + o, ok := t.X.(*ast.Ident) + if !ok { + return true + } + + if o.Name != "cli" { + return true + } + + if !strings.HasSuffix(t.Sel.Name, "Flag") { + return true + } + + for _, v := range cl.Elts { + if kv, ok := v.(*ast.KeyValueExpr); ok { + if kv.Key.(*ast.Ident).Name == "Name" { + fmt.Println(namespace + "." + strings.Trim(kv.Value.(*ast.BasicLit).Value, " \"")) + } + } + } + } + } + return true + }) +} diff --git a/cmd/sshpiperd/snap/hooks/configure b/cmd/sshpiperd/snap/hooks/configure new file mode 100644 index 00000000..4ee3395a --- /dev/null +++ b/cmd/sshpiperd/snap/hooks/configure @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +$SNAP/launcher generate diff --git a/cmd/sshpiperd/snap/launcher/doc.go b/cmd/sshpiperd/snap/launcher/doc.go new file mode 100644 index 00000000..409a75af --- /dev/null +++ b/cmd/sshpiperd/snap/launcher/doc.go @@ -0,0 +1,2 @@ +//go:generate sh -c "go run ../configgen/main.go > configentry.txt" +package main diff --git a/cmd/sshpiperd/snap/launcher/dummy.txt b/cmd/sshpiperd/snap/launcher/dummy.txt new file mode 100644 index 00000000..e69de29b diff --git a/cmd/sshpiperd/snap/launcher/main.go b/cmd/sshpiperd/snap/launcher/main.go new file mode 100644 index 00000000..2adf496b --- /dev/null +++ b/cmd/sshpiperd/snap/launcher/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "embed" + "encoding/json" + "log" + "os" + "os/exec" + "path" + "strings" +) + +//go:embed all:*.txt +var configentry embed.FS + +func main() { + bindir := os.Getenv("SNAP") + datadir := os.Getenv("SNAP_DATA") + + flags := map[string][][]string{} + configfile := path.Join(datadir, "flags.json") + + if len(os.Args) > 1 && os.Args[1] == "generate" { + flags = loadFromSnapctl() + cache, _ := json.Marshal(flags) + if err := os.WriteFile(configfile, cache, 0600); err != nil { + log.Fatal(err) + } + + return + } + + cache, _ := os.ReadFile(configfile) + _ = json.Unmarshal(cache, &flags) + + cmd := exec.Command(path.Join(bindir, "sshpiperd")) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + for _, flag := range flags["sshpiperd"] { + cmd.Args = append(cmd.Args, "--"+flag[0], flag[1]) + } + + for _, plugin := range flags["sshpiperd.plugins"][0] { + cmd.Args = append(cmd.Args, path.Join(bindir, plugin)) + for _, flag := range flags[plugin] { + cmd.Args = append(cmd.Args, "--"+flag[0], flag[1]) + } + cmd.Args = append(cmd.Args, "--") + } + + log.Println("starting sshpiperd with args:", cmd) + _ = cmd.Run() +} + +func loadFromSnapctl() map[string][][]string { + commondir := os.Getenv("SNAP_COMMON") + + flags := map[string][][]string{} + + data, err := configentry.ReadFile("configentry.txt") + if err != nil { + log.Fatal(err) + } + + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + v, err := get(line) + + if err != nil { + log.Fatal(err) + } + + if v == "" { + continue + } + + parts := strings.Split(line, ".") + ns := parts[0] + flag := parts[1] + + flags[ns] = append(flags[ns], []string{flag, v}) + } + + // known defaults + { + v, _ := get("sshpiperd.plugins") + if v == "" { + v = "workingdir" + } + + var plugins []string + for _, str := range strings.Split(v, " ") { + str = strings.TrimSpace(str) + if str != "" { + plugins = append(plugins, str) + } + } + + flags["sshpiperd.plugins"] = [][]string{plugins} + } + + // { + // v, _ := get("sshpiperd.typescript-log-dir") + // if v == "" { + // v = "screenrecord" + // dir := path.Join(commondir, v) + // _ = os.MkdirAll(dir, 0700) + // flags["sshpiperd"] = append(flags["sshpiperd"], []string{"typescript-log-dir", dir}) + // } + // } + + { + v, _ := get("sshpiperd.server-key-generate-mode") + if v == "" { + v = "notexist" + flags["sshpiperd"] = append(flags["sshpiperd"], []string{"server-key-generate-mode", v}) + } + } + + { + v, _ := get("sshpiperd.server-key") + if v == "" { + v = "ssh_host_ed25519_key" + file := path.Join(commondir, v) + flags["sshpiperd"] = append(flags["sshpiperd"], []string{"server-key", file}) + } + } + + { + v, _ := get("workingdir.root") + if v == "" { + v = "workingdir" + + dir := path.Join(commondir, v) + _ = os.MkdirAll(dir, 0700) + flags["workingdir"] = append(flags["workingdir"], []string{"root", dir}) + } + } + + return flags +} + +func get(key string) (string, error) { + cmd := exec.Command("snapctl", "get", key) + output, err := cmd.Output() + if err != nil { + return "", err + } + + value := string(output) + return strings.TrimSpace(value), nil +}