Compare commits

...

22 commits

Author SHA1 Message Date
96405e2521 small fixes and dependency updates 2023-08-25 02:06:53 +02:00
e95910d920 updated dependencies 2023-01-15 16:30:08 +01:00
088bd1f03e added preload estimate quote usage 2022-10-11 23:03:46 +02:00
0c1708e450 fixed / anchor 2022-10-10 18:34:48 +02:00
6122768619 added back anchor and selfheal broken chunks 2022-10-09 01:12:05 +02:00
fd035148bb skip add for zero 2022-10-08 20:39:31 +02:00
470eeb921c added preload transfer rate 2022-10-08 20:35:05 +02:00
0c4376ec32 implemented pledge and unveil on openbsd 2022-10-08 17:48:16 +02:00
3f1752e961 fixed quota delete 2022-10-08 14:03:12 +02:00
88861102fd possible fix for rare panic 2022-10-08 13:45:12 +02:00
f0af1fb04d added channel close 2022-10-08 10:40:28 +02:00
37a6b0ae5f fixed panic and quota 2022-10-08 10:34:16 +02:00
a6b9602821 switch to config file and implement src path filter 2022-10-07 22:57:54 +02:00
055e9db8e2 added file size 2022-10-05 20:35:15 +02:00
00d87ecb5c fixed rare panic 2022-09-16 15:39:38 +02:00
4f043d228d fixed last chunk 2022-08-21 21:47:35 +02:00
70414c4941 fixed preload deadlock 2022-08-21 19:50:33 +02:00
b010b3a93e added preload and streaming buffer pool 2022-08-19 19:01:28 +02:00
cd0b0254c7 fixed preload finish deadlock 2022-08-19 16:00:54 +02:00
caf12d9ca5 fixed preload resumption 2022-08-18 23:10:55 +02:00
304135be2e more async improvements and crypto bugfix 2022-08-17 23:58:46 +02:00
120464c0a5 added experimental async cache write 2022-07-31 12:39:14 +02:00
434 changed files with 52165 additions and 9156 deletions

View file

@ -9,7 +9,7 @@ MD_PROGRAM=metadatagen
MD_CMD=-o $(MD_PROGRAM) cmd/$(MD_PROGRAM)/main.go MD_CMD=-o $(MD_PROGRAM) cmd/$(MD_PROGRAM)/main.go
CTL_PROGRAM=cachectl CTL_PROGRAM=cachectl
CTL_CMD=-o $(CTL_PROGRAM) cmd/$(CTL_PROGRAM)/main.go CTL_CMD=-o $(CTL_PROGRAM) cmd/$(CTL_PROGRAM)/main.go
ENV=CGO_ENABLED=0 GO111MODULE=on ENV=GOGC=0 CGO_ENABLED=0 GO111MODULE=on
all: $(PROGRAM) $(MD_PROGRAM) $(CTL_PROGRAM) all: $(PROGRAM) $(MD_PROGRAM) $(CTL_PROGRAM)
@ -46,6 +46,9 @@ codeqa: govet misspell test
govet: govet:
$(CC) vet ./... $(CC) vet ./...
update:
$(ENV) $(CC) get -u ./...
misspell: misspell:
$(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md $(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md
@ -63,4 +66,4 @@ else
$(info skipping tests of other platforms) $(info skipping tests of other platforms)
endif endif
.PHONY: setup release release-vendor vendor build clean codeqa gofmt govet misspell staticcheck test .PHONY: setup release release-vendor vendor build clean codeqa gofmt govet update misspell staticcheck test

View file

@ -2,11 +2,14 @@
* printf '\x6c' | dd seek=20 bs=1 count=1 conv=notrunc of=test.dump * printf '\x6c' | dd seek=20 bs=1 count=1 conv=notrunc of=test.dump
* crypto * crypto
* flush last chunk in streaming mode * flush last chunk in streaming mode - check if implemented
* web
* listing of all files in cache
* add breadcrumb
* tuning
* debug logger for writer queue size
* sftp * sftp
* implement read ahead buffer? * implement read ahead buffer?
* add breadcrumb
* config file?
* whitelist allowed src paths

View file

@ -111,7 +111,7 @@ func ls(args []string) (err error) {
if err != nil { if err != nil {
return return
} }
fs, err := parse.FS(args[0], k, true) fs, err := parse.FS(args[0], k, nil, true)
if err != nil { if err != nil {
return return
} }
@ -153,12 +153,12 @@ func cp(args []string) (err error) {
if err != nil { if err != nil {
return return
} }
sfs, err := parse.FS(src, sk, true) sfs, err := parse.FS(src, sk, nil, true)
if err != nil { if err != nil {
return return
} }
defer sfs.Close() defer sfs.Close()
dfs, err := parse.FS(dst, dk, true) dfs, err := parse.FS(dst, dk, nil, true)
if err != nil { if err != nil {
return return
} }
@ -248,7 +248,7 @@ func rm(args []string) (err error) {
if err != nil { if err != nil {
return return
} }
fs, err := parse.FS(src, k, true) fs, err := parse.FS(src, k, nil, true)
if err != nil { if err != nil {
return return
} }

View file

@ -7,15 +7,17 @@ import (
"flag" "flag"
"net/http" "net/http"
//_ "net/http/pprof"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"cachefs/pkg/config"
"cachefs/pkg/fs" "cachefs/pkg/fs"
"cachefs/pkg/provider/parse"
"cachefs/pkg/srv" "cachefs/pkg/srv"
"git.giftfish.de/ston1th/godrop/v2"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/klog/v2/klogr" "k8s.io/klog/v2/klogr"
@ -23,71 +25,47 @@ import (
var ( var (
version string version string
cfg string
src string
dst string
metadata string
listenHttp string
listenWebdav string
listenCache string
quota int64
max int
bs int
block bool
log logr.Logger log logr.Logger
) )
const gib = 1024 * 1024 * 1024
func main() { func main() {
klog.InitFlags(nil) klog.InitFlags(nil)
flag.StringVar(&src, "src", "", "url path to source files (example: file:///mnt/nfs)") flag.StringVar(&cfg, "config", "", "path to config file")
flag.StringVar(&dst, "dst", "", "url path to cache files (example: file:///mnt/cache)")
flag.StringVar(&metadata, "data", "", "path to metadata file")
flag.StringVar(&listenHttp, "listen", "127.0.0.1:8080", "listen addr:port")
flag.StringVar(&listenWebdav, "webdav", "", "listen addr:port for webdav")
flag.StringVar(&listenCache, "cache", "", "listen addr:port for cache only")
flag.IntVar(&max, "max", 1, "max parallel preloads")
flag.IntVar(&bs, "bs", -1, "tune preload buffer size in bytes (default: 8192)")
flag.Int64Var(&quota, "quota", 1, "max disk usage quota for the dst cache in GiB")
flag.BoolVar(&block, "block", true, "block until sftp is connected")
flag.Parse() flag.Parse()
//go func() {
// http.ListenAndServe("127.0.0.1:7777", nil)
//}()
log = klogr.New().WithName("main") log = klogr.New().WithName("main")
log.Info("starting cachefs", "version", version) log.Info("starting cachefs", "version", version)
if fs.SetBufferSize(bs) { c, err := config.ParseFile(cfg)
log.V(2).Info("changed preload buffer size", "size", bs) if err != nil {
klog.Fatalf("init failed: %s", err)
} }
filesystem, err := fs.NewFS( err = dropPrivs(c)
quota*gib, if err != nil {
max, klog.Fatalf("init failed: %s", err)
src, }
os.Getenv("SRC_KEY"),
dst, if fs.SetBufferSize(c.Cache.Buffer) {
os.Getenv("DST_KEY"), log.V(2).Info("changed preload buffer size", "size", c.Cache.Buffer)
metadata, }
block,
klogr.New().WithName("fs"), filesystem, err := fs.NewFSFromConfig(c, klogr.New().WithName("fs"))
)
if err != nil { if err != nil {
klog.Fatalf("init failed: %s", err) klog.Fatalf("init failed: %s", err)
} }
slog := klogr.New().WithName("srv") slog := klogr.New().WithName("srv")
s := &http.Server{ s := &http.Server{
Addr: listenHttp, Addr: c.Server.HTTP.Addr,
Handler: srv.NewFileServer( Handler: srv.NewFileServer(
filesystem, filesystem,
slog, slog,
), ),
} }
log.Info("starting main server", "addr", c.Server.HTTP.Addr)
go func() { go func() {
log.Info("starting main server", "addr", listenHttp)
err := s.ListenAndServe() err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
klog.Fatalf("init failed: %s", err) klog.Fatalf("init failed: %s", err)
@ -96,16 +74,18 @@ func main() {
var ( var (
dav *http.Server dav *http.Server
cache *http.Server cache *http.Server
plain *http.Server
) )
if listenWebdav != "" { davAddr := c.Server.WebDav.Addr
log.Info("starting webdav server", "addr", listenWebdav) if davAddr != "" {
dav = &http.Server{ dav = &http.Server{
Addr: listenWebdav, Addr: davAddr,
Handler: srv.NewWebDavServer( Handler: srv.NewWebDavServer(
fs.NewWebDavFS(filesystem), fs.NewWebDavFS(filesystem),
slog.WithName("webdav"), slog.WithName("webdav"),
), ),
} }
log.Info("starting webdav server", "addr", davAddr)
go func() { go func() {
err := dav.ListenAndServe() err := dav.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
@ -113,15 +93,16 @@ func main() {
} }
}() }()
} }
if listenCache != "" { cacheAddr := c.Server.Cache.Addr
log.Info("starting cache server", "addr", listenCache) if cacheAddr != "" {
cache = &http.Server{ cache = &http.Server{
Addr: listenCache, Addr: cacheAddr,
Handler: srv.NewCacheServer( Handler: srv.NewCacheServer(
filesystem, filesystem,
slog.WithName("cache"), slog.WithName("cache"),
), ),
} }
log.Info("starting cache server", "addr", cacheAddr)
go func() { go func() {
err := cache.ListenAndServe() err := cache.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
@ -129,6 +110,23 @@ func main() {
} }
}() }()
} }
plainAddr := c.Server.Plain.Addr
if plainAddr != "" {
plain = &http.Server{
Addr: plainAddr,
Handler: srv.NewPlainServer(
filesystem,
slog.WithName("plain"),
),
}
log.Info("starting plain server", "addr", plainAddr)
go func() {
err := plain.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
klog.Fatalf("init failed: %s", err)
}
}()
}
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs <-sigs
@ -146,7 +144,42 @@ func main() {
cache.Shutdown(ctx) cache.Shutdown(ctx)
cancel() cancel()
} }
if plain != nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
plain.Shutdown(ctx)
cancel()
}
filesystem.Close() filesystem.Close()
<-filesystem.Done()
log.Info("cachefs shutdown completed") log.Info("cachefs shutdown completed")
os.Exit(0) os.Exit(0)
} }
func dropPrivs(cfg *config.Config) (err error) {
// flock fattr
err = godrop.PledgePromises("stdio rpath wpath cpath inet dns unveil")
if err != nil {
return
}
err = godrop.Unveil(cfg.Cache.Metadata, "rwc")
if err != nil {
return
}
for _, p := range parse.Paths(cfg.Cache.Src.Path) {
err = godrop.Unveil(p, "r")
if err != nil {
return
}
}
for i, p := range parse.Paths(cfg.Cache.Dst.Path) {
if i == 0 {
err = godrop.Unveil(p, "rwc")
} else {
err = godrop.Unveil(p, "r")
}
if err != nil {
return
}
}
return godrop.UnveilBlock()
}

View file

@ -44,7 +44,7 @@ func main() {
} }
encrypted = true encrypted = true
} }
dstfs, err := parse.FS(dst, dstk, true) dstfs, err := parse.FS(dst, dstk, nil, true)
if err != nil { if err != nil {
fatal(err) fatal(err)
} }

40
config.yaml Normal file
View file

@ -0,0 +1,40 @@
server:
# listen addr:port for default http interface
http:
addr: "127.0.0.1:8080"
# listen addr:port for webdav
#webdav:
# addr: ""
# listen addr:port for cache only
#cache:
# addr: ""
# listen addr:port for plain directory listings over http
#plain:
# addr: ""
# regex whitelist for allowed filesystem paths
#path: ""
cache:
# path to metadata file
metadata: "/path/to/metadata.json"
src:
# url path to source files
path: "file:///mnt/nfs"
#path: "sftp://user@src-host:/home/user"
# optional hex encoded encryption key (32 bytes)
#key: ""
dst:
# url path to cache files
path: "file:///mnt/cache"
#path: "sftp://user@dst-host:/home/user"
# optional hex encoded encryption key (32 bytes)
#key: ""
# max parallel preloads
#preloads: 1
# preload buffer size in bytes
#buffer: 8192
# max disk usage quota for the dst cache in GiB
#quota: 1
# block until sftp is connected
#blockSFTP: true

17
go.mod
View file

@ -1,15 +1,16 @@
module cachefs module cachefs
go 1.18 go 1.21
require ( require (
github.com/go-logr/logr v1.2.2 git.giftfish.de/ston1th/godrop/v2 v2.1.1
github.com/pkg/sftp v1.13.4 github.com/go-logr/logr v1.2.4
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 github.com/pkg/sftp v1.13.6
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7 golang.org/x/crypto v0.12.0
golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/net v0.14.0
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/sys v0.11.0
k8s.io/klog/v2 v2.40.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/klog/v2 v2.100.1
) )
require github.com/kr/fs v0.1.0 // indirect require github.com/kr/fs v0.1.0 // indirect

69
go.sum
View file

@ -1,35 +1,62 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= git.giftfish.de/ston1th/godrop/v2 v2.1.1 h1:jCzQPFpRImr2N4kz+4lC7wGZJAWGx9pH5X09nUsRQDU=
git.giftfish.de/ston1th/godrop/v2 v2.1.1/go.mod h1:DWC4iM+u/bpdJsauL00h0V9tZ97d6Bzi2RGaFKlvZoQ=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7 h1:jynE66seADJbyWMUdeOyVTvPtBZt7L6LJHupGwxPZRM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20220314205449-43aec2f8a4e7/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
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/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-20210423185535-09eb48e85fd7/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-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.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.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
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.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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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=
k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=

View file

@ -3,14 +3,15 @@
package chunk package chunk
import ( import (
"golang.org/x/exp/slices" "cmp"
"slices"
) )
type Chunk [2]int64 type Chunk [2]int64
type Chunks []Chunk type Chunks []Chunk
func (Chunks) Less(i, j Chunk) bool { return i[0] < j[0] } func (Chunks) Comp(a, b Chunk) int { return cmp.Compare(a[0], b[0]) }
func (cs Chunks) Exists(off int64, n int, size int64) bool { func (cs Chunks) Exists(off int64, n int, size int64) bool {
end := off + int64(n) end := off + int64(n)
@ -42,7 +43,7 @@ func (cs *Chunks) merge() {
if len(c) < 2 { if len(c) < 2 {
return return
} }
slices.SortFunc(c, c.Less) slices.SortFunc(c, c.Comp)
for i := 0; i < len(c); i++ { for i := 0; i < len(c); i++ {
if i+1 == len(c) { if i+1 == len(c) {
break break

106
pkg/config/config.go Normal file
View file

@ -0,0 +1,106 @@
// Copyright (C) 2022 Marius Schellenberger
package config
import (
"errors"
"os"
"regexp"
"gopkg.in/yaml.v3"
)
func ParseFile(file string) (cfg *Config, err error) {
if file == "" {
return nil, errors.New("missing config file")
}
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
cfg = new(Config)
err = yaml.NewDecoder(f).Decode(cfg)
if err != nil {
return
}
err = Validate(cfg)
return
}
type Config struct {
Server Server `yaml:"server"`
Cache Cache `yaml:"cache"`
Path string `yaml:"path"`
REPath *regexp.Regexp `yaml:"-"`
}
type Server struct {
HTTP Listen `yaml:"http"`
WebDav Listen `yaml:"webdav"`
Cache Listen `yaml:"cache"`
Plain Listen `yaml:"plain"`
}
type Listen struct {
Addr string `yaml:"addr"`
}
type Cache struct {
Metadata string `yaml:"metadata"`
Src Path `yaml:"src"`
Dst Path `yaml:"dst"`
Preloads int `yaml:"preloads"`
Buffer int `yaml:"buffer"`
Quota int64 `yaml:"quota"`
BlockSFTP *bool `yaml:"blockSFTP"`
}
type Path struct {
Path string `yaml:"path"`
Key string `yaml:"key"`
}
const (
defListenHTTP = "127.0.0.1:8080"
defPreloads = 1
defBuffer = 8192
defQuota = 1
defBlockSFTP = true
gib = 1024 * 1024 * 1024
)
func Validate(cfg *Config) error {
if cfg.Server.HTTP.Addr == "" {
cfg.Server.HTTP.Addr = defListenHTTP
}
c := &cfg.Cache
if c.Metadata == "" {
return errors.New("cache.metadata is empty")
}
if c.Src.Path == "" {
return errors.New("cache.src is empty")
}
if c.Dst.Path == "" {
return errors.New("cache.dst is empty")
}
if c.Preloads <= 0 {
c.Preloads = defPreloads
}
if c.Buffer < 1024 {
c.Buffer = defBuffer
}
if c.Quota < 1 {
c.Quota = defQuota
}
c.Quota *= gib
if c.BlockSFTP == nil {
c.BlockSFTP = new(bool)
*c.BlockSFTP = defBlockSFTP
}
var err error
if cfg.Path != "" {
cfg.REPath, err = regexp.Compile(cfg.Path)
}
return err
}

View file

@ -4,7 +4,6 @@ package fs
import ( import (
"io" "io"
"sync"
) )
var Discard io.Writer = discard{} var Discard io.Writer = discard{}
@ -21,30 +20,33 @@ func (discard) WriteString(s string) (int, error) {
return len(s), nil return len(s), nil
} }
const DefaultBufferSize = 8192 const (
StreamingBufferSize = 32768
DefaultBufferSize = 8192
)
var blackHolePool = sync.Pool{ func poolNewFunc(size int) func() *[]byte {
New: func() any { return func() *[]byte {
b := make([]byte, DefaultBufferSize) b := make([]byte, size)
return &b return &b
}, }
} }
var (
streamingPool = newPool(poolNewFunc(StreamingBufferSize))
blackHolePool = newPool(poolNewFunc(DefaultBufferSize))
)
func SetBufferSize(size int) bool { func SetBufferSize(size int) bool {
if size < 0 || size == DefaultBufferSize { if size < 0 || size == DefaultBufferSize {
return false return false
} }
blackHolePool = sync.Pool{ blackHolePool = newPool(poolNewFunc(size))
New: func() any {
b := make([]byte, size)
return &b
},
}
return true return true
} }
func (discard) ReadFrom(r io.Reader) (n int64, err error) { func (discard) ReadFrom(r io.Reader) (n int64, err error) {
bufp := blackHolePool.Get().(*[]byte) bufp := blackHolePool.Get()
readSize := 0 readSize := 0
for { for {
readSize, err = r.Read(*bufp) readSize, err = r.Read(*bufp)

View file

@ -4,6 +4,7 @@ package fs
import ( import (
"cachefs/pkg/provider" "cachefs/pkg/provider"
"cachefs/pkg/provider/crypto"
"context" "context"
"errors" "errors"
"io" "io"
@ -41,6 +42,7 @@ func (p *preload) Read(data []byte) (n int, err error) {
if p.f.hasChunk(n) { if p.f.hasChunk(n) {
p.skipped++ p.skipped++
_, err = p.f.Seek(int64(n), io.SeekCurrent) _, err = p.f.Seek(int64(n), io.SeekCurrent)
n = 0
return return
} }
n, err = p.f.readToCache(data) n, err = p.f.readToCache(data)
@ -48,37 +50,45 @@ func (p *preload) Read(data []byte) (n int, err error) {
return return
} }
func (f *File) Preload(ctx context.Context, unlock func()) { func (f *File) Preload(ctx context.Context, r *Rate, fin, errf, stop func()) {
log := f.log log := f.log
defer r.Stop()
defer f.Close() defer f.Close()
if f.offline { if f.offline {
log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file") log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file")
unlock() errf()
return return
} }
if f.md.FullyCached() { if f.md.FullyCached() {
log.V(2).Info("skipped preload for fully cached file") log.V(2).Info("skipped preload for fully cached file")
unlock() fin()
return return
} }
log.V(2).Info("preload started") log.V(2).Info("preload started")
p := &preload{f: f, ctx: ctx} p := &preload{f: f, ctx: ctx}
_, err := io.Copy(Discard, p) r.SetReader(p)
_, err := io.Copy(Discard, r)
if err == context.Canceled { if err == context.Canceled {
log.V(2).Info("preload canceled", "skipped", p.skipped, "written", p.written) log.V(2).Info("preload canceled", "skipped", p.skipped, "written", p.written)
// do not call unlock() to keep preload in list stop()
return return
} }
perr := false
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
log.Error(err, "error preloading file") log.Error(err, "error preloading file")
perr = true
} }
err = f.md.Close() err = f.md.Close()
if err != nil { if err != nil {
log.Error(err, "error closing cache file") log.Error(err, "error closing cache file")
} }
if perr {
errf()
return
}
log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written) log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written)
unlock() fin()
} }
func (f *File) Read(p []byte) (n int, err error) { func (f *File) Read(p []byte) (n int, err error) {
@ -88,6 +98,10 @@ func (f *File) Read(p []byte) (n int, err error) {
if err == io.EOF { if err == io.EOF {
return return
} }
if err == crypto.ErrDecrypt {
log.V(2).Info("rereading to cache file", "err", err)
return f.readToCache(p)
}
if err != nil { if err != nil {
if !IsIOErr(err) { if !IsIOErr(err) {
log.Error(err, "error reading cache file") log.Error(err, "error reading cache file")
@ -95,7 +109,7 @@ func (f *File) Read(p []byte) (n int, err error) {
} }
n, err = f.readSource(p) n, err = f.readSource(p)
if err != nil { if err != nil {
log.Error(err, "error reading source file") log.Error(err, "error reading fallback source file")
return return
} }
} }
@ -127,7 +141,8 @@ func (f *File) readToCache(p []byte) (n int, err error) {
log := f.log log := f.log
n, err = f.readSource(p) n, err = f.readSource(p)
if n > 0 { if n > 0 {
if n, err = f.md.WriteAt(p[:n], f.offset); err != nil { // async cache write
if _, err = f.md.WriteAt(p[:n], f.offset); err != nil {
log.Error(err, "error writing cache file") log.Error(err, "error writing cache file")
return return
} }
@ -136,7 +151,6 @@ func (f *File) readToCache(p []byte) (n int, err error) {
log.Error(err, "error reading source file") log.Error(err, "error reading source file")
return return
} }
f.md.AddChunk(f.offset, n)
f.offset += int64(n) f.offset += int64(n)
if n > 0 && n < len(p) { if n > 0 && n < len(p) {
_, err = f.Seek(f.offset, io.SeekStart) _, err = f.Seek(f.offset, io.SeekStart)

View file

@ -3,6 +3,7 @@
package fs package fs
import ( import (
"cachefs/pkg/config"
"cachefs/pkg/provider" "cachefs/pkg/provider"
"cachefs/pkg/provider/parse" "cachefs/pkg/provider/parse"
"context" "context"
@ -12,6 +13,8 @@ import (
stdfs "io/fs" stdfs "io/fs"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv"
"strings" "strings"
"github.com/go-logr/logr" "github.com/go-logr/logr"
@ -19,8 +22,8 @@ import (
type FS struct { type FS struct {
log logr.Logger log logr.Logger
done chan struct{}
cancel func() cancel func()
pCancel func()
NoCache http.Handler NoCache http.Handler
src provider.FS src provider.FS
dst provider.FS dst provider.FS
@ -32,7 +35,22 @@ type FS struct {
ph *PreloadHandler ph *PreloadHandler
} }
func NewFS(quota int64, max int, src, srckey, dst, dstkey, metadata string, block bool, log logr.Logger) (fs *FS, err error) { func NewFSFromConfig(cfg *config.Config, log logr.Logger) (fs *FS, err error) {
c := cfg.Cache
return NewFS(
c.Quota,
c.Preloads,
c.Src.Path,
c.Src.Key,
c.Dst.Path,
c.Dst.Key,
c.Metadata,
cfg.REPath,
*c.BlockSFTP,
log,
)
}
func NewFS(quota int64, max int, src, srckey, dst, dstkey, metadata string, re *regexp.Regexp, block bool, log logr.Logger) (fs *FS, err error) {
if src == dst { if src == dst {
return nil, errors.New("src and dst path can not be equal") return nil, errors.New("src and dst path can not be equal")
} }
@ -51,21 +69,22 @@ func NewFS(quota int64, max int, src, srckey, dst, dstkey, metadata string, bloc
} }
log.V(2).Info("using destination encryption") log.V(2).Info("using destination encryption")
} }
srcfs, err := parse.FS(src, srck, block) srcfs, err := parse.FS(src, srck, re, block)
if err != nil { if err != nil {
return nil, fmt.Errorf("srcfs: %w", err) return nil, fmt.Errorf("srcfs: %w", err)
} }
dstfs, err := parse.FS(dst, dstk, block) dstfs, err := parse.FS(dst, dstk, nil, block)
if err != nil { if err != nil {
return nil, fmt.Errorf("dstfs: %w", err) return nil, fmt.Errorf("dstfs: %w", err)
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
pCtx, pCancel := context.WithCancel(context.Background())
fs = &FS{ fs = &FS{
log: log, log: log,
done: make(chan struct{}),
cancel: cancel, cancel: cancel,
pCancel: pCancel,
NoCache: http.FileServer(http.Dir(src)), NoCache: http.FileServer(http.Dir(src)),
src: srcfs, src: srcfs,
dst: dstfs, dst: dstfs,
@ -80,7 +99,7 @@ func NewFS(quota int64, max int, src, srckey, dst, dstkey, metadata string, bloc
if err != nil { if err != nil {
return nil, fmt.Errorf("quota: %w", err) return nil, fmt.Errorf("quota: %w", err)
} }
fs.ph = NewPreloadHandler(ctx, fs, max, log.WithName("preload")) fs.ph = NewPreloadHandler(pCtx, fs, max, log.WithName("preload"))
fs.sc = NewStatCache(ctx) fs.sc = NewStatCache(ctx)
return return
} }
@ -99,6 +118,36 @@ func (fs *FS) Stat(name string) (fi stdfs.FileInfo, err error) {
return return
} }
const (
_ = 1 << (iota * 10)
KiB
MiB
GiB
TiB
)
func FileSize(s int64) string {
switch {
case s >= TiB:
return strconv.FormatFloat(float64(s)/TiB, 'f', 2, 64) + " TiB"
case s >= GiB:
return strconv.FormatFloat(float64(s)/GiB, 'f', 2, 64) + " GiB"
case s >= MiB:
return strconv.FormatFloat(float64(s)/MiB, 'f', 2, 64) + " MiB"
case s >= KiB:
return strconv.FormatFloat(float64(s)/KiB, 'f', 2, 64) + " KiB"
}
return strconv.Itoa(int(s)) + " B"
}
func (fs *FS) FileSize(name string) string {
fi, err := fs.Stat(name)
if err != nil {
return ""
}
return FileSize(fi.Size())
}
func (fs *FS) RemoveDst(name string) error { func (fs *FS) RemoveDst(name string) error {
return fs.dst.Remove(name) return fs.dst.Remove(name)
} }
@ -265,8 +314,14 @@ func (fs *FS) CacheStatus(name string) int {
} }
func (fs *FS) Close() { func (fs *FS) Close() {
fs.pCancel()
<-fs.ph.Done()
fs.cancel() fs.cancel()
fs.src.Close() fs.src.Close()
fs.dst.Close() fs.dst.Close()
<-fs.done }
func (fs *FS) Done() <-chan struct{} {
return fs.mh.Done()
} }

View file

@ -3,15 +3,13 @@
package fs package fs
import ( import (
"cachefs/pkg/chunk"
"cachefs/pkg/provider"
"cachefs/pkg/provider/sftp"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"io/fs" "io/fs"
stdfs "io/fs" stdfs "io/fs"
"maps"
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
@ -20,8 +18,11 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"cachefs/pkg/chunk"
"cachefs/pkg/provider"
"cachefs/pkg/provider/sftp"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/exp/maps"
) )
func now() int64 { func now() int64 {
@ -29,13 +30,14 @@ func now() int64 {
} }
type MetadataHandler struct { type MetadataHandler struct {
mu sync.RWMutex mu sync.RWMutex
log logr.Logger done chan struct{}
md map[string]*Metadata log logr.Logger
fs *FS md map[string]*Metadata
dst string fs *FS
f *os.File dst string
enc *json.Encoder f *os.File
enc *json.Encoder
} }
func MetadataGenerator(file string, dst provider.FS, fstat, encrypted bool) error { func MetadataGenerator(file string, dst provider.FS, fstat, encrypted bool) error {
@ -109,10 +111,11 @@ func NewMetadataHandler(ctx context.Context, fs *FS, file, dst string, log logr.
return nil, errors.New("metadata path is not absolute") return nil, errors.New("metadata path is not absolute")
} }
mh = &MetadataHandler{ mh = &MetadataHandler{
log: log, log: log,
md: make(map[string]*Metadata), done: make(chan struct{}),
fs: fs, md: make(map[string]*Metadata),
dst: dst, fs: fs,
dst: dst,
} }
mh.f, err = os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o640) mh.f, err = os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o640)
if err != nil { if err != nil {
@ -129,13 +132,13 @@ func NewMetadataHandler(ctx context.Context, fs *FS, file, dst string, log logr.
return return
} }
func (mh *MetadataHandler) DeleteOldest() (s int64) { func (mh *MetadataHandler) DeleteOldest() (free int64) {
mh.mu.Lock() mh.mu.Lock()
defer mh.mu.Unlock() defer mh.mu.Unlock()
atime := int64(math.MaxInt64) atime := int64(math.MaxInt64)
var m *Metadata var m *Metadata
for _, v := range mh.md { for _, v := range mh.md {
if v.Atime != 0 && v.f != nil && v.Atime < atime { if v.Atime != 0 && v.Atime < atime {
atime = v.Atime atime = v.Atime
m = v m = v
} }
@ -143,16 +146,16 @@ func (mh *MetadataHandler) DeleteOldest() (s int64) {
if m != nil { if m != nil {
log := mh.log.WithValues("file", m.name) log := mh.log.WithValues("file", m.name)
log.Info("deleting oldest file") log.Info("deleting oldest file")
cs := m.Size s := m.Size
if !m.FullyCached() { if !m.FullyCached() {
cs = m.ChunkSize() s = m.ChunkSize()
} }
err := m.Delete() err := m.Delete()
if err != nil { if err != nil {
log.Error(err, "error deleting oldest file") log.Error(err, "error deleting oldest file")
return return
} }
s = cs free = s
} }
return return
} }
@ -197,7 +200,14 @@ func (mh *MetadataHandler) Metadata(name string, size int64) (md *Metadata) {
} }
return md return md
} }
md = &Metadata{Size: size, fs: mh.fs, name: name} ctx, cancel := context.WithCancel(context.Background())
md = &Metadata{
Size: size,
fs: mh.fs,
name: name,
ctx: ctx,
cancel: cancel,
}
mh.md[name] = md mh.md[name] = md
return return
} }
@ -219,6 +229,7 @@ func (mh *MetadataHandler) init() {
} }
return true return true
} }
v.ctx, v.cancel = context.WithCancel(context.Background())
v.fs = mh.fs v.fs = mh.fs
v.name = k v.name = k
return false return false
@ -226,12 +237,15 @@ func (mh *MetadataHandler) init() {
} }
func (mh *MetadataHandler) flusher(ctx context.Context) { func (mh *MetadataHandler) flusher(ctx context.Context) {
t := time.NewTicker(time.Minute * 5)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
t.Stop()
mh.close() mh.close()
close(mh.done)
return return
case <-time.After(time.Minute * 5): case <-t.C:
} }
mh.cleanupEmptyDirs() mh.cleanupEmptyDirs()
mh.flush() mh.flush()
@ -301,15 +315,20 @@ func (mh *MetadataHandler) flush() {
func (mh *MetadataHandler) close() { func (mh *MetadataHandler) close() {
mh.mu.Lock() mh.mu.Lock()
for _, md := range mh.md { for _, md := range mh.md {
if md.f != nil { md.Close()
md.f.Sync() //if md.f != nil {
md.f.Close() // close(md.wc)
} // md.f.Sync()
// md.f.Close()
//}
} }
mh.mu.Unlock() mh.mu.Unlock()
mh.flush() mh.flush()
mh.f.Close() mh.f.Close()
close(mh.fs.done) }
func (mh *MetadataHandler) Done() <-chan struct{} {
return mh.done
} }
type Error string type Error string
@ -338,54 +357,74 @@ func IsIOErr(err error) bool {
} }
type Metadata struct { type Metadata struct {
mu sync.RWMutex `json:"-"` mu sync.Mutex `json:"-"`
fs *FS `json:"-"` cmu sync.RWMutex `json:"-"`
f provider.File `json:"-"` fs *FS `json:"-"`
name string `json:"-"` f provider.File `json:"-"`
Size int64 `json:"s"` wc chan writeAt `json:"-"`
Atime int64 `json:"a"` done chan struct{} `json:"-"`
Chunks chunk.Chunks `json:"c"` ctx context.Context `json:"-"`
cancel func() `json:"-"`
err atomic.Pointer[mdErr] `json:"-"`
name string `json:"-"`
Size int64 `json:"s"`
Atime int64 `json:"a"`
Chunks chunk.Chunks `json:"c"`
} }
func (md *Metadata) Close() error { func (md *Metadata) Close() (err error) {
md.mu.Lock() md.mu.Lock()
defer md.mu.Unlock() defer md.mu.Unlock()
err := md.f.Close() return md.close()
md.f = nil }
return err
func (md *Metadata) close() (err error) {
md.cancel()
if md.wc != nil {
close(md.wc)
<-md.done
md.wc = nil
}
if md.f != nil {
md.f.Sync()
err = md.f.Close()
md.f = nil
}
return
} }
func (md *Metadata) Delete() error { func (md *Metadata) Delete() error {
md.mu.Lock() md.mu.Lock()
defer md.mu.Unlock() defer md.mu.Unlock()
md.cmu.Lock()
md.Chunks = chunk.Chunks{} md.Chunks = chunk.Chunks{}
md.f.Close() md.cmu.Unlock()
md.f = nil md.close()
atomic.StoreInt64(&md.Atime, now()) atomic.StoreInt64(&md.Atime, now())
return md.fs.RemoveDst(md.name) return md.fs.RemoveDst(md.name)
} }
func (md *Metadata) FullyCached() bool { func (md *Metadata) FullyCached() bool {
md.mu.RLock() md.cmu.RLock()
defer md.mu.RUnlock() defer md.cmu.RUnlock()
return len(md.Chunks) == 1 && md.Size == md.Chunks[0][1] return len(md.Chunks) == 1 && md.Size == md.Chunks[0][1]
} }
func (md *Metadata) HasChunk(off int64, n int) bool { func (md *Metadata) HasChunk(off int64, n int) bool {
md.mu.RLock() md.cmu.RLock()
defer md.mu.RUnlock() defer md.cmu.RUnlock()
return md.Chunks.Exists(off, n, md.Size) return md.Chunks.Exists(off, n, md.Size)
} }
func (md *Metadata) ChunkSize() int64 { func (md *Metadata) ChunkSize() int64 {
md.mu.RLock() md.cmu.RLock()
defer md.mu.RUnlock() defer md.cmu.RUnlock()
return md.Chunks.Size() return md.Chunks.Size()
} }
func (md *Metadata) AddChunk(off int64, n int) { func (md *Metadata) addChunk(off int64, n int) {
md.mu.Lock() md.cmu.Lock()
defer md.mu.Unlock() defer md.cmu.Unlock()
md.Chunks.Add(off, n) md.Chunks.Add(off, n)
} }
@ -400,27 +439,73 @@ func (md *Metadata) AddChunk(off int64, n int) {
// return md.f.ReadAt(p, pos) // return md.f.ReadAt(p, pos)
//} //}
type mdErr struct {
err error
}
type writeAt struct {
pos int64
data []byte
ret *[]byte
}
func (md *Metadata) resetChans() {
md.wc = make(chan writeAt, 10)
md.done = make(chan struct{})
}
func (md *Metadata) writer() {
md.resetChans()
md.err.Store(nil)
go func() {
for wa := range md.wc {
atomic.StoreInt64(&md.Atime, now())
n, err := md.f.WriteAt(wa.data, wa.pos)
if err == nil {
md.addChunk(wa.pos, n)
md.fs.q.Add(n)
} else {
md.err.Store(&mdErr{err})
}
if wa.ret != nil {
streamingPool.Put(wa.ret)
}
}
close(md.done)
}()
}
func (md *Metadata) WriteAt(data []byte, pos int64) (int, error) { func (md *Metadata) WriteAt(data []byte, pos int64) (int, error) {
err := md.err.Load()
if err != nil && err.err != nil {
return 0, err.err
}
if md.f == nil { if md.f == nil {
err := md.openCacheFile() err := md.openCacheFile()
if err != nil { if err != nil {
return 0, err return 0, err
} }
} }
atomic.StoreInt64(&md.Atime, now()) if md.ctx.Err() == context.Canceled {
md.fs.q.Add(len(data)) return 0, context.Canceled
return md.f.WriteAt(data, pos)
}
func (md *Metadata) openCacheFile() error {
md.mu.Lock()
defer md.mu.Unlock()
if md.f != nil {
return nil
} }
f, err := md.fs.openCacheFile(md.name, md.Size) wa := writeAt{pos: pos}
md.f = f ret := streamingPool.Get()
return err n := len(data)
buf := *ret
if n > len(buf) {
streamingPool.Put(ret)
buf = make([]byte, n)
} else {
wa.ret = ret
}
copy(buf, data)
wa.data = buf[:n]
select {
case <-md.ctx.Done():
case md.wc <- wa:
}
return n, nil
} }
func (md *Metadata) Stat() (os.FileInfo, error) { func (md *Metadata) Stat() (os.FileInfo, error) {
@ -432,3 +517,22 @@ func (md *Metadata) Stat() (os.FileInfo, error) {
} }
return md.f.Stat() return md.f.Stat()
} }
func (md *Metadata) openCacheFile() error {
md.mu.Lock()
defer md.mu.Unlock()
if md.f != nil {
return nil
}
f, err := md.fs.openCacheFile(md.name, md.Size)
if err != nil {
return err
}
md.f = f
if md.ctx.Err() != nil {
md.cancel()
md.ctx, md.cancel = context.WithCancel(context.Background())
}
md.writer()
return nil
}

27
pkg/fs/pool.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright (C) 2022 Marius Schellenberger
package fs
import (
"sync"
)
type pool[T any] struct {
p sync.Pool
}
func newPool[T any](newf func() T) *pool[T] {
return &pool[T]{
p: sync.Pool{
New: func() any { return newf() },
},
}
}
func (p *pool[T]) Get() T {
return p.p.Get().(T)
}
func (p *pool[T]) Put(v T) {
p.p.Put(v)
}

View file

@ -3,19 +3,24 @@
package fs package fs
import ( import (
"cmp"
"context" "context"
"io"
"slices"
"strconv"
"sync" "sync"
"sync/atomic"
"time"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/exp/slices"
) )
type queue []*Preload type queue []*Preload
func (queue) LessReverse(i, j *Preload) bool { return i.Prio > j.Prio } func (queue) CompReverse(a, b *Preload) int { return cmp.Compare(b.Prio, a.Prio) }
func (q *queue) Sort() { func (q *queue) Sort() {
slices.SortFunc(*q, q.LessReverse) slices.SortFunc(*q, q.CompReverse)
} }
func (q *queue) Add(ph *PreloadHandler, name string, prio int) { func (q *queue) Add(ph *PreloadHandler, name string, prio int) {
@ -29,14 +34,23 @@ func (q *queue) Add(ph *PreloadHandler, name string, prio int) {
return return
} }
} }
*q = append(*q, &Preload{Name: name, ph: ph}) p := &Preload{Name: name, ph: ph}
*q = append(*q, p)
file, err := ph.fs.Open(name)
if err == nil {
defer file.Close()
f, ok := file.(*File)
if ok {
p.Size = f.size()
}
}
q.Sort() q.Sort()
} }
func (q *queue) Remove(name string) { func (q *queue) Remove(name string, collect bool) {
for i, p := range *q { for i, p := range *q {
if p.Name == name { if p.Name == name {
p.stop() p.stop(collect)
*q = slices.Delete(*q, i, i+1) *q = slices.Delete(*q, i, i+1)
q.Sort() q.Sort()
return return
@ -44,24 +58,115 @@ func (q *queue) Remove(name string) {
} }
} }
func (q *queue) GetPreload(name string) *Preload {
for _, p := range *q {
if p.Name == name {
return p
}
}
return nil
}
type Rate struct {
r io.Reader
done chan struct{}
rate atomic.Pointer[string]
num int64
last int64
interval int
}
var zeroRate = fmtRate(0)
func newRate() *Rate {
r := &Rate{
done: make(chan struct{}),
interval: 10,
}
r.rate.Store(&zeroRate)
return r
}
func (r *Rate) SetReader(reader io.Reader) {
r.r = reader
go r.sampler()
}
func (r *Rate) Read(b []byte) (n int, err error) {
n, err = r.r.Read(b)
if n > 0 {
atomic.AddInt64(&r.num, int64(n))
}
return
}
func fmtRate(r int) string {
switch {
case r >= GiB:
return strconv.FormatFloat(float64(r)/GiB, 'f', 2, 64) + " GiB/s"
case r >= MiB:
return strconv.FormatFloat(float64(r)/MiB, 'f', 2, 64) + " MiB/s"
case r >= KiB:
return strconv.FormatFloat(float64(r)/KiB, 'f', 2, 64) + " KiB/s"
}
return strconv.Itoa(r) + " B/s"
}
func (r *Rate) sampler() {
t := time.NewTicker(time.Duration(r.interval) * time.Second)
for {
select {
case <-r.done:
t.Stop()
return
case <-t.C:
num := atomic.LoadInt64(&r.num)
rate := int(num-r.last) / r.interval
r.last = num
s := fmtRate(rate)
r.rate.Store(&s)
}
}
}
func (r *Rate) String() string {
s := r.rate.Load()
if s == nil {
return ""
}
return *s
}
func (r *Rate) Stop() {
close(r.done)
r.rate.Store(&zeroRate)
}
type Preload struct { type Preload struct {
ph *PreloadHandler ph *PreloadHandler
rate *Rate
delay time.Duration
Name string Name string
cancel func() cancel func()
Prio int Prio int
Status int Status int
Errc int
Size int64
Running bool Running bool
} }
type Preloads []Preload type Preloads []Preload
type PreloadHandler struct { type PreloadHandler struct {
mu sync.RWMutex mu sync.RWMutex
log logr.Logger log logr.Logger
fs *FS fs *FS
q queue q queue
fin chan string fin chan string
max int err chan string
stop chan struct{}
done chan struct{}
max int
} }
func NewPreloadHandler(ctx context.Context, fs *FS, max int, log logr.Logger) (ph *PreloadHandler) { func NewPreloadHandler(ctx context.Context, fs *FS, max int, log logr.Logger) (ph *PreloadHandler) {
@ -69,20 +174,23 @@ func NewPreloadHandler(ctx context.Context, fs *FS, max int, log logr.Logger) (p
max = 1 max = 1
} }
ph = &PreloadHandler{ ph = &PreloadHandler{
log: log, log: log,
fs: fs, fs: fs,
q: make(queue, 0), q: make(queue, 0),
fin: make(chan string, 1), fin: make(chan string, 1),
max: max, err: make(chan string, 1),
stop: make(chan struct{}, 1),
done: make(chan struct{}),
max: max,
} }
go ph.preloadFinish(ctx) go ph.preloadStatus(ctx)
return return
} }
func (ph *PreloadHandler) RemovePreload(name string) { func (ph *PreloadHandler) RemovePreload(name string) {
ph.mu.RLock() ph.mu.RLock()
defer ph.mu.RUnlock() defer ph.mu.RUnlock()
ph.q.Remove(name) ph.q.Remove(name, true)
ph.schedule() ph.schedule()
} }
@ -107,21 +215,32 @@ func (ph *PreloadHandler) Preload(name string, prio int) {
func (ph *PreloadHandler) schedule() { func (ph *PreloadHandler) schedule() {
for i := 0; i < len(ph.q); i++ { for i := 0; i < len(ph.q); i++ {
if i < ph.max { if i < ph.max {
ph.q[i].start() go ph.q[i].start()
} else { } else {
ph.q[i].stop() ph.q[i].stop(true)
} }
} }
} }
func (p *Preload) Rate() string {
if p.rate != nil {
return p.rate.String()
}
return ""
}
func (p *Preload) start() { func (p *Preload) start() {
if p.Running { if p.Running {
return return
} }
if p.delay != 0 {
time.Sleep(p.delay)
}
name := p.Name name := p.Name
file, err := p.ph.fs.Open(name) file, err := p.ph.fs.Open(name)
if err != nil { if err != nil {
p.ph.log.Error(err, "error staring next preload", "file", name) p.ph.log.Error(err, "error staring next preload", "file", name)
p.ph.err <- name
return return
} }
f, ok := file.(*File) f, ok := file.(*File)
@ -131,19 +250,33 @@ func (p *Preload) start() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
p.Running = true p.Running = true
p.cancel = cancel p.cancel = cancel
go f.Preload(ctx, func() { p.rate = newRate()
p.ph.fin <- name p.Size = f.size()
}) go f.Preload(ctx,
p.rate,
func() {
p.ph.fin <- name
},
func() {
p.ph.err <- name
},
func() {
p.ph.stop <- struct{}{}
},
)
} }
func (p *Preload) stop() { func (p *Preload) stop(collect bool) {
if p.Running { if p.Running {
p.cancel()
p.Running = false p.Running = false
p.cancel()
if collect {
<-p.ph.stop
}
} }
} }
func (ph *PreloadHandler) preloadFinish(ctx context.Context) { func (ph *PreloadHandler) preloadStatus(ctx context.Context) {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -151,22 +284,45 @@ func (ph *PreloadHandler) preloadFinish(ctx context.Context) {
return return
case name := <-ph.fin: case name := <-ph.fin:
ph.mu.Lock() ph.mu.Lock()
ph.q.Remove(name) ph.q.Remove(name, false)
ph.schedule() ph.schedule()
ph.mu.Unlock() ph.mu.Unlock()
case name := <-ph.err:
ph.mu.Lock()
p := ph.q.GetPreload(name)
if p != nil {
p.stop(false)
p.delay = time.Second * 2
if p.Errc >= 10 {
p.Prio = -10
ph.schedule()
} else {
p.Errc += 1
go p.start()
}
}
ph.mu.Unlock()
} }
} }
} }
func (ph *PreloadHandler) close() { func (ph *PreloadHandler) close() {
ph.mu.Lock()
defer ph.mu.Unlock()
n := len(ph.q)
for _, p := range ph.q { for _, p := range ph.q {
p.stop() p.stop(false)
} }
for { for i := 0; i < n; i++ {
select { select {
case <-ph.fin: case <-ph.fin:
default: case <-ph.err:
return case <-ph.stop:
} }
} }
close(ph.done)
}
func (ph *PreloadHandler) Done() <-chan struct{} {
return ph.done
} }

View file

@ -42,8 +42,8 @@ func (q *Quota) cleanup() {
} }
q.log.Info("quota usage", "current", atomic.LoadInt64(&q.cur), "max", q.max) q.log.Info("quota usage", "current", atomic.LoadInt64(&q.cur), "max", q.max)
defer q.mu.Unlock() defer q.mu.Unlock()
size := q.fs.mh.DeleteOldest() free := q.fs.mh.DeleteOldest()
atomic.AddInt64(&q.cur, -size) atomic.AddInt64(&q.cur, -free)
} }
func (q *Quota) Add(n int) { func (q *Quota) Add(n int) {

View file

@ -1,3 +1,6 @@
// Copyright (C) 2022 Marius Schellenberger
// crypto parts taken from https://github.com/filosottile/age
package crypto package crypto
import ( import (

View file

@ -1,3 +1,5 @@
// Copyright (C) 2022 Marius Schellenberger
package crypto package crypto
import ( import (
@ -139,7 +141,6 @@ func (f *file) Seek(offset int64, whence int) (n int64, err error) {
return return
} }
f.nonce = [NonceSize]byte{} f.nonce = [NonceSize]byte{}
//inc(&f.nonce, cn)
put(&f.nonce, cn) put(&f.nonce, cn)
f.r.nonce = f.nonce f.r.nonce = f.nonce
f.w.nonce = f.nonce f.w.nonce = f.nonce
@ -164,7 +165,7 @@ func (f *file) ReadAt(p []byte, pos int64) (n int, err error) {
defer f.rmu.Unlock() defer f.rmu.Unlock()
cn, _, roff := align(pos) cn, _, roff := align(pos)
var last bool var last bool
if f.r.cn != cn { if f.r.cn != cn || roff >= int64(len(f.r.unread)) {
_, err = f.Seek(pos, io.SeekStart) _, err = f.Seek(pos, io.SeekStart)
if err != nil { if err != nil {
return return
@ -184,26 +185,35 @@ func (f *file) ReadAt(p []byte, pos int64) (n int, err error) {
func (f *file) WriteAt(data []byte, pos int64) (n int, err error) { func (f *file) WriteAt(data []byte, pos int64) (n int, err error) {
f.wmu.Lock() f.wmu.Lock()
defer f.wmu.Unlock() defer f.wmu.Unlock()
cn, off, woff := align(pos) for n != len(data) {
c, ok := f.w.cm[cn] cn, off, woff := align(pos + int64(n))
if !ok { c, ok := f.w.cm[cn]
c = &chunkWriter{offset: off + KDFNonceSize, w: f.w, cn: cn} if !ok {
//inc(&c.nonce, cn) c = &chunkWriter{offset: off + KDFNonceSize, w: f.w, cn: cn}
put(&c.nonce, cn) put(&c.nonce, cn)
f.w.cm[cn] = c f.w.cm[cn] = c
if f.w.last == nil { if f.w.last == nil {
f.w.last = c f.w.last = c
} else if cn > f.w.last.cn { } else if cn > f.w.last.cn {
f.w.last = c f.w.last = c
}
}
w, e := c.writeAt(data[n:], woff)
n += w
if c.full() {
if f.w.last != nil && f.w.last.cn == cn {
f.w.last = nil
}
// async flush
go func() {
c.flush(notLastChunk)
}()
delete(f.w.cm, cn)
}
if e != nil {
err = e
return
} }
}
n, err = c.writeAt(data, woff)
if c.full() {
// async flush
go func() {
c.flush(notLastChunk)
}()
delete(f.w.cm, cn)
} }
return return
} }

View file

@ -1,3 +1,5 @@
// Copyright (C) 2022 Marius Schellenberger
package crypto package crypto
import ( import (

View file

@ -1,3 +1,6 @@
// Copyright (C) 2022 Marius Schellenberger
// crypto parts taken from https://github.com/filosottile/age
package crypto package crypto
import ( import (

View file

@ -1,3 +1,5 @@
// Copyright (C) 2022 Marius Schellenberger
package crypto package crypto
import ( import (

View file

@ -1,3 +1,6 @@
// Copyright (C) 2022 Marius Schellenberger
// crypto parts taken from https://github.com/filosottile/age
package crypto package crypto
import ( import (
@ -77,6 +80,8 @@ func (r *reader) Read(p []byte) (int, error) {
return n, nil return n, nil
} }
var ErrDecrypt = errors.New("failed to decrypt and authenticate payload chunk")
func (r *reader) readChunk() (last bool, err error) { func (r *reader) readChunk() (last bool, err error) {
if len(r.unread) != 0 { if len(r.unread) != 0 {
panic("stream: internal error: readChunk called with dirty buffer") panic("stream: internal error: readChunk called with dirty buffer")
@ -97,7 +102,6 @@ func (r *reader) readChunk() (last bool, err error) {
return false, err return false, err
} }
//outBuf := make([]byte, 0, ChunkSize)
out, err := r.a.Open(r.outBuf, r.nonce[:], in, nil) out, err := r.a.Open(r.outBuf, r.nonce[:], in, nil)
if err != nil && !last { if err != nil && !last {
// Check if this was a full-length final chunk. // Check if this was a full-length final chunk.
@ -106,11 +110,10 @@ func (r *reader) readChunk() (last bool, err error) {
out, err = r.a.Open(r.outBuf, r.nonce[:], in, nil) out, err = r.a.Open(r.outBuf, r.nonce[:], in, nil)
} }
if err != nil { if err != nil {
return false, errors.New("failed to decrypt and authenticate payload chunk") return false, ErrDecrypt
} }
incNonce(&r.nonce) incNonce(&r.nonce)
//r.unread = r.buf[:copy(r.buf[:], out)]
size := FullSize size := FullSize
if len(out) < FullSize { if len(out) < FullSize {
size = len(out) size = len(out)

View file

@ -1,3 +1,6 @@
// Copyright (C) 2022 Marius Schellenberger
// crypto parts taken from https://github.com/filosottile/age
package crypto package crypto
import ( import (

View file

@ -0,0 +1,55 @@
// Copyright (C) 2022 Marius Schellenberger
package filter
import (
"cachefs/pkg/provider"
"io/fs"
"path/filepath"
)
var _ provider.File = (*file)(nil)
type file struct {
provider.File
fs *FS
path string
}
func newFile(f provider.File, fs *FS, path string) (nf provider.File, err error) {
nf = &file{
File: f,
fs: fs,
path: path,
}
return nf, nil
}
func (f *file) Readdir(n int) ([]fs.FileInfo, error) {
fis, err := f.File.Readdir(n)
if err != nil {
return nil, err
}
fio := make([]fs.FileInfo, len(fis))
i := 0
for _, fi := range fis {
file := filepath.Join(f.path, fi.Name())
if f.fs.match(file) == nil {
fio[i] = fi
i++
}
}
return fio[:i], nil
}
func (f *file) ReadDir(n int) ([]fs.DirEntry, error) {
fis, err := f.Readdir(n)
if err != nil {
return nil, err
}
fio := make([]fs.DirEntry, len(fis))
for i, fi := range fis {
fio[i] = fs.FileInfoToDirEntry(fi)
}
return fio, nil
}

80
pkg/provider/filter/fs.go Normal file
View file

@ -0,0 +1,80 @@
// Copyright (C) 2022 Marius Schellenberger
package filter
import (
stdfs "io/fs"
"os"
"regexp"
"cachefs/pkg/provider"
)
var _ provider.FS = (*FS)(nil)
type FS struct {
provider.FS
re *regexp.Regexp
}
func NewFS(fs provider.FS, re *regexp.Regexp) (*FS, error) {
return &FS{
FS: fs,
re: re,
}, nil
}
func (fs *FS) match(p string) error {
if fs.re.MatchString(p) {
return nil
}
return stdfs.ErrNotExist
}
func (fs *FS) Stat(p string) (stdfs.FileInfo, error) {
err := fs.match(p)
if err != nil {
return nil, err
}
return fs.FS.Stat(p)
}
func (fs *FS) Remove(p string) error {
err := fs.match(p)
if err != nil {
return err
}
return fs.FS.Remove(p)
}
func (fs *FS) Open(p string) (provider.File, error) {
err := fs.match(p)
if err != nil {
return nil, err
}
f, err := fs.FS.Open(p)
if err != nil {
return nil, err
}
return newFile(f, fs, p)
}
func (fs *FS) OpenFile(p string, flags int, mode os.FileMode) (provider.File, error) {
err := fs.match(p)
if err != nil {
return nil, err
}
f, err := fs.FS.OpenFile(p, flags, mode)
if err != nil {
return nil, err
}
return newFile(f, fs, p)
}
func (fs *FS) MkdirAll(p string, mode os.FileMode) error {
err := fs.match(p)
if err != nil {
return err
}
return fs.FS.MkdirAll(p, mode)
}

View file

@ -5,12 +5,14 @@ package parse
import ( import (
"cachefs/pkg/provider" "cachefs/pkg/provider"
"cachefs/pkg/provider/crypto" "cachefs/pkg/provider/crypto"
"cachefs/pkg/provider/filter"
"cachefs/pkg/provider/os" "cachefs/pkg/provider/os"
"cachefs/pkg/provider/sftp" "cachefs/pkg/provider/sftp"
"errors" "errors"
"fmt" "fmt"
neturl "net/url" neturl "net/url"
"path/filepath" "path/filepath"
"regexp"
"k8s.io/klog/v2/klogr" "k8s.io/klog/v2/klogr"
) )
@ -36,7 +38,32 @@ func TryParse(url string) error {
return nil return nil
} }
func FS(url string, key []byte, block bool) (provider.FS, error) { func Paths(url string) (paths []string) {
u, err := neturl.Parse(url)
if err != nil {
return
}
switch u.Scheme {
case "file":
paths = append(paths, u.Path)
case "sftp":
q, err := neturl.ParseQuery(u.RawQuery)
if err != nil {
return
}
known, err := sftp.KnownHosts(q)
if err != nil {
return
}
paths = append(paths, known)
if key := q.Get(sftp.KeyParam); key != "" {
paths = append(paths, key)
}
}
return
}
func FS(url string, key []byte, re *regexp.Regexp, block bool) (provider.FS, error) {
u, err := neturl.Parse(url) u, err := neturl.Parse(url)
if err != nil { if err != nil {
return nil, err return nil, err
@ -58,6 +85,12 @@ func FS(url string, key []byte, block bool) (provider.FS, error) {
} }
if key != nil { if key != nil {
fs, err = crypto.NewFS(fs, key) fs, err = crypto.NewFS(fs, key)
if err != nil {
return nil, err
}
}
if re != nil {
fs, err = filter.NewFS(fs, re)
} }
return fs, err return fs, err
} }

View file

@ -1,4 +1,5 @@
// Copyright (C) 2022 Marius Schellenberger // Copyright (C) 2022 Marius Schellenberger
package sftp package sftp
import ( import (

View file

@ -197,14 +197,19 @@ func (fs *FS) MkdirAll(p string, _ os.FileMode) error {
func (fs *FS) Close() error { func (fs *FS) Close() error {
fs.cancel() fs.cancel()
fs.client.Close() err := fs.client.Close()
return fs.c.Close() if fs.c != nil {
return fs.c.Close()
}
return err
} }
func (fs *FS) path(p string) string { func (fs *FS) path(p string) string {
return filepath.Join(fs.root, filepath.FromSlash(path.Clean("/"+p))) return filepath.Join(fs.root, filepath.FromSlash(path.Clean("/"+p)))
} }
const KeyParam = "key"
func sshConfig(u *url.URL) (c *ssh.ClientConfig, err error) { func sshConfig(u *url.URL) (c *ssh.ClientConfig, err error) {
if u.User == nil { if u.User == nil {
return nil, errors.New("missing username") return nil, errors.New("missing username")
@ -223,13 +228,9 @@ func sshConfig(u *url.URL) (c *ssh.ClientConfig, err error) {
User: u.User.Username(), User: u.User.Username(),
Timeout: time.Second * 30, Timeout: time.Second * 30,
} }
known := q.Get("known_hosts") known, err := KnownHosts(q)
if known == "" { if err != nil {
home, err := os.UserHomeDir() return nil, err
if err != nil {
return nil, fmt.Errorf("error reading user home: %w", err)
}
known = filepath.Join(home, ".ssh", "known_hosts")
} }
c.HostKeyCallback, err = knownhosts.New(known) c.HostKeyCallback, err = knownhosts.New(known)
if err != nil { if err != nil {
@ -238,7 +239,7 @@ func sshConfig(u *url.URL) (c *ssh.ClientConfig, err error) {
if pw, ok := u.User.Password(); ok { if pw, ok := u.User.Password(); ok {
c.Auth = []ssh.AuthMethod{ssh.Password(pw)} c.Auth = []ssh.AuthMethod{ssh.Password(pw)}
} else { } else {
buf, err := os.ReadFile(q.Get("key")) buf, err := os.ReadFile(q.Get(KeyParam))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -247,3 +248,15 @@ func sshConfig(u *url.URL) (c *ssh.ClientConfig, err error) {
} }
return return
} }
func KnownHosts(q url.Values) (string, error) {
known := q.Get("known_hosts")
if known == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("error reading user home: %w", err)
}
known = filepath.Join(home, ".ssh", "known_hosts")
}
return known, nil
}

View file

@ -82,7 +82,7 @@ func (cs *CacheServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Del("Last-Modified") h.Del("Last-Modified")
paths, err := i.GetPaths(p, cs.fs, true) paths, pathAnchor, err := i.GetPaths(p, cs.fs, true)
if err == io.EOF { if err == io.EOF {
w.WriteHeader(i.Status()) w.WriteHeader(i.Status())
return return
@ -94,7 +94,7 @@ func (cs *CacheServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Set(csp, indexCSP) h.Set(csp, indexCSP)
w.WriteHeader(i.Status()) w.WriteHeader(i.Status())
err = cache.Execute(w, data{Paths: paths}) err = cache.Execute(w, data{Paths: paths, PathAnchor: pathAnchor})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View file

@ -4,14 +4,14 @@ package srv
import ( import (
"bytes" "bytes"
"cmp"
"encoding/xml" "encoding/xml"
"html/template" "html/template"
"net/http" "net/http"
"slices"
"strings" "strings"
"cachefs/pkg/fs" "cachefs/pkg/fs"
"golang.org/x/exp/slices"
) )
type statusInterceptor struct { type statusInterceptor struct {
@ -47,26 +47,30 @@ type dirContents struct {
} }
type dir struct { type dir struct {
Name template.HTML Name template.HTML
URI template.HTML URI template.HTML
Anchor string
} }
type dirs []dir type dirs []dir
func (dirs) Less(i, j dir) bool { return i.Name < j.Name } func (dirs) Comp(a, b dir) int { return cmp.Compare(a.Name, b.Name) }
type file struct { type file struct {
Name template.HTML Name template.HTML
URI template.HTML URI template.HTML
Anchor string Anchor string
Size string
Rate string
Status int Status int
Prio int Prio int
Errc int
Running bool Running bool
} }
type files []file type files []file
func (files) Less(i, j file) bool { return i.Name < j.Name } func (files) Comp(a, b file) int { return cmp.Compare(a.Name, b.Name) }
type responseInterceptor struct { type responseInterceptor struct {
buf bytes.Buffer buf bytes.Buffer
@ -93,7 +97,19 @@ func (r *responseInterceptor) Status() int {
return r.status return r.status
} }
func (r *responseInterceptor) GetPaths(path string, fs *fs.FS, relative bool) (dc dirContents, err error) { func xmldecode(s string) string {
s = strings.ReplaceAll(s, "&amp;", "&")
s = strings.ReplaceAll(s, "&#39;", "'")
return s
}
func urlencode(s string) string {
s = strings.ReplaceAll(s, "[", "%5B")
s = strings.ReplaceAll(s, "]", "%5D")
return s
}
func (r *responseInterceptor) GetPaths(path string, filesystem *fs.FS, relative bool) (dc dirContents, pa string, err error) {
buf := r.buf.Bytes() buf := r.buf.Bytes()
buf = bytes.ReplaceAll(buf, []byte{'&'}, []byte("&#38;")) buf = bytes.ReplaceAll(buf, []byte{'&'}, []byte("&#38;"))
err = xml.Unmarshal(buf, &dc) err = xml.Unmarshal(buf, &dc)
@ -102,48 +118,60 @@ func (r *responseInterceptor) GetPaths(path string, fs *fs.FS, relative bool) (d
} }
dc.Base = path dc.Base = path
path = strings.TrimSuffix(path, "/") path = strings.TrimSuffix(path, "/")
if path != "/" && path != "" {
pa = "#" + anchor(path)
}
for _, p := range dc.AllPaths { for _, p := range dc.AllPaths {
p = strings.ReplaceAll(p, "&amp;", "&") p = xmldecode(p)
p = strings.ReplaceAll(p, "&#39;", "'")
name := template.HTML(p) name := template.HTML(p)
full := path + "/" + p full := path + "/" + p
uri := template.HTML(full) uri := template.HTML(urlencode(full))
if relative { if relative {
uri = template.HTML(p) uri = template.HTML(urlencode(p))
} }
if p[len(p)-1] == '/' { if p[len(p)-1] == '/' {
dc.Dirs = append(dc.Dirs, dir{ dc.Dirs = append(dc.Dirs, dir{
Name: name, Name: name,
URI: uri, URI: uri,
Anchor: pathAnchor(p),
}) })
} else { } else {
dc.Files = append(dc.Files, file{ dc.Files = append(dc.Files, file{
Name: name, Name: name,
URI: uri, URI: uri,
Anchor: anchor(p), Anchor: anchor(p),
Status: fs.CacheStatus(full), Size: filesystem.FileSize(full),
Status: filesystem.CacheStatus(full),
}) })
} }
} }
slices.SortFunc(dc.Dirs, dc.Dirs.Less) slices.SortFunc(dc.Dirs, dc.Dirs.Comp)
slices.SortFunc(dc.Files, dc.Files.Less) slices.SortFunc(dc.Files, dc.Files.Comp)
return return
} }
func getPreloads(path string, fs *fs.FS) (dc dirContents) { func getPreloads(path string, filesystem *fs.FS) (dc dirContents, size int64) {
dc.Base = path dc.Base = path
for _, p := range fs.Preloads() { for _, p := range filesystem.Preloads() {
size += p.Size
dc.Files = append(dc.Files, file{ dc.Files = append(dc.Files, file{
Name: template.HTML(p.Name), Name: template.HTML(p.Name),
URI: template.HTML(p.Name), URI: template.HTML(urlencode(p.Name)),
Size: fs.FileSize(p.Size),
Rate: p.Rate(),
Status: p.Status, Status: p.Status,
Prio: p.Prio, Prio: p.Prio,
Errc: p.Errc,
Running: p.Running, Running: p.Running,
}) })
} }
return return
} }
func pathAnchor(s string) string {
return anchor(strings.TrimSuffix(s, "/"))
}
func anchor(s string) string { func anchor(s string) string {
if i := strings.LastIndex(s, "/"); i > 0 && i+1 < len(s) { if i := strings.LastIndex(s, "/"); i > 0 && i+1 < len(s) {
s = s[i+1:] s = s[i+1:]

47
pkg/srv/plain.go Normal file
View file

@ -0,0 +1,47 @@
// Copyright (C) 2022 Marius Schellenberger
package srv
import (
"net/http"
"strings"
"cachefs/pkg/fs"
"github.com/go-logr/logr"
)
type PlainServer struct {
log logr.Logger
fs *fs.FS
h http.Handler
}
func NewPlainServer(fs *fs.FS, log logr.Logger) http.Handler {
return &PlainServer{log, fs, http.FileServer(fs)}
}
func skipPlainLog(path string) bool {
return strings.HasSuffix(path, "favicon.ico")
}
func (ps *PlainServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s := &statusInterceptor{w: w}
if !skipPlainLog(r.URL.Path) {
defer func() {
if s.Status() == http.StatusPartialContent {
return
}
ps.log.Info("access",
"client", r.RemoteAddr,
"method", r.Method,
"status", s.Status(),
"uri", r.RequestURI,
)
}()
}
r.Header.Del("If-Modified-Since")
r.Header.Del("Cache-Control")
ps.h.ServeHTTP(s, r)
w.Header().Del("Last-Modified")
}

View file

@ -25,9 +25,11 @@ const (
) )
type data struct { type data struct {
QuotaCur float64 QuotaCur float64
QuotaMax float64 QuotaEst float64
Paths dirContents QuotaMax float64
Paths dirContents
PathAnchor string
} }
type FileServer struct { type FileServer struct {
@ -57,7 +59,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"client", r.RemoteAddr, "client", r.RemoteAddr,
"method", r.Method, "method", r.Method,
"status", s.Status(), "status", s.Status(),
"uri", r.URL.Path, "uri", r.RequestURI,
) )
}() }()
} }
@ -117,10 +119,12 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if option == "preloads" { if option == "preloads" {
h.Set(csp, indexCSP) h.Set(csp, indexCSP)
cur, max := fs.fs.QuotaUsage() cur, max := fs.fs.QuotaUsage()
paths, size := getPreloads(p, fs.fs)
err = preloads.Execute(w, data{ err = preloads.Execute(w, data{
QuotaCur: math.Round(float64(cur)/gib*100) / 100, QuotaCur: math.Round(float64(cur)/gib*100) / 100,
QuotaEst: math.Round(float64(cur+size)/gib*100) / 100,
QuotaMax: float64(max) / gib, QuotaMax: float64(max) / gib,
Paths: getPreloads(p, fs.fs), Paths: paths,
}) })
if err != nil { if err != nil {
fs.log.Error(err, "error rendering preloads") fs.log.Error(err, "error rendering preloads")
@ -136,7 +140,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Del("Last-Modified") h.Del("Last-Modified")
paths, err := i.GetPaths(p, fs.fs, false) paths, pathAnchor, err := i.GetPaths(p, fs.fs, false)
if err == io.EOF { if err == io.EOF {
w.WriteHeader(i.Status()) w.WriteHeader(i.Status())
return return
@ -148,7 +152,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Set(csp, indexCSP) h.Set(csp, indexCSP)
w.WriteHeader(i.Status()) w.WriteHeader(i.Status())
err = index.Execute(w, data{Paths: paths}) err = index.Execute(w, data{Paths: paths, PathAnchor: pathAnchor})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View file

@ -17,7 +17,7 @@
<tr class="listitem"> <tr class="listitem">
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td> <td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
<td class="listoptions"> <td class="listoptions">
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a> {{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span>{{$s.Size}}</span><a href="{{$s.URI}}?o=v">[v]</a>
</td> </td>
</tr> </tr>
<tr class="spacer"><td colspan="2"> <tr class="spacer"><td colspan="2">

View file

@ -94,6 +94,9 @@ td {
.green { .green {
background-color: #4caf50; background-color: #4caf50;
} }
.status-red {
color: #e74c3c;
}
.status-yellow { .status-yellow {
color: #f39c12; color: #f39c12;
} }

View file

@ -5,13 +5,13 @@
<a href="/?o=preloads">[Preloads]</a></pre> <a href="/?o=preloads">[Preloads]</a></pre>
<table> <table>
<tr class="listitem"> <tr class="listitem">
<td class="listpath up"><a href="../">../</a></td> <td class="listpath up"><a href="../{{.PathAnchor}}">../</a></td>
<td class="listoptions">[dir]</td> <td class="listoptions">[dir]</td>
</tr> </tr>
<tr class="spacer"><td colspan="2"></td></tr> <tr class="spacer"><td colspan="2"></td></tr>
{{range $s := .Paths.Dirs -}} {{range $s := .Paths.Dirs -}}
<tr class="listitem"> <tr class="listitem">
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td> <td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
<td class="listoptions">[dir]</td> <td class="listoptions">[dir]</td>
</tr> </tr>
<tr class="spacer"><td colspan="2"></td></tr> <tr class="spacer"><td colspan="2"></td></tr>
@ -20,7 +20,7 @@
<tr class="listitem"> <tr class="listitem">
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td> <td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
<td class="listoptions"> <td class="listoptions">
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.URI}}?o=n">[n]</a><a href="{{$s.URI}}?o=p">[p]</a> {{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span>{{$s.Size}}</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.URI}}?o=n">[n]</a><a href="{{$s.URI}}?o=p">[p]</a>
</td> </td>
</tr> </tr>
<tr class="spacer"><td colspan="2"> <tr class="spacer"><td colspan="2">

View file

@ -3,7 +3,7 @@
[+]: increase priority [+]: increase priority
[-]: decrease priority [-]: decrease priority
[s]: stop preloading [s]: stop preloading
Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB Quota: {{.QuotaCur}} ({{.QuotaEst}}) / {{.QuotaMax}} GiB
</pre> </pre>
<table> <table>
<tr class="listitem"> <tr class="listitem">
@ -14,7 +14,19 @@ Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB
<tr class="listitem"> <tr class="listitem">
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td> <td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
<td class="listoptions"> <td class="listoptions">
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span class="status-{{if $s.Running}}green">[running{{else}}yellow">[queued{{end}}]</span><span>{{$s.Prio}}</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s&r=preloads">[s]</a><a href="{{$s.Name}}?o=i&r=preloads">[+]</a><a href="{{$s.Name}}?o=d&r=preloads">[-]</a> {{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}
{{if $s.Running -}}
<span class="status-green">[running]</span>
{{else -}}
{{if ge $s.Errc 10 -}}
<span class="status-red">[error]</span>
{{else -}}
<span class="status-yellow">[queued]</span>
{{end -}}
{{end -}}
<span>{{$s.Rate}}</span>
<span>{{$s.Size}}</span>
<span>{{$s.Prio}}</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s&r=preloads">[s]</a><a href="{{$s.Name}}?o=i&r=preloads">[+]</a><a href="{{$s.Name}}?o=d&r=preloads">[-]</a>
</td> </td>
</tr> </tr>
<tr class="spacer"><td colspan="2"> <tr class="spacer"><td colspan="2">

View file

@ -4,12 +4,12 @@ package srv
import ( import (
"net/http" "net/http"
"slices"
"strings" "strings"
"cachefs/pkg/fs" "cachefs/pkg/fs"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/exp/slices"
"golang.org/x/net/webdav" "golang.org/x/net/webdav"
) )

View file

@ -0,0 +1,3 @@
port80
port80fg
port80and443

View file

@ -0,0 +1,24 @@
Copyright (C) 2022 Marius Schellenberger
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The names of the authors and/or contributors may not be used to
endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,5 @@
# godrop - drop privileges
Godrop is a simple library to drop privileges on Linux and OpenBSD.
See the examples directory on how to use the `Drop` and `MultiDrop` functions.

View file

@ -0,0 +1,137 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build go1.11
// +build go1.11
// Package godrop provides a simple library to drop privileges on Linux and OpenBSD.
package godrop
import (
"errors"
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"syscall"
)
// Config represents the drop config
type Config struct {
// User is the user to drop privileges to.
User string
// Group is the group to drop privileges to.
Group string
// Chroot is the directory to chroot into. Leave this emptry for no chroot.
// When compiling without cgo, make sure the chroot directory contains the /etc/passwd and /etc/group files.
Chroot string
// Set to true, to run the process in the foreground.
Foreground bool
}
// Drop will spawn a new process and hand over the listening socket file descriptor
func Drop(c Config, f func() (net.Listener, error)) error {
return MultiDrop(c, func() ([]net.Listener, error) {
l, err := f()
return []net.Listener{l}, err
})
}
// MultiDrop will spawn a new process and hand over the all listening sockets
func MultiDrop(c Config, f func() ([]net.Listener, error)) error {
uid, err := userID(c.User)
if err != nil {
return errors.New("godrop: " + err.Error())
}
if uid == 0 {
return fmt.Errorf("godrop: you can't drop privileges to uid 0 (%s)", c.User)
}
gid, err := groupID(c.Group)
if err != nil {
return errors.New("godrop: " + err.Error())
}
switch os.Getuid() {
case 0:
cmd := exec.Command(os.Args[0], os.Args[1:]...)
ln, err := f()
if err != nil {
return errors.New("godrop: " + err.Error())
}
for i, v := range ln {
var f *os.File
switch l := v.(type) {
case *net.TCPListener:
f, err = l.File()
l.Close()
case *net.UnixListener:
f, err = l.File()
l.Close()
default:
return fmt.Errorf("godrop: index %d listener is not type of either *net.TCPListener or *net.UnixListener", i)
}
if err != nil {
return fmt.Errorf("godrop: index %d %s", i, err)
}
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Chroot: c.Chroot,
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
Setsid: true,
}
if c.Foreground {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Start(); err != nil {
return errors.New("godrop: " + err.Error())
}
if c.Foreground {
go func() {
term := make(chan os.Signal)
signal.Notify(term, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGSTOP, syscall.SIGCONT, syscall.SIGUSR1, syscall.SIGUSR2)
for {
select {
case sig := <-term:
cmd.Process.Signal(sig)
return
case sig := <-sigs:
cmd.Process.Signal(sig)
}
}
}()
_ = cmd.Wait()
os.Exit(int(cmd.ProcessState.Sys().(syscall.WaitStatus)))
}
cmd.Process.Release()
os.Exit(0)
case uid:
return nil
}
return errors.New("godrop: dropping priviledges failed")
}
// GetListener returns the listener socket of file descriptor 3
func GetListener() (net.Listener, error) {
return GetListenerFd(3)
}
// GetListenerFd returns the listener socket of the given file descriptor
func GetListenerFd(fd int) (net.Listener, error) {
if fd < 3 {
return nil, errors.New("godrop: fd is less than 3")
}
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileListener(f)
}

View file

@ -0,0 +1,35 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build go1.11
// +build go1.11
package godrop
import (
"os/user"
"strconv"
)
func atoi(a string) (int, error) {
i, err := strconv.Atoi(a)
if err != nil {
return -1, err
}
return i, nil
}
func userID(username string) (int, error) {
u, err := user.Lookup(username)
if err != nil {
return -1, err
}
return atoi(u.Uid)
}
func groupID(name string) (int, error) {
g, err := user.LookupGroup(name)
if err != nil {
return -1, err
}
return atoi(g.Gid)
}

View file

@ -0,0 +1,15 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build !openbsd
// +build !openbsd
package godrop
// Pledge is currently only supported on OpenBSD.
func Pledge(promises, execpromises string) error { return nil }
// PledgePromises is currently only supported on OpenBSD.
func PledgePromises(promises string) error { return nil }
// PledgeExecPromises is currently only supported on OpenBSD.
func PledgeExecpromises(execpromises string) error { return nil }

View file

@ -0,0 +1,44 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build openbsd
// +build openbsd
package godrop
import (
"errors"
"golang.org/x/sys/unix"
)
// Pledge is a wrapper for x/sys/unix Pledge.
//
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
func Pledge(promises, execpromises string) (err error) {
err = unix.Pledge(promises, execpromises)
if err != nil {
err = errors.New("pledge: " + err.Error())
}
return
}
// PledgePromises is a wrapper for x/sys/unix PledgePromises.
//
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
func PledgePromises(promises string) (err error) {
err = unix.PledgePromises(promises)
if err != nil {
err = errors.New("pledge: " + err.Error())
}
return
}
// PledgeExecpromises is a wrapper for x/sys/unix PledgeExecpromises.
//
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
func PledgeExecpromises(execpromises string) (err error) {
err = unix.PledgeExecpromises(execpromises)
if err != nil {
err = errors.New("pledge: " + err.Error())
}
return
}

View file

@ -0,0 +1,12 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build !openbsd
// +build !openbsd
package godrop
// Unveil is currently only supported on OpenBSD.
func Unveil(path, flags string) error { return nil }
// UnveilBlock is currently only supported on OpenBSD.
func UnveilBlock() error { return nil }

View file

@ -0,0 +1,33 @@
// Copyright (C) 2022 Marius Schellenberger
//go:build openbsd
// +build openbsd
package godrop
import (
"errors"
"golang.org/x/sys/unix"
)
// Unveil is a wrapper for x/sys/unix Unveil.
//
// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage.
func Unveil(path, flags string) (err error) {
err = unix.Unveil(path, flags)
if err != nil {
err = errors.New("unveil: " + err.Error())
}
return
}
// UnveilBlock is a wrapper for x/sys/unix UnveilBlock.
//
// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage.
func UnveilBlock() (err error) {
err = unix.UnveilBlock()
if err != nil {
err = errors.New("unveil: " + err.Error())
}
return
}

View file

@ -6,7 +6,6 @@ linters:
disable-all: true disable-all: true
enable: enable:
- asciicheck - asciicheck
- deadcode
- errcheck - errcheck
- forcetypeassert - forcetypeassert
- gocritic - gocritic
@ -18,10 +17,8 @@ linters:
- misspell - misspell
- revive - revive
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- varcheck
issues: issues:
exclude-use-default: false exclude-use-default: false

View file

@ -105,14 +105,18 @@ with higher verbosity means more (and less important) logs will be generated.
There are implementations for the following logging libraries: There are implementations for the following logging libraries:
- **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr) - **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr)
- **a testing.T** (for use in Go tests, with JSON-like output): [testr](https://github.com/go-logr/logr/tree/master/testr)
- **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr) - **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr)
- **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr) - **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr)
- **a testing.T** (with klog-like text output): [ktesting](https://git.k8s.io/klog/ktesting)
- **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr) - **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr)
- **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr) - **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr)
- **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr) - **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr)
- **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend) - **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend)
- **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr) - **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr)
- **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr) - **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr)
- **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0)
- **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing)
## FAQ ## FAQ

View file

@ -20,35 +20,5 @@ package logr
// used whenever the caller is not interested in the logs. Logger instances // used whenever the caller is not interested in the logs. Logger instances
// produced by this function always compare as equal. // produced by this function always compare as equal.
func Discard() Logger { func Discard() Logger {
return Logger{ return New(nil)
level: 0,
sink: discardLogSink{},
}
}
// discardLogSink is a LogSink that discards all messages.
type discardLogSink struct{}
// Verify that it actually implements the interface
var _ LogSink = discardLogSink{}
func (l discardLogSink) Init(RuntimeInfo) {
}
func (l discardLogSink) Enabled(int) bool {
return false
}
func (l discardLogSink) Info(int, string, ...interface{}) {
}
func (l discardLogSink) Error(error, string, ...interface{}) {
}
func (l discardLogSink) WithValues(...interface{}) LogSink {
return l
}
func (l discardLogSink) WithName(string) LogSink {
return l
} }

View file

@ -21,7 +21,7 @@ limitations under the License.
// to back that API. Packages in the Go ecosystem can depend on this package, // to back that API. Packages in the Go ecosystem can depend on this package,
// while callers can implement logging with whatever backend is appropriate. // while callers can implement logging with whatever backend is appropriate.
// //
// Usage // # Usage
// //
// Logging is done using a Logger instance. Logger is a concrete type with // Logging is done using a Logger instance. Logger is a concrete type with
// methods, which defers the actual logging to a LogSink interface. The main // methods, which defers the actual logging to a LogSink interface. The main
@ -30,16 +30,20 @@ limitations under the License.
// "structured logging". // "structured logging".
// //
// With Go's standard log package, we might write: // With Go's standard log package, we might write:
// log.Printf("setting target value %s", targetValue) //
// log.Printf("setting target value %s", targetValue)
// //
// With logr's structured logging, we'd write: // With logr's structured logging, we'd write:
// logger.Info("setting target", "value", targetValue) //
// logger.Info("setting target", "value", targetValue)
// //
// Errors are much the same. Instead of: // Errors are much the same. Instead of:
// log.Printf("failed to open the pod bay door for user %s: %v", user, err) //
// log.Printf("failed to open the pod bay door for user %s: %v", user, err)
// //
// We'd write: // We'd write:
// logger.Error(err, "failed to open the pod bay door", "user", user) //
// logger.Error(err, "failed to open the pod bay door", "user", user)
// //
// Info() and Error() are very similar, but they are separate methods so that // Info() and Error() are very similar, but they are separate methods so that
// LogSink implementations can choose to do things like attach additional // LogSink implementations can choose to do things like attach additional
@ -47,7 +51,7 @@ limitations under the License.
// always logged, regardless of the current verbosity. If there is no error // always logged, regardless of the current verbosity. If there is no error
// instance available, passing nil is valid. // instance available, passing nil is valid.
// //
// Verbosity // # Verbosity
// //
// Often we want to log information only when the application in "verbose // Often we want to log information only when the application in "verbose
// mode". To write log lines that are more verbose, Logger has a V() method. // mode". To write log lines that are more verbose, Logger has a V() method.
@ -58,20 +62,22 @@ limitations under the License.
// Error messages do not have a verbosity level and are always logged. // Error messages do not have a verbosity level and are always logged.
// //
// Where we might have written: // Where we might have written:
// if flVerbose >= 2 { //
// log.Printf("an unusual thing happened") // if flVerbose >= 2 {
// } // log.Printf("an unusual thing happened")
// }
// //
// We can write: // We can write:
// logger.V(2).Info("an unusual thing happened")
// //
// Logger Names // logger.V(2).Info("an unusual thing happened")
//
// # Logger Names
// //
// Logger instances can have name strings so that all messages logged through // Logger instances can have name strings so that all messages logged through
// that instance have additional context. For example, you might want to add // that instance have additional context. For example, you might want to add
// a subsystem name: // a subsystem name:
// //
// logger.WithName("compactor").Info("started", "time", time.Now()) // logger.WithName("compactor").Info("started", "time", time.Now())
// //
// The WithName() method returns a new Logger, which can be passed to // The WithName() method returns a new Logger, which can be passed to
// constructors or other functions for further use. Repeated use of WithName() // constructors or other functions for further use. Repeated use of WithName()
@ -82,25 +88,27 @@ limitations under the License.
// joining operation (e.g. whitespace, commas, periods, slashes, brackets, // joining operation (e.g. whitespace, commas, periods, slashes, brackets,
// quotes, etc). // quotes, etc).
// //
// Saved Values // # Saved Values
// //
// Logger instances can store any number of key/value pairs, which will be // Logger instances can store any number of key/value pairs, which will be
// logged alongside all messages logged through that instance. For example, // logged alongside all messages logged through that instance. For example,
// you might want to create a Logger instance per managed object: // you might want to create a Logger instance per managed object:
// //
// With the standard log package, we might write: // With the standard log package, we might write:
// log.Printf("decided to set field foo to value %q for object %s/%s", //
// targetValue, object.Namespace, object.Name) // log.Printf("decided to set field foo to value %q for object %s/%s",
// targetValue, object.Namespace, object.Name)
// //
// With logr we'd write: // With logr we'd write:
// // Elsewhere: set up the logger to log the object name.
// obj.logger = mainLogger.WithValues(
// "name", obj.name, "namespace", obj.namespace)
// //
// // later on... // // Elsewhere: set up the logger to log the object name.
// obj.logger.Info("setting foo", "value", targetValue) // obj.logger = mainLogger.WithValues(
// "name", obj.name, "namespace", obj.namespace)
// //
// Best Practices // // later on...
// obj.logger.Info("setting foo", "value", targetValue)
//
// # Best Practices
// //
// Logger has very few hard rules, with the goal that LogSink implementations // Logger has very few hard rules, with the goal that LogSink implementations
// might have a lot of freedom to differentiate. There are, however, some // might have a lot of freedom to differentiate. There are, however, some
@ -115,15 +123,24 @@ limitations under the License.
// may be any Go value, but how the value is formatted is determined by the // may be any Go value, but how the value is formatted is determined by the
// LogSink implementation. // LogSink implementation.
// //
// Key Naming Conventions // Logger instances are meant to be passed around by value. Code that receives
// such a value can call its methods without having to check whether the
// instance is ready for use.
//
// Calling methods with the null logger (Logger{}) as instance will crash
// because it has no LogSink. Therefore this null logger should never be passed
// around. For cases where passing a logger is optional, a pointer to Logger
// should be used.
//
// # Key Naming Conventions
// //
// Keys are not strictly required to conform to any specification or regex, but // Keys are not strictly required to conform to any specification or regex, but
// it is recommended that they: // it is recommended that they:
// * be human-readable and meaningful (not auto-generated or simple ordinals) // - be human-readable and meaningful (not auto-generated or simple ordinals)
// * be constant (not dependent on input data) // - be constant (not dependent on input data)
// * contain only printable characters // - contain only printable characters
// * not contain whitespace or punctuation // - not contain whitespace or punctuation
// * use lower case for simple keys and lowerCamelCase for more complex ones // - use lower case for simple keys and lowerCamelCase for more complex ones
// //
// These guidelines help ensure that log data is processed properly regardless // These guidelines help ensure that log data is processed properly regardless
// of the log implementation. For example, log implementations will try to // of the log implementation. For example, log implementations will try to
@ -132,51 +149,54 @@ limitations under the License.
// While users are generally free to use key names of their choice, it's // While users are generally free to use key names of their choice, it's
// generally best to avoid using the following keys, as they're frequently used // generally best to avoid using the following keys, as they're frequently used
// by implementations: // by implementations:
// * "caller": the calling information (file/line) of a particular log line // - "caller": the calling information (file/line) of a particular log line
// * "error": the underlying error value in the `Error` method // - "error": the underlying error value in the `Error` method
// * "level": the log level // - "level": the log level
// * "logger": the name of the associated logger // - "logger": the name of the associated logger
// * "msg": the log message // - "msg": the log message
// * "stacktrace": the stack trace associated with a particular log line or // - "stacktrace": the stack trace associated with a particular log line or
// error (often from the `Error` message) // error (often from the `Error` message)
// * "ts": the timestamp for a log line // - "ts": the timestamp for a log line
// //
// Implementations are encouraged to make use of these keys to represent the // Implementations are encouraged to make use of these keys to represent the
// above concepts, when necessary (for example, in a pure-JSON output form, it // above concepts, when necessary (for example, in a pure-JSON output form, it
// would be necessary to represent at least message and timestamp as ordinary // would be necessary to represent at least message and timestamp as ordinary
// named values). // named values).
// //
// Break Glass // # Break Glass
// //
// Implementations may choose to give callers access to the underlying // Implementations may choose to give callers access to the underlying
// logging implementation. The recommended pattern for this is: // logging implementation. The recommended pattern for this is:
// // Underlier exposes access to the underlying logging implementation. //
// // Since callers only have a logr.Logger, they have to know which // // Underlier exposes access to the underlying logging implementation.
// // implementation is in use, so this interface is less of an abstraction // // Since callers only have a logr.Logger, they have to know which
// // and more of way to test type conversion. // // implementation is in use, so this interface is less of an abstraction
// type Underlier interface { // // and more of way to test type conversion.
// GetUnderlying() <underlying-type> // type Underlier interface {
// } // GetUnderlying() <underlying-type>
// }
// //
// Logger grants access to the sink to enable type assertions like this: // Logger grants access to the sink to enable type assertions like this:
// func DoSomethingWithImpl(log logr.Logger) { //
// if underlier, ok := log.GetSink()(impl.Underlier) { // func DoSomethingWithImpl(log logr.Logger) {
// implLogger := underlier.GetUnderlying() // if underlier, ok := log.GetSink().(impl.Underlier); ok {
// ... // implLogger := underlier.GetUnderlying()
// } // ...
// } // }
// }
// //
// Custom `With*` functions can be implemented by copying the complete // Custom `With*` functions can be implemented by copying the complete
// Logger struct and replacing the sink in the copy: // Logger struct and replacing the sink in the copy:
// // WithFooBar changes the foobar parameter in the log sink and returns a //
// // new logger with that modified sink. It does nothing for loggers where // // WithFooBar changes the foobar parameter in the log sink and returns a
// // the sink doesn't support that parameter. // // new logger with that modified sink. It does nothing for loggers where
// func WithFoobar(log logr.Logger, foobar int) logr.Logger { // // the sink doesn't support that parameter.
// if foobarLogSink, ok := log.GetSink()(FoobarSink); ok { // func WithFoobar(log logr.Logger, foobar int) logr.Logger {
// log = log.WithSink(foobarLogSink.WithFooBar(foobar)) // if foobarLogSink, ok := log.GetSink().(FoobarSink); ok {
// } // log = log.WithSink(foobarLogSink.WithFooBar(foobar))
// return log // }
// } // return log
// }
// //
// Don't use New to construct a new Logger with a LogSink retrieved from an // Don't use New to construct a new Logger with a LogSink retrieved from an
// existing Logger. Source code attribution might not work correctly and // existing Logger. Source code attribution might not work correctly and
@ -192,11 +212,14 @@ import (
) )
// New returns a new Logger instance. This is primarily used by libraries // New returns a new Logger instance. This is primarily used by libraries
// implementing LogSink, rather than end users. // implementing LogSink, rather than end users. Passing a nil sink will create
// a Logger which discards all log lines.
func New(sink LogSink) Logger { func New(sink LogSink) Logger {
logger := Logger{} logger := Logger{}
logger.setSink(sink) logger.setSink(sink)
sink.Init(runtimeInfo) if sink != nil {
sink.Init(runtimeInfo)
}
return logger return logger
} }
@ -235,7 +258,7 @@ type Logger struct {
// Enabled tests whether this Logger is enabled. For example, commandline // Enabled tests whether this Logger is enabled. For example, commandline
// flags might be used to set the logging verbosity and disable some info logs. // flags might be used to set the logging verbosity and disable some info logs.
func (l Logger) Enabled() bool { func (l Logger) Enabled() bool {
return l.sink.Enabled(l.level) return l.sink != nil && l.sink.Enabled(l.level)
} }
// Info logs a non-error message with the given key/value pairs as context. // Info logs a non-error message with the given key/value pairs as context.
@ -245,6 +268,9 @@ func (l Logger) Enabled() bool {
// information. The key/value pairs must alternate string keys and arbitrary // information. The key/value pairs must alternate string keys and arbitrary
// values. // values.
func (l Logger) Info(msg string, keysAndValues ...interface{}) { func (l Logger) Info(msg string, keysAndValues ...interface{}) {
if l.sink == nil {
return
}
if l.Enabled() { if l.Enabled() {
if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { if withHelper, ok := l.sink.(CallStackHelperLogSink); ok {
withHelper.GetCallStackHelper()() withHelper.GetCallStackHelper()()
@ -264,6 +290,9 @@ func (l Logger) Info(msg string, keysAndValues ...interface{}) {
// triggered this log line, if present. The err parameter is optional // triggered this log line, if present. The err parameter is optional
// and nil may be passed instead of an error instance. // and nil may be passed instead of an error instance.
func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) {
if l.sink == nil {
return
}
if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { if withHelper, ok := l.sink.(CallStackHelperLogSink); ok {
withHelper.GetCallStackHelper()() withHelper.GetCallStackHelper()()
} }
@ -275,6 +304,9 @@ func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) {
// level means a log message is less important. Negative V-levels are treated // level means a log message is less important. Negative V-levels are treated
// as 0. // as 0.
func (l Logger) V(level int) Logger { func (l Logger) V(level int) Logger {
if l.sink == nil {
return l
}
if level < 0 { if level < 0 {
level = 0 level = 0
} }
@ -285,6 +317,9 @@ func (l Logger) V(level int) Logger {
// WithValues returns a new Logger instance with additional key/value pairs. // WithValues returns a new Logger instance with additional key/value pairs.
// See Info for documentation on how key/value pairs work. // See Info for documentation on how key/value pairs work.
func (l Logger) WithValues(keysAndValues ...interface{}) Logger { func (l Logger) WithValues(keysAndValues ...interface{}) Logger {
if l.sink == nil {
return l
}
l.setSink(l.sink.WithValues(keysAndValues...)) l.setSink(l.sink.WithValues(keysAndValues...))
return l return l
} }
@ -295,6 +330,9 @@ func (l Logger) WithValues(keysAndValues ...interface{}) Logger {
// contain only letters, digits, and hyphens (see the package documentation for // contain only letters, digits, and hyphens (see the package documentation for
// more information). // more information).
func (l Logger) WithName(name string) Logger { func (l Logger) WithName(name string) Logger {
if l.sink == nil {
return l
}
l.setSink(l.sink.WithName(name)) l.setSink(l.sink.WithName(name))
return l return l
} }
@ -315,6 +353,9 @@ func (l Logger) WithName(name string) Logger {
// WithCallDepth(1) because it works with implementions that support the // WithCallDepth(1) because it works with implementions that support the
// CallDepthLogSink and/or CallStackHelperLogSink interfaces. // CallDepthLogSink and/or CallStackHelperLogSink interfaces.
func (l Logger) WithCallDepth(depth int) Logger { func (l Logger) WithCallDepth(depth int) Logger {
if l.sink == nil {
return l
}
if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { if withCallDepth, ok := l.sink.(CallDepthLogSink); ok {
l.setSink(withCallDepth.WithCallDepth(depth)) l.setSink(withCallDepth.WithCallDepth(depth))
} }
@ -336,6 +377,9 @@ func (l Logger) WithCallDepth(depth int) Logger {
// implementation does not support either of these, the original Logger will be // implementation does not support either of these, the original Logger will be
// returned. // returned.
func (l Logger) WithCallStackHelper() (func(), Logger) { func (l Logger) WithCallStackHelper() (func(), Logger) {
if l.sink == nil {
return func() {}, l
}
var helper func() var helper func()
if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { if withCallDepth, ok := l.sink.(CallDepthLogSink); ok {
l.setSink(withCallDepth.WithCallDepth(1)) l.setSink(withCallDepth.WithCallDepth(1))
@ -348,6 +392,11 @@ func (l Logger) WithCallStackHelper() (func(), Logger) {
return helper, l return helper, l
} }
// IsZero returns true if this logger is an uninitialized zero value
func (l Logger) IsZero() bool {
return l.sink == nil
}
// contextKey is how we find Loggers in a context.Context. // contextKey is how we find Loggers in a context.Context.
type contextKey struct{} type contextKey struct{}
@ -433,7 +482,7 @@ type LogSink interface {
WithName(name string) LogSink WithName(name string) LogSink
} }
// CallDepthLogSink represents a Logger that knows how to climb the call stack // CallDepthLogSink represents a LogSink that knows how to climb the call stack
// to identify the original call site and can offset the depth by a specified // to identify the original call site and can offset the depth by a specified
// number of frames. This is useful for users who have helper functions // number of frames. This is useful for users who have helper functions
// between the "real" call site and the actual calls to Logger methods. // between the "real" call site and the actual calls to Logger methods.
@ -458,7 +507,7 @@ type CallDepthLogSink interface {
WithCallDepth(depth int) LogSink WithCallDepth(depth int) LogSink
} }
// CallStackHelperLogSink represents a Logger that knows how to climb // CallStackHelperLogSink represents a LogSink that knows how to climb
// the call stack to identify the original call site and can skip // the call stack to identify the original call site and can skip
// intermediate helper functions if they mark themselves as // intermediate helper functions if they mark themselves as
// helper. Go's testing package uses that approach. // helper. Go's testing package uses that approach.

33
vendor/github.com/pkg/sftp/attrs.go generated vendored
View file

@ -1,7 +1,7 @@
package sftp package sftp
// ssh_FXP_ATTRS support // ssh_FXP_ATTRS support
// see http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5 // see https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
import ( import (
"os" "os"
@ -69,6 +69,20 @@ func fileInfoFromStat(stat *FileStat, name string) os.FileInfo {
} }
} }
// FileInfoUidGid extends os.FileInfo and adds callbacks for Uid and Gid retrieval,
// as an alternative to *syscall.Stat_t objects on unix systems.
type FileInfoUidGid interface {
os.FileInfo
Uid() uint32
Gid() uint32
}
// FileInfoUidGid extends os.FileInfo and adds a callbacks for extended data retrieval.
type FileInfoExtendedData interface {
os.FileInfo
Extended() []StatExtended
}
func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) { func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
mtime := fi.ModTime().Unix() mtime := fi.ModTime().Unix()
atime := mtime atime := mtime
@ -86,5 +100,22 @@ func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
// os specific file stat decoding // os specific file stat decoding
fileStatFromInfoOs(fi, &flags, fileStat) fileStatFromInfoOs(fi, &flags, fileStat)
// The call above will include the sshFileXferAttrUIDGID in case
// the os.FileInfo can be casted to *syscall.Stat_t on unix.
// If fi implements FileInfoUidGid, retrieve Uid, Gid from it instead.
if fiExt, ok := fi.(FileInfoUidGid); ok {
flags |= sshFileXferAttrUIDGID
fileStat.UID = fiExt.Uid()
fileStat.GID = fiExt.Gid()
}
// if fi implements FileInfoExtendedData, retrieve extended data from it
if fiExt, ok := fi.(FileInfoExtendedData); ok {
fileStat.Extended = fiExt.Extended()
if len(fileStat.Extended) > 0 {
flags |= sshFileXferAttrExtended
}
}
return flags, fileStat return flags, fileStat
} }

View file

@ -1,3 +1,4 @@
//go:build plan9 || windows || android
// +build plan9 windows android // +build plan9 windows android
package sftp package sftp

View file

@ -1,3 +1,4 @@
//go:build darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || aix || js
// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js // +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
package sftp package sftp

168
vendor/github.com/pkg/sftp/client.go generated vendored
View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"io" "io"
"math" "math"
"os" "os"
@ -226,15 +227,22 @@ func NewClientPipe(rd io.Reader, wr io.WriteCloser, opts ...ClientOption) (*Clie
if err := sftp.sendInit(); err != nil { if err := sftp.sendInit(); err != nil {
wr.Close() wr.Close()
return nil, err return nil, fmt.Errorf("error sending init packet to server: %w", err)
} }
if err := sftp.recvVersion(); err != nil { if err := sftp.recvVersion(); err != nil {
wr.Close() wr.Close()
return nil, err return nil, fmt.Errorf("error receiving version packet from server: %w", err)
} }
sftp.clientConn.wg.Add(1) sftp.clientConn.wg.Add(1)
go sftp.loop() go func() {
defer sftp.clientConn.wg.Done()
if err := sftp.clientConn.recv(); err != nil {
sftp.clientConn.broadcastErr(err)
}
}()
return sftp, nil return sftp, nil
} }
@ -251,11 +259,11 @@ func (c *Client) Create(path string) (*File, error) {
return c.open(path, flags(os.O_RDWR|os.O_CREATE|os.O_TRUNC)) return c.open(path, flags(os.O_RDWR|os.O_CREATE|os.O_TRUNC))
} }
const sftpProtocolVersion = 3 // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 const sftpProtocolVersion = 3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
func (c *Client) sendInit() error { func (c *Client) sendInit() error {
return c.clientConn.conn.sendPacket(&sshFxInitPacket{ return c.clientConn.conn.sendPacket(&sshFxInitPacket{
Version: sftpProtocolVersion, // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 Version: sftpProtocolVersion, // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
}) })
} }
@ -267,8 +275,13 @@ func (c *Client) nextID() uint32 {
func (c *Client) recvVersion() error { func (c *Client) recvVersion() error {
typ, data, err := c.recvPacket(0) typ, data, err := c.recvPacket(0)
if err != nil { if err != nil {
if err == io.EOF {
return fmt.Errorf("server unexpectedly closed connection: %w", io.ErrUnexpectedEOF)
}
return err return err
} }
if typ != sshFxpVersion { if typ != sshFxpVersion {
return &unexpectedPacketErr{sshFxpVersion, typ} return &unexpectedPacketErr{sshFxpVersion, typ}
} }
@ -277,6 +290,7 @@ func (c *Client) recvVersion() error {
if err != nil { if err != nil {
return err return err
} }
if version != sftpProtocolVersion { if version != sftpProtocolVersion {
return &unexpectedVersionErr{sftpProtocolVersion, version} return &unexpectedVersionErr{sftpProtocolVersion, version}
} }
@ -910,6 +924,45 @@ func (c *Client) MkdirAll(path string) error {
return nil return nil
} }
// RemoveAll delete files recursively in the directory and Recursively delete subdirectories.
// An error will be returned if no file or directory with the specified path exists
func (c *Client) RemoveAll(path string) error {
// Get the file/directory information
fi, err := c.Stat(path)
if err != nil {
return err
}
if fi.IsDir() {
// Delete files recursively in the directory
files, err := c.ReadDir(path)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
// Recursively delete subdirectories
err = c.RemoveAll(path + "/" + file.Name())
if err != nil {
return err
}
} else {
// Delete individual files
err = c.Remove(path + "/" + file.Name())
if err != nil {
return err
}
}
}
}
return c.Remove(path)
}
// File represents a remote file. // File represents a remote file.
type File struct { type File struct {
c *Client c *Client
@ -999,9 +1052,6 @@ func (f *File) readAtSequential(b []byte, off int64) (read int, err error) {
read += n read += n
} }
if err != nil { if err != nil {
if errors.Is(err, io.EOF) {
return read, nil // return nil explicitly.
}
return read, err return read, err
} }
} }
@ -1179,11 +1229,11 @@ func (f *File) writeToSequential(w io.Writer) (written int64, err error) {
if n > 0 { if n > 0 {
f.offset += int64(n) f.offset += int64(n)
m, err2 := w.Write(b[:n]) m, err := w.Write(b[:n])
written += int64(m) written += int64(m)
if err == nil { if err != nil {
err = err2 return written, err
} }
} }
@ -1461,11 +1511,20 @@ func (f *File) writeAtConcurrent(b []byte, off int64) (int, error) {
cancel := make(chan struct{}) cancel := make(chan struct{})
type work struct { type work struct {
b []byte id uint32
res chan result
off int64 off int64
} }
workCh := make(chan work) workCh := make(chan work)
concurrency := len(b)/f.c.maxPacket + 1
if concurrency > f.c.maxConcurrentRequests || concurrency < 1 {
concurrency = f.c.maxConcurrentRequests
}
pool := newResChanPool(concurrency)
// Slice: cut up the Read into any number of buffers of length <= f.c.maxPacket, and at appropriate offsets. // Slice: cut up the Read into any number of buffers of length <= f.c.maxPacket, and at appropriate offsets.
go func() { go func() {
defer close(workCh) defer close(workCh)
@ -1479,8 +1538,20 @@ func (f *File) writeAtConcurrent(b []byte, off int64) (int, error) {
wb = wb[:chunkSize] wb = wb[:chunkSize]
} }
id := f.c.nextID()
res := pool.Get()
off := off + int64(read)
f.c.dispatchRequest(res, &sshFxpWritePacket{
ID: id,
Handle: f.handle,
Offset: uint64(off),
Length: uint32(len(wb)),
Data: wb,
})
select { select {
case workCh <- work{wb, off + int64(read)}: case workCh <- work{id, res, off}:
case <-cancel: case <-cancel:
return return
} }
@ -1495,11 +1566,6 @@ func (f *File) writeAtConcurrent(b []byte, off int64) (int, error) {
} }
errCh := make(chan wErr) errCh := make(chan wErr)
concurrency := len(b)/f.c.maxPacket + 1
if concurrency > f.c.maxConcurrentRequests || concurrency < 1 {
concurrency = f.c.maxConcurrentRequests
}
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(concurrency) wg.Add(concurrency)
for i := 0; i < concurrency; i++ { for i := 0; i < concurrency; i++ {
@ -1507,13 +1573,22 @@ func (f *File) writeAtConcurrent(b []byte, off int64) (int, error) {
go func() { go func() {
defer wg.Done() defer wg.Done()
ch := make(chan result, 1) // reusable channel per mapper. for work := range workCh {
s := <-work.res
pool.Put(work.res)
err := s.err
if err == nil {
switch s.typ {
case sshFxpStatus:
err = normaliseError(unmarshalStatus(work.id, s.data))
default:
err = unimplementedPacketErr(s.typ)
}
}
for packet := range workCh {
n, err := f.writeChunkAt(ch, packet.b, packet.off)
if err != nil { if err != nil {
// return the offset as the start + how much we wrote before the error. errCh <- wErr{work.off, err}
errCh <- wErr{packet.off + int64(n), err}
} }
} }
}() }()
@ -1598,8 +1673,9 @@ func (f *File) ReadFromWithConcurrency(r io.Reader, concurrency int) (read int64
cancel := make(chan struct{}) cancel := make(chan struct{})
type work struct { type work struct {
b []byte id uint32
n int res chan result
off int64 off int64
} }
workCh := make(chan work) workCh := make(chan work)
@ -1614,24 +1690,34 @@ func (f *File) ReadFromWithConcurrency(r io.Reader, concurrency int) (read int64
concurrency = f.c.maxConcurrentRequests concurrency = f.c.maxConcurrentRequests
} }
pool := newBufPool(concurrency, f.c.maxPacket) pool := newResChanPool(concurrency)
// Slice: cut up the Read into any number of buffers of length <= f.c.maxPacket, and at appropriate offsets. // Slice: cut up the Read into any number of buffers of length <= f.c.maxPacket, and at appropriate offsets.
go func() { go func() {
defer close(workCh) defer close(workCh)
b := make([]byte, f.c.maxPacket)
off := f.offset off := f.offset
for { for {
b := pool.Get()
n, err := r.Read(b) n, err := r.Read(b)
if n > 0 { if n > 0 {
read += int64(n) read += int64(n)
id := f.c.nextID()
res := pool.Get()
f.c.dispatchRequest(res, &sshFxpWritePacket{
ID: id,
Handle: f.handle,
Offset: uint64(off),
Length: uint32(n),
Data: b[:n],
})
select { select {
case workCh <- work{b, n, off}: case workCh <- work{id, res, off}:
// We need the pool.Put(b) to put the whole slice, not just trunced.
case <-cancel: case <-cancel:
return return
} }
@ -1655,15 +1741,23 @@ func (f *File) ReadFromWithConcurrency(r io.Reader, concurrency int) (read int64
go func() { go func() {
defer wg.Done() defer wg.Done()
ch := make(chan result, 1) // reusable channel per mapper. for work := range workCh {
s := <-work.res
pool.Put(work.res)
for packet := range workCh { err := s.err
n, err := f.writeChunkAt(ch, packet.b[:packet.n], packet.off) if err == nil {
if err != nil { switch s.typ {
// return the offset as the start + how much we wrote before the error. case sshFxpStatus:
errCh <- rwErr{packet.off + int64(n), err} err = normaliseError(unmarshalStatus(work.id, s.data))
default:
err = unimplementedPacketErr(s.typ)
}
}
if err != nil {
errCh <- rwErr{work.off, err}
} }
pool.Put(packet.b)
} }
}() }()
} }

12
vendor/github.com/pkg/sftp/conn.go generated vendored
View file

@ -18,7 +18,9 @@ type conn struct {
} }
// the orderID is used in server mode if the allocator is enabled. // the orderID is used in server mode if the allocator is enabled.
// For the client mode just pass 0 // For the client mode just pass 0.
// It returns io.EOF if the connection is closed and
// there are no more packets to read.
func (c *conn) recvPacket(orderID uint32) (uint8, []byte, error) { func (c *conn) recvPacket(orderID uint32) (uint8, []byte, error) {
return recvPacket(c, c.alloc, orderID) return recvPacket(c, c.alloc, orderID)
} }
@ -61,14 +63,6 @@ func (c *clientConn) Close() error {
return c.conn.Close() return c.conn.Close()
} }
func (c *clientConn) loop() {
defer c.wg.Done()
err := c.recv()
if err != nil {
c.broadcastErr(err)
}
}
// recv continuously reads from the server and forwards responses to the // recv continuously reads from the server and forwards responses to the
// appropriate channel. // appropriate channel.
func (c *clientConn) recv() error { func (c *clientConn) recv() error {

View file

@ -1,3 +1,4 @@
//go:build debug
// +build debug // +build debug
package sftp package sftp

1
vendor/github.com/pkg/sftp/fuzz.go generated vendored
View file

@ -1,3 +1,4 @@
//go:build gofuzz
// +build gofuzz // +build gofuzz
package sftp package sftp

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// Attributes related flags. // Attributes related flags.
const ( const (
@ -12,7 +12,7 @@ const (
// Attributes defines the file attributes type defined in draft-ietf-secsh-filexfer-02 // Attributes defines the file attributes type defined in draft-ietf-secsh-filexfer-02
// //
// Defined in: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5 // Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
type Attributes struct { type Attributes struct {
Flags uint32 Flags uint32
@ -74,7 +74,6 @@ func (a *Attributes) SetPermissions(perms FileMode) {
// GetACModTime returns the ATime and MTime fields and a bool that is true if and only if the values are valid/defined. // GetACModTime returns the ATime and MTime fields and a bool that is true if and only if the values are valid/defined.
func (a *Attributes) GetACModTime() (atime, mtime uint32, ok bool) { func (a *Attributes) GetACModTime() (atime, mtime uint32, ok bool) {
return a.ATime, a.MTime, a.Flags&AttrACModTime != 0 return a.ATime, a.MTime, a.Flags&AttrACModTime != 0
return a.ATime, a.MTime, a.Flags&AttrACModTime != 0
} }
// SetACModTime is a convenience function that sets the ATime and MTime fields, // SetACModTime is a convenience function that sets the ATime and MTime fields,
@ -117,32 +116,32 @@ func (a *Attributes) Len() int {
} }
// MarshalInto marshals e onto the end of the given Buffer. // MarshalInto marshals e onto the end of the given Buffer.
func (a *Attributes) MarshalInto(b *Buffer) { func (a *Attributes) MarshalInto(buf *Buffer) {
b.AppendUint32(a.Flags) buf.AppendUint32(a.Flags)
if a.Flags&AttrSize != 0 { if a.Flags&AttrSize != 0 {
b.AppendUint64(a.Size) buf.AppendUint64(a.Size)
} }
if a.Flags&AttrUIDGID != 0 { if a.Flags&AttrUIDGID != 0 {
b.AppendUint32(a.UID) buf.AppendUint32(a.UID)
b.AppendUint32(a.GID) buf.AppendUint32(a.GID)
} }
if a.Flags&AttrPermissions != 0 { if a.Flags&AttrPermissions != 0 {
b.AppendUint32(uint32(a.Permissions)) buf.AppendUint32(uint32(a.Permissions))
} }
if a.Flags&AttrACModTime != 0 { if a.Flags&AttrACModTime != 0 {
b.AppendUint32(a.ATime) buf.AppendUint32(a.ATime)
b.AppendUint32(a.MTime) buf.AppendUint32(a.MTime)
} }
if a.Flags&AttrExtended != 0 { if a.Flags&AttrExtended != 0 {
b.AppendUint32(uint32(len(a.ExtendedAttributes))) buf.AppendUint32(uint32(len(a.ExtendedAttributes)))
for _, ext := range a.ExtendedAttributes { for _, ext := range a.ExtendedAttributes {
ext.MarshalInto(b) ext.MarshalInto(buf)
} }
} }
} }
@ -157,74 +156,51 @@ func (a *Attributes) MarshalBinary() ([]byte, error) {
// UnmarshalFrom unmarshals an Attributes from the given Buffer into e. // UnmarshalFrom unmarshals an Attributes from the given Buffer into e.
// //
// NOTE: The values of fields not covered in the a.Flags are explicitly undefined. // NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
func (a *Attributes) UnmarshalFrom(b *Buffer) (err error) { func (a *Attributes) UnmarshalFrom(buf *Buffer) (err error) {
flags, err := b.ConsumeUint32() flags := buf.ConsumeUint32()
if err != nil {
return err
}
return a.XXX_UnmarshalByFlags(flags, b) return a.XXX_UnmarshalByFlags(flags, buf)
} }
// XXX_UnmarshalByFlags uses the pre-existing a.Flags field to determine which fields to decode. // XXX_UnmarshalByFlags uses the pre-existing a.Flags field to determine which fields to decode.
// DO NOT USE THIS: it is an anti-corruption function to implement existing internal usage in pkg/sftp. // DO NOT USE THIS: it is an anti-corruption function to implement existing internal usage in pkg/sftp.
// This function is not a part of any compatibility promise. // This function is not a part of any compatibility promise.
func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, b *Buffer) (err error) { func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, buf *Buffer) (err error) {
a.Flags = flags a.Flags = flags
// Short-circuit dummy attributes. // Short-circuit dummy attributes.
if a.Flags == 0 { if a.Flags == 0 {
return nil return buf.Err
} }
if a.Flags&AttrSize != 0 { if a.Flags&AttrSize != 0 {
if a.Size, err = b.ConsumeUint64(); err != nil { a.Size = buf.ConsumeUint64()
return err
}
} }
if a.Flags&AttrUIDGID != 0 { if a.Flags&AttrUIDGID != 0 {
if a.UID, err = b.ConsumeUint32(); err != nil { a.UID = buf.ConsumeUint32()
return err a.GID = buf.ConsumeUint32()
}
if a.GID, err = b.ConsumeUint32(); err != nil {
return err
}
} }
if a.Flags&AttrPermissions != 0 { if a.Flags&AttrPermissions != 0 {
m, err := b.ConsumeUint32() a.Permissions = FileMode(buf.ConsumeUint32())
if err != nil {
return err
}
a.Permissions = FileMode(m)
} }
if a.Flags&AttrACModTime != 0 { if a.Flags&AttrACModTime != 0 {
if a.ATime, err = b.ConsumeUint32(); err != nil { a.ATime = buf.ConsumeUint32()
return err a.MTime = buf.ConsumeUint32()
}
if a.MTime, err = b.ConsumeUint32(); err != nil {
return err
}
} }
if a.Flags&AttrExtended != 0 { if a.Flags&AttrExtended != 0 {
count, err := b.ConsumeUint32() count := buf.ConsumeCount()
if err != nil {
return err
}
a.ExtendedAttributes = make([]ExtendedAttribute, count) a.ExtendedAttributes = make([]ExtendedAttribute, count)
for i := range a.ExtendedAttributes { for i := range a.ExtendedAttributes {
a.ExtendedAttributes[i].UnmarshalFrom(b) a.ExtendedAttributes[i].UnmarshalFrom(buf)
} }
} }
return nil return buf.Err
} }
// UnmarshalBinary decodes the binary encoding of Attributes into e. // UnmarshalBinary decodes the binary encoding of Attributes into e.
@ -234,7 +210,7 @@ func (a *Attributes) UnmarshalBinary(data []byte) error {
// ExtendedAttribute defines the extended file attribute type defined in draft-ietf-secsh-filexfer-02 // ExtendedAttribute defines the extended file attribute type defined in draft-ietf-secsh-filexfer-02
// //
// Defined in: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5 // Defined in: https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-5
type ExtendedAttribute struct { type ExtendedAttribute struct {
Type string Type string
Data string Data string
@ -246,9 +222,9 @@ func (e *ExtendedAttribute) Len() int {
} }
// MarshalInto marshals e onto the end of the given Buffer. // MarshalInto marshals e onto the end of the given Buffer.
func (e *ExtendedAttribute) MarshalInto(b *Buffer) { func (e *ExtendedAttribute) MarshalInto(buf *Buffer) {
b.AppendString(e.Type) buf.AppendString(e.Type)
b.AppendString(e.Data) buf.AppendString(e.Data)
} }
// MarshalBinary returns e as the binary encoding of e. // MarshalBinary returns e as the binary encoding of e.
@ -259,16 +235,13 @@ func (e *ExtendedAttribute) MarshalBinary() ([]byte, error) {
} }
// UnmarshalFrom unmarshals an ExtendedAattribute from the given Buffer into e. // UnmarshalFrom unmarshals an ExtendedAattribute from the given Buffer into e.
func (e *ExtendedAttribute) UnmarshalFrom(b *Buffer) (err error) { func (e *ExtendedAttribute) UnmarshalFrom(buf *Buffer) (err error) {
if e.Type, err = b.ConsumeString(); err != nil { *e = ExtendedAttribute{
return err Type: buf.ConsumeString(),
Data: buf.ConsumeString(),
} }
if e.Data, err = b.ConsumeString(); err != nil { return buf.Err
return err
}
return nil
} }
// UnmarshalBinary decodes the binary encoding of ExtendedAttribute into e. // UnmarshalBinary decodes the binary encoding of ExtendedAttribute into e.
@ -291,11 +264,11 @@ func (e *NameEntry) Len() int {
} }
// MarshalInto marshals e onto the end of the given Buffer. // MarshalInto marshals e onto the end of the given Buffer.
func (e *NameEntry) MarshalInto(b *Buffer) { func (e *NameEntry) MarshalInto(buf *Buffer) {
b.AppendString(e.Filename) buf.AppendString(e.Filename)
b.AppendString(e.Longname) buf.AppendString(e.Longname)
e.Attrs.MarshalInto(b) e.Attrs.MarshalInto(buf)
} }
// MarshalBinary returns e as the binary encoding of e. // MarshalBinary returns e as the binary encoding of e.
@ -308,16 +281,13 @@ func (e *NameEntry) MarshalBinary() ([]byte, error) {
// UnmarshalFrom unmarshals an NameEntry from the given Buffer into e. // UnmarshalFrom unmarshals an NameEntry from the given Buffer into e.
// //
// NOTE: The values of fields not covered in the a.Flags are explicitly undefined. // NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
func (e *NameEntry) UnmarshalFrom(b *Buffer) (err error) { func (e *NameEntry) UnmarshalFrom(buf *Buffer) (err error) {
if e.Filename, err = b.ConsumeString(); err != nil { *e = NameEntry{
return err Filename: buf.ConsumeString(),
Longname: buf.ConsumeString(),
} }
if e.Longname, err = b.ConsumeString(); err != nil { return e.Attrs.UnmarshalFrom(buf)
return err
}
return e.Attrs.UnmarshalFrom(b)
} }
// UnmarshalBinary decodes the binary encoding of NameEntry into e. // UnmarshalBinary decodes the binary encoding of NameEntry into e.

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
import ( import (
"encoding/binary" "encoding/binary"
@ -17,6 +17,7 @@ var (
type Buffer struct { type Buffer struct {
b []byte b []byte
off int off int
Err error
} }
// NewBuffer creates and initializes a new buffer using buf as its initial contents. // NewBuffer creates and initializes a new buffer using buf as its initial contents.
@ -51,14 +52,17 @@ func (b *Buffer) Cap() int { return cap(b.b) }
// Reset resets the buffer to be empty, but it retains the underlying storage for use by future Appends. // Reset resets the buffer to be empty, but it retains the underlying storage for use by future Appends.
func (b *Buffer) Reset() { func (b *Buffer) Reset() {
b.b = b.b[:0] *b = Buffer{
b.off = 0 b: b.b[:0],
}
} }
// StartPacket resets and initializes the buffer to be ready to start marshaling a packet into. // StartPacket resets and initializes the buffer to be ready to start marshaling a packet into.
// It truncates the buffer, reserves space for uint32(length), then appends the given packetType and requestID. // It truncates the buffer, reserves space for uint32(length), then appends the given packetType and requestID.
func (b *Buffer) StartPacket(packetType PacketType, requestID uint32) { func (b *Buffer) StartPacket(packetType PacketType, requestID uint32) {
b.b, b.off = append(b.b[:0], make([]byte, 4)...), 0 *b = Buffer{
b: append(b.b[:0], make([]byte, 4)...),
}
b.AppendUint8(uint8(packetType)) b.AppendUint8(uint8(packetType))
b.AppendUint32(requestID) b.AppendUint32(requestID)
@ -81,15 +85,21 @@ func (b *Buffer) Packet(payload []byte) (header, payloadPassThru []byte, err err
} }
// ConsumeUint8 consumes a single byte from the buffer. // ConsumeUint8 consumes a single byte from the buffer.
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeUint8() (uint8, error) { func (b *Buffer) ConsumeUint8() uint8 {
if b.Err != nil {
return 0
}
if b.Len() < 1 { if b.Len() < 1 {
return 0, ErrShortPacket b.off = len(b.b)
b.Err = ErrShortPacket
return 0
} }
var v uint8 var v uint8
v, b.off = b.b[b.off], b.off+1 v, b.off = b.b[b.off], b.off+1
return v, nil return v
} }
// AppendUint8 appends a single byte into the buffer. // AppendUint8 appends a single byte into the buffer.
@ -98,14 +108,9 @@ func (b *Buffer) AppendUint8(v uint8) {
} }
// ConsumeBool consumes a single byte from the buffer, and returns true if that byte is non-zero. // ConsumeBool consumes a single byte from the buffer, and returns true if that byte is non-zero.
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeBool() (bool, error) { func (b *Buffer) ConsumeBool() bool {
v, err := b.ConsumeUint8() return b.ConsumeUint8() != 0
if err != nil {
return false, err
}
return v != 0, nil
} }
// AppendBool appends a single bool into the buffer. // AppendBool appends a single bool into the buffer.
@ -119,15 +124,21 @@ func (b *Buffer) AppendBool(v bool) {
} }
// ConsumeUint16 consumes a single uint16 from the buffer, in network byte order (big-endian). // ConsumeUint16 consumes a single uint16 from the buffer, in network byte order (big-endian).
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeUint16() (uint16, error) { func (b *Buffer) ConsumeUint16() uint16 {
if b.Err != nil {
return 0
}
if b.Len() < 2 { if b.Len() < 2 {
return 0, ErrShortPacket b.off = len(b.b)
b.Err = ErrShortPacket
return 0
} }
v := binary.BigEndian.Uint16(b.b[b.off:]) v := binary.BigEndian.Uint16(b.b[b.off:])
b.off += 2 b.off += 2
return v, nil return v
} }
// AppendUint16 appends single uint16 into the buffer, in network byte order (big-endian). // AppendUint16 appends single uint16 into the buffer, in network byte order (big-endian).
@ -146,15 +157,21 @@ func unmarshalUint32(b []byte) uint32 {
} }
// ConsumeUint32 consumes a single uint32 from the buffer, in network byte order (big-endian). // ConsumeUint32 consumes a single uint32 from the buffer, in network byte order (big-endian).
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeUint32() (uint32, error) { func (b *Buffer) ConsumeUint32() uint32 {
if b.Err != nil {
return 0
}
if b.Len() < 4 { if b.Len() < 4 {
return 0, ErrShortPacket b.off = len(b.b)
b.Err = ErrShortPacket
return 0
} }
v := binary.BigEndian.Uint32(b.b[b.off:]) v := binary.BigEndian.Uint32(b.b[b.off:])
b.off += 4 b.off += 4
return v, nil return v
} }
// AppendUint32 appends a single uint32 into the buffer, in network byte order (big-endian). // AppendUint32 appends a single uint32 into the buffer, in network byte order (big-endian).
@ -167,16 +184,33 @@ func (b *Buffer) AppendUint32(v uint32) {
) )
} }
// ConsumeCount consumes a single uint32 count from the buffer, in network byte order (big-endian) as an int.
// If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeCount() int {
return int(b.ConsumeUint32())
}
// AppendCount appends a single int length as a uint32 into the buffer, in network byte order (big-endian).
func (b *Buffer) AppendCount(v int) {
b.AppendUint32(uint32(v))
}
// ConsumeUint64 consumes a single uint64 from the buffer, in network byte order (big-endian). // ConsumeUint64 consumes a single uint64 from the buffer, in network byte order (big-endian).
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeUint64() (uint64, error) { func (b *Buffer) ConsumeUint64() uint64 {
if b.Err != nil {
return 0
}
if b.Len() < 8 { if b.Len() < 8 {
return 0, ErrShortPacket b.off = len(b.b)
b.Err = ErrShortPacket
return 0
} }
v := binary.BigEndian.Uint64(b.b[b.off:]) v := binary.BigEndian.Uint64(b.b[b.off:])
b.off += 8 b.off += 8
return v, nil return v
} }
// AppendUint64 appends a single uint64 into the buffer, in network byte order (big-endian). // AppendUint64 appends a single uint64 into the buffer, in network byte order (big-endian).
@ -194,14 +228,9 @@ func (b *Buffer) AppendUint64(v uint64) {
} }
// ConsumeInt64 consumes a single int64 from the buffer, in network byte order (big-endian) with twos complement. // ConsumeInt64 consumes a single int64 from the buffer, in network byte order (big-endian) with twos complement.
// If the buffer does not have enough data, it will return ErrShortPacket. // If the buffer does not have enough data, it will set Err to ErrShortPacket.
func (b *Buffer) ConsumeInt64() (int64, error) { func (b *Buffer) ConsumeInt64() int64 {
u, err := b.ConsumeUint64() return int64(b.ConsumeUint64())
if err != nil {
return 0, err
}
return int64(u), err
} }
// AppendInt64 appends a single int64 into the buffer, in network byte order (big-endian) with twos complement. // AppendInt64 appends a single int64 into the buffer, in network byte order (big-endian) with twos complement.
@ -211,29 +240,52 @@ func (b *Buffer) AppendInt64(v int64) {
// ConsumeByteSlice consumes a single string of raw binary data from the buffer. // ConsumeByteSlice consumes a single string of raw binary data from the buffer.
// A string is a uint32 length, followed by that number of raw bytes. // A string is a uint32 length, followed by that number of raw bytes.
// If the buffer does not have enough data, or defines a length larger than available, it will return ErrShortPacket. // If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
// //
// The returned slice aliases the buffer contents, and is valid only as long as the buffer is not reused // The returned slice aliases the buffer contents, and is valid only as long as the buffer is not reused
// (that is, only until the next call to Reset, PutLength, StartPacket, or UnmarshalBinary). // (that is, only until the next call to Reset, PutLength, StartPacket, or UnmarshalBinary).
// //
// In no case will any Consume calls return overlapping slice aliases, // In no case will any Consume calls return overlapping slice aliases,
// and Append calls are guaranteed to not disturb this slice alias. // and Append calls are guaranteed to not disturb this slice alias.
func (b *Buffer) ConsumeByteSlice() ([]byte, error) { func (b *Buffer) ConsumeByteSlice() []byte {
length, err := b.ConsumeUint32() length := int(b.ConsumeUint32())
if err != nil { if b.Err != nil {
return nil, err return nil
} }
if b.Len() < int(length) { if b.Len() < length || length < 0 {
return nil, ErrShortPacket b.off = len(b.b)
b.Err = ErrShortPacket
return nil
} }
v := b.b[b.off:] v := b.b[b.off:]
if len(v) > int(length) { if len(v) > length || cap(v) > length {
v = v[:length:length] v = v[:length:length]
} }
b.off += int(length) b.off += int(length)
return v, nil return v
}
// ConsumeByteSliceCopy consumes a single string of raw binary data as a copy from the buffer.
// A string is a uint32 length, followed by that number of raw bytes.
// If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
//
// The returned slice does not alias any buffer contents,
// and will therefore be valid even if the buffer is later reused.
//
// If hint has sufficient capacity to hold the data, it will be reused and overwritten,
// otherwise a new backing slice will be allocated and returned.
func (b *Buffer) ConsumeByteSliceCopy(hint []byte) []byte {
data := b.ConsumeByteSlice()
if grow := len(data) - len(hint); grow > 0 {
hint = append(hint, make([]byte, grow)...)
}
n := copy(hint, data)
hint = hint[:n]
return hint
} }
// AppendByteSlice appends a single string of raw binary data into the buffer. // AppendByteSlice appends a single string of raw binary data into the buffer.
@ -245,17 +297,12 @@ func (b *Buffer) AppendByteSlice(v []byte) {
// ConsumeString consumes a single string of binary data from the buffer. // ConsumeString consumes a single string of binary data from the buffer.
// A string is a uint32 length, followed by that number of raw bytes. // A string is a uint32 length, followed by that number of raw bytes.
// If the buffer does not have enough data, or defines a length larger than available, it will return ErrShortPacket. // If the buffer does not have enough data, or defines a length larger than available, it will set Err to ErrShortPacket.
// //
// NOTE: Go implicitly assumes that strings contain UTF-8 encoded data. // NOTE: Go implicitly assumes that strings contain UTF-8 encoded data.
// All caveats on using arbitrary binary data in Go strings applies. // All caveats on using arbitrary binary data in Go strings applies.
func (b *Buffer) ConsumeString() (string, error) { func (b *Buffer) ConsumeString() string {
v, err := b.ConsumeByteSlice() return string(b.ConsumeByteSlice())
if err != nil {
return "", err
}
return string(v), nil
} }
// AppendString appends a single string of binary data into the buffer. // AppendString appends a single string of binary data into the buffer.

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
import ( import (
"encoding" "encoding"
@ -86,8 +86,9 @@ func (p *ExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
// If the extension has not been registered, then a new Buffer will be allocated. // If the extension has not been registered, then a new Buffer will be allocated.
// Then the request-specific-data will be unmarshaled from the rest of the buffer. // Then the request-specific-data will be unmarshaled from the rest of the buffer.
func (p *ExtendedPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *ExtendedPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.ExtendedRequest, err = buf.ConsumeString(); err != nil { p.ExtendedRequest = buf.ConsumeString()
return err if buf.Err != nil {
return buf.Err
} }
if p.Data == nil { if p.Data == nil {

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// ExtensionPair defines the extension-pair type defined in draft-ietf-secsh-filexfer-13. // ExtensionPair defines the extension-pair type defined in draft-ietf-secsh-filexfer-13.
// This type is backwards-compatible with how draft-ietf-secsh-filexfer-02 defines extensions. // This type is backwards-compatible with how draft-ietf-secsh-filexfer-02 defines extensions.
@ -29,15 +29,12 @@ func (e *ExtensionPair) MarshalBinary() ([]byte, error) {
// UnmarshalFrom unmarshals an ExtensionPair from the given Buffer into e. // UnmarshalFrom unmarshals an ExtensionPair from the given Buffer into e.
func (e *ExtensionPair) UnmarshalFrom(buf *Buffer) (err error) { func (e *ExtensionPair) UnmarshalFrom(buf *Buffer) (err error) {
if e.Name, err = buf.ConsumeString(); err != nil { *e = ExtensionPair{
return err Name: buf.ConsumeString(),
Data: buf.ConsumeString(),
} }
if e.Data, err = buf.ConsumeString(); err != nil { return buf.Err
return err
}
return nil
} }
// UnmarshalBinary decodes the binary encoding of ExtensionPair into e. // UnmarshalBinary decodes the binary encoding of ExtensionPair into e.

View file

@ -1,5 +1,5 @@
// Package filexfer implements the wire encoding for secsh-filexfer as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 // Package sshfx implements the wire encoding for secsh-filexfer as described in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
package filexfer package sshfx
// PacketMarshaller narrowly defines packets that will only be transmitted. // PacketMarshaller narrowly defines packets that will only be transmitted.
// //

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
import ( import (
"fmt" "fmt"
@ -10,7 +10,7 @@ type Status uint32
// Defines the various SSH_FX_* values. // Defines the various SSH_FX_* values.
const ( const (
// see draft-ietf-secsh-filexfer-02 // see draft-ietf-secsh-filexfer-02
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-7
StatusOK = Status(iota) StatusOK = Status(iota)
StatusEOF StatusEOF
StatusNoSuchFile StatusNoSuchFile
@ -21,28 +21,28 @@ const (
StatusConnectionLost StatusConnectionLost
StatusOPUnsupported StatusOPUnsupported
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-7 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-03.txt#section-7
StatusV4InvalidHandle StatusV4InvalidHandle
StatusV4NoSuchPath StatusV4NoSuchPath
StatusV4FileAlreadyExists StatusV4FileAlreadyExists
StatusV4WriteProtect StatusV4WriteProtect
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-7 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-04.txt#section-7
StatusV4NoMedia StatusV4NoMedia
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-7 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-05.txt#section-7
StatusV5NoSpaceOnFilesystem StatusV5NoSpaceOnFilesystem
StatusV5QuotaExceeded StatusV5QuotaExceeded
StatusV5UnknownPrincipal StatusV5UnknownPrincipal
StatusV5LockConflict StatusV5LockConflict
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-06#section-8 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-06.txt#section-8
StatusV6DirNotEmpty StatusV6DirNotEmpty
StatusV6NotADirectory StatusV6NotADirectory
StatusV6InvalidFilename StatusV6InvalidFilename
StatusV6LinkLoop StatusV6LinkLoop
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-8 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-8
StatusV6CannotDelete StatusV6CannotDelete
StatusV6InvalidParameter StatusV6InvalidParameter
StatusV6FileIsADirectory StatusV6FileIsADirectory
@ -50,10 +50,10 @@ const (
StatusV6ByteRangeLockRefused StatusV6ByteRangeLockRefused
StatusV6DeletePending StatusV6DeletePending
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-8.1 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-8.1
StatusV6FileCorrupt StatusV6FileCorrupt
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-10#section-9.1 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-10.txt#section-9.1
StatusV6OwnerInvalid StatusV6OwnerInvalid
StatusV6GroupInvalid StatusV6GroupInvalid

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
import ( import (
"fmt" "fmt"
@ -9,7 +9,7 @@ type PacketType uint8
// Request packet types. // Request packet types.
const ( const (
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
PacketTypeInit = PacketType(iota + 1) PacketTypeInit = PacketType(iota + 1)
PacketTypeVersion PacketTypeVersion
PacketTypeOpen PacketTypeOpen
@ -31,17 +31,17 @@ const (
PacketTypeReadLink PacketTypeReadLink
PacketTypeSymlink PacketTypeSymlink
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-3.3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-07.txt#section-3.3
PacketTypeV6Link PacketTypeV6Link
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-3.3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-08.txt#section-3.3
PacketTypeV6Block PacketTypeV6Block
PacketTypeV6Unblock PacketTypeV6Unblock
) )
// Response packet types. // Response packet types.
const ( const (
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
PacketTypeStatus = PacketType(iota + 101) PacketTypeStatus = PacketType(iota + 101)
PacketTypeHandle PacketTypeHandle
PacketTypeData PacketTypeData
@ -51,7 +51,7 @@ const (
// Extended packet types. // Extended packet types.
const ( const (
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
PacketTypeExtended = PacketType(iota + 200) PacketTypeExtended = PacketType(iota + 200)
PacketTypeExtendedReply PacketTypeExtendedReply
) )
@ -122,3 +122,48 @@ func (f PacketType) String() string {
return fmt.Sprintf("SSH_FXP_UNKNOWN(%d)", f) return fmt.Sprintf("SSH_FXP_UNKNOWN(%d)", f)
} }
} }
func newPacketFromType(typ PacketType) (Packet, error) {
switch typ {
case PacketTypeOpen:
return new(OpenPacket), nil
case PacketTypeClose:
return new(ClosePacket), nil
case PacketTypeRead:
return new(ReadPacket), nil
case PacketTypeWrite:
return new(WritePacket), nil
case PacketTypeLStat:
return new(LStatPacket), nil
case PacketTypeFStat:
return new(FStatPacket), nil
case PacketTypeSetstat:
return new(SetstatPacket), nil
case PacketTypeFSetstat:
return new(FSetstatPacket), nil
case PacketTypeOpenDir:
return new(OpenDirPacket), nil
case PacketTypeReadDir:
return new(ReadDirPacket), nil
case PacketTypeRemove:
return new(RemovePacket), nil
case PacketTypeMkdir:
return new(MkdirPacket), nil
case PacketTypeRmdir:
return new(RmdirPacket), nil
case PacketTypeRealPath:
return new(RealPathPacket), nil
case PacketTypeStat:
return new(StatPacket), nil
case PacketTypeRename:
return new(RenamePacket), nil
case PacketTypeReadLink:
return new(ReadLinkPacket), nil
case PacketTypeSymlink:
return new(SymlinkPacket), nil
case PacketTypeExtended:
return new(ExtendedPacket), nil
default:
return nil, fmt.Errorf("unexpected request packet type: %v", typ)
}
}

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// ClosePacket defines the SSH_FXP_CLOSE packet. // ClosePacket defines the SSH_FXP_CLOSE packet.
type ClosePacket struct { type ClosePacket struct {
@ -27,18 +27,18 @@ func (p *ClosePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *ClosePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *ClosePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = ClosePacket{
return err Handle: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// ReadPacket defines the SSH_FXP_READ packet. // ReadPacket defines the SSH_FXP_READ packet.
type ReadPacket struct { type ReadPacket struct {
Handle string Handle string
Offset uint64 Offset uint64
Len uint32 Length uint32
} }
// Type returns the SSH_FXP_xy value associated with this packet type. // Type returns the SSH_FXP_xy value associated with this packet type.
@ -58,7 +58,7 @@ func (p *ReadPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
buf.StartPacket(PacketTypeRead, reqid) buf.StartPacket(PacketTypeRead, reqid)
buf.AppendString(p.Handle) buf.AppendString(p.Handle)
buf.AppendUint64(p.Offset) buf.AppendUint64(p.Offset)
buf.AppendUint32(p.Len) buf.AppendUint32(p.Length)
return buf.Packet(payload) return buf.Packet(payload)
} }
@ -66,19 +66,13 @@ func (p *ReadPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *ReadPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *ReadPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = ReadPacket{
return err Handle: buf.ConsumeString(),
Offset: buf.ConsumeUint64(),
Length: buf.ConsumeUint32(),
} }
if p.Offset, err = buf.ConsumeUint64(); err != nil { return buf.Err
return err
}
if p.Len, err = buf.ConsumeUint32(); err != nil {
return err
}
return nil
} }
// WritePacket defines the SSH_FXP_WRITE packet. // WritePacket defines the SSH_FXP_WRITE packet.
@ -121,26 +115,13 @@ func (p *WritePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// //
// This means this _does not_ alias any of the data buffer that is passed in. // This means this _does not_ alias any of the data buffer that is passed in.
func (p *WritePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *WritePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = WritePacket{
return err Handle: buf.ConsumeString(),
Offset: buf.ConsumeUint64(),
Data: buf.ConsumeByteSliceCopy(p.Data),
} }
if p.Offset, err = buf.ConsumeUint64(); err != nil { return buf.Err
return err
}
data, err := buf.ConsumeByteSlice()
if err != nil {
return err
}
if len(p.Data) < len(data) {
p.Data = make([]byte, len(data))
}
n := copy(p.Data, data)
p.Data = p.Data[:n]
return nil
} }
// FStatPacket defines the SSH_FXP_FSTAT packet. // FStatPacket defines the SSH_FXP_FSTAT packet.
@ -170,11 +151,11 @@ func (p *FStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *FStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *FStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = FStatPacket{
return err Handle: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// FSetstatPacket defines the SSH_FXP_FSETSTAT packet. // FSetstatPacket defines the SSH_FXP_FSETSTAT packet.
@ -207,8 +188,8 @@ func (p *FSetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *FSetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *FSetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = FSetstatPacket{
return err Handle: buf.ConsumeString(),
} }
return p.Attrs.UnmarshalFrom(buf) return p.Attrs.UnmarshalFrom(buf)
@ -241,9 +222,9 @@ func (p *ReadDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *ReadDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *ReadDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = ReadDirPacket{
return err Handle: buf.ConsumeString(),
} }
return nil return buf.Err
} }

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// InitPacket defines the SSH_FXP_INIT packet. // InitPacket defines the SSH_FXP_INIT packet.
type InitPacket struct { type InitPacket struct {
@ -33,8 +33,8 @@ func (p *InitPacket) MarshalBinary() ([]byte, error) {
func (p *InitPacket) UnmarshalBinary(data []byte) (err error) { func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
buf := NewBuffer(data) buf := NewBuffer(data)
if p.Version, err = buf.ConsumeUint32(); err != nil { *p = InitPacket{
return err Version: buf.ConsumeUint32(),
} }
for buf.Len() > 0 { for buf.Len() > 0 {
@ -46,7 +46,7 @@ func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
p.Extensions = append(p.Extensions, &ext) p.Extensions = append(p.Extensions, &ext)
} }
return nil return buf.Err
} }
// VersionPacket defines the SSH_FXP_VERSION packet. // VersionPacket defines the SSH_FXP_VERSION packet.
@ -82,8 +82,8 @@ func (p *VersionPacket) MarshalBinary() ([]byte, error) {
func (p *VersionPacket) UnmarshalBinary(data []byte) (err error) { func (p *VersionPacket) UnmarshalBinary(data []byte) (err error) {
buf := NewBuffer(data) buf := NewBuffer(data)
if p.Version, err = buf.ConsumeUint32(); err != nil { *p = VersionPacket{
return err Version: buf.ConsumeUint32(),
} }
for buf.Len() > 0 { for buf.Len() > 0 {

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// SSH_FXF_* flags. // SSH_FXF_* flags.
const ( const (
@ -43,12 +43,9 @@ func (p *OpenPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *OpenPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *OpenPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Filename, err = buf.ConsumeString(); err != nil { *p = OpenPacket{
return err Filename: buf.ConsumeString(),
} PFlags: buf.ConsumeUint32(),
if p.PFlags, err = buf.ConsumeUint32(); err != nil {
return err
} }
return p.Attrs.UnmarshalFrom(buf) return p.Attrs.UnmarshalFrom(buf)
@ -81,9 +78,9 @@ func (p *OpenDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *OpenDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *OpenDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = OpenDirPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }

View file

@ -1,59 +1,13 @@
package filexfer package sshfx
import ( import (
"errors" "errors"
"fmt"
"io" "io"
) )
// smallBufferSize is an initial allocation minimal capacity. // smallBufferSize is an initial allocation minimal capacity.
const smallBufferSize = 64 const smallBufferSize = 64
func newPacketFromType(typ PacketType) (Packet, error) {
switch typ {
case PacketTypeOpen:
return new(OpenPacket), nil
case PacketTypeClose:
return new(ClosePacket), nil
case PacketTypeRead:
return new(ReadPacket), nil
case PacketTypeWrite:
return new(WritePacket), nil
case PacketTypeLStat:
return new(LStatPacket), nil
case PacketTypeFStat:
return new(FStatPacket), nil
case PacketTypeSetstat:
return new(SetstatPacket), nil
case PacketTypeFSetstat:
return new(FSetstatPacket), nil
case PacketTypeOpenDir:
return new(OpenDirPacket), nil
case PacketTypeReadDir:
return new(ReadDirPacket), nil
case PacketTypeRemove:
return new(RemovePacket), nil
case PacketTypeMkdir:
return new(MkdirPacket), nil
case PacketTypeRmdir:
return new(RmdirPacket), nil
case PacketTypeRealPath:
return new(RealPathPacket), nil
case PacketTypeStat:
return new(StatPacket), nil
case PacketTypeRename:
return new(RenamePacket), nil
case PacketTypeReadLink:
return new(ReadLinkPacket), nil
case PacketTypeSymlink:
return new(SymlinkPacket), nil
case PacketTypeExtended:
return new(ExtendedPacket), nil
default:
return nil, fmt.Errorf("unexpected request packet type: %v", typ)
}
}
// RawPacket implements the general packet format from draft-ietf-secsh-filexfer-02 // RawPacket implements the general packet format from draft-ietf-secsh-filexfer-02
// //
// RawPacket is intended for use in clients receiving responses, // RawPacket is intended for use in clients receiving responses,
@ -63,7 +17,7 @@ func newPacketFromType(typ PacketType) (Packet, error) {
// For servers expecting to receive arbitrary request packet types, // For servers expecting to receive arbitrary request packet types,
// use RequestPacket. // use RequestPacket.
// //
// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3 // Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
type RawPacket struct { type RawPacket struct {
PacketType PacketType PacketType PacketType
RequestID uint32 RequestID uint32
@ -110,19 +64,14 @@ func (p *RawPacket) MarshalBinary() ([]byte, error) {
// The Data field will alias the passed in Buffer, // The Data field will alias the passed in Buffer,
// so the buffer passed in should not be reused before RawPacket.Reset(). // so the buffer passed in should not be reused before RawPacket.Reset().
func (p *RawPacket) UnmarshalFrom(buf *Buffer) error { func (p *RawPacket) UnmarshalFrom(buf *Buffer) error {
typ, err := buf.ConsumeUint8() *p = RawPacket{
if err != nil { PacketType: PacketType(buf.ConsumeUint8()),
return err RequestID: buf.ConsumeUint32(),
}
p.PacketType = PacketType(typ)
if p.RequestID, err = buf.ConsumeUint32(); err != nil {
return err
} }
p.Data = *buf p.Data = *buf
return nil
return buf.Err
} }
// UnmarshalBinary decodes a full raw packet out of the given data. // UnmarshalBinary decodes a full raw packet out of the given data.
@ -225,7 +174,7 @@ func (p *RawPacket) ReadFrom(r io.Reader, b []byte, maxPacketLength uint32) erro
// where automatic unmarshaling of the packet body does not make sense, // where automatic unmarshaling of the packet body does not make sense,
// use RawPacket. // use RawPacket.
// //
// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3 // Defined in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-3
type RequestPacket struct { type RequestPacket struct {
RequestID uint32 RequestID uint32
@ -268,18 +217,19 @@ func (p *RequestPacket) MarshalBinary() ([]byte, error) {
// The Request field may alias the passed in Buffer, (e.g. SSH_FXP_WRITE), // The Request field may alias the passed in Buffer, (e.g. SSH_FXP_WRITE),
// so the buffer passed in should not be reused before RequestPacket.Reset(). // so the buffer passed in should not be reused before RequestPacket.Reset().
func (p *RequestPacket) UnmarshalFrom(buf *Buffer) error { func (p *RequestPacket) UnmarshalFrom(buf *Buffer) error {
typ, err := buf.ConsumeUint8() typ := PacketType(buf.ConsumeUint8())
if buf.Err != nil {
return buf.Err
}
req, err := newPacketFromType(typ)
if err != nil { if err != nil {
return err return err
} }
p.Request, err = newPacketFromType(PacketType(typ)) *p = RequestPacket{
if err != nil { RequestID: buf.ConsumeUint32(),
return err Request: req,
}
if p.RequestID, err = buf.ConsumeUint32(); err != nil {
return err
} }
return p.Request.UnmarshalPacketBody(buf) return p.Request.UnmarshalPacketBody(buf)

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// LStatPacket defines the SSH_FXP_LSTAT packet. // LStatPacket defines the SSH_FXP_LSTAT packet.
type LStatPacket struct { type LStatPacket struct {
@ -27,11 +27,11 @@ func (p *LStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *LStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *LStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = LStatPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// SetstatPacket defines the SSH_FXP_SETSTAT packet. // SetstatPacket defines the SSH_FXP_SETSTAT packet.
@ -64,8 +64,8 @@ func (p *SetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *SetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *SetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = SetstatPacket{
return err Path: buf.ConsumeString(),
} }
return p.Attrs.UnmarshalFrom(buf) return p.Attrs.UnmarshalFrom(buf)
@ -98,11 +98,11 @@ func (p *RemovePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *RemovePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *RemovePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = RemovePacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// MkdirPacket defines the SSH_FXP_MKDIR packet. // MkdirPacket defines the SSH_FXP_MKDIR packet.
@ -135,8 +135,8 @@ func (p *MkdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *MkdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *MkdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = MkdirPacket{
return err Path: buf.ConsumeString(),
} }
return p.Attrs.UnmarshalFrom(buf) return p.Attrs.UnmarshalFrom(buf)
@ -169,11 +169,11 @@ func (p *RmdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []b
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *RmdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *RmdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = RmdirPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// RealPathPacket defines the SSH_FXP_REALPATH packet. // RealPathPacket defines the SSH_FXP_REALPATH packet.
@ -203,11 +203,11 @@ func (p *RealPathPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *RealPathPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *RealPathPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = RealPathPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// StatPacket defines the SSH_FXP_STAT packet. // StatPacket defines the SSH_FXP_STAT packet.
@ -237,11 +237,11 @@ func (p *StatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *StatPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *StatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = StatPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// RenamePacket defines the SSH_FXP_RENAME packet. // RenamePacket defines the SSH_FXP_RENAME packet.
@ -274,15 +274,12 @@ func (p *RenamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *RenamePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *RenamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.OldPath, err = buf.ConsumeString(); err != nil { *p = RenamePacket{
return err OldPath: buf.ConsumeString(),
NewPath: buf.ConsumeString(),
} }
if p.NewPath, err = buf.ConsumeString(); err != nil { return buf.Err
return err
}
return nil
} }
// ReadLinkPacket defines the SSH_FXP_READLINK packet. // ReadLinkPacket defines the SSH_FXP_READLINK packet.
@ -312,18 +309,18 @@ func (p *ReadLinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *ReadLinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *ReadLinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Path, err = buf.ConsumeString(); err != nil { *p = ReadLinkPacket{
return err Path: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// SymlinkPacket defines the SSH_FXP_SYMLINK packet. // SymlinkPacket defines the SSH_FXP_SYMLINK packet.
// //
// The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed. // The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed.
// Unfortunately, the reversal was not noticed until the server was widely deployed. // Unfortunately, the reversal was not noticed until the server was widely deployed.
// Covered in Section 3.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL // Covered in Section 4.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
type SymlinkPacket struct { type SymlinkPacket struct {
LinkPath string LinkPath string
TargetPath string TargetPath string
@ -355,14 +352,11 @@ func (p *SymlinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload [
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *SymlinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *SymlinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
// Arguments were inadvertently reversed. *p = SymlinkPacket{
if p.TargetPath, err = buf.ConsumeString(); err != nil { // Arguments were inadvertently reversed.
return err TargetPath: buf.ConsumeString(),
LinkPath: buf.ConsumeString(),
} }
if p.LinkPath, err = buf.ConsumeString(); err != nil { return buf.Err
return err
}
return nil
} }

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
// FileMode represents a files mode and permission bits. // FileMode represents a files mode and permission bits.
// The bits are defined according to POSIX standards, // The bits are defined according to POSIX standards,

View file

@ -1,4 +1,4 @@
package filexfer package sshfx
import ( import (
"fmt" "fmt"
@ -6,7 +6,7 @@ import (
// StatusPacket defines the SSH_FXP_STATUS packet. // StatusPacket defines the SSH_FXP_STATUS packet.
// //
// Specified in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7 // Specified in https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-7
type StatusPacket struct { type StatusPacket struct {
StatusCode Status StatusCode Status
ErrorMessage string ErrorMessage string
@ -19,7 +19,7 @@ func (p *StatusPacket) Error() string {
return "sftp: " + p.StatusCode.String() return "sftp: " + p.StatusCode.String()
} }
return fmt.Sprintf("sftp: %q (%s)", p.ErrorMessage, p.StatusCode) return fmt.Sprintf("sftp: %s: %q", p.StatusCode, p.ErrorMessage)
} }
// Is returns true if target is a StatusPacket with the same StatusCode, // Is returns true if target is a StatusPacket with the same StatusCode,
@ -57,21 +57,13 @@ func (p *StatusPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *StatusPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *StatusPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
statusCode, err := buf.ConsumeUint32() *p = StatusPacket{
if err != nil { StatusCode: Status(buf.ConsumeUint32()),
return err ErrorMessage: buf.ConsumeString(),
} LanguageTag: buf.ConsumeString(),
p.StatusCode = Status(statusCode)
if p.ErrorMessage, err = buf.ConsumeString(); err != nil {
return err
} }
if p.LanguageTag, err = buf.ConsumeString(); err != nil { return buf.Err
return err
}
return nil
} }
// HandlePacket defines the SSH_FXP_HANDLE packet. // HandlePacket defines the SSH_FXP_HANDLE packet.
@ -101,11 +93,11 @@ func (p *HandlePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *HandlePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *HandlePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
if p.Handle, err = buf.ConsumeString(); err != nil { *p = HandlePacket{
return err Handle: buf.ConsumeString(),
} }
return nil return buf.Err
} }
// DataPacket defines the SSH_FXP_DATA packet. // DataPacket defines the SSH_FXP_DATA packet.
@ -143,18 +135,11 @@ func (p *DataPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
// //
// This means this _does not_ alias any of the data buffer that is passed in. // This means this _does not_ alias any of the data buffer that is passed in.
func (p *DataPacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *DataPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
data, err := buf.ConsumeByteSlice() *p = DataPacket{
if err != nil { Data: buf.ConsumeByteSliceCopy(p.Data),
return err
} }
if len(p.Data) < len(data) { return buf.Err
p.Data = make([]byte, len(data))
}
n := copy(p.Data, data)
p.Data = p.Data[:n]
return nil
} }
// NamePacket defines the SSH_FXP_NAME packet. // NamePacket defines the SSH_FXP_NAME packet.
@ -193,14 +178,16 @@ func (p *NamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []by
// UnmarshalPacketBody unmarshals the packet body from the given Buffer. // UnmarshalPacketBody unmarshals the packet body from the given Buffer.
// It is assumed that the uint32(request-id) has already been consumed. // It is assumed that the uint32(request-id) has already been consumed.
func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) { func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
count, err := buf.ConsumeUint32() count := buf.ConsumeCount()
if err != nil { if buf.Err != nil {
return err return buf.Err
} }
p.Entries = make([]*NameEntry, 0, count) *p = NamePacket{
Entries: make([]*NameEntry, 0, count),
}
for i := uint32(0); i < count; i++ { for i := 0; i < count; i++ {
var e NameEntry var e NameEntry
if err := e.UnmarshalFrom(buf); err != nil { if err := e.UnmarshalFrom(buf); err != nil {
return err return err
@ -209,7 +196,7 @@ func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
p.Entries = append(p.Entries, &e) p.Entries = append(p.Entries, &e)
} }
return nil return buf.Err
} }
// AttrsPacket defines the SSH_FXP_ATTRS packet. // AttrsPacket defines the SSH_FXP_ATTRS packet.

View file

@ -60,6 +60,13 @@ func runLs(idLookup NameLookupFileLister, dirent os.FileInfo) string {
uid = lsFormatID(sys.UID) uid = lsFormatID(sys.UID)
gid = lsFormatID(sys.GID) gid = lsFormatID(sys.GID)
default: default:
if fiExt, ok := dirent.(FileInfoUidGid); ok {
uid = lsFormatID(fiExt.Uid())
gid = lsFormatID(fiExt.Gid())
break
}
numLinks, uid, gid = lsLinksUIDGID(dirent) numLinks, uid, gid = lsLinksUIDGID(dirent)
} }

View file

@ -1,3 +1,4 @@
//go:build plan9
// +build plan9 // +build plan9
package sftp package sftp

View file

@ -1,3 +1,4 @@
//go:build windows || android
// +build windows android // +build windows android
package sftp package sftp

View file

@ -1,3 +1,4 @@
//go:build aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris || js
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js // +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
package sftp package sftp

View file

@ -40,7 +40,7 @@ func newPktMgr(sender packetSender) *packetManager {
return s return s
} }
//// packet ordering // // packet ordering
func (s *packetManager) newOrderID() uint32 { func (s *packetManager) newOrderID() uint32 {
s.packetCount++ s.packetCount++
return s.packetCount return s.packetCount
@ -89,7 +89,7 @@ func (o orderedPackets) Sort() {
}) })
} }
//// packet registry // // packet registry
// register incoming packets to be handled // register incoming packets to be handled
func (s *packetManager) incomingPacket(pkt orderedRequest) { func (s *packetManager) incomingPacket(pkt orderedRequest) {
s.working.Add(1) s.working.Add(1)

View file

@ -31,7 +31,7 @@ type notReadOnly interface {
notReadOnly() notReadOnly()
} }
//// define types by adding methods // // define types by adding methods
// hasPath // hasPath
func (p *sshFxpLstatPacket) getPath() string { return p.Path } func (p *sshFxpLstatPacket) getPath() string { return p.Path }
func (p *sshFxpStatPacket) getPath() string { return p.Path } func (p *sshFxpStatPacket) getPath() string { return p.Path }

25
vendor/github.com/pkg/sftp/packet.go generated vendored
View file

@ -71,6 +71,15 @@ func marshalFileInfo(b []byte, fi os.FileInfo) []byte {
b = marshalUint32(b, fileStat.Mtime) b = marshalUint32(b, fileStat.Mtime)
} }
if flags&sshFileXferAttrExtended != 0 {
b = marshalUint32(b, uint32(len(fileStat.Extended)))
for _, attr := range fileStat.Extended {
b = marshalString(b, attr.ExtType)
b = marshalString(b, attr.ExtData)
}
}
return b return b
} }
@ -281,6 +290,11 @@ func recvPacket(r io.Reader, alloc *allocator, orderID uint32) (uint8, []byte, e
b = make([]byte, length) b = make([]byte, length)
} }
if _, err := io.ReadFull(r, b[:length]); err != nil { if _, err := io.ReadFull(r, b[:length]); err != nil {
// ReadFull only returns EOF if it has read no bytes.
// In this case, that means a partial packet, and thus unexpected.
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
debug("recv packet %d bytes: err %v", length, err) debug("recv packet %d bytes: err %v", length, err)
return 0, nil, err return 0, nil, err
} }
@ -522,7 +536,12 @@ func (p *sshFxpRmdirPacket) UnmarshalBinary(b []byte) error {
} }
type sshFxpSymlinkPacket struct { type sshFxpSymlinkPacket struct {
ID uint32 ID uint32
// The order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed.
// Unfortunately, the reversal was not noticed until the server was widely deployed.
// Covered in Section 4.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
Targetpath string Targetpath string
Linkpath string Linkpath string
} }
@ -1242,7 +1261,7 @@ func (p *sshFxpExtendedPacketPosixRename) UnmarshalBinary(b []byte) error {
} }
func (p *sshFxpExtendedPacketPosixRename) respond(s *Server) responsePacket { func (p *sshFxpExtendedPacketPosixRename) respond(s *Server) responsePacket {
err := os.Rename(p.Oldpath, p.Newpath) err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath))
return statusFromError(p.ID, err) return statusFromError(p.ID, err)
} }
@ -1271,6 +1290,6 @@ func (p *sshFxpExtendedPacketHardlink) UnmarshalBinary(b []byte) error {
} }
func (p *sshFxpExtendedPacketHardlink) respond(s *Server) responsePacket { func (p *sshFxpExtendedPacketHardlink) respond(s *Server) responsePacket {
err := os.Link(p.Oldpath, p.Newpath) err := os.Link(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath))
return statusFromError(p.ID, err) return statusFromError(p.ID, err)
} }

View file

@ -1,3 +1,4 @@
//go:build !debug
// +build !debug // +build !debug
package sftp package sftp

View file

@ -391,21 +391,6 @@ func (fs *root) Filelist(r *Request) (ListerAt, error) {
return nil, err return nil, err
} }
return listerat{file}, nil return listerat{file}, nil
case "Readlink":
symlink, err := fs.readlink(r.Filepath)
if err != nil {
return nil, err
}
// SFTP-v2: The server will respond with a SSH_FXP_NAME packet containing only
// one name and a dummy attributes value.
return listerat{
&memFile{
name: symlink,
err: os.ErrNotExist, // prevent accidental use as a reader/writer.
},
}, nil
} }
return nil, errors.New("unsupported") return nil, errors.New("unsupported")
@ -434,7 +419,7 @@ func (fs *root) readdir(pathname string) ([]os.FileInfo, error) {
return files, nil return files, nil
} }
func (fs *root) readlink(pathname string) (string, error) { func (fs *root) Readlink(pathname string) (string, error) {
file, err := fs.lfetch(pathname) file, err := fs.lfetch(pathname)
if err != nil { if err != nil {
return "", err return "", err
@ -464,19 +449,10 @@ func (fs *root) Lstat(r *Request) (ListerAt, error) {
return listerat{file}, nil return listerat{file}, nil
} }
// implements RealpathFileLister interface
func (fs *root) Realpath(p string) string {
if fs.startDirectory == "" || fs.startDirectory == "/" {
return cleanPath(p)
}
return cleanPathWithBase(fs.startDirectory, p)
}
// In memory file-system-y thing that the Hanlders live on // In memory file-system-y thing that the Hanlders live on
type root struct { type root struct {
rootFile *memFile rootFile *memFile
mockErr error mockErr error
startDirectory string
mu sync.Mutex mu sync.Mutex
files map[string]*memFile files map[string]*memFile
@ -534,8 +510,8 @@ func (fs *root) exists(path string) bool {
return err != os.ErrNotExist return err != os.ErrNotExist
} }
func (fs *root) fetch(path string) (*memFile, error) { func (fs *root) fetch(pathname string) (*memFile, error) {
file, err := fs.lfetch(path) file, err := fs.lfetch(pathname)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -546,7 +522,12 @@ func (fs *root) fetch(path string) (*memFile, error) {
return nil, errTooManySymlinks return nil, errTooManySymlinks
} }
file, err = fs.lfetch(file.symlink) linkTarget := file.symlink
if !path.IsAbs(linkTarget) {
linkTarget = path.Join(path.Dir(file.name), linkTarget)
}
file, err = fs.lfetch(linkTarget)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -74,6 +74,11 @@ type StatVFSFileCmder interface {
// FileLister should return an object that fulfils the ListerAt interface // FileLister should return an object that fulfils the ListerAt interface
// Note in cases of an error, the error text will be sent to the client. // Note in cases of an error, the error text will be sent to the client.
// Called for Methods: List, Stat, Readlink // Called for Methods: List, Stat, Readlink
//
// Since Filelist returns an os.FileInfo, this can make it non-ideal for implementing Readlink.
// This is because the Name receiver method defined by that interface defines that it should only return the base name.
// However, Readlink is required to be capable of returning essentially any arbitrary valid path relative or absolute.
// In order to implement this more expressive requirement, implement [ReadlinkFileLister] which will then be used instead.
type FileLister interface { type FileLister interface {
Filelist(*Request) (ListerAt, error) Filelist(*Request) (ListerAt, error)
} }
@ -87,10 +92,33 @@ type LstatFileLister interface {
} }
// RealPathFileLister is a FileLister that implements the Realpath method. // RealPathFileLister is a FileLister that implements the Realpath method.
// We use "/" as start directory for relative paths, implementing this // The built-in RealPath implementation does not resolve symbolic links.
// interface you can customize the start directory. // By implementing this interface you can customize the returned path
// and, for example, resolve symbolinc links if needed for your use case.
// You have to return an absolute POSIX path. // You have to return an absolute POSIX path.
//
// Up to v1.13.5 the signature for the RealPath method was:
//
// # RealPath(string) string
//
// we have added a legacyRealPathFileLister that implements the old method
// to ensure that your code does not break.
// You should use the new method signature to avoid future issues
type RealPathFileLister interface { type RealPathFileLister interface {
FileLister
RealPath(string) (string, error)
}
// ReadlinkFileLister is a FileLister that implements the Readlink method.
// By implementing the Readlink method, it is possible to return any arbitrary valid path relative or absolute.
// This allows giving a better response than via the default FileLister (which is limited to os.FileInfo, whose Name method should only return the base name of a file)
type ReadlinkFileLister interface {
FileLister
Readlink(string) (string, error)
}
// This interface is here for backward compatibility only
type legacyRealPathFileLister interface {
FileLister FileLister
RealPath(string) string RealPath(string) string
} }
@ -103,11 +131,19 @@ type NameLookupFileLister interface {
LookupGroupName(string) string LookupGroupName(string) string
} }
// ListerAt does for file lists what io.ReaderAt does for files. // ListerAt does for file lists what io.ReaderAt does for files, i.e. a []os.FileInfo buffer is passed to the ListAt function
// ListAt should return the number of entries copied and an io.EOF // and the entries that are populated in the buffer will be passed to the client.
// error if at end of list. This is testable by comparing how many you //
// copied to how many could be copied (eg. n < len(ls) below). // ListAt should return the number of entries copied and an io.EOF error if at end of list.
// This is testable by comparing how many you copied to how many could be copied (eg. n < len(ls) below).
// The copy() builtin is best for the copying. // The copy() builtin is best for the copying.
//
// Uid and gid information will on unix systems be retrieved from [os.FileInfo.Sys]
// if this function returns a [syscall.Stat_t] when called on a populated entry.
// Alternatively, if the entry implements [FileInfoUidGid], it will be used for uid and gid information.
//
// If a populated entry implements [FileInfoExtendedData], extended attributes will also be returned to the client.
//
// Note in cases of an error, the error text will be sent to the client. // Note in cases of an error, the error text will be sent to the client.
type ListerAt interface { type ListerAt interface {
ListAt([]os.FileInfo, int64) (int, error) ListAt([]os.FileInfo, int64) (int, error)

View file

@ -1,10 +1,9 @@
//go:build plan9
// +build plan9 // +build plan9
package sftp package sftp
import ( import (
"path"
"path/filepath"
"syscall" "syscall"
) )
@ -15,20 +14,3 @@ func fakeFileInfoSys() interface{} {
func testOsSys(sys interface{}) error { func testOsSys(sys interface{}) error {
return nil return nil
} }
func toLocalPath(p string) string {
lp := filepath.FromSlash(p)
if path.IsAbs(p) {
tmp := lp[1:]
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes is absolute,
// then we have a filepath encoded with a prefix '/'.
// e.g. "/#s/boot" to "#s/boot"
return tmp
}
}
return lp
}

View file

@ -28,7 +28,7 @@ then sends to the client.
Handler for "Put" method and returns an io.Writer for the file which the server Handler for "Put" method and returns an io.Writer for the file which the server
then writes the uploaded file to. The file opening "pflags" are currently then writes the uploaded file to. The file opening "pflags" are currently
preserved in the Request.Flags field as a 32bit bitmask value. See the [SFTP preserved in the Request.Flags field as a 32bit bitmask value. See the [SFTP
spec](https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.3) for spec](https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt#section-6.3) for
details. details.
### Filecmd(*Request) error ### Filecmd(*Request) error

View file

@ -27,6 +27,8 @@ type RequestServer struct {
*serverConn *serverConn
pktMgr *packetManager pktMgr *packetManager
startDirectory string
mu sync.RWMutex mu sync.RWMutex
handleCount int handleCount int
openRequests map[string]*Request openRequests map[string]*Request
@ -47,6 +49,14 @@ func WithRSAllocator() RequestServerOption {
} }
} }
// WithStartDirectory sets a start directory to use as base for relative paths.
// If unset the default is "/"
func WithStartDirectory(startDirectory string) RequestServerOption {
return func(rs *RequestServer) {
rs.startDirectory = cleanPath(startDirectory)
}
}
// NewRequestServer creates/allocates/returns new RequestServer. // NewRequestServer creates/allocates/returns new RequestServer.
// Normally there will be one server per user-session. // Normally there will be one server per user-session.
func NewRequestServer(rwc io.ReadWriteCloser, h Handlers, options ...RequestServerOption) *RequestServer { func NewRequestServer(rwc io.ReadWriteCloser, h Handlers, options ...RequestServerOption) *RequestServer {
@ -62,6 +72,8 @@ func NewRequestServer(rwc io.ReadWriteCloser, h Handlers, options ...RequestServ
serverConn: svrConn, serverConn: svrConn,
pktMgr: newPktMgr(svrConn), pktMgr: newPktMgr(svrConn),
startDirectory: "/",
openRequests: make(map[string]*Request), openRequests: make(map[string]*Request),
} }
@ -207,14 +219,23 @@ func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedR
rpkt = statusFromError(pkt.ID, rs.closeRequest(handle)) rpkt = statusFromError(pkt.ID, rs.closeRequest(handle))
case *sshFxpRealpathPacket: case *sshFxpRealpathPacket:
var realPath string var realPath string
if realPather, ok := rs.Handlers.FileList.(RealPathFileLister); ok { var err error
realPath = realPather.RealPath(pkt.getPath())
} else { switch pather := rs.Handlers.FileList.(type) {
realPath = cleanPath(pkt.getPath()) case RealPathFileLister:
realPath, err = pather.RealPath(pkt.getPath())
case legacyRealPathFileLister:
realPath = pather.RealPath(pkt.getPath())
default:
realPath = cleanPathWithBase(rs.startDirectory, pkt.getPath())
}
if err != nil {
rpkt = statusFromError(pkt.ID, err)
} else {
rpkt = cleanPacketPath(pkt, realPath)
} }
rpkt = cleanPacketPath(pkt, realPath)
case *sshFxpOpendirPacket: case *sshFxpOpendirPacket:
request := requestFromPacket(ctx, pkt) request := requestFromPacket(ctx, pkt, rs.startDirectory)
handle := rs.nextRequest(request) handle := rs.nextRequest(request)
rpkt = request.opendir(rs.Handlers, pkt) rpkt = request.opendir(rs.Handlers, pkt)
if _, ok := rpkt.(*sshFxpHandlePacket); !ok { if _, ok := rpkt.(*sshFxpHandlePacket); !ok {
@ -222,7 +243,7 @@ func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedR
rs.closeRequest(handle) rs.closeRequest(handle)
} }
case *sshFxpOpenPacket: case *sshFxpOpenPacket:
request := requestFromPacket(ctx, pkt) request := requestFromPacket(ctx, pkt, rs.startDirectory)
handle := rs.nextRequest(request) handle := rs.nextRequest(request)
rpkt = request.open(rs.Handlers, pkt) rpkt = request.open(rs.Handlers, pkt)
if _, ok := rpkt.(*sshFxpHandlePacket); !ok { if _, ok := rpkt.(*sshFxpHandlePacket); !ok {
@ -235,7 +256,10 @@ func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedR
if !ok { if !ok {
rpkt = statusFromError(pkt.ID, EBADF) rpkt = statusFromError(pkt.ID, EBADF)
} else { } else {
request = NewRequest("Stat", request.Filepath) request = &Request{
Method: "Stat",
Filepath: cleanPathWithBase(rs.startDirectory, request.Filepath),
}
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
} }
case *sshFxpFsetstatPacket: case *sshFxpFsetstatPacket:
@ -244,15 +268,24 @@ func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedR
if !ok { if !ok {
rpkt = statusFromError(pkt.ID, EBADF) rpkt = statusFromError(pkt.ID, EBADF)
} else { } else {
request = NewRequest("Setstat", request.Filepath) request = &Request{
Method: "Setstat",
Filepath: cleanPathWithBase(rs.startDirectory, request.Filepath),
}
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
} }
case *sshFxpExtendedPacketPosixRename: case *sshFxpExtendedPacketPosixRename:
request := NewRequest("PosixRename", pkt.Oldpath) request := &Request{
request.Target = pkt.Newpath Method: "PosixRename",
Filepath: cleanPathWithBase(rs.startDirectory, pkt.Oldpath),
Target: cleanPathWithBase(rs.startDirectory, pkt.Newpath),
}
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
case *sshFxpExtendedPacketStatVFS: case *sshFxpExtendedPacketStatVFS:
request := NewRequest("StatVFS", pkt.Path) request := &Request{
Method: "StatVFS",
Filepath: cleanPathWithBase(rs.startDirectory, pkt.Path),
}
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
case hasHandle: case hasHandle:
handle := pkt.getHandle() handle := pkt.getHandle()
@ -263,7 +296,7 @@ func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedR
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
} }
case hasPath: case hasPath:
request := requestFromPacket(ctx, pkt) request := requestFromPacket(ctx, pkt, rs.startDirectory)
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID) rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
request.close() request.close()
default: default:

View file

@ -1,3 +1,4 @@
//go:build !windows && !plan9
// +build !windows,!plan9 // +build !windows,!plan9
package sftp package sftp
@ -21,7 +22,3 @@ func testOsSys(sys interface{}) error {
} }
return nil return nil
} }
func toLocalPath(p string) string {
return p
}

View file

@ -168,9 +168,11 @@ func (r *Request) copy() *Request {
} }
// New Request initialized based on packet data // New Request initialized based on packet data
func requestFromPacket(ctx context.Context, pkt hasPath) *Request { func requestFromPacket(ctx context.Context, pkt hasPath, baseDir string) *Request {
method := requestMethod(pkt) request := &Request{
request := NewRequest(method, pkt.getPath()) Method: requestMethod(pkt),
Filepath: cleanPathWithBase(baseDir, pkt.getPath()),
}
request.ctx, request.cancelCtx = context.WithCancel(ctx) request.ctx, request.cancelCtx = context.WithCancel(ctx)
switch p := pkt.(type) { switch p := pkt.(type) {
@ -180,13 +182,14 @@ func requestFromPacket(ctx context.Context, pkt hasPath) *Request {
request.Flags = p.Flags request.Flags = p.Flags
request.Attrs = p.Attrs.([]byte) request.Attrs = p.Attrs.([]byte)
case *sshFxpRenamePacket: case *sshFxpRenamePacket:
request.Target = cleanPath(p.Newpath) request.Target = cleanPathWithBase(baseDir, p.Newpath)
case *sshFxpSymlinkPacket: case *sshFxpSymlinkPacket:
// NOTE: given a POSIX compliant signature: symlink(target, linkpath string) // NOTE: given a POSIX compliant signature: symlink(target, linkpath string)
// this makes Request.Target the linkpath, and Request.Filepath the target. // this makes Request.Target the linkpath, and Request.Filepath the target.
request.Target = cleanPath(p.Linkpath) request.Target = cleanPathWithBase(baseDir, p.Linkpath)
request.Filepath = p.Targetpath
case *sshFxpExtendedPacketHardlink: case *sshFxpExtendedPacketHardlink:
request.Target = cleanPath(p.Newpath) request.Target = cleanPathWithBase(baseDir, p.Newpath)
} }
return request return request
} }
@ -292,7 +295,12 @@ func (r *Request) call(handlers Handlers, pkt requestPacket, alloc *allocator, o
return filecmd(handlers.FileCmd, r, pkt) return filecmd(handlers.FileCmd, r, pkt)
case "List": case "List":
return filelist(handlers.FileList, r, pkt) return filelist(handlers.FileList, r, pkt)
case "Stat", "Lstat", "Readlink": case "Stat", "Lstat":
return filestat(handlers.FileList, r, pkt)
case "Readlink":
if readlinkFileLister, ok := handlers.FileList.(ReadlinkFileLister); ok {
return readlink(readlinkFileLister, r, pkt)
}
return filestat(handlers.FileList, r, pkt) return filestat(handlers.FileList, r, pkt)
default: default:
return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method)) return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method))
@ -596,6 +604,23 @@ func filestat(h FileLister, r *Request, pkt requestPacket) responsePacket {
} }
} }
func readlink(readlinkFileLister ReadlinkFileLister, r *Request, pkt requestPacket) responsePacket {
resolved, err := readlinkFileLister.Readlink(r.Filepath)
if err != nil {
return statusFromError(pkt.id(), err)
}
return &sshFxpNamePacket{
ID: pkt.id(),
NameAttrs: []*sshFxpNameAttr{
{
Name: resolved,
LongName: resolved,
Attrs: emptyFileStat,
},
},
}
}
// init attributes of request object from packet data // init attributes of request object from packet data
func requestMethod(p requestPacket) (method string) { func requestMethod(p requestPacket) (method string) {
switch p.(type) { switch p.(type) {

View file

@ -1,8 +1,6 @@
package sftp package sftp
import ( import (
"path"
"path/filepath"
"syscall" "syscall"
) )
@ -13,32 +11,3 @@ func fakeFileInfoSys() interface{} {
func testOsSys(sys interface{}) error { func testOsSys(sys interface{}) error {
return nil return nil
} }
func toLocalPath(p string) string {
lp := filepath.FromSlash(p)
if path.IsAbs(p) {
tmp := lp
for len(tmp) > 0 && tmp[0] == '\\' {
tmp = tmp[1:]
}
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes is absolute,
// then we have a filepath encoded with a prefix '/'.
// e.g. "/C:/Windows" to "C:\\Windows"
return tmp
}
tmp += "\\"
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes but with extra end slash is absolute,
// then we have a filepath encoded with a prefix '/' and a dropped '/' at the end.
// e.g. "/C:" to "C:\\"
return tmp
}
}
return lp
}

62
vendor/github.com/pkg/sftp/server.go generated vendored
View file

@ -24,7 +24,7 @@ const (
// Server is an SSH File Transfer Protocol (sftp) server. // Server is an SSH File Transfer Protocol (sftp) server.
// This is intended to provide the sftp subsystem to an ssh server daemon. // This is intended to provide the sftp subsystem to an ssh server daemon.
// This implementation currently supports most of sftp server protocol version 3, // This implementation currently supports most of sftp server protocol version 3,
// as specified at http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 // as specified at https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
type Server struct { type Server struct {
*serverConn *serverConn
debugStream io.Writer debugStream io.Writer
@ -33,6 +33,7 @@ type Server struct {
openFiles map[string]*os.File openFiles map[string]*os.File
openFilesLock sync.RWMutex openFilesLock sync.RWMutex
handleCount int handleCount int
workDir string
} }
func (svr *Server) nextHandle(f *os.File) string { func (svr *Server) nextHandle(f *os.File) string {
@ -128,6 +129,16 @@ func WithAllocator() ServerOption {
} }
} }
// WithServerWorkingDirectory sets a working directory to use as base
// for relative paths.
// If unset the default is current working directory (os.Getwd).
func WithServerWorkingDirectory(workDir string) ServerOption {
return func(s *Server) error {
s.workDir = cleanPath(workDir)
return nil
}
}
type rxPacket struct { type rxPacket struct {
pktType fxp pktType fxp
pktBytes []byte pktBytes []byte
@ -174,7 +185,7 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
case *sshFxpStatPacket: case *sshFxpStatPacket:
// stat the requested file // stat the requested file
info, err := os.Stat(toLocalPath(p.Path)) info, err := os.Stat(s.toLocalPath(p.Path))
rpkt = &sshFxpStatResponse{ rpkt = &sshFxpStatResponse{
ID: p.ID, ID: p.ID,
info: info, info: info,
@ -184,7 +195,7 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
case *sshFxpLstatPacket: case *sshFxpLstatPacket:
// stat the requested file // stat the requested file
info, err := os.Lstat(toLocalPath(p.Path)) info, err := os.Lstat(s.toLocalPath(p.Path))
rpkt = &sshFxpStatResponse{ rpkt = &sshFxpStatResponse{
ID: p.ID, ID: p.ID,
info: info, info: info,
@ -208,24 +219,24 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
case *sshFxpMkdirPacket: case *sshFxpMkdirPacket:
// TODO FIXME: ignore flags field // TODO FIXME: ignore flags field
err := os.Mkdir(toLocalPath(p.Path), 0755) err := os.Mkdir(s.toLocalPath(p.Path), 0o755)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRmdirPacket: case *sshFxpRmdirPacket:
err := os.Remove(toLocalPath(p.Path)) err := os.Remove(s.toLocalPath(p.Path))
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRemovePacket: case *sshFxpRemovePacket:
err := os.Remove(toLocalPath(p.Filename)) err := os.Remove(s.toLocalPath(p.Filename))
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRenamePacket: case *sshFxpRenamePacket:
err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath)) err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath))
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpSymlinkPacket: case *sshFxpSymlinkPacket:
err := os.Symlink(toLocalPath(p.Targetpath), toLocalPath(p.Linkpath)) err := os.Symlink(s.toLocalPath(p.Targetpath), s.toLocalPath(p.Linkpath))
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpClosePacket: case *sshFxpClosePacket:
rpkt = statusFromError(p.ID, s.closeHandle(p.Handle)) rpkt = statusFromError(p.ID, s.closeHandle(p.Handle))
case *sshFxpReadlinkPacket: case *sshFxpReadlinkPacket:
f, err := os.Readlink(toLocalPath(p.Path)) f, err := os.Readlink(s.toLocalPath(p.Path))
rpkt = &sshFxpNamePacket{ rpkt = &sshFxpNamePacket{
ID: p.ID, ID: p.ID,
NameAttrs: []*sshFxpNameAttr{ NameAttrs: []*sshFxpNameAttr{
@ -240,7 +251,7 @@ func handlePacket(s *Server, p orderedRequest) error {
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
} }
case *sshFxpRealpathPacket: case *sshFxpRealpathPacket:
f, err := filepath.Abs(toLocalPath(p.Path)) f, err := filepath.Abs(s.toLocalPath(p.Path))
f = cleanPath(f) f = cleanPath(f)
rpkt = &sshFxpNamePacket{ rpkt = &sshFxpNamePacket{
ID: p.ID, ID: p.ID,
@ -256,13 +267,14 @@ func handlePacket(s *Server, p orderedRequest) error {
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
} }
case *sshFxpOpendirPacket: case *sshFxpOpendirPacket:
p.Path = toLocalPath(p.Path) lp := s.toLocalPath(p.Path)
if stat, err := os.Stat(p.Path); err != nil { if stat, err := os.Stat(lp); err != nil {
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
} else if !stat.IsDir() { } else if !stat.IsDir() {
rpkt = statusFromError(p.ID, &os.PathError{ rpkt = statusFromError(p.ID, &os.PathError{
Path: p.Path, Err: syscall.ENOTDIR}) Path: lp, Err: syscall.ENOTDIR,
})
} else { } else {
rpkt = (&sshFxpOpenPacket{ rpkt = (&sshFxpOpenPacket{
ID: p.ID, ID: p.ID,
@ -315,7 +327,7 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
// Serve serves SFTP connections until the streams stop or the SFTP subsystem // Serve serves SFTP connections until the streams stop or the SFTP subsystem
// is stopped. // is stopped. It returns nil if the server exits cleanly.
func (svr *Server) Serve() error { func (svr *Server) Serve() error {
defer func() { defer func() {
if svr.pktMgr.alloc != nil { if svr.pktMgr.alloc != nil {
@ -341,6 +353,10 @@ func (svr *Server) Serve() error {
for { for {
pktType, pktBytes, err = svr.serverConn.recvPacket(svr.pktMgr.getNextOrderID()) pktType, pktBytes, err = svr.serverConn.recvPacket(svr.pktMgr.getNextOrderID())
if err != nil { if err != nil {
// Check whether the connection terminated cleanly in-between packets.
if err == io.EOF {
err = nil
}
// we don't care about releasing allocated pages here, the server will quit and the allocator freed // we don't care about releasing allocated pages here, the server will quit and the allocator freed
break break
} }
@ -446,7 +462,7 @@ func (p *sshFxpOpenPacket) respond(svr *Server) responsePacket {
osFlags |= os.O_EXCL osFlags |= os.O_EXCL
} }
f, err := os.OpenFile(toLocalPath(p.Path), osFlags, 0644) f, err := os.OpenFile(svr.toLocalPath(p.Path), osFlags, 0o644)
if err != nil { if err != nil {
return statusFromError(p.ID, err) return statusFromError(p.ID, err)
} }
@ -484,7 +500,7 @@ func (p *sshFxpSetstatPacket) respond(svr *Server) responsePacket {
b := p.Attrs.([]byte) b := p.Attrs.([]byte)
var err error var err error
p.Path = toLocalPath(p.Path) p.Path = svr.toLocalPath(p.Path)
debug("setstat name \"%s\"", p.Path) debug("setstat name \"%s\"", p.Path)
if (p.Flags & sshFileXferAttrSize) != 0 { if (p.Flags & sshFileXferAttrSize) != 0 {
@ -603,13 +619,15 @@ func statusFromError(id uint32, err error) *sshFxpStatusPacket {
return ret return ret
} }
switch e := err.(type) { if errors.Is(err, io.EOF) {
case fxerr: ret.StatusError.Code = sshFxEOF
return ret
}
var e fxerr
if errors.As(err, &e) {
ret.StatusError.Code = uint32(e) ret.StatusError.Code = uint32(e)
default: return ret
if e == io.EOF {
ret.StatusError.Code = sshFxEOF
}
} }
return ret return ret

27
vendor/github.com/pkg/sftp/server_plan9.go generated vendored Normal file
View file

@ -0,0 +1,27 @@
package sftp
import (
"path"
"path/filepath"
)
func (s *Server) toLocalPath(p string) string {
if s.workDir != "" && !path.IsAbs(p) {
p = path.Join(s.workDir, p)
}
lp := filepath.FromSlash(p)
if path.IsAbs(p) {
tmp := lp[1:]
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes is absolute,
// then we have a filepath encoded with a prefix '/'.
// e.g. "/#s/boot" to "#s/boot"
return tmp
}
}
return lp
}

View file

@ -1,3 +1,4 @@
//go:build darwin || linux
// +build darwin linux // +build darwin linux
// fill in statvfs structure with OS specific values // fill in statvfs structure with OS specific values

View file

@ -1,3 +1,4 @@
//go:build linux
// +build linux // +build linux
package sftp package sftp

View file

@ -1,3 +1,4 @@
//go:build !darwin && !linux && !plan9
// +build !darwin,!linux,!plan9 // +build !darwin,!linux,!plan9
package sftp package sftp

16
vendor/github.com/pkg/sftp/server_unix.go generated vendored Normal file
View file

@ -0,0 +1,16 @@
//go:build !windows && !plan9
// +build !windows,!plan9
package sftp
import (
"path"
)
func (s *Server) toLocalPath(p string) string {
if s.workDir != "" && !path.IsAbs(p) {
p = path.Join(s.workDir, p)
}
return p
}

39
vendor/github.com/pkg/sftp/server_windows.go generated vendored Normal file
View file

@ -0,0 +1,39 @@
package sftp
import (
"path"
"path/filepath"
)
func (s *Server) toLocalPath(p string) string {
if s.workDir != "" && !path.IsAbs(p) {
p = path.Join(s.workDir, p)
}
lp := filepath.FromSlash(p)
if path.IsAbs(p) {
tmp := lp
for len(tmp) > 0 && tmp[0] == '\\' {
tmp = tmp[1:]
}
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes is absolute,
// then we have a filepath encoded with a prefix '/'.
// e.g. "/C:/Windows" to "C:\\Windows"
return tmp
}
tmp += "\\"
if filepath.IsAbs(tmp) {
// If the FromSlash without any starting slashes but with extra end slash is absolute,
// then we have a filepath encoded with a prefix '/' and a dropped '/' at the end.
// e.g. "/C:" to "C:\\"
return tmp
}
}
return lp
}

2
vendor/github.com/pkg/sftp/sftp.go generated vendored
View file

@ -1,5 +1,5 @@
// Package sftp implements the SSH File Transfer Protocol as described in // Package sftp implements the SSH File Transfer Protocol as described in
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02 // https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
package sftp package sftp
import ( import (

View file

@ -1,3 +1,4 @@
//go:build !plan9
// +build !plan9 // +build !plan9
package sftp package sftp
@ -23,7 +24,7 @@ func translateErrno(errno syscall.Errno) uint32 {
return sshFxOk return sshFxOk
case syscall.ENOENT: case syscall.ENOENT:
return sshFxNoSuchFile return sshFxNoSuchFile
case syscall.EPERM: case syscall.EACCES, syscall.EPERM:
return sshFxPermissionDenied return sshFxPermissionDenied
} }

View file

@ -1,3 +1,4 @@
//go:build plan9 || windows || (js && wasm)
// +build plan9 windows js,wasm // +build plan9 windows js,wasm
// Go defines S_IFMT on windows, plan9 and js/wasm as 0x1f000 instead of // Go defines S_IFMT on windows, plan9 and js/wasm as 0x1f000 instead of

View file

@ -1,4 +1,6 @@
// +build !plan9,!windows //go:build !plan9 && !windows && (!js || !wasm)
// +build !plan9
// +build !windows
// +build !js !wasm // +build !js !wasm
package sftp package sftp

3
vendor/golang.org/x/crypto/AUTHORS generated vendored
View file

@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at https://tip.golang.org/AUTHORS.

Some files were not shown because too many files have changed in this diff Show more