this repo has no description smallweb.run
smallweb
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix sftp handler

pomdtr 7a9660a5 797c21c0

+295 -34
+2 -2
.vscode/launch.json
··· 7 7 "program": "${workspaceFolder}/main.go", 8 8 "args": [ 9 9 "up", 10 - "--http-addr=:7778", 11 - "--ssh-addr=:2222" 10 + "--addr=:7778", 11 + "--ssh-addr=:2223" 12 12 ], 13 13 "env": { 14 14 "SMALLWEB_DIR": "${workspaceFolder}/examples",
+23 -20
cmd/up.go
··· 26 26 "github.com/charmbracelet/ssh" 27 27 "github.com/charmbracelet/wish" 28 28 "github.com/creack/pty" 29 - "github.com/picosh/pobj" 30 - "github.com/pomdtr/smallweb/storage" 31 29 32 - "github.com/picosh/send/protocols/rsync" 33 - "github.com/picosh/send/protocols/scp" 34 - "github.com/picosh/send/protocols/sftp" 35 30 "github.com/pomdtr/smallweb/app" 31 + "github.com/pomdtr/smallweb/sftp" 36 32 "github.com/pomdtr/smallweb/watcher" 37 33 gossh "golang.org/x/crypto/ssh" 38 34 "gopkg.in/natefinch/lumberjack.v2" ··· 179 175 } 180 176 181 177 if flags.sshAddr != "" { 182 - if !utils.FileExists(flags.sshHostKey) { 183 - _, err := keygen.New(flags.sshHostKey, keygen.WithWrite()) 178 + hostKey := flags.sshHostKey 179 + if hostKey == "" { 180 + homeDir, err := os.UserHomeDir() 181 + if err != nil { 182 + return fmt.Errorf("failed to get home directory: %v", err) 183 + } 184 + hostKey = filepath.Join(homeDir, ".ssh", "smallweb") 185 + } 186 + 187 + if !utils.FileExists(hostKey) { 188 + _, err := keygen.New(hostKey, keygen.WithWrite()) 184 189 if err != nil { 185 190 return fmt.Errorf("failed to generate host key: %v", err) 186 191 } 187 192 } 188 193 189 - handler := pobj.NewUploadAssetHandler(&pobj.Config{ 190 - Storage: &storage.StorageFS{ 191 - Dir: k.String("dir"), 192 - Logger: consoleLogger, 193 - }, 194 - Logger: consoleLogger, 195 - }) 194 + root, err := os.OpenRoot(k.String("dir")) 195 + if err != nil { 196 + return fmt.Errorf("failed to open root: %v", err) 197 + } 198 + 199 + fmt.Fprintf(os.Stderr, root.Name()) 196 200 197 201 srv, err := wish.NewServer( 198 202 wish.WithAddress(flags.sshAddr), 199 - wish.WithHostKeyPath(flags.sshHostKey), 203 + wish.WithHostKeyPath(hostKey), 200 204 wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { 201 205 authorizedKeyPaths := []string{filepath.Join(k.String("dir"), ".smallweb", "authorized_keys")} 202 206 ··· 230 234 231 235 return false 232 236 }), 233 - sftp.SSHOption(handler), 237 + // TODO: re-implement sftp 238 + sftp.SSHOption(root, nil), 234 239 wish.WithMiddleware(func(next ssh.Handler) ssh.Handler { 235 240 return func(sess ssh.Session) { 236 241 var cmd *exec.Cmd ··· 322 327 } 323 328 } 324 329 }, 325 - rsync.Middleware(handler), 326 - scp.Middleware(handler), 327 330 ), 328 331 ) 329 332 ··· 331 334 return fmt.Errorf("failed to create ssh server: %v", err) 332 335 } 333 336 334 - fmt.Fprintln(cmd.ErrOrStderr(), "Starting ssh server on", flags.sshAddr) 337 + fmt.Fprintf(cmd.ErrOrStderr(), "Starting ssh server on %s...\n", flags.sshAddr) 335 338 if err = srv.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 336 339 fmt.Fprintf(cmd.ErrOrStderr(), "failed to start ssh server: %v\n", err) 337 340 } ··· 348 351 349 352 cmd.Flags().StringVar(&flags.addr, "addr", "", "address to listen on") 350 353 cmd.Flags().StringVar(&flags.sshAddr, "ssh-addr", "", "address to listen on for ssh/sftp") 351 - cmd.Flags().StringVar(&flags.sshHostKey, "ssh-host-key", fmt.Sprintf("%s/.ssh/smallweb", os.Getenv("HOME")), "ssh host key") 354 + cmd.Flags().StringVar(&flags.sshHostKey, "ssh-host-key", "", "ssh host key") 352 355 cmd.Flags().StringVar(&flags.tlsCert, "tls-cert", "", "tls certificate file") 353 356 cmd.Flags().StringVar(&flags.tlsKey, "tls-key", "", "tls key file") 354 357 cmd.Flags().BoolVar(&flags.cron, "cron", false, "enable cron jobs")
+2 -6
go.mod
··· 1 1 module github.com/pomdtr/smallweb 2 2 3 - go 1.23 3 + go 1.24 4 4 5 5 require ( 6 6 github.com/Masterminds/semver v1.5.0 ··· 30 30 github.com/knadh/koanf/providers/posflag v0.1.0 31 31 github.com/picosh/pobj v0.0.0-20250115045405-73c816ed76c2 32 32 github.com/picosh/send v0.0.0-20250121195737-daab6db117d5 33 + github.com/pkg/sftp v1.13.7 33 34 github.com/robfig/cron/v3 v3.0.1 34 35 golang.org/x/crypto v0.32.0 35 36 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ··· 51 52 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect 52 53 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 // indirect 53 54 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 54 - github.com/DavidGamba/go-getoptions v0.29.0 // indirect 55 55 github.com/ProtonMail/go-crypto v1.1.5 // indirect 56 56 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 57 57 github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect ··· 106 106 github.com/golang/protobuf v1.5.4 // indirect 107 107 github.com/google/go-cmp v0.6.0 // indirect 108 108 github.com/google/s2a-go v0.1.8 // indirect 109 - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 110 109 github.com/google/uuid v1.6.0 // indirect 111 110 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 112 111 github.com/googleapis/gax-go/v2 v2.13.0 // indirect ··· 148 147 github.com/mitchellh/go-wordwrap v1.0.1 // indirect 149 148 github.com/mitchellh/mapstructure v1.5.0 // indirect 150 149 github.com/mitchellh/reflectwalk v1.0.2 // indirect 151 - github.com/mmcloughlin/md4 v0.1.2 // indirect 152 150 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 153 151 github.com/modern-go/reflect2 v1.0.2 // indirect 154 152 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect ··· 156 154 github.com/muesli/reflow v0.3.0 // indirect 157 155 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect 158 156 github.com/philhofer/fwd v1.1.2 // indirect 159 - github.com/picosh/go-rsync-receiver v0.0.0-20250121150813-93b4f1b7aa4b // indirect 160 157 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 161 158 github.com/pkg/errors v0.9.1 // indirect 162 - github.com/pkg/sftp v1.13.7 // indirect 163 159 github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 164 160 github.com/prometheus/client_model v0.5.0 // indirect 165 161 github.com/prometheus/common v0.45.0 // indirect
-6
go.sum
··· 37 37 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 38 38 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 39 39 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 40 - github.com/DavidGamba/go-getoptions v0.29.0 h1:cU8MjOyfAyPZke4hrgEuiGBJHS9PFYPAHve2fhDhdDk= 41 - github.com/DavidGamba/go-getoptions v0.29.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84= 42 40 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 43 41 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 44 42 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= ··· 336 334 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 337 335 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 338 336 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 339 - github.com/mmcloughlin/md4 v0.1.2 h1:kGYl+iNbxhyz4u76ka9a+0TXP9KWt/LmnM0QhZwhcBo= 340 - github.com/mmcloughlin/md4 v0.1.2/go.mod h1:AAxFX59fddW0IguqNzWlf1lazh1+rXeIt/Bj49cqDTQ= 341 337 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 342 338 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 343 339 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= ··· 365 361 github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= 366 362 github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 367 363 github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 368 - github.com/picosh/go-rsync-receiver v0.0.0-20250121150813-93b4f1b7aa4b h1:PQvHqrncGJzo1z10l5pP8nDWHbYG2WnCH31D9EDbu8I= 369 - github.com/picosh/go-rsync-receiver v0.0.0-20250121150813-93b4f1b7aa4b/go.mod h1:i0iR3W4GSm1PuvVxB9OH32E5jP+CYkVb2NQSe0JCtlo= 370 364 github.com/picosh/pobj v0.0.0-20250115045405-73c816ed76c2 h1:fOz+o8pymr93p5OeJkehxkunWeFyVranWBsOmEE0OkI= 371 365 github.com/picosh/pobj v0.0.0-20250115045405-73c816ed76c2/go.mod h1:cF+eAl4G1vU+WOD8cYCKaxokHo6MWmbR8J4/SJnvESg= 372 366 github.com/picosh/send v0.0.0-20250121195737-daab6db117d5 h1:pcgpzOJAag0FkGa5he6Fl7k5E3ekZQh3e2qpjGGYipU=
+195
sftp/handler.go
··· 1 + package sftp 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "io" 7 + "os" 8 + "path/filepath" 9 + "strings" 10 + 11 + "github.com/charmbracelet/ssh" 12 + "github.com/charmbracelet/wish" 13 + "github.com/pkg/sftp" 14 + ) 15 + 16 + // Based on https://github.com/pkg/sftp/blob/master/request-example.go 17 + 18 + type handler struct { 19 + session ssh.Session 20 + root *os.Root 21 + } 22 + 23 + func (h *handler) Filecmd(r *sftp.Request) error { 24 + switch r.Method { 25 + case "Rename": 26 + if _, err := h.root.Stat(strings.TrimPrefix(r.Filepath, "/")); err != nil { 27 + return err 28 + } 29 + 30 + if _, err := h.root.Stat(strings.TrimPrefix(r.Target, "/")); err == nil { 31 + return fmt.Errorf("target file exists") 32 + } else if !errors.Is(err, os.ErrNotExist) { 33 + return err 34 + } 35 + 36 + src := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Filepath, "/")) 37 + dst := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Target, "/")) 38 + return os.Rename(src, dst) 39 + case "Link": 40 + if _, err := h.root.Stat(strings.TrimPrefix(r.Filepath, "/")); err != nil { 41 + return err 42 + } 43 + 44 + if _, err := h.root.Stat(strings.TrimPrefix(r.Target, "/")); err == nil { 45 + return fmt.Errorf("target file exists") 46 + } else if !errors.Is(err, os.ErrNotExist) { 47 + return err 48 + } 49 + 50 + src := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Filepath, "/")) 51 + dst := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Target, "/")) 52 + return os.Link(src, dst) 53 + case "Symlink": 54 + if _, err := h.root.Stat(strings.TrimPrefix(r.Filepath, "/")); err != nil { 55 + return fmt.Errorf("file does not exist") 56 + } 57 + 58 + if _, err := h.root.Stat(strings.TrimPrefix(r.Target, "/")); err == nil { 59 + return fmt.Errorf("target file exists") 60 + } else if !errors.Is(err, os.ErrNotExist) { 61 + return err 62 + } 63 + 64 + src := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Filepath, "/")) 65 + dst := filepath.Join(h.root.Name(), strings.TrimPrefix(r.Target, "/")) 66 + return os.Symlink(src, dst) 67 + case "Rmdir": 68 + return h.root.Remove(strings.TrimPrefix(r.Filepath, "/")) 69 + case "Remove": 70 + return h.root.Remove(strings.TrimPrefix(r.Filepath, "/")) 71 + case "Mkdir": 72 + return h.root.Mkdir(strings.TrimPrefix(r.Filepath, "/"), 0777) 73 + case "Setstat": 74 + return nil 75 + } 76 + return errors.New("unsupported") 77 + } 78 + 79 + func (h *handler) Filelist(r *sftp.Request) (sftp.ListerAt, error) { 80 + switch r.Method { 81 + case "List": 82 + if r.Filepath == "/" { 83 + entries, err := os.ReadDir(h.root.Name()) 84 + if err != nil { 85 + return nil, err 86 + } 87 + 88 + var fileInfos []os.FileInfo 89 + for _, entry := range entries { 90 + fileInfo, err := entry.Info() 91 + if err != nil { 92 + return nil, err 93 + } 94 + fileInfos = append(fileInfos, fileInfo) 95 + } 96 + 97 + return listerat(fileInfos), nil 98 + } 99 + 100 + f, err := h.root.Open(strings.TrimPrefix(r.Filepath, "/")) 101 + if err != nil { 102 + return nil, err 103 + } 104 + 105 + entries, err := f.ReadDir(0) 106 + if err != nil { 107 + return nil, err 108 + } 109 + 110 + var fileInfos []os.FileInfo 111 + for _, entry := range entries { 112 + fileInfo, err := entry.Info() 113 + if err != nil { 114 + return nil, err 115 + } 116 + fileInfos = append(fileInfos, fileInfo) 117 + } 118 + 119 + return listerat(fileInfos), nil 120 + 121 + case "Stat": 122 + info, err := h.root.Stat(strings.TrimPrefix(r.Filepath, "/")) 123 + if err != nil { 124 + return nil, err 125 + } 126 + 127 + return listerat([]os.FileInfo{info}), nil 128 + } 129 + 130 + return nil, errors.New("unsupported") 131 + } 132 + 133 + func (h *handler) Filewrite(r *sftp.Request) (io.WriterAt, error) { 134 + f, err := h.root.OpenFile(strings.TrimPrefix(r.Filepath, "/"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 135 + if err != nil { 136 + return nil, err 137 + } 138 + 139 + return f, nil 140 + } 141 + 142 + func (h *handler) Fileread(r *sftp.Request) (io.ReaderAt, error) { 143 + if r.Filepath == "/" { 144 + return nil, os.ErrInvalid 145 + } 146 + 147 + return h.root.Open(strings.TrimPrefix(r.Filepath, "/")) 148 + } 149 + 150 + type handlererr struct { 151 + Handler *handler 152 + } 153 + 154 + func (f *handlererr) Filecmd(r *sftp.Request) error { 155 + err := f.Handler.Filecmd(r) 156 + if err != nil { 157 + wish.Errorln(f.Handler.session, err) 158 + } 159 + return err 160 + } 161 + func (f *handlererr) Filelist(r *sftp.Request) (sftp.ListerAt, error) { 162 + result, err := f.Handler.Filelist(r) 163 + if err != nil { 164 + wish.Errorln(f.Handler.session, err) 165 + } 166 + return result, err 167 + } 168 + func (f *handlererr) Filewrite(r *sftp.Request) (io.WriterAt, error) { 169 + result, err := f.Handler.Filewrite(r) 170 + if err != nil { 171 + wish.Errorln(f.Handler.session, err) 172 + } 173 + return result, err 174 + } 175 + func (f *handlererr) Fileread(r *sftp.Request) (io.ReaderAt, error) { 176 + result, err := f.Handler.Fileread(r) 177 + if err != nil { 178 + wish.Errorln(f.Handler.session, err) 179 + } 180 + return result, err 181 + } 182 + 183 + type listerat []os.FileInfo 184 + 185 + func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) { 186 + var n int 187 + if offset >= int64(len(f)) { 188 + return 0, io.EOF 189 + } 190 + n = copy(ls, f[offset:]) 191 + if n < len(ls) { 192 + return n, io.EOF 193 + } 194 + return n, nil 195 + }
+73
sftp/wish.go
··· 1 + package sftp 2 + 3 + import ( 4 + "errors" 5 + "io" 6 + "log/slog" 7 + "os" 8 + "path/filepath" 9 + 10 + "github.com/charmbracelet/ssh" 11 + "github.com/charmbracelet/wish" 12 + "github.com/pkg/sftp" 13 + ) 14 + 15 + func SSHOption(rootPath string, logger *slog.Logger) ssh.Option { 16 + return func(server *ssh.Server) error { 17 + if server.SubsystemHandlers == nil { 18 + server.SubsystemHandlers = map[string]ssh.SubsystemHandler{} 19 + } 20 + 21 + server.SubsystemHandlers["sftp"] = SubsystemHandler(rootPath, logger) 22 + return nil 23 + } 24 + } 25 + 26 + func SubsystemHandler(rootPath string, logger *slog.Logger) ssh.SubsystemHandler { 27 + return func(session ssh.Session) { 28 + defer func() { 29 + if r := recover(); r != nil { 30 + if logger != nil { 31 + logger.Error("error running sftp middleware", "err", r) 32 + } 33 + wish.Println(session, "error running sftp middleware, check the flags you are using") 34 + } 35 + }() 36 + 37 + if session.User() != "_" { 38 + rootPath = filepath.Join(rootPath, session.User()) 39 + } 40 + 41 + root, err := os.OpenRoot(rootPath) 42 + if err != nil { 43 + if logger != nil { 44 + logger.Error("Error opening root", "err", err) 45 + } 46 + 47 + wish.Errorln(session, err) 48 + } 49 + 50 + handler := &handlererr{ 51 + Handler: &handler{ 52 + session: session, 53 + root: root, 54 + }, 55 + } 56 + 57 + handlers := sftp.Handlers{ 58 + FilePut: handler, 59 + FileList: handler, 60 + FileGet: handler, 61 + FileCmd: handler, 62 + } 63 + 64 + requestServer := sftp.NewRequestServer(session, handlers) 65 + 66 + if err := requestServer.Serve(); err != nil && !errors.Is(err, io.EOF) { 67 + if logger != nil { 68 + logger.Error("Error serving sftp subsystem", "err", err) 69 + } 70 + wish.Errorln(session, err) 71 + } 72 + } 73 + }