Working on SFTP
This commit is contained in:
parent
93631b4e3d
commit
0ee29e31dd
81 changed files with 12748 additions and 154 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
||||||
bagage
|
bagage
|
||||||
.env
|
.env
|
||||||
|
*.swp
|
||||||
|
id_rsa
|
||||||
|
id_rsa.pub
|
||||||
|
|
82
auth_ldap.go
82
auth_ldap.go
|
@ -18,41 +18,17 @@ type LdapPreAuth struct {
|
||||||
|
|
||||||
func (l LdapPreAuth) WithCreds(username, password string) http.Handler {
|
func (l LdapPreAuth) WithCreds(username, password string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var e *LdapWrongPasswordError
|
||||||
|
|
||||||
// 1. Connect to the server
|
access_key, secret_key, err := LdapGetS3(l.WithConfig, username, password)
|
||||||
conn, err := ldapConnect(l.WithConfig)
|
|
||||||
if err != nil {
|
|
||||||
l.OnFailure.WithError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 2. Authenticate with provided credentials
|
if err == nil {
|
||||||
// @FIXME we should better check the error, it could also be due to an LDAP error
|
l.OnCreds.WithCreds(access_key, secret_key).ServeHTTP(w, r)
|
||||||
err = conn.auth(username, password)
|
} else if errors.As(err, &e) {
|
||||||
if err != nil {
|
|
||||||
l.OnWrongPassword.WithError(err).ServeHTTP(w, r)
|
l.OnWrongPassword.WithError(err).ServeHTTP(w, r)
|
||||||
return
|
} else {
|
||||||
|
l.OnFailure.WithError(e).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fetch user's profile
|
|
||||||
profile, err := conn.profile()
|
|
||||||
if err != nil {
|
|
||||||
l.OnFailure.WithError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Basic checks upon users' attributes
|
|
||||||
access_key := profile.GetAttributeValue("garage_s3_access_key")
|
|
||||||
secret_key := profile.GetAttributeValue("garage_s3_secret_key")
|
|
||||||
if access_key == "" || secret_key == "" {
|
|
||||||
err = errors.New(fmt.Sprintf("Either access key or secret key is missing in LDAP for %s", conn.userDn))
|
|
||||||
l.OnFailure.WithError(err).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Send fetched credentials to the next middleware
|
|
||||||
l.OnCreds.WithCreds(access_key, secret_key).ServeHTTP(w, r)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +42,50 @@ type ldapConnector struct {
|
||||||
userDn string
|
userDn string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LdapError struct {
|
||||||
|
Username string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
func (e *LdapError) Error() string { return "ldap error for "+e.Username+": "+e.Err.Error() }
|
||||||
|
type LdapWrongPasswordError struct { LdapError }
|
||||||
|
|
||||||
|
func LdapGetS3(c *Config, username, password string) (access_key, secret_key string, werr error) {
|
||||||
|
// 1. Connect to the server
|
||||||
|
conn, err := ldapConnect(c)
|
||||||
|
if err != nil {
|
||||||
|
werr = &LdapError { username, err }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// 2. Authenticate with provided credentials
|
||||||
|
// @FIXME we should better check the error, it could also be due to an LDAP error
|
||||||
|
err = conn.auth(username, password)
|
||||||
|
if err != nil {
|
||||||
|
werr = &LdapWrongPasswordError { LdapError { username, err } }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch user's profile
|
||||||
|
profile, err := conn.profile()
|
||||||
|
if err != nil {
|
||||||
|
werr = &LdapError { username, err }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Basic checks upon users' attributes
|
||||||
|
access_key = profile.GetAttributeValue("garage_s3_access_key")
|
||||||
|
secret_key = profile.GetAttributeValue("garage_s3_secret_key")
|
||||||
|
if access_key == "" || secret_key == "" {
|
||||||
|
err = errors.New(fmt.Sprintf("Either access key or secret key is missing in LDAP for %s", conn.userDn))
|
||||||
|
werr = &LdapError { username, err }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Send fetched credentials to the next middleware
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func ldapConnect(c *Config) (ldapConnector, error) {
|
func ldapConnect(c *Config) (ldapConnector, error) {
|
||||||
ldapSock, err := ldap.Dial("tcp", c.LdapServer)
|
ldapSock, err := ldap.Dial("tcp", c.LdapServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,6 +14,7 @@ type Config struct {
|
||||||
UserNameAttr string `env:"BAGAGE_LDAP_USERNAME_ATTR" default:"cn"`
|
UserNameAttr string `env:"BAGAGE_LDAP_USERNAME_ATTR" default:"cn"`
|
||||||
Endpoint string `env:"BAGAGE_S3_ENDPOINT" default:"garage.deuxfleurs.fr"`
|
Endpoint string `env:"BAGAGE_S3_ENDPOINT" default:"garage.deuxfleurs.fr"`
|
||||||
UseSSL bool `env:"BAGAGE_S3_SSL" default:"true"`
|
UseSSL bool `env:"BAGAGE_S3_SSL" default:"true"`
|
||||||
|
SSHKey string `env:"BAGAGE_SSH_KEY" default:"id_rsa"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) LoadWithDefault() *Config {
|
func (c *Config) LoadWithDefault() *Config {
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -4,6 +4,9 @@ go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-ldap/ldap/v3 v3.4.1
|
github.com/go-ldap/ldap/v3 v3.4.1
|
||||||
|
github.com/kr/fs v0.1.0
|
||||||
github.com/minio/minio-go/v7 v7.0.12
|
github.com/minio/minio-go/v7 v7.0.12
|
||||||
|
github.com/pkg/sftp v1.13.4
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b
|
||||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
||||||
)
|
)
|
||||||
|
|
18
go.sum
18
go.sum
|
@ -21,6 +21,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
||||||
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
@ -38,6 +40,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
|
||||||
|
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
|
||||||
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/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||||
|
@ -51,16 +55,19 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
|
||||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU=
|
|
||||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -69,9 +76,11 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
|
||||||
|
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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=
|
||||||
|
@ -84,5 +93,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
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=
|
||||||
|
|
326
internal/encoding/ssh/filexfer/attrs.go
Normal file
326
internal/encoding/ssh/filexfer/attrs.go
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// Attributes related flags.
|
||||||
|
const (
|
||||||
|
AttrSize = 1 << iota // SSH_FILEXFER_ATTR_SIZE
|
||||||
|
AttrUIDGID // SSH_FILEXFER_ATTR_UIDGID
|
||||||
|
AttrPermissions // SSH_FILEXFER_ATTR_PERMISSIONS
|
||||||
|
AttrACModTime // SSH_FILEXFER_ACMODTIME
|
||||||
|
|
||||||
|
AttrExtended = 1 << 31 // SSH_FILEXFER_ATTR_EXTENDED
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
type Attributes struct {
|
||||||
|
Flags uint32
|
||||||
|
|
||||||
|
// AttrSize
|
||||||
|
Size uint64
|
||||||
|
|
||||||
|
// AttrUIDGID
|
||||||
|
UID uint32
|
||||||
|
GID uint32
|
||||||
|
|
||||||
|
// AttrPermissions
|
||||||
|
Permissions FileMode
|
||||||
|
|
||||||
|
// AttrACmodTime
|
||||||
|
ATime uint32
|
||||||
|
MTime uint32
|
||||||
|
|
||||||
|
// AttrExtended
|
||||||
|
ExtendedAttributes []ExtendedAttribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the Size field and a bool that is true if and only if the value is valid/defined.
|
||||||
|
func (a *Attributes) GetSize() (size uint64, ok bool) {
|
||||||
|
return a.Size, a.Flags&AttrSize != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize is a convenience function that sets the Size field,
|
||||||
|
// and marks the field as valid/defined in Flags.
|
||||||
|
func (a *Attributes) SetSize(size uint64) {
|
||||||
|
a.Flags |= AttrSize
|
||||||
|
a.Size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUIDGID returns the UID and GID fields and a bool that is true if and only if the values are valid/defined.
|
||||||
|
func (a *Attributes) GetUIDGID() (uid, gid uint32, ok bool) {
|
||||||
|
return a.UID, a.GID, a.Flags&AttrUIDGID != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUIDGID is a convenience function that sets the UID and GID fields,
|
||||||
|
// and marks the fields as valid/defined in Flags.
|
||||||
|
func (a *Attributes) SetUIDGID(uid, gid uint32) {
|
||||||
|
a.Flags |= AttrUIDGID
|
||||||
|
a.UID = uid
|
||||||
|
a.GID = gid
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermissions returns the Permissions field and a bool that is true if and only if the value is valid/defined.
|
||||||
|
func (a *Attributes) GetPermissions() (perms FileMode, ok bool) {
|
||||||
|
return a.Permissions, a.Flags&AttrPermissions != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPermissions is a convenience function that sets the Permissions field,
|
||||||
|
// and marks the field as valid/defined in Flags.
|
||||||
|
func (a *Attributes) SetPermissions(perms FileMode) {
|
||||||
|
a.Flags |= AttrPermissions
|
||||||
|
a.Permissions = perms
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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,
|
||||||
|
// and marks the fields as valid/defined in Flags.
|
||||||
|
func (a *Attributes) SetACModTime(atime, mtime uint32) {
|
||||||
|
a.Flags |= AttrACModTime
|
||||||
|
a.ATime = atime
|
||||||
|
a.MTime = mtime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of bytes a would marshal into.
|
||||||
|
func (a *Attributes) Len() int {
|
||||||
|
length := 4
|
||||||
|
|
||||||
|
if a.Flags&AttrSize != 0 {
|
||||||
|
length += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrUIDGID != 0 {
|
||||||
|
length += 4 + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrPermissions != 0 {
|
||||||
|
length += 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrACModTime != 0 {
|
||||||
|
length += 4 + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrExtended != 0 {
|
||||||
|
length += 4
|
||||||
|
|
||||||
|
for _, ext := range a.ExtendedAttributes {
|
||||||
|
length += ext.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto marshals e onto the end of the given Buffer.
|
||||||
|
func (a *Attributes) MarshalInto(b *Buffer) {
|
||||||
|
b.AppendUint32(a.Flags)
|
||||||
|
|
||||||
|
if a.Flags&AttrSize != 0 {
|
||||||
|
b.AppendUint64(a.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrUIDGID != 0 {
|
||||||
|
b.AppendUint32(a.UID)
|
||||||
|
b.AppendUint32(a.GID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrPermissions != 0 {
|
||||||
|
b.AppendUint32(uint32(a.Permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrACModTime != 0 {
|
||||||
|
b.AppendUint32(a.ATime)
|
||||||
|
b.AppendUint32(a.MTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrExtended != 0 {
|
||||||
|
b.AppendUint32(uint32(len(a.ExtendedAttributes)))
|
||||||
|
|
||||||
|
for _, ext := range a.ExtendedAttributes {
|
||||||
|
ext.MarshalInto(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns a as the binary encoding of a.
|
||||||
|
func (a *Attributes) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := NewBuffer(make([]byte, 0, a.Len()))
|
||||||
|
a.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom unmarshals an Attributes from the given Buffer into e.
|
||||||
|
//
|
||||||
|
// NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
|
||||||
|
func (a *Attributes) UnmarshalFrom(b *Buffer) (err error) {
|
||||||
|
flags, err := b.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.XXX_UnmarshalByFlags(flags, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// This function is not a part of any compatibility promise.
|
||||||
|
func (a *Attributes) XXX_UnmarshalByFlags(flags uint32, b *Buffer) (err error) {
|
||||||
|
a.Flags = flags
|
||||||
|
|
||||||
|
// Short-circuit dummy attributes.
|
||||||
|
if a.Flags == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrSize != 0 {
|
||||||
|
if a.Size, err = b.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrUIDGID != 0 {
|
||||||
|
if a.UID, err = b.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.GID, err = b.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrPermissions != 0 {
|
||||||
|
m, err := b.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Permissions = FileMode(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrACModTime != 0 {
|
||||||
|
if a.ATime, err = b.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.MTime, err = b.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Flags&AttrExtended != 0 {
|
||||||
|
count, err := b.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ExtendedAttributes = make([]ExtendedAttribute, count)
|
||||||
|
for i := range a.ExtendedAttributes {
|
||||||
|
a.ExtendedAttributes[i].UnmarshalFrom(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the binary encoding of Attributes into e.
|
||||||
|
func (a *Attributes) UnmarshalBinary(data []byte) error {
|
||||||
|
return a.UnmarshalFrom(NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
type ExtendedAttribute struct {
|
||||||
|
Type string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of bytes e would marshal into.
|
||||||
|
func (e *ExtendedAttribute) Len() int {
|
||||||
|
return 4 + len(e.Type) + 4 + len(e.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto marshals e onto the end of the given Buffer.
|
||||||
|
func (e *ExtendedAttribute) MarshalInto(b *Buffer) {
|
||||||
|
b.AppendString(e.Type)
|
||||||
|
b.AppendString(e.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns e as the binary encoding of e.
|
||||||
|
func (e *ExtendedAttribute) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := NewBuffer(make([]byte, 0, e.Len()))
|
||||||
|
e.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom unmarshals an ExtendedAattribute from the given Buffer into e.
|
||||||
|
func (e *ExtendedAttribute) UnmarshalFrom(b *Buffer) (err error) {
|
||||||
|
if e.Type, err = b.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Data, err = b.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the binary encoding of ExtendedAttribute into e.
|
||||||
|
func (e *ExtendedAttribute) UnmarshalBinary(data []byte) error {
|
||||||
|
return e.UnmarshalFrom(NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameEntry implements the SSH_FXP_NAME repeated data type from draft-ietf-secsh-filexfer-02
|
||||||
|
//
|
||||||
|
// This type is incompatible with versions 4 or higher.
|
||||||
|
type NameEntry struct {
|
||||||
|
Filename string
|
||||||
|
Longname string
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of bytes e would marshal into.
|
||||||
|
func (e *NameEntry) Len() int {
|
||||||
|
return 4 + len(e.Filename) + 4 + len(e.Longname) + e.Attrs.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto marshals e onto the end of the given Buffer.
|
||||||
|
func (e *NameEntry) MarshalInto(b *Buffer) {
|
||||||
|
b.AppendString(e.Filename)
|
||||||
|
b.AppendString(e.Longname)
|
||||||
|
|
||||||
|
e.Attrs.MarshalInto(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns e as the binary encoding of e.
|
||||||
|
func (e *NameEntry) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := NewBuffer(make([]byte, 0, e.Len()))
|
||||||
|
e.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom unmarshals an NameEntry from the given Buffer into e.
|
||||||
|
//
|
||||||
|
// NOTE: The values of fields not covered in the a.Flags are explicitly undefined.
|
||||||
|
func (e *NameEntry) UnmarshalFrom(b *Buffer) (err error) {
|
||||||
|
if e.Filename, err = b.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Longname, err = b.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Attrs.UnmarshalFrom(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the binary encoding of NameEntry into e.
|
||||||
|
func (e *NameEntry) UnmarshalBinary(data []byte) error {
|
||||||
|
return e.UnmarshalFrom(NewBuffer(data))
|
||||||
|
}
|
231
internal/encoding/ssh/filexfer/attrs_test.go
Normal file
231
internal/encoding/ssh/filexfer/attrs_test.go
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttributes(t *testing.T) {
|
||||||
|
const (
|
||||||
|
size = 0x123456789ABCDEF0
|
||||||
|
uid = 1000
|
||||||
|
gid = 100
|
||||||
|
perms = 0x87654321
|
||||||
|
atime = 0x2A2B2C2D
|
||||||
|
mtime = 0x42434445
|
||||||
|
)
|
||||||
|
|
||||||
|
extAttr := ExtendedAttribute{
|
||||||
|
Type: "foo",
|
||||||
|
Data: "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := &Attributes{
|
||||||
|
Size: size,
|
||||||
|
UID: uid,
|
||||||
|
GID: gid,
|
||||||
|
Permissions: perms,
|
||||||
|
ATime: atime,
|
||||||
|
MTime: mtime,
|
||||||
|
ExtendedAttributes: []ExtendedAttribute{
|
||||||
|
extAttr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
flags uint32
|
||||||
|
encoded []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
encoded: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size",
|
||||||
|
flags: AttrSize,
|
||||||
|
encoded: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uidgid",
|
||||||
|
flags: AttrUIDGID,
|
||||||
|
encoded: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x03, 0xE8,
|
||||||
|
0x00, 0x00, 0x00, 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "permissions",
|
||||||
|
flags: AttrPermissions,
|
||||||
|
encoded: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "acmodtime",
|
||||||
|
flags: AttrACModTime,
|
||||||
|
encoded: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x08,
|
||||||
|
0x2A, 0x2B, 0x2C, 0x2D,
|
||||||
|
0x42, 0x43, 0x44, 0x45,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extended",
|
||||||
|
flags: AttrExtended,
|
||||||
|
encoded: []byte{
|
||||||
|
0x80, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'b', 'a', 'r',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size uidgid permisssions acmodtime extended",
|
||||||
|
flags: AttrSize | AttrUIDGID | AttrPermissions | AttrACModTime | AttrExtended,
|
||||||
|
encoded: []byte{
|
||||||
|
0x80, 0x00, 0x00, 0x0F,
|
||||||
|
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||||
|
0x00, 0x00, 0x03, 0xE8,
|
||||||
|
0x00, 0x00, 0x00, 100,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x2A, 0x2B, 0x2C, 0x2D,
|
||||||
|
0x42, 0x43, 0x44, 0x45,
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'b', 'a', 'r',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
attr := *attr
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
attr.Flags = tt.flags
|
||||||
|
|
||||||
|
buf, err := attr.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, tt.encoded) {
|
||||||
|
t.Fatalf("MarshalBinary() = %X, but wanted %X", buf, tt.encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
attr = Attributes{}
|
||||||
|
|
||||||
|
if err := attr.UnmarshalBinary(buf); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags != tt.flags {
|
||||||
|
t.Errorf("UnmarshalBinary(): Flags was %x, but wanted %x", attr.Flags, tt.flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags&AttrSize != 0 && attr.Size != size {
|
||||||
|
t.Errorf("UnmarshalBinary(): Size was %x, but wanted %x", attr.Size, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags&AttrUIDGID != 0 {
|
||||||
|
if attr.UID != uid {
|
||||||
|
t.Errorf("UnmarshalBinary(): UID was %x, but wanted %x", attr.UID, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.GID != gid {
|
||||||
|
t.Errorf("UnmarshalBinary(): GID was %x, but wanted %x", attr.GID, gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags&AttrPermissions != 0 && attr.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalBinary(): Permissions was %#v, but wanted %#v", attr.Permissions, perms)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags&AttrACModTime != 0 {
|
||||||
|
if attr.ATime != atime {
|
||||||
|
t.Errorf("UnmarshalBinary(): ATime was %x, but wanted %x", attr.ATime, atime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.MTime != mtime {
|
||||||
|
t.Errorf("UnmarshalBinary(): MTime was %x, but wanted %x", attr.MTime, mtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr.Flags&AttrExtended != 0 {
|
||||||
|
extAttrs := attr.ExtendedAttributes
|
||||||
|
|
||||||
|
if count := len(extAttrs); count != 1 {
|
||||||
|
t.Fatalf("UnmarshalBinary(): len(ExtendedAttributes) was %d, but wanted %d", count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := extAttrs[0]; got != extAttr {
|
||||||
|
t.Errorf("UnmarshalBinary(): ExtendedAttributes[0] was %#v, but wanted %#v", got, extAttr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameEntry(t *testing.T) {
|
||||||
|
const (
|
||||||
|
filename = "foo"
|
||||||
|
longname = "bar"
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
e := &NameEntry{
|
||||||
|
Filename: filename,
|
||||||
|
Longname: longname,
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := e.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'b', 'a', 'r',
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = NameEntry{}
|
||||||
|
|
||||||
|
if err := e.UnmarshalBinary(buf); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Filename != filename {
|
||||||
|
t.Errorf("UnmarhsalFrom(): Filename was %q, but expected %q", e.Filename, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Longname != longname {
|
||||||
|
t.Errorf("UnmarhsalFrom(): Longname was %q, but expected %q", e.Longname, longname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Attrs.Flags != AttrPermissions {
|
||||||
|
t.Errorf("UnmarshalBinary(): Attrs.Flag was %#x, but expected %#x", e.Attrs.Flags, AttrPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Attrs.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalBinary(): Attrs.Permissions was %#v, but expected %#v", e.Attrs.Permissions, perms)
|
||||||
|
}
|
||||||
|
}
|
293
internal/encoding/ssh/filexfer/buffer.go
Normal file
293
internal/encoding/ssh/filexfer/buffer.go
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Various encoding errors.
|
||||||
|
var (
|
||||||
|
ErrShortPacket = errors.New("packet too short")
|
||||||
|
ErrLongPacket = errors.New("packet too long")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Buffer wraps up the various encoding details of the SSH format.
|
||||||
|
//
|
||||||
|
// Data types are encoded as per section 4 from https://tools.ietf.org/html/draft-ietf-secsh-architecture-09#page-8
|
||||||
|
type Buffer struct {
|
||||||
|
b []byte
|
||||||
|
off int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuffer creates and initializes a new buffer using buf as its initial contents.
|
||||||
|
// The new buffer takes ownership of buf, and the caller should not use buf after this call.
|
||||||
|
//
|
||||||
|
// In most cases, new(Buffer) (or just declaring a Buffer variable) is sufficient to initialize a Buffer.
|
||||||
|
func NewBuffer(buf []byte) *Buffer {
|
||||||
|
return &Buffer{
|
||||||
|
b: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMarshalBuffer creates a new Buffer ready to start marshaling a Packet into.
|
||||||
|
// It preallocates enough space for uint32(length), uint8(type), uint32(request-id) and size more bytes.
|
||||||
|
func NewMarshalBuffer(size int) *Buffer {
|
||||||
|
return NewBuffer(make([]byte, 4+1+4+size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a slice of length b.Len() holding the unconsumed bytes in the Buffer.
|
||||||
|
// The slice is valid for use only until the next buffer modification
|
||||||
|
// (that is, only until the next call to an Append or Consume method).
|
||||||
|
func (b *Buffer) Bytes() []byte {
|
||||||
|
return b.b[b.off:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of unconsumed bytes in the buffer.
|
||||||
|
func (b *Buffer) Len() int { return len(b.b) - b.off }
|
||||||
|
|
||||||
|
// Cap returns the capacity of the buffer’s underlying byte slice,
|
||||||
|
// that is, the total space allocated for the buffer’s data.
|
||||||
|
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.
|
||||||
|
func (b *Buffer) Reset() {
|
||||||
|
b.b = b.b[:0]
|
||||||
|
b.off = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *Buffer) StartPacket(packetType PacketType, requestID uint32) {
|
||||||
|
b.b, b.off = append(b.b[:0], make([]byte, 4)...), 0
|
||||||
|
|
||||||
|
b.AppendUint8(uint8(packetType))
|
||||||
|
b.AppendUint32(requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet finalizes the packet started from StartPacket.
|
||||||
|
// It is expected that this will end the ownership of the underlying byte-slice,
|
||||||
|
// and so the returned byte-slices may be reused the same as any other byte-slice,
|
||||||
|
// the caller should not use this buffer after this call.
|
||||||
|
//
|
||||||
|
// It writes the packet body length into the first four bytes of the buffer in network byte order (big endian).
|
||||||
|
// The packet body length is the length of this buffer less the 4-byte length itself, plus the length of payload.
|
||||||
|
//
|
||||||
|
// It is assumed that no Consume methods have been called on this buffer,
|
||||||
|
// and so it returns the whole underlying slice.
|
||||||
|
func (b *Buffer) Packet(payload []byte) (header, payloadPassThru []byte, err error) {
|
||||||
|
b.PutLength(len(b.b) - 4 + len(payload))
|
||||||
|
|
||||||
|
return b.b, payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeUint8 consumes a single byte from the buffer.
|
||||||
|
// If the buffer does not have enough data, it will return ErrShortPacket.
|
||||||
|
func (b *Buffer) ConsumeUint8() (uint8, error) {
|
||||||
|
if b.Len() < 1 {
|
||||||
|
return 0, ErrShortPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
var v uint8
|
||||||
|
v, b.off = b.b[b.off], b.off+1
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint8 appends a single byte into the buffer.
|
||||||
|
func (b *Buffer) AppendUint8(v uint8) {
|
||||||
|
b.b = append(b.b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *Buffer) ConsumeBool() (bool, error) {
|
||||||
|
v, err := b.ConsumeUint8()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBool appends a single bool into the buffer.
|
||||||
|
// It encodes it as a single byte, with false as 0, and true as 1.
|
||||||
|
func (b *Buffer) AppendBool(v bool) {
|
||||||
|
if v {
|
||||||
|
b.AppendUint8(1)
|
||||||
|
} else {
|
||||||
|
b.AppendUint8(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *Buffer) ConsumeUint16() (uint16, error) {
|
||||||
|
if b.Len() < 2 {
|
||||||
|
return 0, ErrShortPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
v := binary.BigEndian.Uint16(b.b[b.off:])
|
||||||
|
b.off += 2
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint16 appends single uint16 into the buffer, in network byte order (big-endian).
|
||||||
|
func (b *Buffer) AppendUint16(v uint16) {
|
||||||
|
b.b = append(b.b,
|
||||||
|
byte(v>>8),
|
||||||
|
byte(v>>0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalUint32 is used internally to read the packet length.
|
||||||
|
// It is unsafe, and so not exported.
|
||||||
|
// Even within this package, its use should be avoided.
|
||||||
|
func unmarshalUint32(b []byte) uint32 {
|
||||||
|
return binary.BigEndian.Uint32(b[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *Buffer) ConsumeUint32() (uint32, error) {
|
||||||
|
if b.Len() < 4 {
|
||||||
|
return 0, ErrShortPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
v := binary.BigEndian.Uint32(b.b[b.off:])
|
||||||
|
b.off += 4
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint32 appends a single uint32 into the buffer, in network byte order (big-endian).
|
||||||
|
func (b *Buffer) AppendUint32(v uint32) {
|
||||||
|
b.b = append(b.b,
|
||||||
|
byte(v>>24),
|
||||||
|
byte(v>>16),
|
||||||
|
byte(v>>8),
|
||||||
|
byte(v>>0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *Buffer) ConsumeUint64() (uint64, error) {
|
||||||
|
if b.Len() < 8 {
|
||||||
|
return 0, ErrShortPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
v := binary.BigEndian.Uint64(b.b[b.off:])
|
||||||
|
b.off += 8
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint64 appends a single uint64 into the buffer, in network byte order (big-endian).
|
||||||
|
func (b *Buffer) AppendUint64(v uint64) {
|
||||||
|
b.b = append(b.b,
|
||||||
|
byte(v>>56),
|
||||||
|
byte(v>>48),
|
||||||
|
byte(v>>40),
|
||||||
|
byte(v>>32),
|
||||||
|
byte(v>>24),
|
||||||
|
byte(v>>16),
|
||||||
|
byte(v>>8),
|
||||||
|
byte(v>>0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeInt64 consumes a single int64 from the buffer, in network byte order (big-endian) with two’s complement.
|
||||||
|
// If the buffer does not have enough data, it will return ErrShortPacket.
|
||||||
|
func (b *Buffer) ConsumeInt64() (int64, error) {
|
||||||
|
u, err := 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 two’s complement.
|
||||||
|
func (b *Buffer) AppendInt64(v int64) {
|
||||||
|
b.AppendUint64(uint64(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// If the buffer does not have enough data, or defines a length larger than available, it will return ErrShortPacket.
|
||||||
|
//
|
||||||
|
// 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).
|
||||||
|
//
|
||||||
|
// In no case will any Consume calls return overlapping slice aliases,
|
||||||
|
// and Append calls are guaranteed to not disturb this slice alias.
|
||||||
|
func (b *Buffer) ConsumeByteSlice() ([]byte, error) {
|
||||||
|
length, err := b.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Len() < int(length) {
|
||||||
|
return nil, ErrShortPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
v := b.b[b.off:]
|
||||||
|
if len(v) > int(length) {
|
||||||
|
v = v[:length:length]
|
||||||
|
}
|
||||||
|
b.off += int(length)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendByteSlice appends a single string of raw binary data into the buffer.
|
||||||
|
// A string is a uint32 length, followed by that number of raw bytes.
|
||||||
|
func (b *Buffer) AppendByteSlice(v []byte) {
|
||||||
|
b.AppendUint32(uint32(len(v)))
|
||||||
|
b.b = append(b.b, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeString consumes a single string of binary data 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 return ErrShortPacket.
|
||||||
|
//
|
||||||
|
// NOTE: Go implicitly assumes that strings contain UTF-8 encoded data.
|
||||||
|
// All caveats on using arbitrary binary data in Go strings applies.
|
||||||
|
func (b *Buffer) ConsumeString() (string, error) {
|
||||||
|
v, err := b.ConsumeByteSlice()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendString appends a single string of binary data into the buffer.
|
||||||
|
// A string is a uint32 length, followed by that number of raw bytes.
|
||||||
|
func (b *Buffer) AppendString(v string) {
|
||||||
|
b.AppendByteSlice([]byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutLength writes the given size into the first four bytes of the buffer in network byte order (big endian).
|
||||||
|
func (b *Buffer) PutLength(size int) {
|
||||||
|
if len(b.b) < 4 {
|
||||||
|
b.b = append(b.b, make([]byte, 4-len(b.b))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(b.b, uint32(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns a clone of the full internal buffer.
|
||||||
|
func (b *Buffer) MarshalBinary() ([]byte, error) {
|
||||||
|
clone := make([]byte, len(b.b))
|
||||||
|
n := copy(clone, b.b)
|
||||||
|
return clone[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary sets the internal buffer of b to be a clone of data, and zeros the internal offset.
|
||||||
|
func (b *Buffer) UnmarshalBinary(data []byte) error {
|
||||||
|
if grow := len(data) - len(b.b); grow > 0 {
|
||||||
|
b.b = append(b.b, make([]byte, grow)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := copy(b.b, data)
|
||||||
|
b.b = b.b[:n]
|
||||||
|
b.off = 0
|
||||||
|
return nil
|
||||||
|
}
|
142
internal/encoding/ssh/filexfer/extended_packets.go
Normal file
142
internal/encoding/ssh/filexfer/extended_packets.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtendedData aliases the untyped interface composition of encoding.BinaryMarshaler and encoding.BinaryUnmarshaler.
|
||||||
|
type ExtendedData = interface {
|
||||||
|
encoding.BinaryMarshaler
|
||||||
|
encoding.BinaryUnmarshaler
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedDataConstructor defines a function that returns a new(ArbitraryExtendedPacket).
|
||||||
|
type ExtendedDataConstructor func() ExtendedData
|
||||||
|
|
||||||
|
var extendedPacketTypes = struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
constructors map[string]ExtendedDataConstructor
|
||||||
|
}{
|
||||||
|
constructors: make(map[string]ExtendedDataConstructor),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterExtendedPacketType defines a specific ExtendedDataConstructor for the given extension string.
|
||||||
|
func RegisterExtendedPacketType(extension string, constructor ExtendedDataConstructor) {
|
||||||
|
extendedPacketTypes.mu.Lock()
|
||||||
|
defer extendedPacketTypes.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exist := extendedPacketTypes.constructors[extension]; exist {
|
||||||
|
panic("encoding/ssh/filexfer: multiple registration of extended packet type " + extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
extendedPacketTypes.constructors[extension] = constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExtendedPacket(extension string) ExtendedData {
|
||||||
|
extendedPacketTypes.mu.RLock()
|
||||||
|
defer extendedPacketTypes.mu.RUnlock()
|
||||||
|
|
||||||
|
if f := extendedPacketTypes.constructors[extension]; f != nil {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedPacket defines the SSH_FXP_CLOSE packet.
|
||||||
|
type ExtendedPacket struct {
|
||||||
|
ExtendedRequest string
|
||||||
|
|
||||||
|
Data ExtendedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ExtendedPacket) Type() PacketType {
|
||||||
|
return PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
//
|
||||||
|
// The Data is marshaled into binary, and returned as the payload.
|
||||||
|
func (p *ExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.ExtendedRequest) // string(extended-request)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeExtended, reqid)
|
||||||
|
buf.AppendString(p.ExtendedRequest)
|
||||||
|
|
||||||
|
if p.Data != nil {
|
||||||
|
payload, err = p.Data.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
//
|
||||||
|
// If p.Data is nil, and the extension has been registered, a new type will be made from the registration.
|
||||||
|
// 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.
|
||||||
|
func (p *ExtendedPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.ExtendedRequest, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Data == nil {
|
||||||
|
p.Data = newExtendedPacket(p.ExtendedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Data.UnmarshalBinary(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedReplyPacket defines the SSH_FXP_CLOSE packet.
|
||||||
|
type ExtendedReplyPacket struct {
|
||||||
|
Data ExtendedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ExtendedReplyPacket) Type() PacketType {
|
||||||
|
return PacketTypeExtendedReply
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
//
|
||||||
|
// The Data is marshaled into binary, and returned as the payload.
|
||||||
|
func (p *ExtendedReplyPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
buf = NewMarshalBuffer(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeExtendedReply, reqid)
|
||||||
|
|
||||||
|
if p.Data != nil {
|
||||||
|
payload, err = p.Data.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
//
|
||||||
|
// If p.Data is nil, and there is request-specific-data,
|
||||||
|
// then the request-specific-data will be wrapped in a Buffer and assigned to p.Data.
|
||||||
|
func (p *ExtendedReplyPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Data == nil {
|
||||||
|
p.Data = new(Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Data.UnmarshalBinary(buf.Bytes())
|
||||||
|
}
|
240
internal/encoding/ssh/filexfer/extended_packets_test.go
Normal file
240
internal/encoding/ssh/filexfer/extended_packets_test.go
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testExtendedData struct {
|
||||||
|
value uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *testExtendedData) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := NewBuffer(make([]byte, 0, 4))
|
||||||
|
|
||||||
|
buf.AppendUint8(d.value ^ 0x2a)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *testExtendedData) UnmarshalBinary(data []byte) error {
|
||||||
|
buf := NewBuffer(data)
|
||||||
|
|
||||||
|
v, err := buf.ConsumeUint8()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.value = v ^ 0x2a
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &ExtendedPacket{}
|
||||||
|
|
||||||
|
func TestExtendedPacketNoData(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
extendedRequest = "foo@example"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ExtendedPacket{
|
||||||
|
ExtendedRequest: extendedRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 20,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 11, 'f', 'o', 'o', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extendedRequest {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtendedPacketTestData(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
extendedRequest = "foo@example"
|
||||||
|
textValue = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = 13
|
||||||
|
|
||||||
|
p := &ExtendedPacket{
|
||||||
|
ExtendedRequest: extendedRequest,
|
||||||
|
Data: &testExtendedData{
|
||||||
|
value: textValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 21,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 11, 'f', 'o', 'o', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
||||||
|
0x27,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedPacket{
|
||||||
|
Data: new(testExtendedData),
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extendedRequest {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf, ok := p.Data.(*testExtendedData); !ok {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was type %T, but expected %T", p.Data, buf)
|
||||||
|
|
||||||
|
} else if buf.value != value {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data.value was %#x, but expected %#x", buf.value, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extendedRequest {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extendedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantBuffer := []byte{0x27}
|
||||||
|
|
||||||
|
if buf, ok := p.Data.(*Buffer); !ok {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was type %T, but expected %T", p.Data, buf)
|
||||||
|
|
||||||
|
} else if !bytes.Equal(buf.b, wantBuffer) {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was %X, but expected %X", buf.b, wantBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &ExtendedReplyPacket{}
|
||||||
|
|
||||||
|
func TestExtendedReplyNoData(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ExtendedReplyPacket{}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 5,
|
||||||
|
201,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedReplyPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtendedReplyPacketTestData(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
textValue = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = 13
|
||||||
|
|
||||||
|
p := &ExtendedReplyPacket{
|
||||||
|
Data: &testExtendedData{
|
||||||
|
value: textValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 6,
|
||||||
|
201,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x27,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedReplyPacket{
|
||||||
|
Data: new(testExtendedData),
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf, ok := p.Data.(*testExtendedData); !ok {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was type %T, but expected %T", p.Data, buf)
|
||||||
|
|
||||||
|
} else if buf.value != value {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data.value was %#x, but expected %#x", buf.value, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ExtendedReplyPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantBuffer := []byte{0x27}
|
||||||
|
|
||||||
|
if buf, ok := p.Data.(*Buffer); !ok {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was type %T, but expected %T", p.Data, buf)
|
||||||
|
|
||||||
|
} else if !bytes.Equal(buf.b, wantBuffer) {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was %X, but expected %X", buf.b, wantBuffer)
|
||||||
|
}
|
||||||
|
}
|
46
internal/encoding/ssh/filexfer/extensions.go
Normal file
46
internal/encoding/ssh/filexfer/extensions.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Defined in: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-4.2
|
||||||
|
type ExtensionPair struct {
|
||||||
|
Name string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of bytes e would marshal into.
|
||||||
|
func (e *ExtensionPair) Len() int {
|
||||||
|
return 4 + len(e.Name) + 4 + len(e.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto marshals e onto the end of the given Buffer.
|
||||||
|
func (e *ExtensionPair) MarshalInto(buf *Buffer) {
|
||||||
|
buf.AppendString(e.Name)
|
||||||
|
buf.AppendString(e.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns e as the binary encoding of e.
|
||||||
|
func (e *ExtensionPair) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := NewBuffer(make([]byte, 0, e.Len()))
|
||||||
|
e.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom unmarshals an ExtensionPair from the given Buffer into e.
|
||||||
|
func (e *ExtensionPair) UnmarshalFrom(buf *Buffer) (err error) {
|
||||||
|
if e.Name, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Data, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the binary encoding of ExtensionPair into e.
|
||||||
|
func (e *ExtensionPair) UnmarshalBinary(data []byte) error {
|
||||||
|
return e.UnmarshalFrom(NewBuffer(data))
|
||||||
|
}
|
49
internal/encoding/ssh/filexfer/extensions_test.go
Normal file
49
internal/encoding/ssh/filexfer/extensions_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtensionPair(t *testing.T) {
|
||||||
|
const (
|
||||||
|
name = "foo"
|
||||||
|
data = "1"
|
||||||
|
)
|
||||||
|
|
||||||
|
pair := &ExtensionPair{
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := pair.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 3,
|
||||||
|
'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 1,
|
||||||
|
'1',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Errorf("ExtensionPair.MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*pair = ExtensionPair{}
|
||||||
|
|
||||||
|
if err := pair.UnmarshalBinary(buf); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.Name != name {
|
||||||
|
t.Errorf("ExtensionPair.UnmarshalBinary(): Name was %q, but expected %q", pair.Name, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.Data != data {
|
||||||
|
t.Errorf("RawPacket.UnmarshalBinary(): Data was %q, but expected %q", pair.Data, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
internal/encoding/ssh/filexfer/filexfer.go
Normal file
54
internal/encoding/ssh/filexfer/filexfer.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Package filexfer implements the wire encoding for secsh-filexfer as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// PacketMarshaller narrowly defines packets that will only be transmitted.
|
||||||
|
//
|
||||||
|
// ExtendedPacket types will often only implement this interface,
|
||||||
|
// since decoding the whole packet body of an ExtendedPacket can only be done dependent on the ExtendedRequest field.
|
||||||
|
type PacketMarshaller interface {
|
||||||
|
// MarshalPacket is the primary intended way to encode a packet.
|
||||||
|
// The request-id for the packet is set from reqid.
|
||||||
|
//
|
||||||
|
// An optional buffer may be given in b.
|
||||||
|
// If the buffer has a minimum capacity, it shall be truncated and used to marshal the header into.
|
||||||
|
// The minimum capacity for the packet must be a constant expression, and should be at least 9.
|
||||||
|
//
|
||||||
|
// It shall return the main body of the encoded packet in header,
|
||||||
|
// and may optionally return an additional payload to be written immediately after the header.
|
||||||
|
//
|
||||||
|
// It shall encode in the first 4-bytes of the header the proper length of the rest of the header+payload.
|
||||||
|
MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet defines the behavior of a full generic SFTP packet.
|
||||||
|
//
|
||||||
|
// InitPacket, and VersionPacket are not generic SFTP packets, and instead implement (Un)MarshalBinary.
|
||||||
|
//
|
||||||
|
// ExtendedPacket types should not iplement this interface,
|
||||||
|
// since decoding the whole packet body of an ExtendedPacket can only be done dependent on the ExtendedRequest field.
|
||||||
|
type Packet interface {
|
||||||
|
PacketMarshaller
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with the specific packet.
|
||||||
|
Type() PacketType
|
||||||
|
|
||||||
|
// UnmarshalPacketBody decodes a packet body from the given Buffer.
|
||||||
|
// It is assumed that the common header values of the length, type and request-id have already been consumed.
|
||||||
|
//
|
||||||
|
// Implementations should not alias the given Buffer,
|
||||||
|
// instead they can consider prepopulating an internal buffer as a hint,
|
||||||
|
// and copying into that buffer if it has sufficient length.
|
||||||
|
UnmarshalPacketBody(buf *Buffer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposePacket converts returns from MarshalPacket into an equivalent call to MarshalBinary.
|
||||||
|
func ComposePacket(header, payload []byte, err error) ([]byte, error) {
|
||||||
|
return append(header, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default length values,
|
||||||
|
// Defined in draft-ietf-secsh-filexfer-02 section 3.
|
||||||
|
const (
|
||||||
|
DefaultMaxPacketLength = 34000
|
||||||
|
DefaultMaxDataLength = 32768
|
||||||
|
)
|
147
internal/encoding/ssh/filexfer/fx.go
Normal file
147
internal/encoding/ssh/filexfer/fx.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status defines the SFTP error codes used in SSH_FXP_STATUS response packets.
|
||||||
|
type Status uint32
|
||||||
|
|
||||||
|
// Defines the various SSH_FX_* values.
|
||||||
|
const (
|
||||||
|
// see draft-ietf-secsh-filexfer-02
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
|
||||||
|
StatusOK = Status(iota)
|
||||||
|
StatusEOF
|
||||||
|
StatusNoSuchFile
|
||||||
|
StatusPermissionDenied
|
||||||
|
StatusFailure
|
||||||
|
StatusBadMessage
|
||||||
|
StatusNoConnection
|
||||||
|
StatusConnectionLost
|
||||||
|
StatusOPUnsupported
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-7
|
||||||
|
StatusV4InvalidHandle
|
||||||
|
StatusV4NoSuchPath
|
||||||
|
StatusV4FileAlreadyExists
|
||||||
|
StatusV4WriteProtect
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-7
|
||||||
|
StatusV4NoMedia
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-7
|
||||||
|
StatusV5NoSpaceOnFilesystem
|
||||||
|
StatusV5QuotaExceeded
|
||||||
|
StatusV5UnknownPrincipal
|
||||||
|
StatusV5LockConflict
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-06#section-8
|
||||||
|
StatusV6DirNotEmpty
|
||||||
|
StatusV6NotADirectory
|
||||||
|
StatusV6InvalidFilename
|
||||||
|
StatusV6LinkLoop
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-8
|
||||||
|
StatusV6CannotDelete
|
||||||
|
StatusV6InvalidParameter
|
||||||
|
StatusV6FileIsADirectory
|
||||||
|
StatusV6ByteRangeLockConflict
|
||||||
|
StatusV6ByteRangeLockRefused
|
||||||
|
StatusV6DeletePending
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-8.1
|
||||||
|
StatusV6FileCorrupt
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-10#section-9.1
|
||||||
|
StatusV6OwnerInvalid
|
||||||
|
StatusV6GroupInvalid
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1
|
||||||
|
StatusV6NoMatchingByteRangeLock
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Status) Error() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is returns true if the target is the same Status code,
|
||||||
|
// or target is a StatusPacket with the same Status code.
|
||||||
|
func (s Status) Is(target error) bool {
|
||||||
|
if target, ok := target.(*StatusPacket); ok {
|
||||||
|
return target.StatusCode == s
|
||||||
|
}
|
||||||
|
|
||||||
|
return s == target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Status) String() string {
|
||||||
|
switch s {
|
||||||
|
case StatusOK:
|
||||||
|
return "SSH_FX_OK"
|
||||||
|
case StatusEOF:
|
||||||
|
return "SSH_FX_EOF"
|
||||||
|
case StatusNoSuchFile:
|
||||||
|
return "SSH_FX_NO_SUCH_FILE"
|
||||||
|
case StatusPermissionDenied:
|
||||||
|
return "SSH_FX_PERMISSION_DENIED"
|
||||||
|
case StatusFailure:
|
||||||
|
return "SSH_FX_FAILURE"
|
||||||
|
case StatusBadMessage:
|
||||||
|
return "SSH_FX_BAD_MESSAGE"
|
||||||
|
case StatusNoConnection:
|
||||||
|
return "SSH_FX_NO_CONNECTION"
|
||||||
|
case StatusConnectionLost:
|
||||||
|
return "SSH_FX_CONNECTION_LOST"
|
||||||
|
case StatusOPUnsupported:
|
||||||
|
return "SSH_FX_OP_UNSUPPORTED"
|
||||||
|
case StatusV4InvalidHandle:
|
||||||
|
return "SSH_FX_INVALID_HANDLE"
|
||||||
|
case StatusV4NoSuchPath:
|
||||||
|
return "SSH_FX_NO_SUCH_PATH"
|
||||||
|
case StatusV4FileAlreadyExists:
|
||||||
|
return "SSH_FX_FILE_ALREADY_EXISTS"
|
||||||
|
case StatusV4WriteProtect:
|
||||||
|
return "SSH_FX_WRITE_PROTECT"
|
||||||
|
case StatusV4NoMedia:
|
||||||
|
return "SSH_FX_NO_MEDIA"
|
||||||
|
case StatusV5NoSpaceOnFilesystem:
|
||||||
|
return "SSH_FX_NO_SPACE_ON_FILESYSTEM"
|
||||||
|
case StatusV5QuotaExceeded:
|
||||||
|
return "SSH_FX_QUOTA_EXCEEDED"
|
||||||
|
case StatusV5UnknownPrincipal:
|
||||||
|
return "SSH_FX_UNKNOWN_PRINCIPAL"
|
||||||
|
case StatusV5LockConflict:
|
||||||
|
return "SSH_FX_LOCK_CONFLICT"
|
||||||
|
case StatusV6DirNotEmpty:
|
||||||
|
return "SSH_FX_DIR_NOT_EMPTY"
|
||||||
|
case StatusV6NotADirectory:
|
||||||
|
return "SSH_FX_NOT_A_DIRECTORY"
|
||||||
|
case StatusV6InvalidFilename:
|
||||||
|
return "SSH_FX_INVALID_FILENAME"
|
||||||
|
case StatusV6LinkLoop:
|
||||||
|
return "SSH_FX_LINK_LOOP"
|
||||||
|
case StatusV6CannotDelete:
|
||||||
|
return "SSH_FX_CANNOT_DELETE"
|
||||||
|
case StatusV6InvalidParameter:
|
||||||
|
return "SSH_FX_INVALID_PARAMETER"
|
||||||
|
case StatusV6FileIsADirectory:
|
||||||
|
return "SSH_FX_FILE_IS_A_DIRECTORY"
|
||||||
|
case StatusV6ByteRangeLockConflict:
|
||||||
|
return "SSH_FX_BYTE_RANGE_LOCK_CONFLICT"
|
||||||
|
case StatusV6ByteRangeLockRefused:
|
||||||
|
return "SSH_FX_BYTE_RANGE_LOCK_REFUSED"
|
||||||
|
case StatusV6DeletePending:
|
||||||
|
return "SSH_FX_DELETE_PENDING"
|
||||||
|
case StatusV6FileCorrupt:
|
||||||
|
return "SSH_FX_FILE_CORRUPT"
|
||||||
|
case StatusV6OwnerInvalid:
|
||||||
|
return "SSH_FX_OWNER_INVALID"
|
||||||
|
case StatusV6GroupInvalid:
|
||||||
|
return "SSH_FX_GROUP_INVALID"
|
||||||
|
case StatusV6NoMatchingByteRangeLock:
|
||||||
|
return "SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("SSH_FX_UNKNOWN(%d)", s)
|
||||||
|
}
|
||||||
|
}
|
102
internal/encoding/ssh/filexfer/fx_test.go
Normal file
102
internal/encoding/ssh/filexfer/fx_test.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
|
||||||
|
var fxStandardsText = `
|
||||||
|
SSH_FX_OK 0
|
||||||
|
SSH_FX_EOF 1
|
||||||
|
SSH_FX_NO_SUCH_FILE 2
|
||||||
|
SSH_FX_PERMISSION_DENIED 3
|
||||||
|
SSH_FX_FAILURE 4
|
||||||
|
SSH_FX_BAD_MESSAGE 5
|
||||||
|
SSH_FX_NO_CONNECTION 6
|
||||||
|
SSH_FX_CONNECTION_LOST 7
|
||||||
|
SSH_FX_OP_UNSUPPORTED 8
|
||||||
|
SSH_FX_INVALID_HANDLE 9
|
||||||
|
SSH_FX_NO_SUCH_PATH 10
|
||||||
|
SSH_FX_FILE_ALREADY_EXISTS 11
|
||||||
|
SSH_FX_WRITE_PROTECT 12
|
||||||
|
SSH_FX_NO_MEDIA 13
|
||||||
|
SSH_FX_NO_SPACE_ON_FILESYSTEM 14
|
||||||
|
SSH_FX_QUOTA_EXCEEDED 15
|
||||||
|
SSH_FX_UNKNOWN_PRINCIPAL 16
|
||||||
|
SSH_FX_LOCK_CONFLICT 17
|
||||||
|
SSH_FX_DIR_NOT_EMPTY 18
|
||||||
|
SSH_FX_NOT_A_DIRECTORY 19
|
||||||
|
SSH_FX_INVALID_FILENAME 20
|
||||||
|
SSH_FX_LINK_LOOP 21
|
||||||
|
SSH_FX_CANNOT_DELETE 22
|
||||||
|
SSH_FX_INVALID_PARAMETER 23
|
||||||
|
SSH_FX_FILE_IS_A_DIRECTORY 24
|
||||||
|
SSH_FX_BYTE_RANGE_LOCK_CONFLICT 25
|
||||||
|
SSH_FX_BYTE_RANGE_LOCK_REFUSED 26
|
||||||
|
SSH_FX_DELETE_PENDING 27
|
||||||
|
SSH_FX_FILE_CORRUPT 28
|
||||||
|
SSH_FX_OWNER_INVALID 29
|
||||||
|
SSH_FX_GROUP_INVALID 30
|
||||||
|
SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK 31
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestFxNames(t *testing.T) {
|
||||||
|
whitespace := regexp.MustCompile(`[[:space:]]+`)
|
||||||
|
|
||||||
|
scan := bufio.NewScanner(strings.NewReader(fxStandardsText))
|
||||||
|
|
||||||
|
for scan.Scan() {
|
||||||
|
line := scan.Text()
|
||||||
|
if i := strings.Index(line, "//"); i >= 0 {
|
||||||
|
line = line[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := whitespace.Split(line, 2)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
t.Fatalf("unexpected standards text line: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := fields[0], fields[1]
|
||||||
|
n, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fx := Status(n)
|
||||||
|
|
||||||
|
if got := fx.String(); got != name {
|
||||||
|
t.Errorf("fx name mismatch for %d: got %q, but want %q", n, got, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scan.Err(); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusIs(t *testing.T) {
|
||||||
|
status := StatusFailure
|
||||||
|
|
||||||
|
if !errors.Is(status, StatusFailure) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusFailure) != true")
|
||||||
|
}
|
||||||
|
if !errors.Is(status, &StatusPacket{StatusCode: StatusFailure}) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusPacket{StatusFailure}) != true")
|
||||||
|
}
|
||||||
|
if errors.Is(status, StatusOK) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusFailure) == true")
|
||||||
|
}
|
||||||
|
if errors.Is(status, &StatusPacket{StatusCode: StatusOK}) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusPacket{StatusFailure}) == true")
|
||||||
|
}
|
||||||
|
}
|
124
internal/encoding/ssh/filexfer/fxp.go
Normal file
124
internal/encoding/ssh/filexfer/fxp.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PacketType defines the various SFTP packet types.
|
||||||
|
type PacketType uint8
|
||||||
|
|
||||||
|
// Request packet types.
|
||||||
|
const (
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
|
||||||
|
PacketTypeInit = PacketType(iota + 1)
|
||||||
|
PacketTypeVersion
|
||||||
|
PacketTypeOpen
|
||||||
|
PacketTypeClose
|
||||||
|
PacketTypeRead
|
||||||
|
PacketTypeWrite
|
||||||
|
PacketTypeLStat
|
||||||
|
PacketTypeFStat
|
||||||
|
PacketTypeSetstat
|
||||||
|
PacketTypeFSetstat
|
||||||
|
PacketTypeOpenDir
|
||||||
|
PacketTypeReadDir
|
||||||
|
PacketTypeRemove
|
||||||
|
PacketTypeMkdir
|
||||||
|
PacketTypeRmdir
|
||||||
|
PacketTypeRealPath
|
||||||
|
PacketTypeStat
|
||||||
|
PacketTypeRename
|
||||||
|
PacketTypeReadLink
|
||||||
|
PacketTypeSymlink
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-07#section-3.3
|
||||||
|
PacketTypeV6Link
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-08#section-3.3
|
||||||
|
PacketTypeV6Block
|
||||||
|
PacketTypeV6Unblock
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response packet types.
|
||||||
|
const (
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
|
||||||
|
PacketTypeStatus = PacketType(iota + 101)
|
||||||
|
PacketTypeHandle
|
||||||
|
PacketTypeData
|
||||||
|
PacketTypeName
|
||||||
|
PacketTypeAttrs
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extended packet types.
|
||||||
|
const (
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
|
||||||
|
PacketTypeExtended = PacketType(iota + 200)
|
||||||
|
PacketTypeExtendedReply
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f PacketType) String() string {
|
||||||
|
switch f {
|
||||||
|
case PacketTypeInit:
|
||||||
|
return "SSH_FXP_INIT"
|
||||||
|
case PacketTypeVersion:
|
||||||
|
return "SSH_FXP_VERSION"
|
||||||
|
case PacketTypeOpen:
|
||||||
|
return "SSH_FXP_OPEN"
|
||||||
|
case PacketTypeClose:
|
||||||
|
return "SSH_FXP_CLOSE"
|
||||||
|
case PacketTypeRead:
|
||||||
|
return "SSH_FXP_READ"
|
||||||
|
case PacketTypeWrite:
|
||||||
|
return "SSH_FXP_WRITE"
|
||||||
|
case PacketTypeLStat:
|
||||||
|
return "SSH_FXP_LSTAT"
|
||||||
|
case PacketTypeFStat:
|
||||||
|
return "SSH_FXP_FSTAT"
|
||||||
|
case PacketTypeSetstat:
|
||||||
|
return "SSH_FXP_SETSTAT"
|
||||||
|
case PacketTypeFSetstat:
|
||||||
|
return "SSH_FXP_FSETSTAT"
|
||||||
|
case PacketTypeOpenDir:
|
||||||
|
return "SSH_FXP_OPENDIR"
|
||||||
|
case PacketTypeReadDir:
|
||||||
|
return "SSH_FXP_READDIR"
|
||||||
|
case PacketTypeRemove:
|
||||||
|
return "SSH_FXP_REMOVE"
|
||||||
|
case PacketTypeMkdir:
|
||||||
|
return "SSH_FXP_MKDIR"
|
||||||
|
case PacketTypeRmdir:
|
||||||
|
return "SSH_FXP_RMDIR"
|
||||||
|
case PacketTypeRealPath:
|
||||||
|
return "SSH_FXP_REALPATH"
|
||||||
|
case PacketTypeStat:
|
||||||
|
return "SSH_FXP_STAT"
|
||||||
|
case PacketTypeRename:
|
||||||
|
return "SSH_FXP_RENAME"
|
||||||
|
case PacketTypeReadLink:
|
||||||
|
return "SSH_FXP_READLINK"
|
||||||
|
case PacketTypeSymlink:
|
||||||
|
return "SSH_FXP_SYMLINK"
|
||||||
|
case PacketTypeV6Link:
|
||||||
|
return "SSH_FXP_LINK"
|
||||||
|
case PacketTypeV6Block:
|
||||||
|
return "SSH_FXP_BLOCK"
|
||||||
|
case PacketTypeV6Unblock:
|
||||||
|
return "SSH_FXP_UNBLOCK"
|
||||||
|
case PacketTypeStatus:
|
||||||
|
return "SSH_FXP_STATUS"
|
||||||
|
case PacketTypeHandle:
|
||||||
|
return "SSH_FXP_HANDLE"
|
||||||
|
case PacketTypeData:
|
||||||
|
return "SSH_FXP_DATA"
|
||||||
|
case PacketTypeName:
|
||||||
|
return "SSH_FXP_NAME"
|
||||||
|
case PacketTypeAttrs:
|
||||||
|
return "SSH_FXP_ATTRS"
|
||||||
|
case PacketTypeExtended:
|
||||||
|
return "SSH_FXP_EXTENDED"
|
||||||
|
case PacketTypeExtendedReply:
|
||||||
|
return "SSH_FXP_EXTENDED_REPLY"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("SSH_FXP_UNKNOWN(%d)", f)
|
||||||
|
}
|
||||||
|
}
|
84
internal/encoding/ssh/filexfer/fxp_test.go
Normal file
84
internal/encoding/ssh/filexfer/fxp_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This string data is copied verbatim from https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
|
||||||
|
var fxpStandardsText = `
|
||||||
|
SSH_FXP_INIT 1
|
||||||
|
SSH_FXP_VERSION 2
|
||||||
|
SSH_FXP_OPEN 3
|
||||||
|
SSH_FXP_CLOSE 4
|
||||||
|
SSH_FXP_READ 5
|
||||||
|
SSH_FXP_WRITE 6
|
||||||
|
SSH_FXP_LSTAT 7
|
||||||
|
SSH_FXP_FSTAT 8
|
||||||
|
SSH_FXP_SETSTAT 9
|
||||||
|
SSH_FXP_FSETSTAT 10
|
||||||
|
SSH_FXP_OPENDIR 11
|
||||||
|
SSH_FXP_READDIR 12
|
||||||
|
SSH_FXP_REMOVE 13
|
||||||
|
SSH_FXP_MKDIR 14
|
||||||
|
SSH_FXP_RMDIR 15
|
||||||
|
SSH_FXP_REALPATH 16
|
||||||
|
SSH_FXP_STAT 17
|
||||||
|
SSH_FXP_RENAME 18
|
||||||
|
SSH_FXP_READLINK 19
|
||||||
|
SSH_FXP_SYMLINK 20 // Deprecated in filexfer-13 added from filexfer-02
|
||||||
|
SSH_FXP_LINK 21
|
||||||
|
SSH_FXP_BLOCK 22
|
||||||
|
SSH_FXP_UNBLOCK 23
|
||||||
|
|
||||||
|
SSH_FXP_STATUS 101
|
||||||
|
SSH_FXP_HANDLE 102
|
||||||
|
SSH_FXP_DATA 103
|
||||||
|
SSH_FXP_NAME 104
|
||||||
|
SSH_FXP_ATTRS 105
|
||||||
|
|
||||||
|
SSH_FXP_EXTENDED 200
|
||||||
|
SSH_FXP_EXTENDED_REPLY 201
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestFxpNames(t *testing.T) {
|
||||||
|
whitespace := regexp.MustCompile(`[[:space:]]+`)
|
||||||
|
|
||||||
|
scan := bufio.NewScanner(strings.NewReader(fxpStandardsText))
|
||||||
|
|
||||||
|
for scan.Scan() {
|
||||||
|
line := scan.Text()
|
||||||
|
if i := strings.Index(line, "//"); i >= 0 {
|
||||||
|
line = line[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := whitespace.Split(line, 2)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
t.Fatalf("unexpected standards text line: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := fields[0], fields[1]
|
||||||
|
n, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fxp := PacketType(n)
|
||||||
|
|
||||||
|
if got := fxp.String(); got != name {
|
||||||
|
t.Errorf("fxp name mismatch for %d: got %q, but want %q", n, got, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scan.Err(); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
249
internal/encoding/ssh/filexfer/handle_packets.go
Normal file
249
internal/encoding/ssh/filexfer/handle_packets.go
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// ClosePacket defines the SSH_FXP_CLOSE packet.
|
||||||
|
type ClosePacket struct {
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ClosePacket) Type() PacketType {
|
||||||
|
return PacketTypeClose
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *ClosePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Handle) // string(handle)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeClose, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *ClosePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPacket defines the SSH_FXP_READ packet.
|
||||||
|
type ReadPacket struct {
|
||||||
|
Handle string
|
||||||
|
Offset uint64
|
||||||
|
Len uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ReadPacket) Type() PacketType {
|
||||||
|
return PacketTypeRead
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *ReadPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// string(handle) + uint64(offset) + uint32(len)
|
||||||
|
size := 4 + len(p.Handle) + 8 + 4
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeRead, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
buf.AppendUint64(p.Offset)
|
||||||
|
buf.AppendUint32(p.Len)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *ReadPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Offset, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Len, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritePacket defines the SSH_FXP_WRITE packet.
|
||||||
|
type WritePacket struct {
|
||||||
|
Handle string
|
||||||
|
Offset uint64
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *WritePacket) Type() PacketType {
|
||||||
|
return PacketTypeWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *WritePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// string(handle) + uint64(offset) + uint32(len(data)); data content in payload
|
||||||
|
size := 4 + len(p.Handle) + 8 + 4
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeWrite, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
buf.AppendUint64(p.Offset)
|
||||||
|
buf.AppendUint32(uint32(len(p.Data)))
|
||||||
|
|
||||||
|
return buf.Packet(p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
//
|
||||||
|
// If p.Data is already populated, and of sufficient length to hold the data,
|
||||||
|
// then this will copy the data into that byte slice.
|
||||||
|
//
|
||||||
|
// If p.Data has a length insufficient to hold the data,
|
||||||
|
// then this will make a new slice of sufficient length, and copy the data into that.
|
||||||
|
//
|
||||||
|
// This means this _does not_ alias any of the data buffer that is passed in.
|
||||||
|
func (p *WritePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Offset, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
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.
|
||||||
|
type FStatPacket struct {
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *FStatPacket) Type() PacketType {
|
||||||
|
return PacketTypeFStat
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *FStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Handle) // string(handle)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeFStat, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *FStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSetstatPacket defines the SSH_FXP_FSETSTAT packet.
|
||||||
|
type FSetstatPacket struct {
|
||||||
|
Handle string
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *FSetstatPacket) Type() PacketType {
|
||||||
|
return PacketTypeFSetstat
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *FSetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Handle) + p.Attrs.Len() // string(handle) + ATTRS(attrs)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeFSetstat, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
|
||||||
|
p.Attrs.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *FSetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Attrs.UnmarshalFrom(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDirPacket defines the SSH_FXP_READDIR packet.
|
||||||
|
type ReadDirPacket struct {
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ReadDirPacket) Type() PacketType {
|
||||||
|
return PacketTypeReadDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *ReadDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Handle) // string(handle)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeReadDir, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *ReadDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
282
internal/encoding/ssh/filexfer/handle_packets_test.go
Normal file
282
internal/encoding/ssh/filexfer/handle_packets_test.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Packet = &ClosePacket{}
|
||||||
|
|
||||||
|
func TestClosePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ClosePacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 19,
|
||||||
|
4,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ClosePacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &ReadPacket{}
|
||||||
|
|
||||||
|
func TestReadPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
offset = 0x123456789ABCDEF0
|
||||||
|
length = 0xFEDCBA98
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ReadPacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
Offset: offset,
|
||||||
|
Len: length,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 31,
|
||||||
|
5,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||||
|
0xFE, 0xDC, 0xBA, 0x98,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ReadPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Offset != offset {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Offset was %x, but expected %x", p.Offset, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Len != length {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Len was %x, but expected %x", p.Len, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &WritePacket{}
|
||||||
|
|
||||||
|
func TestWritePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
offset = 0x123456789ABCDEF0
|
||||||
|
)
|
||||||
|
|
||||||
|
var payload = []byte(`foobar`)
|
||||||
|
|
||||||
|
p := &WritePacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
Offset: offset,
|
||||||
|
Data: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 37,
|
||||||
|
6,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||||
|
0x00, 0x00, 0x00, 0x06, 'f', 'o', 'o', 'b', 'a', 'r',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = WritePacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Offset != offset {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Offset was %x, but expected %x", p.Offset, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p.Data, payload) {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Data was %X, but expected %X", p.Data, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &FStatPacket{}
|
||||||
|
|
||||||
|
func TestFStatPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &FStatPacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 19,
|
||||||
|
8,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = FStatPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &FSetstatPacket{}
|
||||||
|
|
||||||
|
func TestFSetstatPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &FSetstatPacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 27,
|
||||||
|
10,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = FSetstatPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &ReadDirPacket{}
|
||||||
|
|
||||||
|
func TestReadDirPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ReadDirPacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 19,
|
||||||
|
12,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ReadDirPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
99
internal/encoding/ssh/filexfer/init_packets.go
Normal file
99
internal/encoding/ssh/filexfer/init_packets.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// InitPacket defines the SSH_FXP_INIT packet.
|
||||||
|
type InitPacket struct {
|
||||||
|
Version uint32
|
||||||
|
Extensions []*ExtensionPair
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns p as the binary encoding of p.
|
||||||
|
func (p *InitPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
size := 1 + 4 // byte(type) + uint32(version)
|
||||||
|
|
||||||
|
for _, ext := range p.Extensions {
|
||||||
|
size += ext.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := NewBuffer(make([]byte, 4, 4+size))
|
||||||
|
b.AppendUint8(uint8(PacketTypeInit))
|
||||||
|
b.AppendUint32(p.Version)
|
||||||
|
|
||||||
|
for _, ext := range p.Extensions {
|
||||||
|
ext.MarshalInto(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.PutLength(size)
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary unmarshals a full raw packet out of the given data.
|
||||||
|
// It is assumed that the uint32(length) has already been consumed to receive the data.
|
||||||
|
// It is also assumed that the uint8(type) has already been consumed to which packet to unmarshal into.
|
||||||
|
func (p *InitPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
buf := NewBuffer(data)
|
||||||
|
|
||||||
|
if p.Version, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for buf.Len() > 0 {
|
||||||
|
var ext ExtensionPair
|
||||||
|
if err := ext.UnmarshalFrom(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Extensions = append(p.Extensions, &ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionPacket defines the SSH_FXP_VERSION packet.
|
||||||
|
type VersionPacket struct {
|
||||||
|
Version uint32
|
||||||
|
Extensions []*ExtensionPair
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns p as the binary encoding of p.
|
||||||
|
func (p *VersionPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
size := 1 + 4 // byte(type) + uint32(version)
|
||||||
|
|
||||||
|
for _, ext := range p.Extensions {
|
||||||
|
size += ext.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := NewBuffer(make([]byte, 4, 4+size))
|
||||||
|
b.AppendUint8(uint8(PacketTypeVersion))
|
||||||
|
b.AppendUint32(p.Version)
|
||||||
|
|
||||||
|
for _, ext := range p.Extensions {
|
||||||
|
ext.MarshalInto(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.PutLength(size)
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary unmarshals a full raw packet out of the given data.
|
||||||
|
// It is assumed that the uint32(length) has already been consumed to receive the data.
|
||||||
|
// It is also assumed that the uint8(type) has already been consumed to which packet to unmarshal into.
|
||||||
|
func (p *VersionPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
buf := NewBuffer(data)
|
||||||
|
|
||||||
|
if p.Version, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for buf.Len() > 0 {
|
||||||
|
var ext ExtensionPair
|
||||||
|
if err := ext.UnmarshalFrom(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Extensions = append(p.Extensions, &ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
114
internal/encoding/ssh/filexfer/init_packets_test.go
Normal file
114
internal/encoding/ssh/filexfer/init_packets_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitPacket(t *testing.T) {
|
||||||
|
var version uint8 = 3
|
||||||
|
|
||||||
|
p := &InitPacket{
|
||||||
|
Version: uint32(version),
|
||||||
|
Extensions: []*ExtensionPair{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Data: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 17,
|
||||||
|
1,
|
||||||
|
0x00, 0x00, 0x00, version,
|
||||||
|
0x00, 0x00, 0x00, 3, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 1, '1',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = InitPacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalBinary(buf[5:]); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Version != uint32(version) {
|
||||||
|
t.Errorf("UnmarshalBinary(): Version was %d, but expected %d", p.Version, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Extensions) != 1 {
|
||||||
|
t.Fatalf("UnmarshalBinary(): len(p.Extensions) was %d, but expected %d", len(p.Extensions), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := p.Extensions[0].Name, "foo"; got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): p.Extensions[0].Name was %q, but expected %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := p.Extensions[0].Data, "1"; got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): p.Extensions[0].Data was %q, but expected %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionPacket(t *testing.T) {
|
||||||
|
var version uint8 = 3
|
||||||
|
|
||||||
|
p := &VersionPacket{
|
||||||
|
Version: uint32(version),
|
||||||
|
Extensions: []*ExtensionPair{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Data: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 17,
|
||||||
|
2,
|
||||||
|
0x00, 0x00, 0x00, version,
|
||||||
|
0x00, 0x00, 0x00, 3, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 1, '1',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = VersionPacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalBinary(buf[5:]); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Version != uint32(version) {
|
||||||
|
t.Errorf("UnmarshalBinary(): Version was %d, but expected %d", p.Version, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Extensions) != 1 {
|
||||||
|
t.Fatalf("UnmarshalBinary(): len(p.Extensions) was %d, but expected %d", len(p.Extensions), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := p.Extensions[0].Name, "foo"; got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): p.Extensions[0].Name was %q, but expected %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := p.Extensions[0].Data, "1"; got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): p.Extensions[0].Data was %q, but expected %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
89
internal/encoding/ssh/filexfer/open_packets.go
Normal file
89
internal/encoding/ssh/filexfer/open_packets.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// SSH_FXF_* flags.
|
||||||
|
const (
|
||||||
|
FlagRead = 1 << iota // SSH_FXF_READ
|
||||||
|
FlagWrite // SSH_FXF_WRITE
|
||||||
|
FlagAppend // SSH_FXF_APPEND
|
||||||
|
FlagCreate // SSH_FXF_CREAT
|
||||||
|
FlagTruncate // SSH_FXF_TRUNC
|
||||||
|
FlagExclusive // SSH_FXF_EXCL
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenPacket defines the SSH_FXP_OPEN packet.
|
||||||
|
type OpenPacket struct {
|
||||||
|
Filename string
|
||||||
|
PFlags uint32
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *OpenPacket) Type() PacketType {
|
||||||
|
return PacketTypeOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *OpenPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// string(filename) + uint32(pflags) + ATTRS(attrs)
|
||||||
|
size := 4 + len(p.Filename) + 4 + p.Attrs.Len()
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeOpen, reqid)
|
||||||
|
buf.AppendString(p.Filename)
|
||||||
|
buf.AppendUint32(p.PFlags)
|
||||||
|
|
||||||
|
p.Attrs.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *OpenPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Filename, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.PFlags, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Attrs.UnmarshalFrom(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenDirPacket defines the SSH_FXP_OPENDIR packet.
|
||||||
|
type OpenDirPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *OpenDirPacket) Type() PacketType {
|
||||||
|
return PacketTypeOpenDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *OpenDirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeOpenDir, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *OpenDirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
107
internal/encoding/ssh/filexfer/open_packets_test.go
Normal file
107
internal/encoding/ssh/filexfer/open_packets_test.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Packet = &OpenPacket{}
|
||||||
|
|
||||||
|
func TestOpenPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
filename = "/foo"
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &OpenPacket{
|
||||||
|
Filename: "/foo",
|
||||||
|
PFlags: FlagRead,
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 25,
|
||||||
|
3,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 1,
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = OpenPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Filename != filename {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Filename was %q, but expected %q", p.Filename, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.PFlags != FlagRead {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): PFlags was %#x, but expected %#x", p.PFlags, FlagRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Flags != AttrPermissions {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Flags was %#x, but expected %#x", p.Attrs.Flags, AttrPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Permissions was %#v, but expected %#v", p.Attrs.Permissions, perms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &OpenDirPacket{}
|
||||||
|
|
||||||
|
func TestOpenDirPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &OpenDirPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
11,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = OpenDirPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
73
internal/encoding/ssh/filexfer/openssh/fsync.go
Normal file
73
internal/encoding/ssh/filexfer/openssh/fsync.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensionFSync = "fsync@openssh.com"
|
||||||
|
|
||||||
|
// RegisterExtensionFSync registers the "fsync@openssh.com" extended packet with the encoding/ssh/filexfer package.
|
||||||
|
func RegisterExtensionFSync() {
|
||||||
|
sshfx.RegisterExtendedPacketType(extensionFSync, func() sshfx.ExtendedData {
|
||||||
|
return new(FSyncExtendedPacket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionFSync returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
|
||||||
|
func ExtensionFSync() *sshfx.ExtensionPair {
|
||||||
|
return &sshfx.ExtensionPair{
|
||||||
|
Name: extensionFSync,
|
||||||
|
Data: "1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSyncExtendedPacket defines the fsync@openssh.com extend packet.
|
||||||
|
type FSyncExtendedPacket struct {
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED packet type.
|
||||||
|
func (ep *FSyncExtendedPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
|
||||||
|
func (ep *FSyncExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedPacket{
|
||||||
|
ExtendedRequest: extensionFSync,
|
||||||
|
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data.
|
||||||
|
func (ep *FSyncExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendString(ep.Handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the fsync@openssh.com extended packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
|
||||||
|
func (ep *FSyncExtendedPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
// string(handle)
|
||||||
|
size := 4 + len(ep.Handle)
|
||||||
|
|
||||||
|
buf := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
ep.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the fsync@openssh.com extended packet-specific data from buf.
|
||||||
|
func (ep *FSyncExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the fsync@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *FSyncExtendedPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
62
internal/encoding/ssh/filexfer/openssh/fsync_test.go
Normal file
62
internal/encoding/ssh/filexfer/openssh/fsync_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sshfx.PacketMarshaller = &FSyncExtendedPacket{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterExtensionFSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFSyncExtendedPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &FSyncExtendedPacket{
|
||||||
|
Handle: handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 40,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 17, 'f', 's', 'y', 'n', 'c', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p sshfx.ExtendedPacket
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extensionFSync {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFSync)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*FSyncExtendedPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FSyncExtendedPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Handle was %q, but expected %q", ep.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
79
internal/encoding/ssh/filexfer/openssh/hardlink.go
Normal file
79
internal/encoding/ssh/filexfer/openssh/hardlink.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensionHardlink = "hardlink@openssh.com"
|
||||||
|
|
||||||
|
// RegisterExtensionHardlink registers the "hardlink@openssh.com" extended packet with the encoding/ssh/filexfer package.
|
||||||
|
func RegisterExtensionHardlink() {
|
||||||
|
sshfx.RegisterExtendedPacketType(extensionHardlink, func() sshfx.ExtendedData {
|
||||||
|
return new(HardlinkExtendedPacket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionHardlink returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
|
||||||
|
func ExtensionHardlink() *sshfx.ExtensionPair {
|
||||||
|
return &sshfx.ExtensionPair{
|
||||||
|
Name: extensionHardlink,
|
||||||
|
Data: "1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HardlinkExtendedPacket defines the hardlink@openssh.com extend packet.
|
||||||
|
type HardlinkExtendedPacket struct {
|
||||||
|
OldPath string
|
||||||
|
NewPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED packet type.
|
||||||
|
func (ep *HardlinkExtendedPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
|
||||||
|
func (ep *HardlinkExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedPacket{
|
||||||
|
ExtendedRequest: extensionHardlink,
|
||||||
|
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
|
||||||
|
func (ep *HardlinkExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendString(ep.OldPath)
|
||||||
|
buf.AppendString(ep.NewPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
|
||||||
|
func (ep *HardlinkExtendedPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
// string(oldpath) + string(newpath)
|
||||||
|
size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath)
|
||||||
|
|
||||||
|
buf := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
ep.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf.
|
||||||
|
func (ep *HardlinkExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.OldPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.NewPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *HardlinkExtendedPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
69
internal/encoding/ssh/filexfer/openssh/hardlink_test.go
Normal file
69
internal/encoding/ssh/filexfer/openssh/hardlink_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sshfx.PacketMarshaller = &HardlinkExtendedPacket{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterExtensionHardlink()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHardlinkExtendedPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
oldpath = "/foo"
|
||||||
|
newpath = "/bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &HardlinkExtendedPacket{
|
||||||
|
OldPath: oldpath,
|
||||||
|
NewPath: newpath,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 45,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 20, 'h', 'a', 'r', 'd', 'l', 'i', 'n', 'k', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p sshfx.ExtendedPacket
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extensionHardlink {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionHardlink)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*HardlinkExtendedPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *HardlinkExtendedPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.OldPath != oldpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.NewPath != newpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath)
|
||||||
|
}
|
||||||
|
}
|
2
internal/encoding/ssh/filexfer/openssh/openssh.go
Normal file
2
internal/encoding/ssh/filexfer/openssh/openssh.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// Package openssh implements the openssh secsh-filexfer extensions as described in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
|
||||||
|
package openssh
|
79
internal/encoding/ssh/filexfer/openssh/posix-rename.go
Normal file
79
internal/encoding/ssh/filexfer/openssh/posix-rename.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensionPosixRename = "posix-rename@openssh.com"
|
||||||
|
|
||||||
|
// RegisterExtensionPosixRename registers the "posix-rename@openssh.com" extended packet with the encoding/ssh/filexfer package.
|
||||||
|
func RegisterExtensionPosixRename() {
|
||||||
|
sshfx.RegisterExtendedPacketType(extensionPosixRename, func() sshfx.ExtendedData {
|
||||||
|
return new(PosixRenameExtendedPacket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionPosixRename returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
|
||||||
|
func ExtensionPosixRename() *sshfx.ExtensionPair {
|
||||||
|
return &sshfx.ExtensionPair{
|
||||||
|
Name: extensionPosixRename,
|
||||||
|
Data: "1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PosixRenameExtendedPacket defines the posix-rename@openssh.com extend packet.
|
||||||
|
type PosixRenameExtendedPacket struct {
|
||||||
|
OldPath string
|
||||||
|
NewPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED packet type.
|
||||||
|
func (ep *PosixRenameExtendedPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
|
||||||
|
func (ep *PosixRenameExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedPacket{
|
||||||
|
ExtendedRequest: extensionPosixRename,
|
||||||
|
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
|
||||||
|
func (ep *PosixRenameExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendString(ep.OldPath)
|
||||||
|
buf.AppendString(ep.NewPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the hardlink@openssh.com extended packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
|
||||||
|
func (ep *PosixRenameExtendedPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
// string(oldpath) + string(newpath)
|
||||||
|
size := 4 + len(ep.OldPath) + 4 + len(ep.NewPath)
|
||||||
|
|
||||||
|
buf := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
ep.MarshalInto(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the hardlink@openssh.com extended packet-specific data from buf.
|
||||||
|
func (ep *PosixRenameExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.OldPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.NewPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the hardlink@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *PosixRenameExtendedPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
69
internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
Normal file
69
internal/encoding/ssh/filexfer/openssh/posix-rename_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sshfx.PacketMarshaller = &PosixRenameExtendedPacket{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterExtensionPosixRename()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosixRenameExtendedPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
oldpath = "/foo"
|
||||||
|
newpath = "/bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &PosixRenameExtendedPacket{
|
||||||
|
OldPath: oldpath,
|
||||||
|
NewPath: newpath,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 49,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 24, 'p', 'o', 's', 'i', 'x', '-', 'r', 'e', 'n', 'a', 'm', 'e', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p sshfx.ExtendedPacket
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extensionPosixRename {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionPosixRename)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*PosixRenameExtendedPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *PosixRenameExtendedPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.OldPath != oldpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", ep.OldPath, oldpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.NewPath != newpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", ep.NewPath, newpath)
|
||||||
|
}
|
||||||
|
}
|
256
internal/encoding/ssh/filexfer/openssh/statvfs.go
Normal file
256
internal/encoding/ssh/filexfer/openssh/statvfs.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extensionStatVFS = "statvfs@openssh.com"
|
||||||
|
|
||||||
|
// RegisterExtensionStatVFS registers the "statvfs@openssh.com" extended packet with the encoding/ssh/filexfer package.
|
||||||
|
func RegisterExtensionStatVFS() {
|
||||||
|
sshfx.RegisterExtendedPacketType(extensionStatVFS, func() sshfx.ExtendedData {
|
||||||
|
return new(StatVFSExtendedPacket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
|
||||||
|
func ExtensionStatVFS() *sshfx.ExtensionPair {
|
||||||
|
return &sshfx.ExtensionPair{
|
||||||
|
Name: extensionStatVFS,
|
||||||
|
Data: "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatVFSExtendedPacket defines the statvfs@openssh.com extend packet.
|
||||||
|
type StatVFSExtendedPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED packet type.
|
||||||
|
func (ep *StatVFSExtendedPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
|
||||||
|
func (ep *StatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedPacket{
|
||||||
|
ExtendedRequest: extensionStatVFS,
|
||||||
|
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
|
||||||
|
func (ep *StatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendString(ep.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
|
||||||
|
func (ep *StatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
size := 4 + len(ep.Path) // string(path)
|
||||||
|
|
||||||
|
buf := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
|
||||||
|
ep.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *StatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *StatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionFStatVFS = "fstatvfs@openssh.com"
|
||||||
|
|
||||||
|
// RegisterExtensionFStatVFS registers the "fstatvfs@openssh.com" extended packet with the encoding/ssh/filexfer package.
|
||||||
|
func RegisterExtensionFStatVFS() {
|
||||||
|
sshfx.RegisterExtendedPacketType(extensionFStatVFS, func() sshfx.ExtendedData {
|
||||||
|
return new(FStatVFSExtendedPacket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionFStatVFS returns an ExtensionPair suitable to append into an sshfx.InitPacket or sshfx.VersionPacket.
|
||||||
|
func ExtensionFStatVFS() *sshfx.ExtensionPair {
|
||||||
|
return &sshfx.ExtensionPair{
|
||||||
|
Name: extensionFStatVFS,
|
||||||
|
Data: "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FStatVFSExtendedPacket defines the fstatvfs@openssh.com extend packet.
|
||||||
|
type FStatVFSExtendedPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED packet type.
|
||||||
|
func (ep *FStatVFSExtendedPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended packet.
|
||||||
|
func (ep *FStatVFSExtendedPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedPacket{
|
||||||
|
ExtendedRequest: extensionFStatVFS,
|
||||||
|
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
|
||||||
|
func (ep *FStatVFSExtendedPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendString(ep.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the statvfs@openssh.com extended packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended packet.
|
||||||
|
func (ep *FStatVFSExtendedPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
size := 4 + len(ep.Path) // string(path)
|
||||||
|
|
||||||
|
buf := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
|
||||||
|
ep.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the statvfs@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *FStatVFSExtendedPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the statvfs@openssh.com extended packet-specific data into ep.
|
||||||
|
func (ep *FStatVFSExtendedPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The values for the MountFlags field.
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
|
||||||
|
const (
|
||||||
|
MountFlagsReadOnly = 0x1 // SSH_FXE_STATVFS_ST_RDONLY
|
||||||
|
MountFlagsNoSUID = 0x2 // SSH_FXE_STATVFS_ST_NOSUID
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatVFSExtendedReplyPacket defines the extended reply packet for statvfs@openssh.com and fstatvfs@openssh.com requests.
|
||||||
|
type StatVFSExtendedReplyPacket struct {
|
||||||
|
BlockSize uint64 /* f_bsize: file system block size */
|
||||||
|
FragmentSize uint64 /* f_frsize: fundamental fs block size / fagment size */
|
||||||
|
Blocks uint64 /* f_blocks: number of blocks (unit f_frsize) */
|
||||||
|
BlocksFree uint64 /* f_bfree: free blocks in filesystem */
|
||||||
|
BlocksAvail uint64 /* f_bavail: free blocks for non-root */
|
||||||
|
Files uint64 /* f_files: total file inodes */
|
||||||
|
FilesFree uint64 /* f_ffree: free file inodes */
|
||||||
|
FilesAvail uint64 /* f_favail: free file inodes for to non-root */
|
||||||
|
FilesystemID uint64 /* f_fsid: file system id */
|
||||||
|
MountFlags uint64 /* f_flag: bit mask of mount flag values */
|
||||||
|
MaxNameLength uint64 /* f_namemax: maximum filename length */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_EXTENDED_REPLY packet type.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) Type() sshfx.PacketType {
|
||||||
|
return sshfx.PacketTypeExtendedReply
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns ep as a two-part binary encoding of the full extended reply packet.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
p := &sshfx.ExtendedReplyPacket{
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody returns ep as a two-part binary encoding of the full extended reply packet.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) UnmarshalPacketBody(buf *sshfx.Buffer) (err error) {
|
||||||
|
p := &sshfx.ExtendedReplyPacket{
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
return p.UnmarshalPacketBody(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalInto encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) MarshalInto(buf *sshfx.Buffer) {
|
||||||
|
buf.AppendUint64(ep.BlockSize)
|
||||||
|
buf.AppendUint64(ep.FragmentSize)
|
||||||
|
buf.AppendUint64(ep.Blocks)
|
||||||
|
buf.AppendUint64(ep.BlocksFree)
|
||||||
|
buf.AppendUint64(ep.BlocksAvail)
|
||||||
|
buf.AppendUint64(ep.Files)
|
||||||
|
buf.AppendUint64(ep.FilesFree)
|
||||||
|
buf.AppendUint64(ep.FilesAvail)
|
||||||
|
buf.AppendUint64(ep.FilesystemID)
|
||||||
|
buf.AppendUint64(ep.MountFlags)
|
||||||
|
buf.AppendUint64(ep.MaxNameLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes ep into the binary encoding of the (f)statvfs@openssh.com extended reply packet-specific data.
|
||||||
|
//
|
||||||
|
// NOTE: This _only_ encodes the packet-specific data, it does not encode the full extended reply packet.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
size := 11 * 8 // 11 × uint64(various)
|
||||||
|
|
||||||
|
b := sshfx.NewBuffer(make([]byte, 0, size))
|
||||||
|
ep.MarshalInto(b)
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes the fstatvfs@openssh.com extended reply packet-specific data into ep.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) UnmarshalFrom(buf *sshfx.Buffer) (err error) {
|
||||||
|
if ep.BlockSize, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.FragmentSize, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.Blocks, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.BlocksFree, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.BlocksAvail, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.Files, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.FilesFree, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.FilesAvail, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.FilesystemID, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.MountFlags, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ep.MaxNameLength, err = buf.ConsumeUint64(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the fstatvfs@openssh.com extended reply packet-specific data into ep.
|
||||||
|
func (ep *StatVFSExtendedReplyPacket) UnmarshalBinary(data []byte) (err error) {
|
||||||
|
return ep.UnmarshalFrom(sshfx.NewBuffer(data))
|
||||||
|
}
|
239
internal/encoding/ssh/filexfer/openssh/statvfs_test.go
Normal file
239
internal/encoding/ssh/filexfer/openssh/statvfs_test.go
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
package openssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sshfx.PacketMarshaller = &StatVFSExtendedPacket{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterExtensionStatVFS()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatVFSExtendedPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &StatVFSExtendedPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 36,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 19, 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p sshfx.ExtendedPacket
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extensionStatVFS {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionStatVFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*StatVFSExtendedPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ sshfx.PacketMarshaller = &FStatVFSExtendedPacket{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterExtensionFStatVFS()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFStatVFSExtendedPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &FStatVFSExtendedPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 37,
|
||||||
|
200,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 20, 'f', 's', 't', 'a', 't', 'v', 'f', 's', '@', 'o', 'p', 'e', 'n', 's', 's', 'h', '.', 'c', 'o', 'm',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p sshfx.ExtendedPacket
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ExtendedRequest != extensionFStatVFS {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ExtendedRequest was %q, but expected %q", p.ExtendedRequest, extensionFStatVFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*FStatVFSExtendedPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *FStatVFSExtendedPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", ep.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ sshfx.Packet = &StatVFSExtendedReplyPacket{}
|
||||||
|
|
||||||
|
func TestStatVFSExtendedReplyPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BlockSize = uint64(iota + 13)
|
||||||
|
FragmentSize
|
||||||
|
Blocks
|
||||||
|
BlocksFree
|
||||||
|
BlocksAvail
|
||||||
|
Files
|
||||||
|
FilesFree
|
||||||
|
FilesAvail
|
||||||
|
FilesystemID
|
||||||
|
MountFlags
|
||||||
|
MaxNameLength
|
||||||
|
)
|
||||||
|
|
||||||
|
ep := &StatVFSExtendedReplyPacket{
|
||||||
|
BlockSize: BlockSize,
|
||||||
|
FragmentSize: FragmentSize,
|
||||||
|
Blocks: Blocks,
|
||||||
|
BlocksFree: BlocksFree,
|
||||||
|
BlocksAvail: BlocksAvail,
|
||||||
|
Files: Files,
|
||||||
|
FilesFree: FilesFree,
|
||||||
|
FilesAvail: FilesAvail,
|
||||||
|
FilesystemID: FilesystemID,
|
||||||
|
MountFlags: MountFlags,
|
||||||
|
MaxNameLength: MaxNameLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := sshfx.ComposePacket(ep.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 93,
|
||||||
|
201,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*ep = StatVFSExtendedReplyPacket{}
|
||||||
|
|
||||||
|
p := sshfx.ExtendedReplyPacket{
|
||||||
|
Data: ep,
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(sshfx.NewBuffer(data[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, ok := p.Data.(*StatVFSExtendedReplyPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("UnmarshaledPacketBody(): Data was type %T, but expected *StatVFSExtendedReplyPacket", p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.BlockSize != BlockSize {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): BlockSize was %d, but expected %d", ep.BlockSize, BlockSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.FragmentSize != FragmentSize {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): FragmentSize was %d, but expected %d", ep.FragmentSize, FragmentSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Blocks != Blocks {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Blocks was %d, but expected %d", ep.Blocks, Blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.BlocksFree != BlocksFree {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): BlocksFree was %d, but expected %d", ep.BlocksFree, BlocksFree)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.BlocksAvail != BlocksAvail {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): BlocksAvail was %d, but expected %d", ep.BlocksAvail, BlocksAvail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Files != Files {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Files was %d, but expected %d", ep.Files, Files)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.FilesFree != FilesFree {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): FilesFree was %d, but expected %d", ep.FilesFree, FilesFree)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.FilesAvail != FilesAvail {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): FilesAvail was %d, but expected %d", ep.FilesAvail, FilesAvail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.FilesystemID != FilesystemID {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): FilesystemID was %d, but expected %d", ep.FilesystemID, FilesystemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.MountFlags != MountFlags {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): MountFlags was %d, but expected %d", ep.MountFlags, MountFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.MaxNameLength != MaxNameLength {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): MaxNameLength was %d, but expected %d", ep.MaxNameLength, MaxNameLength)
|
||||||
|
}
|
||||||
|
}
|
323
internal/encoding/ssh/filexfer/packets.go
Normal file
323
internal/encoding/ssh/filexfer/packets.go
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// smallBufferSize is an initial allocation minimal capacity.
|
||||||
|
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 is intended for use in clients receiving responses,
|
||||||
|
// where a response will be expected to be of a limited number of types,
|
||||||
|
// and unmarshaling unknown/unexpected response packets is unnecessary.
|
||||||
|
//
|
||||||
|
// For servers expecting to receive arbitrary request packet types,
|
||||||
|
// use RequestPacket.
|
||||||
|
//
|
||||||
|
// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
|
||||||
|
type RawPacket struct {
|
||||||
|
PacketType PacketType
|
||||||
|
RequestID uint32
|
||||||
|
|
||||||
|
Data Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the Type field defining the SSH_FXP_xy type for this packet.
|
||||||
|
func (p *RawPacket) Type() PacketType {
|
||||||
|
return p.PacketType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the pointers and reference-semantic variables of RawPacket,
|
||||||
|
// releasing underlying resources, and making them and the RawPacket suitable to be reused,
|
||||||
|
// so long as no other references have been kept.
|
||||||
|
func (p *RawPacket) Reset() {
|
||||||
|
p.Data = Buffer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
//
|
||||||
|
// The internal p.RequestID is overridden by the reqid argument.
|
||||||
|
func (p *RawPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
buf = NewMarshalBuffer(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(p.PacketType, reqid)
|
||||||
|
|
||||||
|
return buf.Packet(p.Data.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns p as the binary encoding of p.
|
||||||
|
//
|
||||||
|
// This is a convenience implementation primarily intended for tests,
|
||||||
|
// because it is inefficient with allocations.
|
||||||
|
func (p *RawPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
return ComposePacket(p.MarshalPacket(p.RequestID, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes a RawPacket from the given Buffer into p.
|
||||||
|
//
|
||||||
|
// The Data field will alias the passed in Buffer,
|
||||||
|
// so the buffer passed in should not be reused before RawPacket.Reset().
|
||||||
|
func (p *RawPacket) UnmarshalFrom(buf *Buffer) error {
|
||||||
|
typ, err := buf.ConsumeUint8()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.PacketType = PacketType(typ)
|
||||||
|
|
||||||
|
if p.RequestID, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Data = *buf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes a full raw packet out of the given data.
|
||||||
|
// It is assumed that the uint32(length) has already been consumed to receive the data.
|
||||||
|
//
|
||||||
|
// This is a convenience implementation primarily intended for tests,
|
||||||
|
// because this must clone the given data byte slice,
|
||||||
|
// as Data is not allowed to alias any part of the data byte slice.
|
||||||
|
func (p *RawPacket) UnmarshalBinary(data []byte) error {
|
||||||
|
clone := make([]byte, len(data))
|
||||||
|
n := copy(clone, data)
|
||||||
|
return p.UnmarshalFrom(NewBuffer(clone[:n]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPacket reads a uint32 length-prefixed binary data packet from r.
|
||||||
|
// using the given byte slice as a backing array.
|
||||||
|
//
|
||||||
|
// If the packet length read from r is bigger than maxPacketLength,
|
||||||
|
// or greater than math.MaxInt32 on a 32-bit implementation,
|
||||||
|
// then a `ErrLongPacket` error will be returned.
|
||||||
|
//
|
||||||
|
// If the given byte slice is insufficient to hold the packet,
|
||||||
|
// then it will be extended to fill the packet size.
|
||||||
|
func readPacket(r io.Reader, b []byte, maxPacketLength uint32) ([]byte, error) {
|
||||||
|
if cap(b) < 4 {
|
||||||
|
// We will need allocate our own buffer just for reading the packet length.
|
||||||
|
|
||||||
|
// However, we don’t really want to allocate an extremely narrow buffer (4-bytes),
|
||||||
|
// and cause unnecessary allocation churn from both length reads and small packet reads,
|
||||||
|
// so we use smallBufferSize from the bytes package as a reasonable guess.
|
||||||
|
|
||||||
|
// But if callers really do want to force narrow throw-away allocation of every packet body,
|
||||||
|
// they can do so with a buffer of capacity 4.
|
||||||
|
b = make([]byte, smallBufferSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(r, b[:4]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
length := unmarshalUint32(b)
|
||||||
|
if int(length) < 5 {
|
||||||
|
// Must have at least uint8(type) and uint32(request-id)
|
||||||
|
|
||||||
|
if int(length) < 0 {
|
||||||
|
// Only possible when strconv.IntSize == 32,
|
||||||
|
// the packet length is longer than math.MaxInt32,
|
||||||
|
// and thus longer than any possible slice.
|
||||||
|
return nil, ErrLongPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrShortPacket
|
||||||
|
}
|
||||||
|
if length > maxPacketLength {
|
||||||
|
return nil, ErrLongPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(length) > cap(b) {
|
||||||
|
// We know int(length) must be positive, because of tests above.
|
||||||
|
b = make([]byte, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := io.ReadFull(r, b[:length])
|
||||||
|
return b[:n], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom provides a simple functional packet reader,
|
||||||
|
// using the given byte slice as a backing array.
|
||||||
|
//
|
||||||
|
// To protect against potential denial of service attacks,
|
||||||
|
// if the read packet length is longer than maxPacketLength,
|
||||||
|
// then no packet data will be read, and ErrLongPacket will be returned.
|
||||||
|
// (On 32-bit int architectures, all packets >= 2^31 in length
|
||||||
|
// will return ErrLongPacket regardless of maxPacketLength.)
|
||||||
|
//
|
||||||
|
// If the read packet length is longer than cap(b),
|
||||||
|
// then a throw-away slice will allocated to meet the exact packet length.
|
||||||
|
// This can be used to limit the length of reused buffers,
|
||||||
|
// while still allowing reception of occasional large packets.
|
||||||
|
//
|
||||||
|
// The Data field may alias the passed in byte slice,
|
||||||
|
// so the byte slice passed in should not be reused before RawPacket.Reset().
|
||||||
|
func (p *RawPacket) ReadFrom(r io.Reader, b []byte, maxPacketLength uint32) error {
|
||||||
|
b, err := readPacket(r, b, maxPacketLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.UnmarshalFrom(NewBuffer(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestPacket implements the general packet format from draft-ietf-secsh-filexfer-02
|
||||||
|
// but also automatically decode/encodes valid request packets (2 < type < 100 || type == 200).
|
||||||
|
//
|
||||||
|
// RequestPacket is intended for use in servers receiving requests,
|
||||||
|
// where any arbitrary request may be received, and so decoding them automatically
|
||||||
|
// is useful.
|
||||||
|
//
|
||||||
|
// For clients expecting to receive specific response packet types,
|
||||||
|
// where automatic unmarshaling of the packet body does not make sense,
|
||||||
|
// use RawPacket.
|
||||||
|
//
|
||||||
|
// Defined in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-3
|
||||||
|
type RequestPacket struct {
|
||||||
|
RequestID uint32
|
||||||
|
|
||||||
|
Request Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with the underlying packet.
|
||||||
|
func (p *RequestPacket) Type() PacketType {
|
||||||
|
return p.Request.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the pointers and reference-semantic variables in RequestPacket,
|
||||||
|
// releasing underlying resources, and making them and the RequestPacket suitable to be reused,
|
||||||
|
// so long as no other references have been kept.
|
||||||
|
func (p *RequestPacket) Reset() {
|
||||||
|
p.Request = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
//
|
||||||
|
// The internal p.RequestID is overridden by the reqid argument.
|
||||||
|
func (p *RequestPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
if p.Request == nil {
|
||||||
|
return nil, nil, errors.New("empty request packet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Request.MarshalPacket(reqid, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary returns p as the binary encoding of p.
|
||||||
|
//
|
||||||
|
// This is a convenience implementation primarily intended for tests,
|
||||||
|
// because it is inefficient with allocations.
|
||||||
|
func (p *RequestPacket) MarshalBinary() ([]byte, error) {
|
||||||
|
return ComposePacket(p.MarshalPacket(p.RequestID, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFrom decodes a RequestPacket from the given Buffer into p.
|
||||||
|
//
|
||||||
|
// 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().
|
||||||
|
func (p *RequestPacket) UnmarshalFrom(buf *Buffer) error {
|
||||||
|
typ, err := buf.ConsumeUint8()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Request, err = newPacketFromType(PacketType(typ))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.RequestID, err = buf.ConsumeUint32(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Request.UnmarshalPacketBody(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes a full request packet out of the given data.
|
||||||
|
// It is assumed that the uint32(length) has already been consumed to receive the data.
|
||||||
|
//
|
||||||
|
// This is a convenience implementation primarily intended for tests,
|
||||||
|
// because this must clone the given data byte slice,
|
||||||
|
// as Request is not allowed to alias any part of the data byte slice.
|
||||||
|
func (p *RequestPacket) UnmarshalBinary(data []byte) error {
|
||||||
|
clone := make([]byte, len(data))
|
||||||
|
n := copy(clone, data)
|
||||||
|
return p.UnmarshalFrom(NewBuffer(clone[:n]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom provides a simple functional packet reader,
|
||||||
|
// using the given byte slice as a backing array.
|
||||||
|
//
|
||||||
|
// To protect against potential denial of service attacks,
|
||||||
|
// if the read packet length is longer than maxPacketLength,
|
||||||
|
// then no packet data will be read, and ErrLongPacket will be returned.
|
||||||
|
// (On 32-bit int architectures, all packets >= 2^31 in length
|
||||||
|
// will return ErrLongPacket regardless of maxPacketLength.)
|
||||||
|
//
|
||||||
|
// If the read packet length is longer than cap(b),
|
||||||
|
// then a throw-away slice will allocated to meet the exact packet length.
|
||||||
|
// This can be used to limit the length of reused buffers,
|
||||||
|
// while still allowing reception of occasional large packets.
|
||||||
|
//
|
||||||
|
// The Request field may alias the passed in byte slice,
|
||||||
|
// so the byte slice passed in should not be reused before RawPacket.Reset().
|
||||||
|
func (p *RequestPacket) ReadFrom(r io.Reader, b []byte, maxPacketLength uint32) error {
|
||||||
|
b, err := readPacket(r, b, maxPacketLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.UnmarshalFrom(NewBuffer(b))
|
||||||
|
}
|
132
internal/encoding/ssh/filexfer/packets_test.go
Normal file
132
internal/encoding/ssh/filexfer/packets_test.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRawPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
errMsg = "eof"
|
||||||
|
langTag = "en"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RawPacket{
|
||||||
|
PacketType: PacketTypeStatus,
|
||||||
|
RequestID: id,
|
||||||
|
Data: Buffer{
|
||||||
|
b: []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 'e', 'o', 'f',
|
||||||
|
0x00, 0x00, 0x00, 0x02, 'e', 'n',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 22,
|
||||||
|
101,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 3, 'e', 'o', 'f',
|
||||||
|
0x00, 0x00, 0x00, 2, 'e', 'n',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Errorf("RawPacket.MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RawPacket{}
|
||||||
|
|
||||||
|
if err := p.ReadFrom(bytes.NewReader(buf), nil, DefaultMaxPacketLength); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.PacketType != PacketTypeStatus {
|
||||||
|
t.Errorf("RawPacket.UnmarshalBinary(): Type was %v, but expected %v", p.PacketType, PacketTypeStat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.RequestID != uint32(id) {
|
||||||
|
t.Errorf("RawPacket.UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
want = []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 3, 'e', 'o', 'f',
|
||||||
|
0x00, 0x00, 0x00, 2, 'e', 'n',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p.Data.Bytes(), want) {
|
||||||
|
t.Fatalf("RawPacket.UnmarshalBinary(): Data was %X, but expected %X", p.Data, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp StatusPacket
|
||||||
|
resp.UnmarshalPacketBody(&p.Data)
|
||||||
|
|
||||||
|
if resp.StatusCode != StatusEOF {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): StatusCode was %v, but expected %v", resp.StatusCode, StatusEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.ErrorMessage != errMsg {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): ErrorMessage was %q, but expected %q", resp.ErrorMessage, errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.LanguageTag != langTag {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): LanguageTag was %q, but expected %q", resp.LanguageTag, langTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RequestPacket{
|
||||||
|
RequestID: id,
|
||||||
|
Request: &StatPacket{
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 12,
|
||||||
|
17,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 3, 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Errorf("RequestPacket.MarshalBinary() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RequestPacket{}
|
||||||
|
|
||||||
|
if err := p.ReadFrom(bytes.NewReader(buf), nil, DefaultMaxPacketLength); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.RequestID != uint32(id) {
|
||||||
|
t.Errorf("RequestPacket.UnmarshalBinary(): RequestID was %d, but expected %d", p.RequestID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := p.Request.(*StatPacket)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected Request type was %T, but expected %T", p.Request, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Path != path {
|
||||||
|
t.Errorf("RequestPacket.UnmarshalBinary(): Request.Path was %q, but expected %q", req.Path, path)
|
||||||
|
}
|
||||||
|
}
|
368
internal/encoding/ssh/filexfer/path_packets.go
Normal file
368
internal/encoding/ssh/filexfer/path_packets.go
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// LStatPacket defines the SSH_FXP_LSTAT packet.
|
||||||
|
type LStatPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *LStatPacket) Type() PacketType {
|
||||||
|
return PacketTypeLStat
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *LStatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeLStat, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *LStatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetstatPacket defines the SSH_FXP_SETSTAT packet.
|
||||||
|
type SetstatPacket struct {
|
||||||
|
Path string
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *SetstatPacket) Type() PacketType {
|
||||||
|
return PacketTypeSetstat
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *SetstatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) + p.Attrs.Len() // string(path) + ATTRS(attrs)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeSetstat, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
p.Attrs.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *SetstatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Attrs.UnmarshalFrom(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePacket defines the SSH_FXP_REMOVE packet.
|
||||||
|
type RemovePacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *RemovePacket) Type() PacketType {
|
||||||
|
return PacketTypeRemove
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *RemovePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeRemove, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *RemovePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirPacket defines the SSH_FXP_MKDIR packet.
|
||||||
|
type MkdirPacket struct {
|
||||||
|
Path string
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *MkdirPacket) Type() PacketType {
|
||||||
|
return PacketTypeMkdir
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *MkdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) + p.Attrs.Len() // string(path) + ATTRS(attrs)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeMkdir, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
p.Attrs.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *MkdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Attrs.UnmarshalFrom(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RmdirPacket defines the SSH_FXP_RMDIR packet.
|
||||||
|
type RmdirPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *RmdirPacket) Type() PacketType {
|
||||||
|
return PacketTypeRmdir
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *RmdirPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeRmdir, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *RmdirPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RealPathPacket defines the SSH_FXP_REALPATH packet.
|
||||||
|
type RealPathPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *RealPathPacket) Type() PacketType {
|
||||||
|
return PacketTypeRealPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *RealPathPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeRealPath, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *RealPathPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatPacket defines the SSH_FXP_STAT packet.
|
||||||
|
type StatPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *StatPacket) Type() PacketType {
|
||||||
|
return PacketTypeStat
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *StatPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeStat, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *StatPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenamePacket defines the SSH_FXP_RENAME packet.
|
||||||
|
type RenamePacket struct {
|
||||||
|
OldPath string
|
||||||
|
NewPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *RenamePacket) Type() PacketType {
|
||||||
|
return PacketTypeRename
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *RenamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// string(oldpath) + string(newpath)
|
||||||
|
size := 4 + len(p.OldPath) + 4 + len(p.NewPath)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeRename, reqid)
|
||||||
|
buf.AppendString(p.OldPath)
|
||||||
|
buf.AppendString(p.NewPath)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *RenamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.OldPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.NewPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLinkPacket defines the SSH_FXP_READLINK packet.
|
||||||
|
type ReadLinkPacket struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *ReadLinkPacket) Type() PacketType {
|
||||||
|
return PacketTypeReadLink
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *ReadLinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Path) // string(path)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeReadLink, reqid)
|
||||||
|
buf.AppendString(p.Path)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *ReadLinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Path, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SymlinkPacket defines the SSH_FXP_SYMLINK packet.
|
||||||
|
//
|
||||||
|
// 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 3.1 of https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
|
||||||
|
type SymlinkPacket struct {
|
||||||
|
LinkPath string
|
||||||
|
TargetPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *SymlinkPacket) Type() PacketType {
|
||||||
|
return PacketTypeSymlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *SymlinkPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// string(targetpath) + string(linkpath)
|
||||||
|
size := 4 + len(p.TargetPath) + 4 + len(p.LinkPath)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeSymlink, reqid)
|
||||||
|
|
||||||
|
// Arguments were inadvertently reversed.
|
||||||
|
buf.AppendString(p.TargetPath)
|
||||||
|
buf.AppendString(p.LinkPath)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *SymlinkPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
// Arguments were inadvertently reversed.
|
||||||
|
if p.TargetPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.LinkPath, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
450
internal/encoding/ssh/filexfer/path_packets_test.go
Normal file
450
internal/encoding/ssh/filexfer/path_packets_test.go
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Packet = &LStatPacket{}
|
||||||
|
|
||||||
|
func TestLStatPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &LStatPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
7,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = LStatPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &SetstatPacket{}
|
||||||
|
|
||||||
|
func TestSetstatPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &SetstatPacket{
|
||||||
|
Path: "/foo",
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 21,
|
||||||
|
9,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = SetstatPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Flags != AttrPermissions {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Flags was %#x, but expected %#x", p.Attrs.Flags, AttrPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Permissions was %#v, but expected %#v", p.Attrs.Permissions, perms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &RemovePacket{}
|
||||||
|
|
||||||
|
func TestRemovePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RemovePacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
13,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RemovePacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &MkdirPacket{}
|
||||||
|
|
||||||
|
func TestMkdirPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &MkdirPacket{
|
||||||
|
Path: "/foo",
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 21,
|
||||||
|
14,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = MkdirPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Flags != AttrPermissions {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Flags was %#x, but expected %#x", p.Attrs.Flags, AttrPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Attrs.Permissions was %#v, but expected %#v", p.Attrs.Permissions, perms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &RmdirPacket{}
|
||||||
|
|
||||||
|
func TestRmdirPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RmdirPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
15,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RmdirPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &RealPathPacket{}
|
||||||
|
|
||||||
|
func TestRealPathPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RealPathPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
16,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RealPathPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &StatPacket{}
|
||||||
|
|
||||||
|
func TestStatPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &StatPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
17,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = StatPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &RenamePacket{}
|
||||||
|
|
||||||
|
func TestRenamePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
oldpath = "/foo"
|
||||||
|
newpath = "/bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &RenamePacket{
|
||||||
|
OldPath: oldpath,
|
||||||
|
NewPath: newpath,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 21,
|
||||||
|
18,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = RenamePacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.OldPath != oldpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): OldPath was %q, but expected %q", p.OldPath, oldpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.NewPath != newpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): NewPath was %q, but expected %q", p.NewPath, newpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &ReadLinkPacket{}
|
||||||
|
|
||||||
|
func TestReadLinkPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
path = "/foo"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &ReadLinkPacket{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
19,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = ReadLinkPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != path {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): Path was %q, but expected %q", p.Path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &SymlinkPacket{}
|
||||||
|
|
||||||
|
func TestSymlinkPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
linkpath = "/foo"
|
||||||
|
targetpath = "/bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &SymlinkPacket{
|
||||||
|
LinkPath: linkpath,
|
||||||
|
TargetPath: targetpath,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 21,
|
||||||
|
20,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'b', 'a', 'r', // Arguments were inadvertently reversed.
|
||||||
|
0x00, 0x00, 0x00, 4, '/', 'f', 'o', 'o',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = SymlinkPacket{}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody assumes the (length, type, request-id) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.LinkPath != linkpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): LinkPath was %q, but expected %q", p.LinkPath, linkpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.TargetPath != targetpath {
|
||||||
|
t.Errorf("UnmarshalPacketBody(): TargetPath was %q, but expected %q", p.TargetPath, targetpath)
|
||||||
|
}
|
||||||
|
}
|
114
internal/encoding/ssh/filexfer/permissions.go
Normal file
114
internal/encoding/ssh/filexfer/permissions.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
// FileMode represents a file’s mode and permission bits.
|
||||||
|
// The bits are defined according to POSIX standards,
|
||||||
|
// and may not apply to the OS being built for.
|
||||||
|
type FileMode uint32
|
||||||
|
|
||||||
|
// Permission flags, defined here to avoid potential inconsistencies in individual OS implementations.
|
||||||
|
const (
|
||||||
|
ModePerm FileMode = 0o0777 // S_IRWXU | S_IRWXG | S_IRWXO
|
||||||
|
ModeUserRead FileMode = 0o0400 // S_IRUSR
|
||||||
|
ModeUserWrite FileMode = 0o0200 // S_IWUSR
|
||||||
|
ModeUserExec FileMode = 0o0100 // S_IXUSR
|
||||||
|
ModeGroupRead FileMode = 0o0040 // S_IRGRP
|
||||||
|
ModeGroupWrite FileMode = 0o0020 // S_IWGRP
|
||||||
|
ModeGroupExec FileMode = 0o0010 // S_IXGRP
|
||||||
|
ModeOtherRead FileMode = 0o0004 // S_IROTH
|
||||||
|
ModeOtherWrite FileMode = 0o0002 // S_IWOTH
|
||||||
|
ModeOtherExec FileMode = 0o0001 // S_IXOTH
|
||||||
|
|
||||||
|
ModeSetUID FileMode = 0o4000 // S_ISUID
|
||||||
|
ModeSetGID FileMode = 0o2000 // S_ISGID
|
||||||
|
ModeSticky FileMode = 0o1000 // S_ISVTX
|
||||||
|
|
||||||
|
ModeType FileMode = 0xF000 // S_IFMT
|
||||||
|
ModeNamedPipe FileMode = 0x1000 // S_IFIFO
|
||||||
|
ModeCharDevice FileMode = 0x2000 // S_IFCHR
|
||||||
|
ModeDir FileMode = 0x4000 // S_IFDIR
|
||||||
|
ModeDevice FileMode = 0x6000 // S_IFBLK
|
||||||
|
ModeRegular FileMode = 0x8000 // S_IFREG
|
||||||
|
ModeSymlink FileMode = 0xA000 // S_IFLNK
|
||||||
|
ModeSocket FileMode = 0xC000 // S_IFSOCK
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsDir reports whether m describes a directory.
|
||||||
|
// That is, it tests for m.Type() == ModeDir.
|
||||||
|
func (m FileMode) IsDir() bool {
|
||||||
|
return (m & ModeType) == ModeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular reports whether m describes a regular file.
|
||||||
|
// That is, it tests for m.Type() == ModeRegular
|
||||||
|
func (m FileMode) IsRegular() bool {
|
||||||
|
return (m & ModeType) == ModeRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perm returns the POSIX permission bits in m (m & ModePerm).
|
||||||
|
func (m FileMode) Perm() FileMode {
|
||||||
|
return (m & ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type bits in m (m & ModeType).
|
||||||
|
func (m FileMode) Type() FileMode {
|
||||||
|
return (m & ModeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a `-rwxrwxrwx` style string representing the `ls -l` POSIX permissions string.
|
||||||
|
func (m FileMode) String() string {
|
||||||
|
var buf [10]byte
|
||||||
|
|
||||||
|
switch m.Type() {
|
||||||
|
case ModeRegular:
|
||||||
|
buf[0] = '-'
|
||||||
|
case ModeDir:
|
||||||
|
buf[0] = 'd'
|
||||||
|
case ModeSymlink:
|
||||||
|
buf[0] = 'l'
|
||||||
|
case ModeDevice:
|
||||||
|
buf[0] = 'b'
|
||||||
|
case ModeCharDevice:
|
||||||
|
buf[0] = 'c'
|
||||||
|
case ModeNamedPipe:
|
||||||
|
buf[0] = 'p'
|
||||||
|
case ModeSocket:
|
||||||
|
buf[0] = 's'
|
||||||
|
default:
|
||||||
|
buf[0] = '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
const rwx = "rwxrwxrwx"
|
||||||
|
for i, c := range rwx {
|
||||||
|
if m&(1<<uint(9-1-i)) != 0 {
|
||||||
|
buf[i+1] = byte(c)
|
||||||
|
} else {
|
||||||
|
buf[i+1] = '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m&ModeSetUID != 0 {
|
||||||
|
if buf[3] == 'x' {
|
||||||
|
buf[3] = 's'
|
||||||
|
} else {
|
||||||
|
buf[3] = 'S'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m&ModeSetGID != 0 {
|
||||||
|
if buf[6] == 'x' {
|
||||||
|
buf[6] = 's'
|
||||||
|
} else {
|
||||||
|
buf[6] = 'S'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m&ModeSticky != 0 {
|
||||||
|
if buf[9] == 'x' {
|
||||||
|
buf[9] = 't'
|
||||||
|
} else {
|
||||||
|
buf[9] = 'T'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[:])
|
||||||
|
}
|
243
internal/encoding/ssh/filexfer/response_packets.go
Normal file
243
internal/encoding/ssh/filexfer/response_packets.go
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusPacket defines the SSH_FXP_STATUS packet.
|
||||||
|
//
|
||||||
|
// Specified in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7
|
||||||
|
type StatusPacket struct {
|
||||||
|
StatusCode Status
|
||||||
|
ErrorMessage string
|
||||||
|
LanguageTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error makes StatusPacket an error type.
|
||||||
|
func (p *StatusPacket) Error() string {
|
||||||
|
if p.ErrorMessage == "" {
|
||||||
|
return "sftp: " + p.StatusCode.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("sftp: %q (%s)", p.ErrorMessage, p.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is returns true if target is a StatusPacket with the same StatusCode,
|
||||||
|
// or target is a Status code which is the same as SatusCode.
|
||||||
|
func (p *StatusPacket) Is(target error) bool {
|
||||||
|
if target, ok := target.(*StatusPacket); ok {
|
||||||
|
return p.StatusCode == target.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.StatusCode == target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *StatusPacket) Type() PacketType {
|
||||||
|
return PacketTypeStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *StatusPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
// uint32(error/status code) + string(error message) + string(language tag)
|
||||||
|
size := 4 + 4 + len(p.ErrorMessage) + 4 + len(p.LanguageTag)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeStatus, reqid)
|
||||||
|
buf.AppendUint32(uint32(p.StatusCode))
|
||||||
|
buf.AppendString(p.ErrorMessage)
|
||||||
|
buf.AppendString(p.LanguageTag)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *StatusPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
statusCode, err := buf.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.StatusCode = Status(statusCode)
|
||||||
|
|
||||||
|
if p.ErrorMessage, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.LanguageTag, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePacket defines the SSH_FXP_HANDLE packet.
|
||||||
|
type HandlePacket struct {
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *HandlePacket) Type() PacketType {
|
||||||
|
return PacketTypeHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *HandlePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 + len(p.Handle) // string(handle)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeHandle, reqid)
|
||||||
|
buf.AppendString(p.Handle)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *HandlePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
if p.Handle, err = buf.ConsumeString(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataPacket defines the SSH_FXP_DATA packet.
|
||||||
|
type DataPacket struct {
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *DataPacket) Type() PacketType {
|
||||||
|
return PacketTypeData
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *DataPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 // uint32(len(data)); data content in payload
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeData, reqid)
|
||||||
|
buf.AppendUint32(uint32(len(p.Data)))
|
||||||
|
|
||||||
|
return buf.Packet(p.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
//
|
||||||
|
// If p.Data is already populated, and of sufficient length to hold the data,
|
||||||
|
// then this will copy the data into that byte slice.
|
||||||
|
//
|
||||||
|
// If p.Data has a length insufficient to hold the data,
|
||||||
|
// then this will make a new slice of sufficient length, and copy the data into that.
|
||||||
|
//
|
||||||
|
// This means this _does not_ alias any of the data buffer that is passed in.
|
||||||
|
func (p *DataPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamePacket defines the SSH_FXP_NAME packet.
|
||||||
|
type NamePacket struct {
|
||||||
|
Entries []*NameEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *NamePacket) Type() PacketType {
|
||||||
|
return PacketTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *NamePacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := 4 // uint32(len(entries))
|
||||||
|
|
||||||
|
for _, e := range p.Entries {
|
||||||
|
size += e.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeName, reqid)
|
||||||
|
buf.AppendUint32(uint32(len(p.Entries)))
|
||||||
|
|
||||||
|
for _, e := range p.Entries {
|
||||||
|
e.MarshalInto(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *NamePacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
count, err := buf.ConsumeUint32()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Entries = make([]*NameEntry, 0, count)
|
||||||
|
|
||||||
|
for i := uint32(0); i < count; i++ {
|
||||||
|
var e NameEntry
|
||||||
|
if err := e.UnmarshalFrom(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Entries = append(p.Entries, &e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttrsPacket defines the SSH_FXP_ATTRS packet.
|
||||||
|
type AttrsPacket struct {
|
||||||
|
Attrs Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the SSH_FXP_xy value associated with this packet type.
|
||||||
|
func (p *AttrsPacket) Type() PacketType {
|
||||||
|
return PacketTypeAttrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPacket returns p as a two-part binary encoding of p.
|
||||||
|
func (p *AttrsPacket) MarshalPacket(reqid uint32, b []byte) (header, payload []byte, err error) {
|
||||||
|
buf := NewBuffer(b)
|
||||||
|
if buf.Cap() < 9 {
|
||||||
|
size := p.Attrs.Len() // ATTRS(attrs)
|
||||||
|
buf = NewMarshalBuffer(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.StartPacket(PacketTypeAttrs, reqid)
|
||||||
|
p.Attrs.MarshalInto(buf)
|
||||||
|
|
||||||
|
return buf.Packet(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPacketBody unmarshals the packet body from the given Buffer.
|
||||||
|
// It is assumed that the uint32(request-id) has already been consumed.
|
||||||
|
func (p *AttrsPacket) UnmarshalPacketBody(buf *Buffer) (err error) {
|
||||||
|
return p.Attrs.UnmarshalFrom(buf)
|
||||||
|
}
|
296
internal/encoding/ssh/filexfer/response_packets_test.go
Normal file
296
internal/encoding/ssh/filexfer/response_packets_test.go
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
package filexfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusPacketIs(t *testing.T) {
|
||||||
|
status := &StatusPacket{
|
||||||
|
StatusCode: StatusFailure,
|
||||||
|
ErrorMessage: "error message",
|
||||||
|
LanguageTag: "language tag",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(status, StatusFailure) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusFailure) != true")
|
||||||
|
}
|
||||||
|
if !errors.Is(status, &StatusPacket{StatusCode: StatusFailure}) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusPacket{StatusFailure}) != true")
|
||||||
|
}
|
||||||
|
if errors.Is(status, StatusOK) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusFailure) == true")
|
||||||
|
}
|
||||||
|
if errors.Is(status, &StatusPacket{StatusCode: StatusOK}) {
|
||||||
|
t.Error("errors.Is(StatusFailure, StatusPacket{StatusFailure}) == true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &StatusPacket{}
|
||||||
|
|
||||||
|
func TestStatusPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
statusCode = StatusBadMessage
|
||||||
|
errorMessage = "foo"
|
||||||
|
languageTag = "x-example"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &StatusPacket{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
ErrorMessage: errorMessage,
|
||||||
|
LanguageTag: languageTag,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 29,
|
||||||
|
101,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 5,
|
||||||
|
0x00, 0x00, 0x00, 3, 'f', 'o', 'o',
|
||||||
|
0x00, 0x00, 0x00, 9, 'x', '-', 'e', 'x', 'a', 'm', 'p', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = StatusPacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.StatusCode != statusCode {
|
||||||
|
t.Errorf("UnmarshalBinary(): StatusCode was %v, but expected %v", p.StatusCode, statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ErrorMessage != errorMessage {
|
||||||
|
t.Errorf("UnmarshalBinary(): ErrorMessage was %q, but expected %q", p.ErrorMessage, errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.LanguageTag != languageTag {
|
||||||
|
t.Errorf("UnmarshalBinary(): LanguageTag was %q, but expected %q", p.LanguageTag, languageTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &HandlePacket{}
|
||||||
|
|
||||||
|
func TestHandlePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
handle = "somehandle"
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &HandlePacket{
|
||||||
|
Handle: "somehandle",
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 19,
|
||||||
|
102,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 10, 's', 'o', 'm', 'e', 'h', 'a', 'n', 'd', 'l', 'e',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = HandlePacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Handle != handle {
|
||||||
|
t.Errorf("UnmarshalBinary(): Handle was %q, but expected %q", p.Handle, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &DataPacket{}
|
||||||
|
|
||||||
|
func TestDataPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
)
|
||||||
|
|
||||||
|
var payload = []byte(`foobar`)
|
||||||
|
|
||||||
|
p := &DataPacket{
|
||||||
|
Data: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 15,
|
||||||
|
103,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 6, 'f', 'o', 'o', 'b', 'a', 'r',
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = DataPacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p.Data, payload) {
|
||||||
|
t.Errorf("UnmarshalBinary(): Data was %X, but expected %X", p.Data, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &NamePacket{}
|
||||||
|
|
||||||
|
func TestNamePacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
filename = "foo"
|
||||||
|
longname = "bar"
|
||||||
|
perms = 0x87654300
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &NamePacket{
|
||||||
|
Entries: []*NameEntry{
|
||||||
|
&NameEntry{
|
||||||
|
Filename: filename + "1",
|
||||||
|
Longname: longname + "1",
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions | (1 << 8),
|
||||||
|
Permissions: perms | 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&NameEntry{
|
||||||
|
Filename: filename + "2",
|
||||||
|
Longname: longname + "2",
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions | (2 << 8),
|
||||||
|
Permissions: perms | 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 57,
|
||||||
|
104,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 4, 'f', 'o', 'o', '1',
|
||||||
|
0x00, 0x00, 0x00, 4, 'b', 'a', 'r', '1',
|
||||||
|
0x00, 0x00, 0x01, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 4, 'f', 'o', 'o', '2',
|
||||||
|
0x00, 0x00, 0x00, 4, 'b', 'a', 'r', '2',
|
||||||
|
0x00, 0x00, 0x02, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = NamePacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count := len(p.Entries); count != 2 {
|
||||||
|
t.Fatalf("UnmarshalBinary(): len(NameEntries) was %d, but expected %d", count, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, e := range p.Entries {
|
||||||
|
if got, want := e.Filename, filename+string('1'+rune(i)); got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): Entries[%d].Filename was %q, but expected %q", i, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := e.Longname, longname+string('1'+rune(i)); got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): Entries[%d].Longname was %q, but expected %q", i, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := e.Attrs.Flags, AttrPermissions|((i+1)<<8); got != uint32(want) {
|
||||||
|
t.Errorf("UnmarshalBinary(): Entries[%d].Attrs.Flags was %#x, but expected %#x", i, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := e.Attrs.Permissions, FileMode(perms|(i+1)); got != want {
|
||||||
|
t.Errorf("UnmarshalBinary(): Entries[%d].Attrs.Permissions was %#v, but expected %#v", i, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Packet = &AttrsPacket{}
|
||||||
|
|
||||||
|
func TestAttrsPacket(t *testing.T) {
|
||||||
|
const (
|
||||||
|
id = 42
|
||||||
|
perms = 0x87654321
|
||||||
|
)
|
||||||
|
|
||||||
|
p := &AttrsPacket{
|
||||||
|
Attrs: Attributes{
|
||||||
|
Flags: AttrPermissions,
|
||||||
|
Permissions: perms,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ComposePacket(p.MarshalPacket(id, nil))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte{
|
||||||
|
0x00, 0x00, 0x00, 13,
|
||||||
|
105,
|
||||||
|
0x00, 0x00, 0x00, 42,
|
||||||
|
0x00, 0x00, 0x00, 0x04,
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, want) {
|
||||||
|
t.Fatalf("MarshalPacket() = %X, but wanted %X", buf, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = AttrsPacket{}
|
||||||
|
|
||||||
|
// UnmarshalBinary assumes the uint32(length) + uint8(type) have already been consumed.
|
||||||
|
if err := p.UnmarshalPacketBody(NewBuffer(buf[9:])); err != nil {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Flags != AttrPermissions {
|
||||||
|
t.Errorf("UnmarshalBinary(): Attrs.Flags was %#x, but expected %#x", p.Attrs.Flags, AttrPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Attrs.Permissions != perms {
|
||||||
|
t.Errorf("UnmarshalBinary(): Attrs.Permissions was %#v, but expected %#v", p.Attrs.Permissions, perms)
|
||||||
|
}
|
||||||
|
}
|
157
main.go
157
main.go
|
@ -1,6 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/bagage/sftp"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/bagage/s3"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -11,6 +21,149 @@ func main() {
|
||||||
|
|
||||||
log.Println(config)
|
log.Println(config)
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
|
||||||
|
go httpServer(config, done)
|
||||||
|
go sshServer(config, done)
|
||||||
|
|
||||||
|
err := <- done
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("A component failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type s3creds struct {
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
}
|
||||||
|
var keychain map[string]s3creds
|
||||||
|
|
||||||
|
func sshServer(dconfig* Config, done chan error) {
|
||||||
|
keychain = make(map[string]s3creds)
|
||||||
|
|
||||||
|
config := &ssh.ServerConfig{
|
||||||
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
log.Printf("Login: %s\n", c.User())
|
||||||
|
access_key, secret_key, err := LdapGetS3(dconfig, c.User(), string(pass))
|
||||||
|
if err == nil {
|
||||||
|
keychain[c.User()] = s3creds{ access_key, secret_key }
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
privateBytes, err := ioutil.ReadFile(dconfig.SSHKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to load private key", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to parse private key", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AddHostKey(private)
|
||||||
|
|
||||||
|
// Once a ServerConfig has been configured, connections can be
|
||||||
|
// accepted.
|
||||||
|
listener, err := net.Listen("tcp", "0.0.0.0:2222")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to listen for connection", err)
|
||||||
|
}
|
||||||
|
log.Printf("Listening on %v\n", listener.Addr())
|
||||||
|
|
||||||
|
for {
|
||||||
|
nConn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to accept incoming connection: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go handleSSHConn(nConn, dconfig, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSHConn(nConn net.Conn, dconfig* Config, config *ssh.ServerConfig) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer nConn.Close()
|
||||||
|
|
||||||
|
// Before use, a handshake must be performed on the incoming
|
||||||
|
// net.Conn.
|
||||||
|
serverConn, chans, reqs, err := ssh.NewServerConn(nConn, config)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to handshake: ", err)
|
||||||
|
}
|
||||||
|
defer serverConn.Conn.Close()
|
||||||
|
user := serverConn.Conn.User()
|
||||||
|
log.Printf("SSH connection established for %v\n", user)
|
||||||
|
|
||||||
|
// The incoming Request channel must be serviced.
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
|
||||||
|
// Service the incoming Channel channel.
|
||||||
|
for newChannel := range chans {
|
||||||
|
// Channels have a type, depending on the application level
|
||||||
|
// protocol intended. In the case of an SFTP session, this is "subsystem"
|
||||||
|
// with a payload string of "<length=4>sftp"
|
||||||
|
log.Printf("Incoming channel: %s\n", newChannel.ChannelType())
|
||||||
|
if newChannel.ChannelType() != "session" {
|
||||||
|
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||||
|
log.Printf("Unknown channel type: %s\n", newChannel.ChannelType())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, requests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("could not accept channel.", err)
|
||||||
|
}
|
||||||
|
log.Printf("Channel accepted\n")
|
||||||
|
|
||||||
|
// Sessions have out-of-band requests such as "shell",
|
||||||
|
// "pty-req" and "env". Here we handle only the
|
||||||
|
// "subsystem" request.
|
||||||
|
go func(in <-chan *ssh.Request) {
|
||||||
|
for req := range in {
|
||||||
|
log.Printf("Request: %v\n", req.Type)
|
||||||
|
ok := false
|
||||||
|
switch req.Type {
|
||||||
|
case "subsystem":
|
||||||
|
log.Printf("Subsystem: %s\n", req.Payload[4:])
|
||||||
|
if string(req.Payload[4:]) == "sftp" {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf(" - accepted: %v\n", ok)
|
||||||
|
req.Reply(ok, nil)
|
||||||
|
}
|
||||||
|
}(requests)
|
||||||
|
|
||||||
|
creds := keychain[user]
|
||||||
|
mc, err := minio.New(dconfig.Endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(creds.accessKey, creds.secretKey, ""),
|
||||||
|
Secure: dconfig.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := s3.NewS3FS(mc)
|
||||||
|
server, err := sftp.NewServer(ctx, channel, &fs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Serve(); err == io.EOF {
|
||||||
|
server.Close()
|
||||||
|
log.Print("sftp client exited session.")
|
||||||
|
} else if err != nil {
|
||||||
|
log.Print("sftp server completed with error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpServer(config* Config, done chan error) {
|
||||||
// Assemble components to handle WebDAV requests
|
// Assemble components to handle WebDAV requests
|
||||||
http.Handle(config.DavPath+"/",
|
http.Handle(config.DavPath+"/",
|
||||||
BasicAuthExtract{
|
BasicAuthExtract{
|
||||||
|
@ -30,6 +183,8 @@ func main() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := http.ListenAndServe(config.HttpListen, nil); err != nil {
|
if err := http.ListenAndServe(config.HttpListen, nil); err != nil {
|
||||||
log.Fatalf("Error with WebDAV server: %v", err)
|
done <- fmt.Errorf("Error with WebDAV server: %v", err)
|
||||||
|
} else {
|
||||||
|
done <- nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package s3
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -11,7 +11,6 @@ import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"golang.org/x/net/webdav"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type S3File struct {
|
type S3File struct {
|
||||||
|
@ -20,14 +19,16 @@ type S3File struct {
|
||||||
objw *io.PipeWriter
|
objw *io.PipeWriter
|
||||||
donew chan error
|
donew chan error
|
||||||
pos int64
|
pos int64
|
||||||
path S3Path
|
entries []fs.FileInfo
|
||||||
|
Path S3Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewS3File(s *S3FS, path string) (webdav.File, error) {
|
func NewS3File(s *S3FS, path string) (*S3File, error) {
|
||||||
f := new(S3File)
|
f := new(S3File)
|
||||||
f.fs = s
|
f.fs = s
|
||||||
f.pos = 0
|
f.pos = 0
|
||||||
f.path = NewS3Path(path)
|
f.entries = nil
|
||||||
|
f.Path = NewS3Path(path)
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ func (f *S3File) Close() error {
|
||||||
|
|
||||||
func (f *S3File) loadObject() error {
|
func (f *S3File) loadObject() error {
|
||||||
if f.obj == nil {
|
if f.obj == nil {
|
||||||
obj, err := f.fs.mc.GetObject(f.fs.ctx, f.path.bucket, f.path.key, minio.GetObjectOptions{})
|
obj, err := f.fs.mc.GetObject(f.fs.ctx, f.Path.Bucket, f.Path.Key, minio.GetObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -82,6 +83,19 @@ func (f *S3File) Read(p []byte) (n int, err error) {
|
||||||
return f.obj.Read(p)
|
return f.obj.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *S3File) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
|
if err := f.loadObject(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.obj.ReadAt(p, off)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *S3File) WriteAt(p []byte, off int64) (n int, err error) {
|
||||||
|
return 0, errors.New("not implemented")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (f *S3File) Write(p []byte) (n int, err error) {
|
func (f *S3File) Write(p []byte) (n int, err error) {
|
||||||
/*if f.path.class != OBJECT {
|
/*if f.path.class != OBJECT {
|
||||||
return 0, os.ErrInvalid
|
return 0, os.ErrInvalid
|
||||||
|
@ -96,7 +110,7 @@ func (f *S3File) Write(p []byte) (n int, err error) {
|
||||||
f.donew = make(chan error, 1)
|
f.donew = make(chan error, 1)
|
||||||
f.objw = w
|
f.objw = w
|
||||||
|
|
||||||
contentType := mime.TypeByExtension(path.Ext(f.path.key))
|
contentType := mime.TypeByExtension(path.Ext(f.Path.Key))
|
||||||
go func() {
|
go func() {
|
||||||
/* @FIXME
|
/* @FIXME
|
||||||
PutObject has a strange behaviour when used with unknown size, it supposes the final size will be 5TiB.
|
PutObject has a strange behaviour when used with unknown size, it supposes the final size will be 5TiB.
|
||||||
|
@ -107,7 +121,7 @@ func (f *S3File) Write(p []byte) (n int, err error) {
|
||||||
Because Multipart uploads seems to be limited to 10 000 parts, it might be possible that we are limited to 50 GiB files, which is still good enough.
|
Because Multipart uploads seems to be limited to 10 000 parts, it might be possible that we are limited to 50 GiB files, which is still good enough.
|
||||||
Ref: https://github.com/minio/minio-go/blob/62beca8cd87e9960d88793320220ad2c159bb5e5/api-put-object-common.go#L110-L112
|
Ref: https://github.com/minio/minio-go/blob/62beca8cd87e9960d88793320220ad2c159bb5e5/api-put-object-common.go#L110-L112
|
||||||
*/
|
*/
|
||||||
_, err := f.fs.mc.PutObject(context.Background(), f.path.bucket, f.path.key, r, -1, minio.PutObjectOptions{ContentType: contentType, PartSize: 5*1024*1024})
|
_, err := f.fs.mc.PutObject(context.Background(), f.Path.Bucket, f.Path.Key, r, -1, minio.PutObjectOptions{ContentType: contentType, PartSize: 5*1024*1024})
|
||||||
f.donew <- err
|
f.donew <- err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -133,43 +147,59 @@ If n > 0, ReadDir returns at most n DirEntry records. In this case, if ReadDir r
|
||||||
If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. When it succeeds, it returns a nil error (not io.EOF).
|
If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. When it succeeds, it returns a nil error (not io.EOF).
|
||||||
*/
|
*/
|
||||||
func (f *S3File) Readdir(count int) ([]fs.FileInfo, error) {
|
func (f *S3File) Readdir(count int) ([]fs.FileInfo, error) {
|
||||||
if count > 0 {
|
if f.Path.Class == ROOT {
|
||||||
return nil, errors.New("returning a limited number of directory entry is not supported in readdir")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.path.class == ROOT {
|
|
||||||
return f.readDirRoot(count)
|
return f.readDirRoot(count)
|
||||||
} else {
|
} else {
|
||||||
return f.readDirChild(count)
|
return f.readDirChild(count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *S3File) readDirRoot(count int) ([]fs.FileInfo, error) {
|
func min(a, b int64) int64 {
|
||||||
buckets, err := f.fs.mc.ListBuckets(f.fs.ctx)
|
if a < b {
|
||||||
if err != nil {
|
return a
|
||||||
return nil, err
|
}
|
||||||
}
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
entries := make([]fs.FileInfo, 0, len(buckets))
|
func (f *S3File) readDirRoot(count int) ([]fs.FileInfo, error) {
|
||||||
for _, bucket := range buckets {
|
var err error
|
||||||
//log.Println("Stat from GarageFile.readDirRoot()", "/"+bucket.Name)
|
if f.entries == nil {
|
||||||
nf, err := NewS3Stat(f.fs, "/"+bucket.Name)
|
buckets, err := f.fs.mc.ListBuckets(f.fs.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries = append(entries, nf)
|
|
||||||
|
f.entries = make([]fs.FileInfo, 0, len(buckets))
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
//log.Println("Stat from GarageFile.readDirRoot()", "/"+bucket.Name)
|
||||||
|
nf, err := NewS3Stat(f.fs, "/"+bucket.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.entries = append(f.entries, nf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beg := f.pos
|
||||||
|
end := int64(len(f.entries))
|
||||||
|
if count > 0 {
|
||||||
|
end = min(beg + int64(count), end)
|
||||||
|
}
|
||||||
|
f.pos = end
|
||||||
|
|
||||||
|
if end - beg == 0 {
|
||||||
|
err = io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
return f.entries[beg:end], err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *S3File) readDirChild(count int) ([]fs.FileInfo, error) {
|
func (f *S3File) readDirChild(count int) ([]fs.FileInfo, error) {
|
||||||
prefix := f.path.key
|
prefix := f.Path.Key
|
||||||
if len(prefix) > 0 && prefix[len(prefix)-1:] != "/" {
|
if len(prefix) > 0 && prefix[len(prefix)-1:] != "/" {
|
||||||
prefix = prefix + "/"
|
prefix = prefix + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
objs_info := f.fs.mc.ListObjects(f.fs.ctx, f.path.bucket, minio.ListObjectsOptions{
|
objs_info := f.fs.mc.ListObjects(f.fs.ctx, f.Path.Bucket, minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
})
|
})
|
||||||
|
@ -180,7 +210,7 @@ func (f *S3File) readDirChild(count int) ([]fs.FileInfo, error) {
|
||||||
return nil, object.Err
|
return nil, object.Err
|
||||||
}
|
}
|
||||||
//log.Println("Stat from GarageFile.readDirChild()", path.Join("/", f.path.bucket, object.Key))
|
//log.Println("Stat from GarageFile.readDirChild()", path.Join("/", f.path.bucket, object.Key))
|
||||||
nf, err := NewS3StatFromObjectInfo(f.fs, f.path.bucket, object)
|
nf, err := NewS3StatFromObjectInfo(f.fs, f.Path.Bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -191,5 +221,5 @@ func (f *S3File) readDirChild(count int) ([]fs.FileInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *S3File) Stat() (fs.FileInfo, error) {
|
func (f *S3File) Stat() (fs.FileInfo, error) {
|
||||||
return NewS3Stat(f.fs, f.path.path)
|
return NewS3Stat(f.fs, f.Path.Path)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package s3
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -37,9 +37,9 @@ func (s S3FS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||||
|
|
||||||
p := NewS3Path(name)
|
p := NewS3Path(name)
|
||||||
|
|
||||||
if p.class == ROOT {
|
if p.Class == ROOT {
|
||||||
return errors.New("Unable to create another root folder")
|
return errors.New("Unable to create another root folder")
|
||||||
} else if p.class == BUCKET {
|
} else if p.Class == BUCKET {
|
||||||
log.Println("Creating bucket is not implemented yet")
|
log.Println("Creating bucket is not implemented yet")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func (s S3FS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s S3FS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
func (s S3FS) OpenFile2(ctx context.Context, name string, flag int, perm os.FileMode) (*S3File, error) {
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
|
|
||||||
// If the file does not exist when opening it, we create a stub
|
// If the file does not exist when opening it, we create a stub
|
||||||
|
@ -62,8 +62,8 @@ func (s S3FS) OpenFile(ctx context.Context, name string, flag int, perm os.FileM
|
||||||
st := new(S3Stat)
|
st := new(S3Stat)
|
||||||
st.fs = &s
|
st.fs = &s
|
||||||
st.path = NewS3Path(name)
|
st.path = NewS3Path(name)
|
||||||
st.path.class = OBJECT
|
st.path.Class = OBJECT
|
||||||
st.obj.Key = st.path.key
|
st.obj.Key = st.path.Key
|
||||||
st.obj.LastModified = time.Now()
|
st.obj.LastModified = time.Now()
|
||||||
s.cache[name] = st
|
s.cache[name] = st
|
||||||
}
|
}
|
||||||
|
@ -71,20 +71,24 @@ func (s S3FS) OpenFile(ctx context.Context, name string, flag int, perm os.FileM
|
||||||
return NewS3File(&s, name)
|
return NewS3File(&s, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s S3FS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||||
|
return s.OpenFile2(ctx, name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
func (s S3FS) RemoveAll(ctx context.Context, name string) error {
|
func (s S3FS) RemoveAll(ctx context.Context, name string) error {
|
||||||
//@FIXME nautilus deletes files one by one, at the end, it does not find its folder as it is "already deleted"
|
//@FIXME nautilus deletes files one by one, at the end, it does not find its folder as it is "already deleted"
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
|
|
||||||
p := NewS3Path(name)
|
p := NewS3Path(name)
|
||||||
if p.class == ROOT {
|
if p.Class == ROOT {
|
||||||
return errors.New("Unable to create another root folder")
|
return errors.New("Unable to create another root folder")
|
||||||
} else if p.class == BUCKET {
|
} else if p.Class == BUCKET {
|
||||||
log.Println("Deleting bucket is not implemented yet")
|
log.Println("Deleting bucket is not implemented yet")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
objCh := s.mc.ListObjects(s.ctx, p.bucket, minio.ListObjectsOptions{Prefix: p.key, Recursive: true})
|
objCh := s.mc.ListObjects(s.ctx, p.Bucket, minio.ListObjectsOptions{Prefix: p.Key, Recursive: true})
|
||||||
rmCh := s.mc.RemoveObjects(s.ctx, p.bucket, objCh, minio.RemoveObjectsOptions{})
|
rmCh := s.mc.RemoveObjects(s.ctx, p.Bucket, objCh, minio.RemoveObjectsOptions{})
|
||||||
|
|
||||||
for rErr := range rmCh {
|
for rErr := range rmCh {
|
||||||
return rErr.Err
|
return rErr.Err
|
||||||
|
@ -98,9 +102,9 @@ func (s S3FS) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
|
|
||||||
po := NewS3Path(oldName)
|
po := NewS3Path(oldName)
|
||||||
pn := NewS3Path(newName)
|
pn := NewS3Path(newName)
|
||||||
if po.class == ROOT || pn.class == ROOT {
|
if po.Class == ROOT || pn.Class == ROOT {
|
||||||
return errors.New("Unable to rename root folder")
|
return errors.New("Unable to rename root folder")
|
||||||
} else if po.class == BUCKET || pn.class == BUCKET {
|
} else if po.Class == BUCKET || pn.Class == BUCKET {
|
||||||
log.Println("Moving a bucket is not implemented yet")
|
log.Println("Moving a bucket is not implemented yet")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -111,16 +115,16 @@ func (s S3FS) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Gather all keys, copy the object, delete the original
|
//Gather all keys, copy the object, delete the original
|
||||||
objCh := s.mc.ListObjects(s.ctx, po.bucket, minio.ListObjectsOptions{Prefix: po.key, Recursive: true})
|
objCh := s.mc.ListObjects(s.ctx, po.Bucket, minio.ListObjectsOptions{Prefix: po.Key, Recursive: true})
|
||||||
for obj := range objCh {
|
for obj := range objCh {
|
||||||
src := minio.CopySrcOptions{
|
src := minio.CopySrcOptions{
|
||||||
Bucket: po.bucket,
|
Bucket: po.Bucket,
|
||||||
Object: obj.Key,
|
Object: obj.Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := minio.CopyDestOptions{
|
dst := minio.CopyDestOptions{
|
||||||
Bucket: pn.bucket,
|
Bucket: pn.Bucket,
|
||||||
Object: path.Join(pn.key, obj.Key[len(po.key):]),
|
Object: path.Join(pn.Key, obj.Key[len(po.Key):]),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.mc.CopyObject(s.ctx, dst, src)
|
_, err := s.mc.CopyObject(s.ctx, dst, src)
|
||||||
|
@ -128,7 +132,7 @@ func (s S3FS) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.mc.RemoveObject(s.ctx, po.bucket, obj.Key, minio.RemoveObjectOptions{})
|
err = s.mc.RemoveObject(s.ctx, po.Bucket, obj.Key, minio.RemoveObjectOptions{})
|
||||||
var e minio.ErrorResponse
|
var e minio.ErrorResponse
|
||||||
log.Println(errors.As(err, &e))
|
log.Println(errors.As(err, &e))
|
||||||
log.Println(e)
|
log.Println(e)
|
68
s3/path.go
Normal file
68
s3/path.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Class int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ROOT S3Class = 1 << iota
|
||||||
|
BUCKET
|
||||||
|
COMMON_PREFIX
|
||||||
|
OBJECT
|
||||||
|
OPAQUE_KEY
|
||||||
|
|
||||||
|
KEY = COMMON_PREFIX | OBJECT | OPAQUE_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Path struct {
|
||||||
|
Path string
|
||||||
|
Class S3Class
|
||||||
|
Bucket string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3Path(p string) S3Path {
|
||||||
|
// Remove first dot, eq. relative directory == "/"
|
||||||
|
if len(p) > 0 && p[0] == '.' {
|
||||||
|
p = p[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the first slash if missing
|
||||||
|
p = "/" + p
|
||||||
|
|
||||||
|
// Clean path using golang tools
|
||||||
|
p = path.Clean(p)
|
||||||
|
|
||||||
|
exploded_path := strings.SplitN(p, "/", 3)
|
||||||
|
|
||||||
|
// If there is no bucket name (eg. "/")
|
||||||
|
if len(exploded_path) < 2 || exploded_path[1] == "" {
|
||||||
|
return S3Path{p, ROOT, "", ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no key
|
||||||
|
if len(exploded_path) < 3 || exploded_path[2] == "" {
|
||||||
|
return S3Path{p, BUCKET, exploded_path[1], ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return S3Path{p, OPAQUE_KEY, exploded_path[1], exploded_path[2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrustedS3Path(bucket string, obj minio.ObjectInfo) S3Path {
|
||||||
|
cl := OBJECT
|
||||||
|
if obj.Key[len(obj.Key)-1:] == "/" {
|
||||||
|
cl = COMMON_PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
|
return S3Path{
|
||||||
|
Path: path.Join("/", bucket, obj.Key),
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: obj.Key,
|
||||||
|
Class: cl,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package s3
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -24,7 +24,7 @@ func NewS3StatFromObjectInfo(fs *S3FS, bucket string, obj minio.ObjectInfo) (*S3
|
||||||
s.obj = obj
|
s.obj = obj
|
||||||
s.fs = fs
|
s.fs = fs
|
||||||
|
|
||||||
fs.cache[s.path.path] = s
|
fs.cache[s.path.Path] = s
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,30 +44,30 @@ func NewS3Stat(fs *S3FS, path string) (*S3Stat, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.path.class&OPAQUE_KEY != 0 {
|
if s.path.Class&OPAQUE_KEY != 0 {
|
||||||
return nil, errors.New("Failed to precisely determine the key type, this a logic error.")
|
return nil, errors.New("Failed to precisely determine the key type, this a logic error.")
|
||||||
}
|
}
|
||||||
|
|
||||||
cache[path] = s
|
cache[path] = s
|
||||||
cache[s.path.path] = s
|
cache[s.path.Path] = s
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Stat) Refresh() error {
|
func (s *S3Stat) Refresh() error {
|
||||||
if s.path.class == ROOT || s.path.class == BUCKET {
|
if s.path.Class == ROOT || s.path.Class == BUCKET {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mc := s.fs.mc
|
mc := s.fs.mc
|
||||||
|
|
||||||
// Compute the prefix to have the desired behaviour for our stat logic
|
// Compute the prefix to have the desired behaviour for our stat logic
|
||||||
prefix := s.path.key
|
prefix := s.path.Key
|
||||||
if prefix[len(prefix)-1:] == "/" {
|
if prefix[len(prefix)-1:] == "/" {
|
||||||
prefix = prefix[:len(prefix)-1]
|
prefix = prefix[:len(prefix)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get info and check if the key exists
|
// Get info and check if the key exists
|
||||||
objs_info := mc.ListObjects(s.fs.ctx, s.path.bucket, minio.ListObjectsOptions{
|
objs_info := mc.ListObjects(s.fs.ctx, s.path.Bucket, minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
})
|
})
|
||||||
|
@ -80,7 +80,7 @@ func (s *S3Stat) Refresh() error {
|
||||||
|
|
||||||
if object.Key == prefix || object.Key == prefix+"/" {
|
if object.Key == prefix || object.Key == prefix+"/" {
|
||||||
s.obj = object
|
s.obj = object
|
||||||
s.path = NewTrustedS3Path(s.path.bucket, object)
|
s.path = NewTrustedS3Path(s.path.Bucket, object)
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -94,12 +94,12 @@ func (s *S3Stat) Refresh() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Stat) Name() string {
|
func (s *S3Stat) Name() string {
|
||||||
if s.path.class == ROOT {
|
if s.path.Class == ROOT {
|
||||||
return "/"
|
return "/"
|
||||||
} else if s.path.class == BUCKET {
|
} else if s.path.Class == BUCKET {
|
||||||
return s.path.bucket
|
return s.path.Bucket
|
||||||
} else {
|
} else {
|
||||||
return path.Base(s.path.key)
|
return path.Base(s.path.Key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ func (s *S3Stat) Size() int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Stat) Mode() fs.FileMode {
|
func (s *S3Stat) Mode() fs.FileMode {
|
||||||
if s.path.class == OBJECT {
|
if s.path.Class == OBJECT {
|
||||||
return fs.ModePerm
|
return fs.ModePerm
|
||||||
} else {
|
} else {
|
||||||
return fs.ModeDir | fs.ModePerm
|
return fs.ModeDir | fs.ModePerm
|
||||||
|
@ -120,7 +120,7 @@ func (s *S3Stat) ModTime() time.Time {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Stat) IsDir() bool {
|
func (s *S3Stat) IsDir() bool {
|
||||||
return s.path.class != OBJECT
|
return s.path.Class != OBJECT
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Stat) Sys() interface{} {
|
func (s *S3Stat) Sys() interface{} {
|
57
s3_path.go
57
s3_path.go
|
@ -1,57 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
|
||||||
)
|
|
||||||
|
|
||||||
type S3Class int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ROOT S3Class = 1 << iota
|
|
||||||
BUCKET
|
|
||||||
COMMON_PREFIX
|
|
||||||
OBJECT
|
|
||||||
OPAQUE_KEY
|
|
||||||
|
|
||||||
KEY = COMMON_PREFIX | OBJECT | OPAQUE_KEY
|
|
||||||
)
|
|
||||||
|
|
||||||
type S3Path struct {
|
|
||||||
path string
|
|
||||||
class S3Class
|
|
||||||
bucket string
|
|
||||||
key string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewS3Path(path string) S3Path {
|
|
||||||
exploded_path := strings.SplitN(path, "/", 3)
|
|
||||||
|
|
||||||
// If there is no bucket name (eg. "/")
|
|
||||||
if len(exploded_path) < 2 || exploded_path[1] == "" {
|
|
||||||
return S3Path{path, ROOT, "", ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no key
|
|
||||||
if len(exploded_path) < 3 || exploded_path[2] == "" {
|
|
||||||
return S3Path{path, BUCKET, exploded_path[1], ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
return S3Path{path, OPAQUE_KEY, exploded_path[1], exploded_path[2]}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTrustedS3Path(bucket string, obj minio.ObjectInfo) S3Path {
|
|
||||||
cl := OBJECT
|
|
||||||
if obj.Key[len(obj.Key)-1:] == "/" {
|
|
||||||
cl = COMMON_PREFIX
|
|
||||||
}
|
|
||||||
|
|
||||||
return S3Path{
|
|
||||||
path: path.Join("/", bucket, obj.Key),
|
|
||||||
bucket: bucket,
|
|
||||||
key: obj.Key,
|
|
||||||
class: cl,
|
|
||||||
}
|
|
||||||
}
|
|
101
sftp/allocator.go
Normal file
101
sftp/allocator.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
/*
|
||||||
|
Imported from: https://github.com/pkg/sftp
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type allocator struct {
|
||||||
|
sync.Mutex
|
||||||
|
available [][]byte
|
||||||
|
// map key is the request order
|
||||||
|
used map[uint32][][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAllocator() *allocator {
|
||||||
|
return &allocator{
|
||||||
|
// micro optimization: initialize available pages with an initial capacity
|
||||||
|
available: make([][]byte, 0, SftpServerWorkerCount*2),
|
||||||
|
used: make(map[uint32][][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage returns a previously allocated and unused []byte or create a new one.
|
||||||
|
// The slice have a fixed size = maxMsgLength, this value is suitable for both
|
||||||
|
// receiving new packets and reading the files to serve
|
||||||
|
func (a *allocator) GetPage(requestOrderID uint32) []byte {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
var result []byte
|
||||||
|
|
||||||
|
// get an available page and remove it from the available ones.
|
||||||
|
if len(a.available) > 0 {
|
||||||
|
truncLength := len(a.available) - 1
|
||||||
|
result = a.available[truncLength]
|
||||||
|
|
||||||
|
a.available[truncLength] = nil // clear out the internal pointer
|
||||||
|
a.available = a.available[:truncLength] // truncate the slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// no preallocated slice found, just allocate a new one
|
||||||
|
if result == nil {
|
||||||
|
result = make([]byte, maxMsgLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put result in used pages
|
||||||
|
a.used[requestOrderID] = append(a.used[requestOrderID], result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleasePages marks unused all pages in use for the given requestID
|
||||||
|
func (a *allocator) ReleasePages(requestOrderID uint32) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
if used := a.used[requestOrderID]; len(used) > 0 {
|
||||||
|
a.available = append(a.available, used...)
|
||||||
|
}
|
||||||
|
delete(a.used, requestOrderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free removes all the used and available pages.
|
||||||
|
// Call this method when the allocator is not needed anymore
|
||||||
|
func (a *allocator) Free() {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
a.available = nil
|
||||||
|
a.used = make(map[uint32][][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *allocator) countUsedPages() int {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
num := 0
|
||||||
|
for _, p := range a.used {
|
||||||
|
num += len(p)
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *allocator) countAvailablePages() int {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
return len(a.available)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *allocator) isRequestOrderIDUsed(requestOrderID uint32) bool {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
_, ok := a.used[requestOrderID]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
90
sftp/attrs.go
Normal file
90
sftp/attrs.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
// ssh_FXP_ATTRS support
|
||||||
|
// see http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sshFileXferAttrSize = 0x00000001
|
||||||
|
sshFileXferAttrUIDGID = 0x00000002
|
||||||
|
sshFileXferAttrPermissions = 0x00000004
|
||||||
|
sshFileXferAttrACmodTime = 0x00000008
|
||||||
|
sshFileXferAttrExtended = 0x80000000
|
||||||
|
|
||||||
|
sshFileXferAttrAll = sshFileXferAttrSize | sshFileXferAttrUIDGID | sshFileXferAttrPermissions |
|
||||||
|
sshFileXferAttrACmodTime | sshFileXferAttrExtended
|
||||||
|
)
|
||||||
|
|
||||||
|
// fileInfo is an artificial type designed to satisfy os.FileInfo.
|
||||||
|
type fileInfo struct {
|
||||||
|
name string
|
||||||
|
stat *FileStat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the base name of the file.
|
||||||
|
func (fi *fileInfo) Name() string { return fi.name }
|
||||||
|
|
||||||
|
// Size returns the length in bytes for regular files; system-dependent for others.
|
||||||
|
func (fi *fileInfo) Size() int64 { return int64(fi.stat.Size) }
|
||||||
|
|
||||||
|
// Mode returns file mode bits.
|
||||||
|
func (fi *fileInfo) Mode() os.FileMode { return toFileMode(fi.stat.Mode) }
|
||||||
|
|
||||||
|
// ModTime returns the last modification time of the file.
|
||||||
|
func (fi *fileInfo) ModTime() time.Time { return time.Unix(int64(fi.stat.Mtime), 0) }
|
||||||
|
|
||||||
|
// IsDir returns true if the file is a directory.
|
||||||
|
func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() }
|
||||||
|
|
||||||
|
func (fi *fileInfo) Sys() interface{} { return fi.stat }
|
||||||
|
|
||||||
|
// FileStat holds the original unmarshalled values from a call to READDIR or
|
||||||
|
// *STAT. It is exported for the purposes of accessing the raw values via
|
||||||
|
// os.FileInfo.Sys(). It is also used server side to store the unmarshalled
|
||||||
|
// values for SetStat.
|
||||||
|
type FileStat struct {
|
||||||
|
Size uint64
|
||||||
|
Mode uint32
|
||||||
|
Mtime uint32
|
||||||
|
Atime uint32
|
||||||
|
UID uint32
|
||||||
|
GID uint32
|
||||||
|
Extended []StatExtended
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatExtended contains additional, extended information for a FileStat.
|
||||||
|
type StatExtended struct {
|
||||||
|
ExtType string
|
||||||
|
ExtData string
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileInfoFromStat(stat *FileStat, name string) os.FileInfo {
|
||||||
|
return &fileInfo{
|
||||||
|
name: name,
|
||||||
|
stat: stat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileStatFromInfo(fi os.FileInfo) (uint32, *FileStat) {
|
||||||
|
mtime := fi.ModTime().Unix()
|
||||||
|
atime := mtime
|
||||||
|
var flags uint32 = sshFileXferAttrSize |
|
||||||
|
sshFileXferAttrPermissions |
|
||||||
|
sshFileXferAttrACmodTime
|
||||||
|
|
||||||
|
fileStat := &FileStat{
|
||||||
|
Size: uint64(fi.Size()),
|
||||||
|
Mode: fromFileMode(fi.Mode()),
|
||||||
|
Mtime: uint32(mtime),
|
||||||
|
Atime: uint32(atime),
|
||||||
|
}
|
||||||
|
|
||||||
|
// os specific file stat decoding
|
||||||
|
fileStatFromInfoOs(fi, &flags, fileStat)
|
||||||
|
|
||||||
|
return flags, fileStat
|
||||||
|
}
|
11
sftp/attrs_stubs.go
Normal file
11
sftp/attrs_stubs.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// +build plan9 windows android
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileStatFromInfoOs(fi os.FileInfo, flags *uint32, fileStat *FileStat) {
|
||||||
|
// todo
|
||||||
|
}
|
16
sftp/attrs_unix.go
Normal file
16
sftp/attrs_unix.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris aix js
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileStatFromInfoOs(fi os.FileInfo, flags *uint32, fileStat *FileStat) {
|
||||||
|
if statt, ok := fi.Sys().(*syscall.Stat_t); ok {
|
||||||
|
*flags |= sshFileXferAttrUIDGID
|
||||||
|
fileStat.UID = statt.Uid
|
||||||
|
fileStat.GID = statt.Gid
|
||||||
|
}
|
||||||
|
}
|
1936
sftp/client.go
Normal file
1936
sftp/client.go
Normal file
File diff suppressed because it is too large
Load diff
189
sftp/conn.go
Normal file
189
sftp/conn.go
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conn implements a bidirectional channel on which client and server
|
||||||
|
// connections are multiplexed.
|
||||||
|
type conn struct {
|
||||||
|
io.Reader
|
||||||
|
io.WriteCloser
|
||||||
|
// this is the same allocator used in packet manager
|
||||||
|
alloc *allocator
|
||||||
|
sync.Mutex // used to serialise writes to sendPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
// the orderID is used in server mode if the allocator is enabled.
|
||||||
|
// For the client mode just pass 0
|
||||||
|
func (c *conn) recvPacket(orderID uint32) (uint8, []byte, error) {
|
||||||
|
return recvPacket(c, c.alloc, orderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) sendPacket(m encoding.BinaryMarshaler) error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
return sendPacket(c, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Close() error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
return c.WriteCloser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientConn struct {
|
||||||
|
conn
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
sync.Mutex // protects inflight
|
||||||
|
inflight map[uint32]chan<- result // outstanding requests
|
||||||
|
|
||||||
|
closed chan struct{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until the conn has shut down, and return the error
|
||||||
|
// causing the shutdown. It can be called concurrently from multiple
|
||||||
|
// goroutines.
|
||||||
|
func (c *clientConn) Wait() error {
|
||||||
|
<-c.closed
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the SFTP session.
|
||||||
|
func (c *clientConn) Close() error {
|
||||||
|
defer c.wg.Wait()
|
||||||
|
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
|
||||||
|
// appropriate channel.
|
||||||
|
func (c *clientConn) recv() error {
|
||||||
|
defer c.conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
typ, data, err := c.recvPacket(0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sid, _, err := unmarshalUint32Safe(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, ok := c.getChannel(sid)
|
||||||
|
if !ok {
|
||||||
|
// This is an unexpected occurrence. Send the error
|
||||||
|
// back to all listeners so that they terminate
|
||||||
|
// gracefully.
|
||||||
|
return fmt.Errorf("sid not found: %d", sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch <- result{typ: typ, data: data}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) putChannel(ch chan<- result, sid uint32) bool {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.closed:
|
||||||
|
// already closed with broadcastErr, return error on chan.
|
||||||
|
ch <- result{err: ErrSSHFxConnectionLost}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
c.inflight[sid] = ch
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) getChannel(sid uint32) (chan<- result, bool) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
ch, ok := c.inflight[sid]
|
||||||
|
delete(c.inflight, sid)
|
||||||
|
|
||||||
|
return ch, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// result captures the result of receiving the a packet from the server
|
||||||
|
type result struct {
|
||||||
|
typ byte
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type idmarshaler interface {
|
||||||
|
id() uint32
|
||||||
|
encoding.BinaryMarshaler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) sendPacket(ch chan result, p idmarshaler) (byte, []byte, error) {
|
||||||
|
if cap(ch) < 1 {
|
||||||
|
ch = make(chan result, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.dispatchRequest(ch, p)
|
||||||
|
s := <-ch
|
||||||
|
return s.typ, s.data, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchRequest should ideally only be called by race-detection tests outside of this file,
|
||||||
|
// where you have to ensure two packets are in flight sequentially after each other.
|
||||||
|
func (c *clientConn) dispatchRequest(ch chan<- result, p idmarshaler) {
|
||||||
|
sid := p.id()
|
||||||
|
|
||||||
|
if !c.putChannel(ch, sid) {
|
||||||
|
// already closed.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.conn.sendPacket(p); err != nil {
|
||||||
|
if ch, ok := c.getChannel(sid); ok {
|
||||||
|
ch <- result{err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcastErr sends an error to all goroutines waiting for a response.
|
||||||
|
func (c *clientConn) broadcastErr(err error) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
bcastRes := result{err: ErrSSHFxConnectionLost}
|
||||||
|
for sid, ch := range c.inflight {
|
||||||
|
ch <- bcastRes
|
||||||
|
|
||||||
|
// Replace the chan in inflight,
|
||||||
|
// we have hijacked this chan,
|
||||||
|
// and this guarantees always-only-once sending.
|
||||||
|
c.inflight[sid] = make(chan<- result, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.err = err
|
||||||
|
close(c.closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConn struct {
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverConn) sendError(id uint32, err error) error {
|
||||||
|
return s.sendPacket(statusFromError(id, err))
|
||||||
|
}
|
9
sftp/debug.go
Normal file
9
sftp/debug.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// +build debug
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
func debug(fmt string, args ...interface{}) {
|
||||||
|
log.Printf(fmt, args...)
|
||||||
|
}
|
81
sftp/ls_formatting.go
Normal file
81
sftp/ls_formatting.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sshfx "git.deuxfleurs.fr/Deuxfleurs/bagage/internal/encoding/ssh/filexfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lsFormatID(id uint32) string {
|
||||||
|
return strconv.FormatUint(uint64(id), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
type osIDLookup struct{}
|
||||||
|
|
||||||
|
func (osIDLookup) Filelist(*Request) (ListerAt, error) {
|
||||||
|
return nil, errors.New("unimplemented stub")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (osIDLookup) LookupUserName(uid string) string {
|
||||||
|
u, err := user.LookupId(uid)
|
||||||
|
if err != nil {
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (osIDLookup) LookupGroupName(gid string) string {
|
||||||
|
g, err := user.LookupGroupId(gid)
|
||||||
|
if err != nil {
|
||||||
|
return gid
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// runLs formats the FileInfo as per `ls -l` style, which is in the 'longname' field of a SSH_FXP_NAME entry.
|
||||||
|
// This is a fairly simple implementation, just enough to look close to openssh in simple cases.
|
||||||
|
func runLs(idLookup NameLookupFileLister, dirent os.FileInfo) string {
|
||||||
|
// example from openssh sftp server:
|
||||||
|
// crw-rw-rw- 1 root wheel 0 Jul 31 20:52 ttyvd
|
||||||
|
// format:
|
||||||
|
// {directory / char device / etc}{rwxrwxrwx} {number of links} owner group size month day [time (this year) | year (otherwise)] name
|
||||||
|
|
||||||
|
symPerms := sshfx.FileMode(fromFileMode(dirent.Mode())).String()
|
||||||
|
|
||||||
|
var numLinks uint64 = 1
|
||||||
|
uid, gid := "0", "0"
|
||||||
|
|
||||||
|
switch sys := dirent.Sys().(type) {
|
||||||
|
case *sshfx.Attributes:
|
||||||
|
uid = lsFormatID(sys.UID)
|
||||||
|
gid = lsFormatID(sys.GID)
|
||||||
|
case *FileStat:
|
||||||
|
uid = lsFormatID(sys.UID)
|
||||||
|
gid = lsFormatID(sys.GID)
|
||||||
|
default:
|
||||||
|
numLinks, uid, gid = lsLinksUIDGID(dirent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idLookup != nil {
|
||||||
|
uid, gid = idLookup.LookupUserName(uid), idLookup.LookupGroupName(gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
mtime := dirent.ModTime()
|
||||||
|
date := mtime.Format("Jan 2")
|
||||||
|
|
||||||
|
var yearOrTime string
|
||||||
|
if mtime.Before(time.Now().AddDate(0, -6, 0)) {
|
||||||
|
yearOrTime = mtime.Format("2006")
|
||||||
|
} else {
|
||||||
|
yearOrTime = mtime.Format("15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %4d %-8s %-8s %8d %s %5s %s", symPerms, numLinks, uid, gid, dirent.Size(), date, yearOrTime, dirent.Name())
|
||||||
|
}
|
21
sftp/ls_plan9.go
Normal file
21
sftp/ls_plan9.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// +build plan9
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
|
||||||
|
numLinks = 1
|
||||||
|
uid, gid = "0", "0"
|
||||||
|
|
||||||
|
switch sys := fi.Sys().(type) {
|
||||||
|
case *syscall.Dir:
|
||||||
|
uid = sys.Uid
|
||||||
|
gid = sys.Gid
|
||||||
|
}
|
||||||
|
|
||||||
|
return numLinks, uid, gid
|
||||||
|
}
|
11
sftp/ls_stub.go
Normal file
11
sftp/ls_stub.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// +build windows android
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
|
||||||
|
return 1, "0", "0"
|
||||||
|
}
|
23
sftp/ls_unix.go
Normal file
23
sftp/ls_unix.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris js
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lsLinksUIDGID(fi os.FileInfo) (numLinks uint64, uid, gid string) {
|
||||||
|
numLinks = 1
|
||||||
|
uid, gid = "0", "0"
|
||||||
|
|
||||||
|
switch sys := fi.Sys().(type) {
|
||||||
|
case *syscall.Stat_t:
|
||||||
|
numLinks = uint64(sys.Nlink)
|
||||||
|
uid = lsFormatID(sys.Uid)
|
||||||
|
gid = lsFormatID(sys.Gid)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return numLinks, uid, gid
|
||||||
|
}
|
1276
sftp/packet.go
Normal file
1276
sftp/packet.go
Normal file
File diff suppressed because it is too large
Load diff
221
sftp/packet_manager.go
Normal file
221
sftp/packet_manager.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
/*
|
||||||
|
Imported from: https://github.com/pkg/sftp
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The goal of the packetManager is to keep the outgoing packets in the same
|
||||||
|
// order as the incoming as is requires by section 7 of the RFC.
|
||||||
|
|
||||||
|
type packetManager struct {
|
||||||
|
requests chan orderedPacket
|
||||||
|
responses chan orderedPacket
|
||||||
|
fini chan struct{}
|
||||||
|
incoming orderedPackets
|
||||||
|
outgoing orderedPackets
|
||||||
|
sender packetSender // connection object
|
||||||
|
working *sync.WaitGroup
|
||||||
|
packetCount uint32
|
||||||
|
// it is not nil if the allocator is enabled
|
||||||
|
alloc *allocator
|
||||||
|
}
|
||||||
|
|
||||||
|
type packetSender interface {
|
||||||
|
sendPacket(encoding.BinaryMarshaler) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPktMgr(sender packetSender) *packetManager {
|
||||||
|
s := &packetManager{
|
||||||
|
requests: make(chan orderedPacket, SftpServerWorkerCount),
|
||||||
|
responses: make(chan orderedPacket, SftpServerWorkerCount),
|
||||||
|
fini: make(chan struct{}),
|
||||||
|
incoming: make([]orderedPacket, 0, SftpServerWorkerCount),
|
||||||
|
outgoing: make([]orderedPacket, 0, SftpServerWorkerCount),
|
||||||
|
sender: sender,
|
||||||
|
working: &sync.WaitGroup{},
|
||||||
|
}
|
||||||
|
go s.controller()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
//// packet ordering
|
||||||
|
func (s *packetManager) newOrderID() uint32 {
|
||||||
|
s.packetCount++
|
||||||
|
return s.packetCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the next orderID without incrementing it.
|
||||||
|
// This is used before receiving a new packet, with the allocator enabled, to associate
|
||||||
|
// the slice allocated for the received packet with the orderID that will be used to mark
|
||||||
|
// the allocated slices for reuse once the request is served
|
||||||
|
func (s *packetManager) getNextOrderID() uint32 {
|
||||||
|
return s.packetCount + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderedRequest struct {
|
||||||
|
requestPacket
|
||||||
|
orderid uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *packetManager) newOrderedRequest(p requestPacket) orderedRequest {
|
||||||
|
return orderedRequest{requestPacket: p, orderid: s.newOrderID()}
|
||||||
|
}
|
||||||
|
func (p orderedRequest) orderID() uint32 { return p.orderid }
|
||||||
|
func (p orderedRequest) setOrderID(oid uint32) { p.orderid = oid }
|
||||||
|
|
||||||
|
type orderedResponse struct {
|
||||||
|
responsePacket
|
||||||
|
orderid uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *packetManager) newOrderedResponse(p responsePacket, id uint32,
|
||||||
|
) orderedResponse {
|
||||||
|
return orderedResponse{responsePacket: p, orderid: id}
|
||||||
|
}
|
||||||
|
func (p orderedResponse) orderID() uint32 { return p.orderid }
|
||||||
|
func (p orderedResponse) setOrderID(oid uint32) { p.orderid = oid }
|
||||||
|
|
||||||
|
type orderedPacket interface {
|
||||||
|
id() uint32
|
||||||
|
orderID() uint32
|
||||||
|
}
|
||||||
|
type orderedPackets []orderedPacket
|
||||||
|
|
||||||
|
func (o orderedPackets) Sort() {
|
||||||
|
sort.Slice(o, func(i, j int) bool {
|
||||||
|
return o[i].orderID() < o[j].orderID()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//// packet registry
|
||||||
|
// register incoming packets to be handled
|
||||||
|
func (s *packetManager) incomingPacket(pkt orderedRequest) {
|
||||||
|
s.working.Add(1)
|
||||||
|
s.requests <- pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
// register outgoing packets as being ready
|
||||||
|
func (s *packetManager) readyPacket(pkt orderedResponse) {
|
||||||
|
s.responses <- pkt
|
||||||
|
s.working.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// shut down packetManager controller
|
||||||
|
func (s *packetManager) close() {
|
||||||
|
// pause until current packets are processed
|
||||||
|
s.working.Wait()
|
||||||
|
close(s.fini)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed a worker function, returns a channel for incoming packets.
|
||||||
|
// Keep process packet responses in the order they are received while
|
||||||
|
// maximizing throughput of file transfers.
|
||||||
|
func (s *packetManager) workerChan(runWorker func(chan orderedRequest),
|
||||||
|
) chan orderedRequest {
|
||||||
|
// multiple workers for faster read/writes
|
||||||
|
rwChan := make(chan orderedRequest, SftpServerWorkerCount)
|
||||||
|
for i := 0; i < SftpServerWorkerCount; i++ {
|
||||||
|
runWorker(rwChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// single worker to enforce sequential processing of everything else
|
||||||
|
cmdChan := make(chan orderedRequest)
|
||||||
|
runWorker(cmdChan)
|
||||||
|
|
||||||
|
pktChan := make(chan orderedRequest, SftpServerWorkerCount)
|
||||||
|
go func() {
|
||||||
|
for pkt := range pktChan {
|
||||||
|
switch pkt.requestPacket.(type) {
|
||||||
|
case *sshFxpReadPacket, *sshFxpWritePacket:
|
||||||
|
s.incomingPacket(pkt)
|
||||||
|
rwChan <- pkt
|
||||||
|
continue
|
||||||
|
case *sshFxpClosePacket:
|
||||||
|
// wait for reads/writes to finish when file is closed
|
||||||
|
// incomingPacket() call must occur after this
|
||||||
|
s.working.Wait()
|
||||||
|
}
|
||||||
|
s.incomingPacket(pkt)
|
||||||
|
// all non-RW use sequential cmdChan
|
||||||
|
cmdChan <- pkt
|
||||||
|
}
|
||||||
|
close(rwChan)
|
||||||
|
close(cmdChan)
|
||||||
|
s.close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return pktChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// process packets
|
||||||
|
func (s *packetManager) controller() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case pkt := <-s.requests:
|
||||||
|
debug("incoming id (oid): %v (%v)", pkt.id(), pkt.orderID())
|
||||||
|
s.incoming = append(s.incoming, pkt)
|
||||||
|
s.incoming.Sort()
|
||||||
|
case pkt := <-s.responses:
|
||||||
|
debug("outgoing id (oid): %v (%v)", pkt.id(), pkt.orderID())
|
||||||
|
s.outgoing = append(s.outgoing, pkt)
|
||||||
|
s.outgoing.Sort()
|
||||||
|
case <-s.fini:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.maybeSendPackets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send as many packets as are ready
|
||||||
|
func (s *packetManager) maybeSendPackets() {
|
||||||
|
for {
|
||||||
|
if len(s.outgoing) == 0 || len(s.incoming) == 0 {
|
||||||
|
debug("break! -- outgoing: %v; incoming: %v",
|
||||||
|
len(s.outgoing), len(s.incoming))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out := s.outgoing[0]
|
||||||
|
in := s.incoming[0]
|
||||||
|
// debug("incoming: %v", ids(s.incoming))
|
||||||
|
// debug("outgoing: %v", ids(s.outgoing))
|
||||||
|
if in.orderID() == out.orderID() {
|
||||||
|
debug("Sending packet: %v", out.id())
|
||||||
|
s.sender.sendPacket(out.(encoding.BinaryMarshaler))
|
||||||
|
if s.alloc != nil {
|
||||||
|
// mark for reuse the slices allocated for this request
|
||||||
|
s.alloc.ReleasePages(in.orderID())
|
||||||
|
}
|
||||||
|
// pop off heads
|
||||||
|
copy(s.incoming, s.incoming[1:]) // shift left
|
||||||
|
s.incoming[len(s.incoming)-1] = nil // clear last
|
||||||
|
s.incoming = s.incoming[:len(s.incoming)-1] // remove last
|
||||||
|
copy(s.outgoing, s.outgoing[1:]) // shift left
|
||||||
|
s.outgoing[len(s.outgoing)-1] = nil // clear last
|
||||||
|
s.outgoing = s.outgoing[:len(s.outgoing)-1] // remove last
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func oids(o []orderedPacket) []uint32 {
|
||||||
|
// res := make([]uint32, 0, len(o))
|
||||||
|
// for _, v := range o {
|
||||||
|
// res = append(res, v.orderId())
|
||||||
|
// }
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
// func ids(o []orderedPacket) []uint32 {
|
||||||
|
// res := make([]uint32, 0, len(o))
|
||||||
|
// for _, v := range o {
|
||||||
|
// res = append(res, v.id())
|
||||||
|
// }
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
|
135
sftp/packet_typing.go
Normal file
135
sftp/packet_typing.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// all incoming packets
|
||||||
|
type requestPacket interface {
|
||||||
|
encoding.BinaryUnmarshaler
|
||||||
|
id() uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type responsePacket interface {
|
||||||
|
encoding.BinaryMarshaler
|
||||||
|
id() uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// interfaces to group types
|
||||||
|
type hasPath interface {
|
||||||
|
requestPacket
|
||||||
|
getPath() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type hasHandle interface {
|
||||||
|
requestPacket
|
||||||
|
getHandle() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type notReadOnly interface {
|
||||||
|
notReadOnly()
|
||||||
|
}
|
||||||
|
|
||||||
|
//// define types by adding methods
|
||||||
|
// hasPath
|
||||||
|
func (p *sshFxpLstatPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpStatPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpRmdirPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpReadlinkPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpRealpathPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpMkdirPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpSetstatPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpStatvfsPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpRemovePacket) getPath() string { return p.Filename }
|
||||||
|
func (p *sshFxpRenamePacket) getPath() string { return p.Oldpath }
|
||||||
|
func (p *sshFxpSymlinkPacket) getPath() string { return p.Targetpath }
|
||||||
|
func (p *sshFxpOpendirPacket) getPath() string { return p.Path }
|
||||||
|
func (p *sshFxpOpenPacket) getPath() string { return p.Path }
|
||||||
|
|
||||||
|
func (p *sshFxpExtendedPacketPosixRename) getPath() string { return p.Oldpath }
|
||||||
|
func (p *sshFxpExtendedPacketHardlink) getPath() string { return p.Oldpath }
|
||||||
|
|
||||||
|
// getHandle
|
||||||
|
func (p *sshFxpFstatPacket) getHandle() string { return p.Handle }
|
||||||
|
func (p *sshFxpFsetstatPacket) getHandle() string { return p.Handle }
|
||||||
|
func (p *sshFxpReadPacket) getHandle() string { return p.Handle }
|
||||||
|
func (p *sshFxpWritePacket) getHandle() string { return p.Handle }
|
||||||
|
func (p *sshFxpReaddirPacket) getHandle() string { return p.Handle }
|
||||||
|
func (p *sshFxpClosePacket) getHandle() string { return p.Handle }
|
||||||
|
|
||||||
|
// notReadOnly
|
||||||
|
func (p *sshFxpWritePacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpSetstatPacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpFsetstatPacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpRemovePacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpMkdirPacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpRmdirPacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpRenamePacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpSymlinkPacket) notReadOnly() {}
|
||||||
|
func (p *sshFxpExtendedPacketPosixRename) notReadOnly() {}
|
||||||
|
func (p *sshFxpExtendedPacketHardlink) notReadOnly() {}
|
||||||
|
|
||||||
|
// some packets with ID are missing id()
|
||||||
|
func (p *sshFxpDataPacket) id() uint32 { return p.ID }
|
||||||
|
func (p *sshFxpStatusPacket) id() uint32 { return p.ID }
|
||||||
|
func (p *sshFxpStatResponse) id() uint32 { return p.ID }
|
||||||
|
func (p *sshFxpNamePacket) id() uint32 { return p.ID }
|
||||||
|
func (p *sshFxpHandlePacket) id() uint32 { return p.ID }
|
||||||
|
func (p *StatVFS) id() uint32 { return p.ID }
|
||||||
|
func (p *sshFxVersionPacket) id() uint32 { return 0 }
|
||||||
|
|
||||||
|
// take raw incoming packet data and build packet objects
|
||||||
|
func makePacket(p rxPacket) (requestPacket, error) {
|
||||||
|
var pkt requestPacket
|
||||||
|
switch p.pktType {
|
||||||
|
case sshFxpInit:
|
||||||
|
pkt = &sshFxInitPacket{}
|
||||||
|
case sshFxpLstat:
|
||||||
|
pkt = &sshFxpLstatPacket{}
|
||||||
|
case sshFxpOpen:
|
||||||
|
pkt = &sshFxpOpenPacket{}
|
||||||
|
case sshFxpClose:
|
||||||
|
pkt = &sshFxpClosePacket{}
|
||||||
|
case sshFxpRead:
|
||||||
|
pkt = &sshFxpReadPacket{}
|
||||||
|
case sshFxpWrite:
|
||||||
|
pkt = &sshFxpWritePacket{}
|
||||||
|
case sshFxpFstat:
|
||||||
|
pkt = &sshFxpFstatPacket{}
|
||||||
|
case sshFxpSetstat:
|
||||||
|
pkt = &sshFxpSetstatPacket{}
|
||||||
|
case sshFxpFsetstat:
|
||||||
|
pkt = &sshFxpFsetstatPacket{}
|
||||||
|
case sshFxpOpendir:
|
||||||
|
pkt = &sshFxpOpendirPacket{}
|
||||||
|
case sshFxpReaddir:
|
||||||
|
pkt = &sshFxpReaddirPacket{}
|
||||||
|
case sshFxpRemove:
|
||||||
|
pkt = &sshFxpRemovePacket{}
|
||||||
|
case sshFxpMkdir:
|
||||||
|
pkt = &sshFxpMkdirPacket{}
|
||||||
|
case sshFxpRmdir:
|
||||||
|
pkt = &sshFxpRmdirPacket{}
|
||||||
|
case sshFxpRealpath:
|
||||||
|
pkt = &sshFxpRealpathPacket{}
|
||||||
|
case sshFxpStat:
|
||||||
|
pkt = &sshFxpStatPacket{}
|
||||||
|
case sshFxpRename:
|
||||||
|
pkt = &sshFxpRenamePacket{}
|
||||||
|
case sshFxpReadlink:
|
||||||
|
pkt = &sshFxpReadlinkPacket{}
|
||||||
|
case sshFxpSymlink:
|
||||||
|
pkt = &sshFxpSymlinkPacket{}
|
||||||
|
case sshFxpExtended:
|
||||||
|
pkt = &sshFxpExtendedPacket{}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unhandled packet type: %s", p.pktType)
|
||||||
|
}
|
||||||
|
if err := pkt.UnmarshalBinary(p.pktBytes); err != nil {
|
||||||
|
// Return partially unpacked packet to allow callers to return
|
||||||
|
// error messages appropriately with necessary id() method.
|
||||||
|
return pkt, err
|
||||||
|
}
|
||||||
|
return pkt, nil
|
||||||
|
}
|
79
sftp/pool.go
Normal file
79
sftp/pool.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
// bufPool provides a pool of byte-slices to be reused in various parts of the package.
|
||||||
|
// It is safe to use concurrently through a pointer.
|
||||||
|
type bufPool struct {
|
||||||
|
ch chan []byte
|
||||||
|
blen int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBufPool(depth, bufLen int) *bufPool {
|
||||||
|
return &bufPool{
|
||||||
|
ch: make(chan []byte, depth),
|
||||||
|
blen: bufLen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bufPool) Get() []byte {
|
||||||
|
if p.blen <= 0 {
|
||||||
|
panic("bufPool: new buffer creation length must be greater than zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case b := <-p.ch:
|
||||||
|
if cap(b) < p.blen {
|
||||||
|
// just in case: throw away any buffer with insufficient capacity.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return b[:p.blen]
|
||||||
|
|
||||||
|
default:
|
||||||
|
return make([]byte, p.blen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *bufPool) Put(b []byte) {
|
||||||
|
if p == nil {
|
||||||
|
// functional default: no reuse.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cap(b) < p.blen || cap(b) > p.blen*2 {
|
||||||
|
// DO NOT reuse buffers with insufficient capacity.
|
||||||
|
// This could cause panics when resizing to p.blen.
|
||||||
|
|
||||||
|
// DO NOT reuse buffers with excessive capacity.
|
||||||
|
// This could cause memory leaks.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case p.ch <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type resChanPool chan chan result
|
||||||
|
|
||||||
|
func newResChanPool(depth int) resChanPool {
|
||||||
|
return make(chan chan result, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p resChanPool) Get() chan result {
|
||||||
|
select {
|
||||||
|
case ch := <-p:
|
||||||
|
return ch
|
||||||
|
default:
|
||||||
|
return make(chan result, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p resChanPool) Put(ch chan result) {
|
||||||
|
select {
|
||||||
|
case p <- ch:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
5
sftp/release.go
Normal file
5
sftp/release.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// +build !debug
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
func debug(fmt string, args ...interface{}) {}
|
63
sftp/request-attrs.go
Normal file
63
sftp/request-attrs.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
// Methods on the Request object to make working with the Flags bitmasks and
|
||||||
|
// Attr(ibutes) byte blob easier. Use Pflags() when working with an Open/Write
|
||||||
|
// request and AttrFlags() and Attributes() when working with SetStat requests.
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// FileOpenFlags defines Open and Write Flags. Correlate directly with with os.OpenFile flags
|
||||||
|
// (https://golang.org/pkg/os/#pkg-constants).
|
||||||
|
type FileOpenFlags struct {
|
||||||
|
Read, Write, Append, Creat, Trunc, Excl bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileOpenFlags(flags uint32) FileOpenFlags {
|
||||||
|
return FileOpenFlags{
|
||||||
|
Read: flags&sshFxfRead != 0,
|
||||||
|
Write: flags&sshFxfWrite != 0,
|
||||||
|
Append: flags&sshFxfAppend != 0,
|
||||||
|
Creat: flags&sshFxfCreat != 0,
|
||||||
|
Trunc: flags&sshFxfTrunc != 0,
|
||||||
|
Excl: flags&sshFxfExcl != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pflags converts the bitmap/uint32 from SFTP Open packet pflag values,
|
||||||
|
// into a FileOpenFlags struct with booleans set for flags set in bitmap.
|
||||||
|
func (r *Request) Pflags() FileOpenFlags {
|
||||||
|
return newFileOpenFlags(r.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileAttrFlags that indicate whether SFTP file attributes were passed. When a flag is
|
||||||
|
// true the corresponding attribute should be available from the FileStat
|
||||||
|
// object returned by Attributes method. Used with SetStat.
|
||||||
|
type FileAttrFlags struct {
|
||||||
|
Size, UidGid, Permissions, Acmodtime bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileAttrFlags(flags uint32) FileAttrFlags {
|
||||||
|
return FileAttrFlags{
|
||||||
|
Size: (flags & sshFileXferAttrSize) != 0,
|
||||||
|
UidGid: (flags & sshFileXferAttrUIDGID) != 0,
|
||||||
|
Permissions: (flags & sshFileXferAttrPermissions) != 0,
|
||||||
|
Acmodtime: (flags & sshFileXferAttrACmodTime) != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttrFlags returns a FileAttrFlags boolean struct based on the
|
||||||
|
// bitmap/uint32 file attribute flags from the SFTP packaet.
|
||||||
|
func (r *Request) AttrFlags() FileAttrFlags {
|
||||||
|
return newFileAttrFlags(r.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileMode returns the Mode SFTP file attributes wrapped as os.FileMode
|
||||||
|
func (a FileStat) FileMode() os.FileMode {
|
||||||
|
return os.FileMode(a.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes parses file attributes byte blob and return them in a
|
||||||
|
// FileStat object.
|
||||||
|
func (r *Request) Attributes() *FileStat {
|
||||||
|
fs, _ := unmarshalFileStat(r.Flags, r.Attrs)
|
||||||
|
return fs
|
||||||
|
}
|
54
sftp/request-errors.go
Normal file
54
sftp/request-errors.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
type fxerr uint32
|
||||||
|
|
||||||
|
// Error types that match the SFTP's SSH_FXP_STATUS codes. Gives you more
|
||||||
|
// direct control of the errors being sent vs. letting the library work them
|
||||||
|
// out from the standard os/io errors.
|
||||||
|
const (
|
||||||
|
ErrSSHFxOk = fxerr(sshFxOk)
|
||||||
|
ErrSSHFxEOF = fxerr(sshFxEOF)
|
||||||
|
ErrSSHFxNoSuchFile = fxerr(sshFxNoSuchFile)
|
||||||
|
ErrSSHFxPermissionDenied = fxerr(sshFxPermissionDenied)
|
||||||
|
ErrSSHFxFailure = fxerr(sshFxFailure)
|
||||||
|
ErrSSHFxBadMessage = fxerr(sshFxBadMessage)
|
||||||
|
ErrSSHFxNoConnection = fxerr(sshFxNoConnection)
|
||||||
|
ErrSSHFxConnectionLost = fxerr(sshFxConnectionLost)
|
||||||
|
ErrSSHFxOpUnsupported = fxerr(sshFxOPUnsupported)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated error types, these are aliases for the new ones, please use the new ones directly
|
||||||
|
const (
|
||||||
|
ErrSshFxOk = ErrSSHFxOk
|
||||||
|
ErrSshFxEof = ErrSSHFxEOF
|
||||||
|
ErrSshFxNoSuchFile = ErrSSHFxNoSuchFile
|
||||||
|
ErrSshFxPermissionDenied = ErrSSHFxPermissionDenied
|
||||||
|
ErrSshFxFailure = ErrSSHFxFailure
|
||||||
|
ErrSshFxBadMessage = ErrSSHFxBadMessage
|
||||||
|
ErrSshFxNoConnection = ErrSSHFxNoConnection
|
||||||
|
ErrSshFxConnectionLost = ErrSSHFxConnectionLost
|
||||||
|
ErrSshFxOpUnsupported = ErrSSHFxOpUnsupported
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e fxerr) Error() string {
|
||||||
|
switch e {
|
||||||
|
case ErrSSHFxOk:
|
||||||
|
return "OK"
|
||||||
|
case ErrSSHFxEOF:
|
||||||
|
return "EOF"
|
||||||
|
case ErrSSHFxNoSuchFile:
|
||||||
|
return "no such file"
|
||||||
|
case ErrSSHFxPermissionDenied:
|
||||||
|
return "permission denied"
|
||||||
|
case ErrSSHFxBadMessage:
|
||||||
|
return "bad message"
|
||||||
|
case ErrSSHFxNoConnection:
|
||||||
|
return "no connection"
|
||||||
|
case ErrSSHFxConnectionLost:
|
||||||
|
return "connection lost"
|
||||||
|
case ErrSSHFxOpUnsupported:
|
||||||
|
return "operation unsupported"
|
||||||
|
default:
|
||||||
|
return "failure"
|
||||||
|
}
|
||||||
|
}
|
121
sftp/request-interfaces.go
Normal file
121
sftp/request-interfaces.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WriterAtReaderAt defines the interface to return when a file is to
|
||||||
|
// be opened for reading and writing
|
||||||
|
type WriterAtReaderAt interface {
|
||||||
|
io.WriterAt
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interfaces are differentiated based on required returned values.
|
||||||
|
// All input arguments are to be pulled from Request (the only arg).
|
||||||
|
|
||||||
|
// The Handler interfaces all take the Request object as its only argument.
|
||||||
|
// All the data you should need to handle the call are in the Request object.
|
||||||
|
// The request.Method attribute is initially the most important one as it
|
||||||
|
// determines which Handler gets called.
|
||||||
|
|
||||||
|
// FileReader should return an io.ReaderAt for the filepath
|
||||||
|
// Note in cases of an error, the error text will be sent to the client.
|
||||||
|
// Called for Methods: Get
|
||||||
|
type FileReader interface {
|
||||||
|
Fileread(*Request) (io.ReaderAt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileWriter should return an io.WriterAt for the filepath.
|
||||||
|
//
|
||||||
|
// The request server code will call Close() on the returned io.WriterAt
|
||||||
|
// ojbect if an io.Closer type assertion succeeds.
|
||||||
|
// Note in cases of an error, the error text will be sent to the client.
|
||||||
|
// Note when receiving an Append flag it is important to not open files using
|
||||||
|
// O_APPEND if you plan to use WriteAt, as they conflict.
|
||||||
|
// Called for Methods: Put, Open
|
||||||
|
type FileWriter interface {
|
||||||
|
Filewrite(*Request) (io.WriterAt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFileWriter is a FileWriter that implements the generic OpenFile method.
|
||||||
|
// You need to implement this optional interface if you want to be able
|
||||||
|
// to read and write from/to the same handle.
|
||||||
|
// Called for Methods: Open
|
||||||
|
type OpenFileWriter interface {
|
||||||
|
FileWriter
|
||||||
|
OpenFile(*Request) (WriterAtReaderAt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileCmder should return an error
|
||||||
|
// Note in cases of an error, the error text will be sent to the client.
|
||||||
|
// Called for Methods: Setstat, Rename, Rmdir, Mkdir, Link, Symlink, Remove
|
||||||
|
type FileCmder interface {
|
||||||
|
Filecmd(*Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PosixRenameFileCmder is a FileCmder that implements the PosixRename method.
|
||||||
|
// If this interface is implemented PosixRename requests will call it
|
||||||
|
// otherwise they will be handled in the same way as Rename
|
||||||
|
type PosixRenameFileCmder interface {
|
||||||
|
FileCmder
|
||||||
|
PosixRename(*Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatVFSFileCmder is a FileCmder that implements the StatVFS method.
|
||||||
|
// You need to implement this interface if you want to handle statvfs requests.
|
||||||
|
// Please also be sure that the statvfs@openssh.com extension is enabled
|
||||||
|
type StatVFSFileCmder interface {
|
||||||
|
FileCmder
|
||||||
|
StatVFS(*Request) (*StatVFS, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Called for Methods: List, Stat, Readlink
|
||||||
|
type FileLister interface {
|
||||||
|
Filelist(*Request) (ListerAt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LstatFileLister is a FileLister that implements the Lstat method.
|
||||||
|
// If this interface is implemented Lstat requests will call it
|
||||||
|
// otherwise they will be handled in the same way as Stat
|
||||||
|
type LstatFileLister interface {
|
||||||
|
FileLister
|
||||||
|
Lstat(*Request) (ListerAt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RealPathFileLister is a FileLister that implements the Realpath method.
|
||||||
|
// We use "/" as start directory for relative paths, implementing this
|
||||||
|
// interface you can customize the start directory.
|
||||||
|
// You have to return an absolute POSIX path.
|
||||||
|
type RealPathFileLister interface {
|
||||||
|
FileLister
|
||||||
|
RealPath(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameLookupFileLister is a FileLister that implmeents the LookupUsername and LookupGroupName methods.
|
||||||
|
// If this interface is implemented, then longname ls formatting will use these to convert usernames and groupnames.
|
||||||
|
type NameLookupFileLister interface {
|
||||||
|
FileLister
|
||||||
|
LookupUserName(string) string
|
||||||
|
LookupGroupName(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListerAt does for file lists what io.ReaderAt does for files.
|
||||||
|
// 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.
|
||||||
|
// Note in cases of an error, the error text will be sent to the client.
|
||||||
|
type ListerAt interface {
|
||||||
|
ListAt([]os.FileInfo, int64) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferError is an optional interface that readerAt and writerAt
|
||||||
|
// can implement to be notified about the error causing Serve() to exit
|
||||||
|
// with the request still open
|
||||||
|
type TransferError interface {
|
||||||
|
TransferError(err error)
|
||||||
|
}
|
34
sftp/request-plan9.go
Normal file
34
sftp/request-plan9.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// +build plan9
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeFileInfoSys() interface{} {
|
||||||
|
return &syscall.Dir{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOsSys(sys interface{}) error {
|
||||||
|
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
|
||||||
|
}
|
304
sftp/request-server.go
Normal file
304
sftp/request-server.go
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var maxTxPacket uint32 = 1 << 15
|
||||||
|
|
||||||
|
// Handlers contains the 4 SFTP server request handlers.
|
||||||
|
type Handlers struct {
|
||||||
|
FileGet FileReader
|
||||||
|
FilePut FileWriter
|
||||||
|
FileCmd FileCmder
|
||||||
|
FileList FileLister
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestServer abstracts the sftp protocol with an http request-like protocol
|
||||||
|
type RequestServer struct {
|
||||||
|
Handlers Handlers
|
||||||
|
|
||||||
|
*serverConn
|
||||||
|
pktMgr *packetManager
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
handleCount int
|
||||||
|
openRequests map[string]*Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// A RequestServerOption is a function which applies configuration to a RequestServer.
|
||||||
|
type RequestServerOption func(*RequestServer)
|
||||||
|
|
||||||
|
// WithRSAllocator enable the allocator.
|
||||||
|
// After processing a packet we keep in memory the allocated slices
|
||||||
|
// and we reuse them for new packets.
|
||||||
|
// The allocator is experimental
|
||||||
|
func WithRSAllocator() RequestServerOption {
|
||||||
|
return func(rs *RequestServer) {
|
||||||
|
alloc := newAllocator()
|
||||||
|
rs.pktMgr.alloc = alloc
|
||||||
|
rs.conn.alloc = alloc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestServer creates/allocates/returns new RequestServer.
|
||||||
|
// Normally there will be one server per user-session.
|
||||||
|
func NewRequestServer(rwc io.ReadWriteCloser, h Handlers, options ...RequestServerOption) *RequestServer {
|
||||||
|
svrConn := &serverConn{
|
||||||
|
conn: conn{
|
||||||
|
Reader: rwc,
|
||||||
|
WriteCloser: rwc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rs := &RequestServer{
|
||||||
|
Handlers: h,
|
||||||
|
|
||||||
|
serverConn: svrConn,
|
||||||
|
pktMgr: newPktMgr(svrConn),
|
||||||
|
|
||||||
|
openRequests: make(map[string]*Request),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range options {
|
||||||
|
o(rs)
|
||||||
|
}
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Open packet/Request
|
||||||
|
func (rs *RequestServer) nextRequest(r *Request) string {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
|
rs.handleCount++
|
||||||
|
|
||||||
|
r.handle = strconv.Itoa(rs.handleCount)
|
||||||
|
rs.openRequests[r.handle] = r
|
||||||
|
|
||||||
|
return r.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns Request from openRequests, bool is false if it is missing.
|
||||||
|
//
|
||||||
|
// The Requests in openRequests work essentially as open file descriptors that
|
||||||
|
// you can do different things with. What you are doing with it are denoted by
|
||||||
|
// the first packet of that type (read/write/etc).
|
||||||
|
func (rs *RequestServer) getRequest(handle string) (*Request, bool) {
|
||||||
|
rs.mu.RLock()
|
||||||
|
defer rs.mu.RUnlock()
|
||||||
|
|
||||||
|
r, ok := rs.openRequests[handle]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the Request and clear from openRequests map
|
||||||
|
func (rs *RequestServer) closeRequest(handle string) error {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
|
if r, ok := rs.openRequests[handle]; ok {
|
||||||
|
delete(rs.openRequests, handle)
|
||||||
|
return r.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return EBADF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the read/write/closer to trigger exiting the main server loop
|
||||||
|
func (rs *RequestServer) Close() error { return rs.conn.Close() }
|
||||||
|
|
||||||
|
func (rs *RequestServer) serveLoop(pktChan chan<- orderedRequest) error {
|
||||||
|
defer close(pktChan) // shuts down sftpServerWorkers
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var pkt requestPacket
|
||||||
|
var pktType uint8
|
||||||
|
var pktBytes []byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
pktType, pktBytes, err = rs.serverConn.recvPacket(rs.pktMgr.getNextOrderID())
|
||||||
|
if err != nil {
|
||||||
|
// we don't care about releasing allocated pages here, the server will quit and the allocator freed
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt, err = makePacket(rxPacket{fxp(pktType), pktBytes})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errUnknownExtendedPacket):
|
||||||
|
// do nothing
|
||||||
|
default:
|
||||||
|
debug("makePacket err: %v", err)
|
||||||
|
rs.conn.Close() // shuts down recvPacket
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pktChan <- rs.pktMgr.newOrderedRequest(pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve requests for user session
|
||||||
|
func (rs *RequestServer) Serve() error {
|
||||||
|
defer func() {
|
||||||
|
if rs.pktMgr.alloc != nil {
|
||||||
|
rs.pktMgr.alloc.Free()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
runWorker := func(ch chan orderedRequest) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := rs.packetWorker(ctx, ch); err != nil {
|
||||||
|
rs.conn.Close() // shuts down recvPacket
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
pktChan := rs.pktMgr.workerChan(runWorker)
|
||||||
|
|
||||||
|
err := rs.serveLoop(pktChan)
|
||||||
|
|
||||||
|
wg.Wait() // wait for all workers to exit
|
||||||
|
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
|
||||||
|
// make sure all open requests are properly closed
|
||||||
|
// (eg. possible on dropped connections, client crashes, etc.)
|
||||||
|
for handle, req := range rs.openRequests {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
req.transferError(err)
|
||||||
|
|
||||||
|
delete(rs.openRequests, handle)
|
||||||
|
req.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RequestServer) packetWorker(ctx context.Context, pktChan chan orderedRequest) error {
|
||||||
|
for pkt := range pktChan {
|
||||||
|
orderID := pkt.orderID()
|
||||||
|
if epkt, ok := pkt.requestPacket.(*sshFxpExtendedPacket); ok {
|
||||||
|
if epkt.SpecificPacket != nil {
|
||||||
|
pkt.requestPacket = epkt.SpecificPacket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rpkt responsePacket
|
||||||
|
switch pkt := pkt.requestPacket.(type) {
|
||||||
|
case *sshFxInitPacket:
|
||||||
|
rpkt = &sshFxVersionPacket{Version: sftpProtocolVersion, Extensions: sftpExtensions}
|
||||||
|
case *sshFxpClosePacket:
|
||||||
|
handle := pkt.getHandle()
|
||||||
|
rpkt = statusFromError(pkt.ID, rs.closeRequest(handle))
|
||||||
|
case *sshFxpRealpathPacket:
|
||||||
|
var realPath string
|
||||||
|
if realPather, ok := rs.Handlers.FileList.(RealPathFileLister); ok {
|
||||||
|
realPath = realPather.RealPath(pkt.getPath())
|
||||||
|
} else {
|
||||||
|
realPath = cleanPath(pkt.getPath())
|
||||||
|
}
|
||||||
|
rpkt = cleanPacketPath(pkt, realPath)
|
||||||
|
case *sshFxpOpendirPacket:
|
||||||
|
request := requestFromPacket(ctx, pkt)
|
||||||
|
handle := rs.nextRequest(request)
|
||||||
|
rpkt = request.opendir(rs.Handlers, pkt)
|
||||||
|
if _, ok := rpkt.(*sshFxpHandlePacket); !ok {
|
||||||
|
// if we return an error we have to remove the handle from the active ones
|
||||||
|
rs.closeRequest(handle)
|
||||||
|
}
|
||||||
|
case *sshFxpOpenPacket:
|
||||||
|
request := requestFromPacket(ctx, pkt)
|
||||||
|
handle := rs.nextRequest(request)
|
||||||
|
rpkt = request.open(rs.Handlers, pkt)
|
||||||
|
if _, ok := rpkt.(*sshFxpHandlePacket); !ok {
|
||||||
|
// if we return an error we have to remove the handle from the active ones
|
||||||
|
rs.closeRequest(handle)
|
||||||
|
}
|
||||||
|
case *sshFxpFstatPacket:
|
||||||
|
handle := pkt.getHandle()
|
||||||
|
request, ok := rs.getRequest(handle)
|
||||||
|
if !ok {
|
||||||
|
rpkt = statusFromError(pkt.ID, EBADF)
|
||||||
|
} else {
|
||||||
|
request = NewRequest("Stat", request.Filepath)
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
}
|
||||||
|
case *sshFxpFsetstatPacket:
|
||||||
|
handle := pkt.getHandle()
|
||||||
|
request, ok := rs.getRequest(handle)
|
||||||
|
if !ok {
|
||||||
|
rpkt = statusFromError(pkt.ID, EBADF)
|
||||||
|
} else {
|
||||||
|
request = NewRequest("Setstat", request.Filepath)
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
}
|
||||||
|
case *sshFxpExtendedPacketPosixRename:
|
||||||
|
request := NewRequest("PosixRename", pkt.Oldpath)
|
||||||
|
request.Target = pkt.Newpath
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
case *sshFxpExtendedPacketStatVFS:
|
||||||
|
request := NewRequest("StatVFS", pkt.Path)
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
case hasHandle:
|
||||||
|
handle := pkt.getHandle()
|
||||||
|
request, ok := rs.getRequest(handle)
|
||||||
|
if !ok {
|
||||||
|
rpkt = statusFromError(pkt.id(), EBADF)
|
||||||
|
} else {
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
}
|
||||||
|
case hasPath:
|
||||||
|
request := requestFromPacket(ctx, pkt)
|
||||||
|
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
|
||||||
|
request.close()
|
||||||
|
default:
|
||||||
|
rpkt = statusFromError(pkt.id(), ErrSSHFxOpUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.pktMgr.readyPacket(
|
||||||
|
rs.pktMgr.newOrderedResponse(rpkt, orderID))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean and return name packet for file
|
||||||
|
func cleanPacketPath(pkt *sshFxpRealpathPacket, realPath string) responsePacket {
|
||||||
|
return &sshFxpNamePacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
NameAttrs: []*sshFxpNameAttr{
|
||||||
|
{
|
||||||
|
Name: realPath,
|
||||||
|
LongName: realPath,
|
||||||
|
Attrs: emptyFileStat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure we have a clean POSIX (/) absolute path to work with
|
||||||
|
func cleanPath(p string) string {
|
||||||
|
return cleanPathWithBase("/", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPathWithBase(base, p string) string {
|
||||||
|
p = filepath.ToSlash(filepath.Clean(p))
|
||||||
|
if !path.IsAbs(p) {
|
||||||
|
return path.Join(base, p)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
27
sftp/request-unix.go
Normal file
27
sftp/request-unix.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// +build !windows,!plan9
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeFileInfoSys() interface{} {
|
||||||
|
return &syscall.Stat_t{Uid: 65534, Gid: 65534}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOsSys(sys interface{}) error {
|
||||||
|
fstat := sys.(*FileStat)
|
||||||
|
if fstat.UID != uint32(65534) {
|
||||||
|
return errors.New("Uid failed to match")
|
||||||
|
}
|
||||||
|
if fstat.GID != uint32(65534) {
|
||||||
|
return errors.New("Gid failed to match")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLocalPath(p string) string {
|
||||||
|
return p
|
||||||
|
}
|
628
sftp/request.go
Normal file
628
sftp/request.go
Normal file
|
@ -0,0 +1,628 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxFilelist is the max number of files to return in a readdir batch.
|
||||||
|
var MaxFilelist int64 = 100
|
||||||
|
|
||||||
|
// state encapsulates the reader/writer/readdir from handlers.
|
||||||
|
type state struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
writerAt io.WriterAt
|
||||||
|
readerAt io.ReaderAt
|
||||||
|
writerAtReaderAt WriterAtReaderAt
|
||||||
|
listerAt ListerAt
|
||||||
|
lsoffset int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a shallow copy the state.
|
||||||
|
// This is broken out to specific fields,
|
||||||
|
// because we have to copy around the mutex in state.
|
||||||
|
func (s *state) copy() state {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return state{
|
||||||
|
writerAt: s.writerAt,
|
||||||
|
readerAt: s.readerAt,
|
||||||
|
writerAtReaderAt: s.writerAtReaderAt,
|
||||||
|
listerAt: s.listerAt,
|
||||||
|
lsoffset: s.lsoffset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) setReaderAt(rd io.ReaderAt) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.readerAt = rd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) getReaderAt() io.ReaderAt {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.readerAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) setWriterAt(rd io.WriterAt) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.writerAt = rd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) getWriterAt() io.WriterAt {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.writerAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) setWriterAtReaderAt(rw WriterAtReaderAt) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.writerAtReaderAt = rw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) getWriterAtReaderAt() WriterAtReaderAt {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.writerAtReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) getAllReaderWriters() (io.ReaderAt, io.WriterAt, WriterAtReaderAt) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.readerAt, s.writerAt, s.writerAtReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns current offset for file list
|
||||||
|
func (s *state) lsNext() int64 {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.lsoffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increases next offset
|
||||||
|
func (s *state) lsInc(offset int64) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.lsoffset += offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// manage file read/write state
|
||||||
|
func (s *state) setListerAt(la ListerAt) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.listerAt = la
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) getListerAt() ListerAt {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.listerAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request contains the data and state for the incoming service request.
|
||||||
|
type Request struct {
|
||||||
|
// Get, Put, Setstat, Stat, Rename, Remove
|
||||||
|
// Rmdir, Mkdir, List, Readlink, Link, Symlink
|
||||||
|
Method string
|
||||||
|
Filepath string
|
||||||
|
Flags uint32
|
||||||
|
Attrs []byte // convert to sub-struct
|
||||||
|
Target string // for renames and sym-links
|
||||||
|
handle string
|
||||||
|
|
||||||
|
// reader/writer/readdir from handlers
|
||||||
|
state
|
||||||
|
|
||||||
|
// context lasts duration of request
|
||||||
|
ctx context.Context
|
||||||
|
cancelCtx context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest creates a new Request object.
|
||||||
|
func NewRequest(method, path string) *Request {
|
||||||
|
return &Request{
|
||||||
|
Method: method,
|
||||||
|
Filepath: cleanPath(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a shallow copy of existing request.
|
||||||
|
// This is broken out to specific fields,
|
||||||
|
// because we have to copy around the mutex in state.
|
||||||
|
func (r *Request) copy() *Request {
|
||||||
|
return &Request{
|
||||||
|
Method: r.Method,
|
||||||
|
Filepath: r.Filepath,
|
||||||
|
Flags: r.Flags,
|
||||||
|
Attrs: r.Attrs,
|
||||||
|
Target: r.Target,
|
||||||
|
handle: r.handle,
|
||||||
|
|
||||||
|
state: r.state.copy(),
|
||||||
|
|
||||||
|
ctx: r.ctx,
|
||||||
|
cancelCtx: r.cancelCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Request initialized based on packet data
|
||||||
|
func requestFromPacket(ctx context.Context, pkt hasPath) *Request {
|
||||||
|
method := requestMethod(pkt)
|
||||||
|
request := NewRequest(method, pkt.getPath())
|
||||||
|
request.ctx, request.cancelCtx = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
switch p := pkt.(type) {
|
||||||
|
case *sshFxpOpenPacket:
|
||||||
|
request.Flags = p.Pflags
|
||||||
|
case *sshFxpSetstatPacket:
|
||||||
|
request.Flags = p.Flags
|
||||||
|
request.Attrs = p.Attrs.([]byte)
|
||||||
|
case *sshFxpRenamePacket:
|
||||||
|
request.Target = cleanPath(p.Newpath)
|
||||||
|
case *sshFxpSymlinkPacket:
|
||||||
|
// NOTE: given a POSIX compliant signature: symlink(target, linkpath string)
|
||||||
|
// this makes Request.Target the linkpath, and Request.Filepath the target.
|
||||||
|
request.Target = cleanPath(p.Linkpath)
|
||||||
|
case *sshFxpExtendedPacketHardlink:
|
||||||
|
request.Target = cleanPath(p.Newpath)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context returns the request's context. To change the context,
|
||||||
|
// use WithContext.
|
||||||
|
//
|
||||||
|
// The returned context is always non-nil; it defaults to the
|
||||||
|
// background context.
|
||||||
|
//
|
||||||
|
// For incoming server requests, the context is canceled when the
|
||||||
|
// request is complete or the client's connection closes.
|
||||||
|
func (r *Request) Context() context.Context {
|
||||||
|
if r.ctx != nil {
|
||||||
|
return r.ctx
|
||||||
|
}
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext returns a copy of r with its context changed to ctx.
|
||||||
|
// The provided ctx must be non-nil.
|
||||||
|
func (r *Request) WithContext(ctx context.Context) *Request {
|
||||||
|
if ctx == nil {
|
||||||
|
panic("nil context")
|
||||||
|
}
|
||||||
|
r2 := r.copy()
|
||||||
|
r2.ctx = ctx
|
||||||
|
r2.cancelCtx = nil
|
||||||
|
return r2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close reader/writer if possible
|
||||||
|
func (r *Request) close() error {
|
||||||
|
defer func() {
|
||||||
|
if r.cancelCtx != nil {
|
||||||
|
r.cancelCtx()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
rd, wr, rw := r.getAllReaderWriters()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Close errors on a Writer are far more likely to be the important one.
|
||||||
|
// As they can be information that there was a loss of data.
|
||||||
|
if c, ok := wr.(io.Closer); ok {
|
||||||
|
if err2 := c.Close(); err == nil {
|
||||||
|
// update error if it is still nil
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, ok := rw.(io.Closer); ok {
|
||||||
|
if err2 := c.Close(); err == nil {
|
||||||
|
// update error if it is still nil
|
||||||
|
err = err2
|
||||||
|
|
||||||
|
r.setWriterAtReaderAt(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, ok := rd.(io.Closer); ok {
|
||||||
|
if err2 := c.Close(); err == nil {
|
||||||
|
// update error if it is still nil
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify transfer error if any
|
||||||
|
func (r *Request) transferError(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rd, wr, rw := r.getAllReaderWriters()
|
||||||
|
|
||||||
|
if t, ok := wr.(TransferError); ok {
|
||||||
|
t.TransferError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := rw.(TransferError); ok {
|
||||||
|
t.TransferError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := rd.(TransferError); ok {
|
||||||
|
t.TransferError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// called from worker to handle packet/request
|
||||||
|
func (r *Request) call(handlers Handlers, pkt requestPacket, alloc *allocator, orderID uint32) responsePacket {
|
||||||
|
switch r.Method {
|
||||||
|
case "Get":
|
||||||
|
return fileget(handlers.FileGet, r, pkt, alloc, orderID)
|
||||||
|
case "Put":
|
||||||
|
return fileput(handlers.FilePut, r, pkt, alloc, orderID)
|
||||||
|
case "Open":
|
||||||
|
return fileputget(handlers.FilePut, r, pkt, alloc, orderID)
|
||||||
|
case "Setstat", "Rename", "Rmdir", "Mkdir", "Link", "Symlink", "Remove", "PosixRename", "StatVFS":
|
||||||
|
return filecmd(handlers.FileCmd, r, pkt)
|
||||||
|
case "List":
|
||||||
|
return filelist(handlers.FileList, r, pkt)
|
||||||
|
case "Stat", "Lstat", "Readlink":
|
||||||
|
return filestat(handlers.FileList, r, pkt)
|
||||||
|
default:
|
||||||
|
return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional initialization for Open packets
|
||||||
|
func (r *Request) open(h Handlers, pkt requestPacket) responsePacket {
|
||||||
|
flags := r.Pflags()
|
||||||
|
|
||||||
|
id := pkt.id()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case flags.Write, flags.Append, flags.Creat, flags.Trunc:
|
||||||
|
if flags.Read {
|
||||||
|
if openFileWriter, ok := h.FilePut.(OpenFileWriter); ok {
|
||||||
|
r.Method = "Open"
|
||||||
|
rw, err := openFileWriter.OpenFile(r)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.setWriterAtReaderAt(rw)
|
||||||
|
|
||||||
|
return &sshFxpHandlePacket{
|
||||||
|
ID: id,
|
||||||
|
Handle: r.handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Method = "Put"
|
||||||
|
wr, err := h.FilePut.Filewrite(r)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.setWriterAt(wr)
|
||||||
|
|
||||||
|
case flags.Read:
|
||||||
|
r.Method = "Get"
|
||||||
|
rd, err := h.FileGet.Fileread(r)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.setReaderAt(rd)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return statusFromError(id, errors.New("bad file flags"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sshFxpHandlePacket{
|
||||||
|
ID: id,
|
||||||
|
Handle: r.handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) opendir(h Handlers, pkt requestPacket) responsePacket {
|
||||||
|
r.Method = "List"
|
||||||
|
la, err := h.FileList.Filelist(r)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(pkt.id(), wrapPathError(r.Filepath, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.setListerAt(la)
|
||||||
|
|
||||||
|
return &sshFxpHandlePacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
Handle: r.handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap FileReader handler
|
||||||
|
func fileget(h FileReader, r *Request, pkt requestPacket, alloc *allocator, orderID uint32) responsePacket {
|
||||||
|
rd := r.getReaderAt()
|
||||||
|
if rd == nil {
|
||||||
|
return statusFromError(pkt.id(), errors.New("unexpected read packet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, offset, _ := packetData(pkt, alloc, orderID)
|
||||||
|
|
||||||
|
n, err := rd.ReadAt(data, offset)
|
||||||
|
// only return EOF error if no data left to read
|
||||||
|
if err != nil && (err != io.EOF || n == 0) {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sshFxpDataPacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
Length: uint32(n),
|
||||||
|
Data: data[:n],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap FileWriter handler
|
||||||
|
func fileput(h FileWriter, r *Request, pkt requestPacket, alloc *allocator, orderID uint32) responsePacket {
|
||||||
|
wr := r.getWriterAt()
|
||||||
|
if wr == nil {
|
||||||
|
return statusFromError(pkt.id(), errors.New("unexpected write packet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, offset, _ := packetData(pkt, alloc, orderID)
|
||||||
|
|
||||||
|
_, err := wr.WriteAt(data, offset)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap OpenFileWriter handler
|
||||||
|
func fileputget(h FileWriter, r *Request, pkt requestPacket, alloc *allocator, orderID uint32) responsePacket {
|
||||||
|
rw := r.getWriterAtReaderAt()
|
||||||
|
if rw == nil {
|
||||||
|
return statusFromError(pkt.id(), errors.New("unexpected write and read packet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p := pkt.(type) {
|
||||||
|
case *sshFxpReadPacket:
|
||||||
|
data, offset := p.getDataSlice(alloc, orderID), int64(p.Offset)
|
||||||
|
|
||||||
|
n, err := rw.ReadAt(data, offset)
|
||||||
|
// only return EOF error if no data left to read
|
||||||
|
if err != nil && (err != io.EOF || n == 0) {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sshFxpDataPacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
Length: uint32(n),
|
||||||
|
Data: data[:n],
|
||||||
|
}
|
||||||
|
|
||||||
|
case *sshFxpWritePacket:
|
||||||
|
data, offset := p.Data, int64(p.Offset)
|
||||||
|
|
||||||
|
_, err := rw.WriteAt(data, offset)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return statusFromError(pkt.id(), errors.New("unexpected packet type for read or write"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// file data for additional read/write packets
|
||||||
|
func packetData(p requestPacket, alloc *allocator, orderID uint32) (data []byte, offset int64, length uint32) {
|
||||||
|
switch p := p.(type) {
|
||||||
|
case *sshFxpReadPacket:
|
||||||
|
return p.getDataSlice(alloc, orderID), int64(p.Offset), p.Len
|
||||||
|
case *sshFxpWritePacket:
|
||||||
|
return p.Data, int64(p.Offset), p.Length
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap FileCmder handler
|
||||||
|
func filecmd(h FileCmder, r *Request, pkt requestPacket) responsePacket {
|
||||||
|
switch p := pkt.(type) {
|
||||||
|
case *sshFxpFsetstatPacket:
|
||||||
|
r.Flags = p.Flags
|
||||||
|
r.Attrs = p.Attrs.([]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "PosixRename":
|
||||||
|
if posixRenamer, ok := h.(PosixRenameFileCmder); ok {
|
||||||
|
err := posixRenamer.PosixRename(r)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PosixRenameFileCmder not implemented handle this request as a Rename
|
||||||
|
r.Method = "Rename"
|
||||||
|
err := h.Filecmd(r)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
|
||||||
|
case "StatVFS":
|
||||||
|
if statVFSCmdr, ok := h.(StatVFSFileCmder); ok {
|
||||||
|
stat, err := statVFSCmdr.StatVFS(r)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
stat.ID = pkt.id()
|
||||||
|
return stat
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusFromError(pkt.id(), ErrSSHFxOpUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.Filecmd(r)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap FileLister handler
|
||||||
|
func filelist(h FileLister, r *Request, pkt requestPacket) responsePacket {
|
||||||
|
lister := r.getListerAt()
|
||||||
|
if lister == nil {
|
||||||
|
return statusFromError(pkt.id(), errors.New("unexpected dir packet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := r.lsNext()
|
||||||
|
finfo := make([]os.FileInfo, MaxFilelist)
|
||||||
|
n, err := lister.ListAt(finfo, offset)
|
||||||
|
r.lsInc(int64(n))
|
||||||
|
// ignore EOF as we only return it when there are no results
|
||||||
|
finfo = finfo[:n] // avoid need for nil tests below
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "List":
|
||||||
|
if err != nil && (err != io.EOF || n == 0) {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameAttrs := make([]*sshFxpNameAttr, 0, len(finfo))
|
||||||
|
|
||||||
|
// If the type conversion fails, we get untyped `nil`,
|
||||||
|
// which is handled by not looking up any names.
|
||||||
|
idLookup, _ := h.(NameLookupFileLister)
|
||||||
|
|
||||||
|
for _, fi := range finfo {
|
||||||
|
nameAttrs = append(nameAttrs, &sshFxpNameAttr{
|
||||||
|
Name: fi.Name(),
|
||||||
|
LongName: runLs(idLookup, fi),
|
||||||
|
Attrs: []interface{}{fi},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sshFxpNamePacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
NameAttrs: nameAttrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unexpected method: %s", r.Method)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filestat(h FileLister, r *Request, pkt requestPacket) responsePacket {
|
||||||
|
var lister ListerAt
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if r.Method == "Lstat" {
|
||||||
|
if lstatFileLister, ok := h.(LstatFileLister); ok {
|
||||||
|
lister, err = lstatFileLister.Lstat(r)
|
||||||
|
} else {
|
||||||
|
// LstatFileLister not implemented handle this request as a Stat
|
||||||
|
r.Method = "Stat"
|
||||||
|
lister, err = h.Filelist(r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lister, err = h.Filelist(r)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
finfo := make([]os.FileInfo, 1)
|
||||||
|
n, err := lister.ListAt(finfo, 0)
|
||||||
|
finfo = finfo[:n] // avoid need for nil tests below
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "Stat", "Lstat":
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
err = &os.PathError{
|
||||||
|
Op: strings.ToLower(r.Method),
|
||||||
|
Path: r.Filepath,
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
return &sshFxpStatResponse{
|
||||||
|
ID: pkt.id(),
|
||||||
|
info: finfo[0],
|
||||||
|
}
|
||||||
|
case "Readlink":
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
err = &os.PathError{
|
||||||
|
Op: "readlink",
|
||||||
|
Path: r.Filepath,
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
filename := finfo[0].Name()
|
||||||
|
return &sshFxpNamePacket{
|
||||||
|
ID: pkt.id(),
|
||||||
|
NameAttrs: []*sshFxpNameAttr{
|
||||||
|
{
|
||||||
|
Name: filename,
|
||||||
|
LongName: filename,
|
||||||
|
Attrs: emptyFileStat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unexpected method: %s", r.Method)
|
||||||
|
return statusFromError(pkt.id(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init attributes of request object from packet data
|
||||||
|
func requestMethod(p requestPacket) (method string) {
|
||||||
|
switch p.(type) {
|
||||||
|
case *sshFxpReadPacket, *sshFxpWritePacket, *sshFxpOpenPacket:
|
||||||
|
// set in open() above
|
||||||
|
case *sshFxpOpendirPacket, *sshFxpReaddirPacket:
|
||||||
|
// set in opendir() above
|
||||||
|
case *sshFxpSetstatPacket, *sshFxpFsetstatPacket:
|
||||||
|
method = "Setstat"
|
||||||
|
case *sshFxpRenamePacket:
|
||||||
|
method = "Rename"
|
||||||
|
case *sshFxpSymlinkPacket:
|
||||||
|
method = "Symlink"
|
||||||
|
case *sshFxpRemovePacket:
|
||||||
|
method = "Remove"
|
||||||
|
case *sshFxpStatPacket, *sshFxpFstatPacket:
|
||||||
|
method = "Stat"
|
||||||
|
case *sshFxpLstatPacket:
|
||||||
|
method = "Lstat"
|
||||||
|
case *sshFxpRmdirPacket:
|
||||||
|
method = "Rmdir"
|
||||||
|
case *sshFxpReadlinkPacket:
|
||||||
|
method = "Readlink"
|
||||||
|
case *sshFxpMkdirPacket:
|
||||||
|
method = "Mkdir"
|
||||||
|
case *sshFxpExtendedPacketHardlink:
|
||||||
|
method = "Link"
|
||||||
|
}
|
||||||
|
return method
|
||||||
|
}
|
44
sftp/request_windows.go
Normal file
44
sftp/request_windows.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeFileInfoSys() interface{} {
|
||||||
|
return syscall.Win32FileAttributeData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOsSys(sys interface{}) error {
|
||||||
|
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
|
||||||
|
}
|
643
sftp/server.go
Normal file
643
sftp/server.go
Normal file
|
@ -0,0 +1,643 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
// sftp server counterpart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"encoding"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/bagage/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SftpServerWorkerCount defines the number of workers for the SFTP server
|
||||||
|
SftpServerWorkerCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server is an SSH File Transfer Protocol (sftp) server.
|
||||||
|
// This is intended to provide the sftp subsystem to an ssh server daemon.
|
||||||
|
// This implementation currently supports most of sftp server protocol version 3,
|
||||||
|
// as specified at http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
|
||||||
|
type Server struct {
|
||||||
|
*serverConn
|
||||||
|
debugStream io.Writer
|
||||||
|
readOnly bool
|
||||||
|
pktMgr *packetManager
|
||||||
|
openFiles map[string]*s3.S3File
|
||||||
|
openFilesLock sync.RWMutex
|
||||||
|
handleCount int
|
||||||
|
fs *s3.S3FS
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) nextHandle(f *s3.S3File) string {
|
||||||
|
svr.openFilesLock.Lock()
|
||||||
|
defer svr.openFilesLock.Unlock()
|
||||||
|
svr.handleCount++
|
||||||
|
handle := strconv.Itoa(svr.handleCount)
|
||||||
|
svr.openFiles[handle] = f
|
||||||
|
return handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) closeHandle(handle string) error {
|
||||||
|
svr.openFilesLock.Lock()
|
||||||
|
defer svr.openFilesLock.Unlock()
|
||||||
|
if f, ok := svr.openFiles[handle]; ok {
|
||||||
|
delete(svr.openFiles, handle)
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return EBADF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Server) getHandle(handle string) (*s3.S3File, bool) {
|
||||||
|
svr.openFilesLock.RLock()
|
||||||
|
defer svr.openFilesLock.RUnlock()
|
||||||
|
f, ok := svr.openFiles[handle]
|
||||||
|
return f, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverRespondablePacket interface {
|
||||||
|
encoding.BinaryUnmarshaler
|
||||||
|
id() uint32
|
||||||
|
respond(svr *Server) responsePacket
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new Server instance around the provided streams, serving
|
||||||
|
// content from the root of the filesystem. Optionally, ServerOption
|
||||||
|
// functions may be specified to further configure the Server.
|
||||||
|
//
|
||||||
|
// A subsequent call to Serve() is required to begin serving files over SFTP.
|
||||||
|
func NewServer(ctx context.Context, rwc io.ReadWriteCloser, fs *s3.S3FS, options ...ServerOption) (*Server, error) {
|
||||||
|
svrConn := &serverConn{
|
||||||
|
conn: conn{
|
||||||
|
Reader: rwc,
|
||||||
|
WriteCloser: rwc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s := &Server{
|
||||||
|
serverConn: svrConn,
|
||||||
|
debugStream: ioutil.Discard,
|
||||||
|
pktMgr: newPktMgr(svrConn),
|
||||||
|
openFiles: make(map[string]*s3.S3File),
|
||||||
|
fs: fs,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range options {
|
||||||
|
if err := o(s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ServerOption is a function which applies configuration to a Server.
|
||||||
|
type ServerOption func(*Server) error
|
||||||
|
|
||||||
|
// WithDebug enables Server debugging output to the supplied io.Writer.
|
||||||
|
func WithDebug(w io.Writer) ServerOption {
|
||||||
|
return func(s *Server) error {
|
||||||
|
s.debugStream = w
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOnly configures a Server to serve files in read-only mode.
|
||||||
|
func ReadOnly() ServerOption {
|
||||||
|
return func(s *Server) error {
|
||||||
|
s.readOnly = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllocator enable the allocator.
|
||||||
|
// After processing a packet we keep in memory the allocated slices
|
||||||
|
// and we reuse them for new packets.
|
||||||
|
// The allocator is experimental
|
||||||
|
func WithAllocator() ServerOption {
|
||||||
|
return func(s *Server) error {
|
||||||
|
alloc := newAllocator()
|
||||||
|
s.pktMgr.alloc = alloc
|
||||||
|
s.conn.alloc = alloc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type rxPacket struct {
|
||||||
|
pktType fxp
|
||||||
|
pktBytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up to N parallel servers
|
||||||
|
func (svr *Server) sftpServerWorker(pktChan chan orderedRequest) error {
|
||||||
|
for pkt := range pktChan {
|
||||||
|
// readonly checks
|
||||||
|
readonly := true
|
||||||
|
switch pkt := pkt.requestPacket.(type) {
|
||||||
|
case notReadOnly:
|
||||||
|
readonly = false
|
||||||
|
case *sshFxpOpenPacket:
|
||||||
|
readonly = pkt.readonly()
|
||||||
|
case *sshFxpExtendedPacket:
|
||||||
|
readonly = pkt.readonly()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If server is operating read-only and a write operation is requested,
|
||||||
|
// return permission denied
|
||||||
|
if !readonly && svr.readOnly {
|
||||||
|
svr.pktMgr.readyPacket(
|
||||||
|
svr.pktMgr.newOrderedResponse(statusFromError(pkt.id(), syscall.EPERM), pkt.orderID()),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handlePacket(svr, pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePacket(s *Server, p orderedRequest) error {
|
||||||
|
var rpkt responsePacket
|
||||||
|
orderID := p.orderID()
|
||||||
|
switch p := p.requestPacket.(type) {
|
||||||
|
case *sshFxInitPacket:
|
||||||
|
log.Println("pkt: init")
|
||||||
|
rpkt = &sshFxVersionPacket{
|
||||||
|
Version: sftpProtocolVersion,
|
||||||
|
Extensions: sftpExtensions,
|
||||||
|
}
|
||||||
|
case *sshFxpStatPacket:
|
||||||
|
log.Println("pkt: stat: ", p.Path)
|
||||||
|
// stat the requested file
|
||||||
|
info, err := os.Stat(toLocalPath(p.Path))
|
||||||
|
rpkt = &sshFxpStatResponse{
|
||||||
|
ID: p.ID,
|
||||||
|
info: info,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
case *sshFxpLstatPacket:
|
||||||
|
log.Println("pkt: lstat: ", p.Path)
|
||||||
|
// stat the requested file
|
||||||
|
info, err := os.Lstat(toLocalPath(p.Path))
|
||||||
|
rpkt = &sshFxpStatResponse{
|
||||||
|
ID: p.ID,
|
||||||
|
info: info,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
case *sshFxpFstatPacket:
|
||||||
|
log.Println("pkt: fstat: ", p.Handle)
|
||||||
|
f, ok := s.getHandle(p.Handle)
|
||||||
|
var err error = EBADF
|
||||||
|
var info os.FileInfo
|
||||||
|
if ok {
|
||||||
|
info, err = f.Stat()
|
||||||
|
rpkt = &sshFxpStatResponse{
|
||||||
|
ID: p.ID,
|
||||||
|
info: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
case *sshFxpMkdirPacket:
|
||||||
|
log.Println("pkt: mkdir: ", p.Path)
|
||||||
|
err := os.Mkdir(toLocalPath(p.Path), 0755)
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpRmdirPacket:
|
||||||
|
log.Println("pkt: rmdir: ", p.Path)
|
||||||
|
err := os.Remove(toLocalPath(p.Path))
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpRemovePacket:
|
||||||
|
log.Println("pkt: rm: ", p.Filename)
|
||||||
|
err := os.Remove(toLocalPath(p.Filename))
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpRenamePacket:
|
||||||
|
log.Println("pkt: rename: ", p.Oldpath, ", ", p.Newpath)
|
||||||
|
err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath))
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpSymlinkPacket:
|
||||||
|
log.Println("pkt: ln -s: ", p.Targetpath, ", ", p.Linkpath)
|
||||||
|
err := os.Symlink(toLocalPath(p.Targetpath), toLocalPath(p.Linkpath))
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpClosePacket:
|
||||||
|
log.Println("pkt: close handle: ", p.Handle)
|
||||||
|
rpkt = statusFromError(p.ID, s.closeHandle(p.Handle))
|
||||||
|
case *sshFxpReadlinkPacket:
|
||||||
|
log.Println("pkt: read: ", p.Path)
|
||||||
|
f, err := os.Readlink(toLocalPath(p.Path))
|
||||||
|
rpkt = &sshFxpNamePacket{
|
||||||
|
ID: p.ID,
|
||||||
|
NameAttrs: []*sshFxpNameAttr{
|
||||||
|
{
|
||||||
|
Name: f,
|
||||||
|
LongName: f,
|
||||||
|
Attrs: emptyFileStat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
case *sshFxpRealpathPacket:
|
||||||
|
log.Println("pkt: absolute path: ", p.Path)
|
||||||
|
f := s3.NewS3Path(p.Path).Path
|
||||||
|
rpkt = &sshFxpNamePacket{
|
||||||
|
ID: p.ID,
|
||||||
|
NameAttrs: []*sshFxpNameAttr{
|
||||||
|
{
|
||||||
|
Name: f,
|
||||||
|
LongName: f,
|
||||||
|
Attrs: emptyFileStat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case *sshFxpOpendirPacket:
|
||||||
|
log.Println("pkt: open dir: ", p.Path)
|
||||||
|
p.Path = s3.NewS3Path(p.Path).Path
|
||||||
|
|
||||||
|
if stat, err := s.fs.Stat(s.ctx, p.Path); err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
} else if !stat.IsDir() {
|
||||||
|
rpkt = statusFromError(p.ID, &os.PathError{
|
||||||
|
Path: p.Path, Err: syscall.ENOTDIR})
|
||||||
|
} else {
|
||||||
|
rpkt = (&sshFxpOpenPacket{
|
||||||
|
ID: p.ID,
|
||||||
|
Path: p.Path,
|
||||||
|
Pflags: sshFxfRead,
|
||||||
|
}).respond(s)
|
||||||
|
}
|
||||||
|
case *sshFxpReadPacket:
|
||||||
|
log.Println("pkt: read handle: ", p.Handle)
|
||||||
|
var err error = EBADF
|
||||||
|
f, ok := s.getHandle(p.Handle)
|
||||||
|
if ok {
|
||||||
|
err = nil
|
||||||
|
data := p.getDataSlice(s.pktMgr.alloc, orderID)
|
||||||
|
n, _err := f.ReadAt(data, int64(p.Offset))
|
||||||
|
if _err != nil && (_err != io.EOF || n == 0) {
|
||||||
|
err = _err
|
||||||
|
}
|
||||||
|
rpkt = &sshFxpDataPacket{
|
||||||
|
ID: p.ID,
|
||||||
|
Length: uint32(n),
|
||||||
|
Data: data[:n],
|
||||||
|
// do not use data[:n:n] here to clamp the capacity, we allocated extra capacity above to avoid reallocations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *sshFxpWritePacket:
|
||||||
|
log.Println("pkt: write handle: ", p.Handle, ", Offset: ", p.Offset)
|
||||||
|
f, ok := s.getHandle(p.Handle)
|
||||||
|
var err error = EBADF
|
||||||
|
if ok {
|
||||||
|
_, err = f.WriteAt(p.Data, int64(p.Offset))
|
||||||
|
}
|
||||||
|
rpkt = statusFromError(p.ID, err)
|
||||||
|
case *sshFxpExtendedPacket:
|
||||||
|
log.Println("pkt: extended packet")
|
||||||
|
if p.SpecificPacket == nil {
|
||||||
|
rpkt = statusFromError(p.ID, ErrSSHFxOpUnsupported)
|
||||||
|
} else {
|
||||||
|
rpkt = p.respond(s)
|
||||||
|
}
|
||||||
|
case serverRespondablePacket:
|
||||||
|
log.Println("pkt: respondable")
|
||||||
|
rpkt = p.respond(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected packet type %T", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.pktMgr.readyPacket(s.pktMgr.newOrderedResponse(rpkt, orderID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve serves SFTP connections until the streams stop or the SFTP subsystem
|
||||||
|
// is stopped.
|
||||||
|
func (svr *Server) Serve() error {
|
||||||
|
defer func() {
|
||||||
|
if svr.pktMgr.alloc != nil {
|
||||||
|
svr.pktMgr.alloc.Free()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
runWorker := func(ch chan orderedRequest) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := svr.sftpServerWorker(ch); err != nil {
|
||||||
|
svr.conn.Close() // shuts down recvPacket
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
pktChan := svr.pktMgr.workerChan(runWorker)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var pkt requestPacket
|
||||||
|
var pktType uint8
|
||||||
|
var pktBytes []byte
|
||||||
|
for {
|
||||||
|
pktType, pktBytes, err = svr.serverConn.recvPacket(svr.pktMgr.getNextOrderID())
|
||||||
|
if err != nil {
|
||||||
|
// we don't care about releasing allocated pages here, the server will quit and the allocator freed
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt, err = makePacket(rxPacket{fxp(pktType), pktBytes})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errUnknownExtendedPacket):
|
||||||
|
//if err := svr.serverConn.sendError(pkt, ErrSshFxOpUnsupported); err != nil {
|
||||||
|
// debug("failed to send err packet: %v", err)
|
||||||
|
// svr.conn.Close() // shuts down recvPacket
|
||||||
|
// break
|
||||||
|
//}
|
||||||
|
default:
|
||||||
|
debug("makePacket err: %v", err)
|
||||||
|
svr.conn.Close() // shuts down recvPacket
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pktChan <- svr.pktMgr.newOrderedRequest(pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(pktChan) // shuts down sftpServerWorkers
|
||||||
|
wg.Wait() // wait for all workers to exit
|
||||||
|
|
||||||
|
// close any still-open files
|
||||||
|
for handle, file := range svr.openFiles {
|
||||||
|
fmt.Fprintf(svr.debugStream, "sftp server file with handle %q left open: %v\n", handle, file.Path.Path)
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
return err // error from recvPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
type ider interface {
|
||||||
|
id() uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// The init packet has no ID, so we just return a zero-value ID
|
||||||
|
func (p *sshFxInitPacket) id() uint32 { return 0 }
|
||||||
|
|
||||||
|
type sshFxpStatResponse struct {
|
||||||
|
ID uint32
|
||||||
|
info os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpStatResponse) marshalPacket() ([]byte, []byte, error) {
|
||||||
|
l := 4 + 1 + 4 // uint32(length) + byte(type) + uint32(id)
|
||||||
|
|
||||||
|
b := make([]byte, 4, l)
|
||||||
|
b = append(b, sshFxpAttrs)
|
||||||
|
b = marshalUint32(b, p.ID)
|
||||||
|
|
||||||
|
var payload []byte
|
||||||
|
payload = marshalFileInfo(payload, p.info)
|
||||||
|
|
||||||
|
return b, payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpStatResponse) MarshalBinary() ([]byte, error) {
|
||||||
|
header, payload, err := p.marshalPacket()
|
||||||
|
return append(header, payload...), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyFileStat = []interface{}{uint32(0)}
|
||||||
|
|
||||||
|
func (p *sshFxpOpenPacket) readonly() bool {
|
||||||
|
return !p.hasPflags(sshFxfWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpOpenPacket) hasPflags(flags ...uint32) bool {
|
||||||
|
for _, f := range flags {
|
||||||
|
if p.Pflags&f == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpOpenPacket) respond(svr *Server) responsePacket {
|
||||||
|
log.Println("pkt: open: ", p.Path)
|
||||||
|
var osFlags int
|
||||||
|
if p.hasPflags(sshFxfRead, sshFxfWrite) {
|
||||||
|
osFlags |= os.O_RDWR
|
||||||
|
} else if p.hasPflags(sshFxfWrite) {
|
||||||
|
osFlags |= os.O_WRONLY
|
||||||
|
} else if p.hasPflags(sshFxfRead) {
|
||||||
|
osFlags |= os.O_RDONLY
|
||||||
|
} else {
|
||||||
|
// how are they opening?
|
||||||
|
return statusFromError(p.ID, syscall.EINVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't use O_APPEND flag as it conflicts with WriteAt.
|
||||||
|
// The sshFxfAppend flag is a no-op here as the client sends the offsets.
|
||||||
|
// @FIXME these flags are currently ignored
|
||||||
|
if p.hasPflags(sshFxfCreat) {
|
||||||
|
osFlags |= os.O_CREATE
|
||||||
|
}
|
||||||
|
if p.hasPflags(sshFxfTrunc) {
|
||||||
|
osFlags |= os.O_TRUNC
|
||||||
|
}
|
||||||
|
if p.hasPflags(sshFxfExcl) {
|
||||||
|
osFlags |= os.O_EXCL
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := svr.fs.OpenFile2(svr.ctx, p.Path, osFlags, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := svr.nextHandle(f)
|
||||||
|
return &sshFxpHandlePacket{ID: p.ID, Handle: handle}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpReaddirPacket) respond(svr *Server) responsePacket {
|
||||||
|
log.Println("pkt: readdir: ", p.Handle)
|
||||||
|
f, ok := svr.getHandle(p.Handle)
|
||||||
|
if !ok {
|
||||||
|
return statusFromError(p.ID, EBADF)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirents, err := f.Readdir(128)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idLookup := osIDLookup{}
|
||||||
|
|
||||||
|
ret := &sshFxpNamePacket{ID: p.ID}
|
||||||
|
for _, dirent := range dirents {
|
||||||
|
ret.NameAttrs = append(ret.NameAttrs, &sshFxpNameAttr{
|
||||||
|
Name: dirent.Name(),
|
||||||
|
LongName: runLs(idLookup, dirent),
|
||||||
|
Attrs: []interface{}{dirent},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpSetstatPacket) respond(svr *Server) responsePacket {
|
||||||
|
log.Println("pkt: setstat: ", p.Path)
|
||||||
|
// additional unmarshalling is required for each possibility here
|
||||||
|
b := p.Attrs.([]byte)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
p.Path = toLocalPath(p.Path)
|
||||||
|
|
||||||
|
debug("setstat name \"%s\"", p.Path)
|
||||||
|
if (p.Flags & sshFileXferAttrSize) != 0 {
|
||||||
|
var size uint64
|
||||||
|
if size, b, err = unmarshalUint64Safe(b); err == nil {
|
||||||
|
err = os.Truncate(p.Path, int64(size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrPermissions) != 0 {
|
||||||
|
var mode uint32
|
||||||
|
if mode, b, err = unmarshalUint32Safe(b); err == nil {
|
||||||
|
err = os.Chmod(p.Path, os.FileMode(mode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrACmodTime) != 0 {
|
||||||
|
var atime uint32
|
||||||
|
var mtime uint32
|
||||||
|
if atime, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else if mtime, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else {
|
||||||
|
atimeT := time.Unix(int64(atime), 0)
|
||||||
|
mtimeT := time.Unix(int64(mtime), 0)
|
||||||
|
err = os.Chtimes(p.Path, atimeT, mtimeT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrUIDGID) != 0 {
|
||||||
|
var uid uint32
|
||||||
|
var gid uint32
|
||||||
|
if uid, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else if gid, _, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else {
|
||||||
|
err = os.Chown(p.Path, int(uid), int(gid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *sshFxpFsetstatPacket) respond(svr *Server) responsePacket {
|
||||||
|
log.Println("pkt: fsetstat: ", p.Handle)
|
||||||
|
f, ok := svr.getHandle(p.Handle)
|
||||||
|
if !ok {
|
||||||
|
return statusFromError(p.ID, EBADF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional unmarshalling is required for each possibility here
|
||||||
|
//b := p.Attrs.([]byte)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
debug("fsetstat name \"%s\"", f.Path.Path)
|
||||||
|
if (p.Flags & sshFileXferAttrSize) != 0 {
|
||||||
|
/*var size uint64
|
||||||
|
if size, b, err = unmarshalUint64Safe(b); err == nil {
|
||||||
|
err = f.Truncate(int64(size))
|
||||||
|
}*/
|
||||||
|
log.Println("WARN: changing size of the file is not supported")
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrPermissions) != 0 {
|
||||||
|
/*var mode uint32
|
||||||
|
if mode, b, err = unmarshalUint32Safe(b); err == nil {
|
||||||
|
err = f.Chmod(os.FileMode(mode))
|
||||||
|
}*/
|
||||||
|
log.Println("WARN: chmod not supported")
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrACmodTime) != 0 {
|
||||||
|
/*var atime uint32
|
||||||
|
var mtime uint32
|
||||||
|
if atime, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else if mtime, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else {
|
||||||
|
atimeT := time.Unix(int64(atime), 0)
|
||||||
|
mtimeT := time.Unix(int64(mtime), 0)
|
||||||
|
err = os.Chtimes(f.Name(), atimeT, mtimeT)
|
||||||
|
}*/
|
||||||
|
log.Println("WARN: chtimes not supported")
|
||||||
|
}
|
||||||
|
if (p.Flags & sshFileXferAttrUIDGID) != 0 {
|
||||||
|
/*var uid uint32
|
||||||
|
var gid uint32
|
||||||
|
if uid, b, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else if gid, _, err = unmarshalUint32Safe(b); err != nil {
|
||||||
|
} else {
|
||||||
|
err = f.Chown(int(uid), int(gid))
|
||||||
|
}*/
|
||||||
|
log.Println("WARN: chown not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusFromError(id uint32, err error) *sshFxpStatusPacket {
|
||||||
|
ret := &sshFxpStatusPacket{
|
||||||
|
ID: id,
|
||||||
|
StatusError: StatusError{
|
||||||
|
// sshFXOk = 0
|
||||||
|
// sshFXEOF = 1
|
||||||
|
// sshFXNoSuchFile = 2 ENOENT
|
||||||
|
// sshFXPermissionDenied = 3
|
||||||
|
// sshFXFailure = 4
|
||||||
|
// sshFXBadMessage = 5
|
||||||
|
// sshFXNoConnection = 6
|
||||||
|
// sshFXConnectionLost = 7
|
||||||
|
// sshFXOPUnsupported = 8
|
||||||
|
Code: sshFxOk,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("statusFromError: error is %T %#v", err, err)
|
||||||
|
ret.StatusError.Code = sshFxFailure
|
||||||
|
ret.StatusError.msg = err.Error()
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
ret.StatusError.Code = sshFxNoSuchFile
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
if code, ok := translateSyscallError(err); ok {
|
||||||
|
ret.StatusError.Code = code
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := err.(type) {
|
||||||
|
case fxerr:
|
||||||
|
ret.StatusError.Code = uint32(e)
|
||||||
|
default:
|
||||||
|
if e == io.EOF {
|
||||||
|
ret.StatusError.Code = sshFxEOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
21
sftp/server_statvfs_darwin.go
Normal file
21
sftp/server_statvfs_darwin.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func statvfsFromStatfst(stat *syscall.Statfs_t) (*StatVFS, error) {
|
||||||
|
return &StatVFS{
|
||||||
|
Bsize: uint64(stat.Bsize),
|
||||||
|
Frsize: uint64(stat.Bsize), // fragment size is a linux thing; use block size here
|
||||||
|
Blocks: stat.Blocks,
|
||||||
|
Bfree: stat.Bfree,
|
||||||
|
Bavail: stat.Bavail,
|
||||||
|
Files: stat.Files,
|
||||||
|
Ffree: stat.Ffree,
|
||||||
|
Favail: stat.Ffree, // not sure how to calculate Favail
|
||||||
|
Fsid: uint64(uint64(stat.Fsid.Val[1])<<32 | uint64(stat.Fsid.Val[0])), // endianness?
|
||||||
|
Flag: uint64(stat.Flags), // assuming POSIX?
|
||||||
|
Namemax: 1024, // man 2 statfs shows: #define MAXPATHLEN 1024
|
||||||
|
}, nil
|
||||||
|
}
|
29
sftp/server_statvfs_impl.go
Normal file
29
sftp/server_statvfs_impl.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// +build darwin linux
|
||||||
|
|
||||||
|
// fill in statvfs structure with OS specific values
|
||||||
|
// Statfs_t is different per-kernel, and only exists on some unixes (not Solaris for instance)
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
|
||||||
|
retPkt, err := getStatVFSForPath(p.Path)
|
||||||
|
if err != nil {
|
||||||
|
return statusFromError(p.ID, err)
|
||||||
|
}
|
||||||
|
retPkt.ID = p.ID
|
||||||
|
|
||||||
|
return retPkt
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatVFSForPath(name string) (*StatVFS, error) {
|
||||||
|
var stat syscall.Statfs_t
|
||||||
|
if err := syscall.Statfs(name, &stat); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return statvfsFromStatfst(&stat)
|
||||||
|
}
|
22
sftp/server_statvfs_linux.go
Normal file
22
sftp/server_statvfs_linux.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func statvfsFromStatfst(stat *syscall.Statfs_t) (*StatVFS, error) {
|
||||||
|
return &StatVFS{
|
||||||
|
Bsize: uint64(stat.Bsize),
|
||||||
|
Frsize: uint64(stat.Frsize),
|
||||||
|
Blocks: stat.Blocks,
|
||||||
|
Bfree: stat.Bfree,
|
||||||
|
Bavail: stat.Bavail,
|
||||||
|
Files: stat.Files,
|
||||||
|
Ffree: stat.Ffree,
|
||||||
|
Favail: stat.Ffree, // not sure how to calculate Favail
|
||||||
|
Flag: uint64(stat.Flags), // assuming POSIX?
|
||||||
|
Namemax: uint64(stat.Namelen),
|
||||||
|
}, nil
|
||||||
|
}
|
13
sftp/server_statvfs_plan9.go
Normal file
13
sftp/server_statvfs_plan9.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
|
||||||
|
return statusFromError(p.ID, syscall.EPLAN9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatVFSForPath(name string) (*StatVFS, error) {
|
||||||
|
return nil, syscall.EPLAN9
|
||||||
|
}
|
15
sftp/server_statvfs_stubs.go
Normal file
15
sftp/server_statvfs_stubs.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// +build !darwin,!linux,!plan9
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
|
||||||
|
return statusFromError(p.ID, syscall.ENOTSUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatVFSForPath(name string) (*StatVFS, error) {
|
||||||
|
return nil, syscall.ENOTSUP
|
||||||
|
}
|
258
sftp/sftp.go
Normal file
258
sftp/sftp.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
// Package sftp implements the SSH File Transfer Protocol as described in
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sshFxpInit = 1
|
||||||
|
sshFxpVersion = 2
|
||||||
|
sshFxpOpen = 3
|
||||||
|
sshFxpClose = 4
|
||||||
|
sshFxpRead = 5
|
||||||
|
sshFxpWrite = 6
|
||||||
|
sshFxpLstat = 7
|
||||||
|
sshFxpFstat = 8
|
||||||
|
sshFxpSetstat = 9
|
||||||
|
sshFxpFsetstat = 10
|
||||||
|
sshFxpOpendir = 11
|
||||||
|
sshFxpReaddir = 12
|
||||||
|
sshFxpRemove = 13
|
||||||
|
sshFxpMkdir = 14
|
||||||
|
sshFxpRmdir = 15
|
||||||
|
sshFxpRealpath = 16
|
||||||
|
sshFxpStat = 17
|
||||||
|
sshFxpRename = 18
|
||||||
|
sshFxpReadlink = 19
|
||||||
|
sshFxpSymlink = 20
|
||||||
|
sshFxpStatus = 101
|
||||||
|
sshFxpHandle = 102
|
||||||
|
sshFxpData = 103
|
||||||
|
sshFxpName = 104
|
||||||
|
sshFxpAttrs = 105
|
||||||
|
sshFxpExtended = 200
|
||||||
|
sshFxpExtendedReply = 201
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sshFxOk = 0
|
||||||
|
sshFxEOF = 1
|
||||||
|
sshFxNoSuchFile = 2
|
||||||
|
sshFxPermissionDenied = 3
|
||||||
|
sshFxFailure = 4
|
||||||
|
sshFxBadMessage = 5
|
||||||
|
sshFxNoConnection = 6
|
||||||
|
sshFxConnectionLost = 7
|
||||||
|
sshFxOPUnsupported = 8
|
||||||
|
|
||||||
|
// see draft-ietf-secsh-filexfer-13
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1
|
||||||
|
sshFxInvalidHandle = 9
|
||||||
|
sshFxNoSuchPath = 10
|
||||||
|
sshFxFileAlreadyExists = 11
|
||||||
|
sshFxWriteProtect = 12
|
||||||
|
sshFxNoMedia = 13
|
||||||
|
sshFxNoSpaceOnFilesystem = 14
|
||||||
|
sshFxQuotaExceeded = 15
|
||||||
|
sshFxUnknownPrincipal = 16
|
||||||
|
sshFxLockConflict = 17
|
||||||
|
sshFxDirNotEmpty = 18
|
||||||
|
sshFxNotADirectory = 19
|
||||||
|
sshFxInvalidFilename = 20
|
||||||
|
sshFxLinkLoop = 21
|
||||||
|
sshFxCannotDelete = 22
|
||||||
|
sshFxInvalidParameter = 23
|
||||||
|
sshFxFileIsADirectory = 24
|
||||||
|
sshFxByteRangeLockConflict = 25
|
||||||
|
sshFxByteRangeLockRefused = 26
|
||||||
|
sshFxDeletePending = 27
|
||||||
|
sshFxFileCorrupt = 28
|
||||||
|
sshFxOwnerInvalid = 29
|
||||||
|
sshFxGroupInvalid = 30
|
||||||
|
sshFxNoMatchingByteRangeLock = 31
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sshFxfRead = 0x00000001
|
||||||
|
sshFxfWrite = 0x00000002
|
||||||
|
sshFxfAppend = 0x00000004
|
||||||
|
sshFxfCreat = 0x00000008
|
||||||
|
sshFxfTrunc = 0x00000010
|
||||||
|
sshFxfExcl = 0x00000020
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// supportedSFTPExtensions defines the supported extensions
|
||||||
|
supportedSFTPExtensions = []sshExtensionPair{
|
||||||
|
{"hardlink@openssh.com", "1"},
|
||||||
|
{"posix-rename@openssh.com", "1"},
|
||||||
|
{"statvfs@openssh.com", "2"},
|
||||||
|
}
|
||||||
|
sftpExtensions = supportedSFTPExtensions
|
||||||
|
)
|
||||||
|
|
||||||
|
type fxp uint8
|
||||||
|
|
||||||
|
func (f fxp) String() string {
|
||||||
|
switch f {
|
||||||
|
case sshFxpInit:
|
||||||
|
return "SSH_FXP_INIT"
|
||||||
|
case sshFxpVersion:
|
||||||
|
return "SSH_FXP_VERSION"
|
||||||
|
case sshFxpOpen:
|
||||||
|
return "SSH_FXP_OPEN"
|
||||||
|
case sshFxpClose:
|
||||||
|
return "SSH_FXP_CLOSE"
|
||||||
|
case sshFxpRead:
|
||||||
|
return "SSH_FXP_READ"
|
||||||
|
case sshFxpWrite:
|
||||||
|
return "SSH_FXP_WRITE"
|
||||||
|
case sshFxpLstat:
|
||||||
|
return "SSH_FXP_LSTAT"
|
||||||
|
case sshFxpFstat:
|
||||||
|
return "SSH_FXP_FSTAT"
|
||||||
|
case sshFxpSetstat:
|
||||||
|
return "SSH_FXP_SETSTAT"
|
||||||
|
case sshFxpFsetstat:
|
||||||
|
return "SSH_FXP_FSETSTAT"
|
||||||
|
case sshFxpOpendir:
|
||||||
|
return "SSH_FXP_OPENDIR"
|
||||||
|
case sshFxpReaddir:
|
||||||
|
return "SSH_FXP_READDIR"
|
||||||
|
case sshFxpRemove:
|
||||||
|
return "SSH_FXP_REMOVE"
|
||||||
|
case sshFxpMkdir:
|
||||||
|
return "SSH_FXP_MKDIR"
|
||||||
|
case sshFxpRmdir:
|
||||||
|
return "SSH_FXP_RMDIR"
|
||||||
|
case sshFxpRealpath:
|
||||||
|
return "SSH_FXP_REALPATH"
|
||||||
|
case sshFxpStat:
|
||||||
|
return "SSH_FXP_STAT"
|
||||||
|
case sshFxpRename:
|
||||||
|
return "SSH_FXP_RENAME"
|
||||||
|
case sshFxpReadlink:
|
||||||
|
return "SSH_FXP_READLINK"
|
||||||
|
case sshFxpSymlink:
|
||||||
|
return "SSH_FXP_SYMLINK"
|
||||||
|
case sshFxpStatus:
|
||||||
|
return "SSH_FXP_STATUS"
|
||||||
|
case sshFxpHandle:
|
||||||
|
return "SSH_FXP_HANDLE"
|
||||||
|
case sshFxpData:
|
||||||
|
return "SSH_FXP_DATA"
|
||||||
|
case sshFxpName:
|
||||||
|
return "SSH_FXP_NAME"
|
||||||
|
case sshFxpAttrs:
|
||||||
|
return "SSH_FXP_ATTRS"
|
||||||
|
case sshFxpExtended:
|
||||||
|
return "SSH_FXP_EXTENDED"
|
||||||
|
case sshFxpExtendedReply:
|
||||||
|
return "SSH_FXP_EXTENDED_REPLY"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fx uint8
|
||||||
|
|
||||||
|
func (f fx) String() string {
|
||||||
|
switch f {
|
||||||
|
case sshFxOk:
|
||||||
|
return "SSH_FX_OK"
|
||||||
|
case sshFxEOF:
|
||||||
|
return "SSH_FX_EOF"
|
||||||
|
case sshFxNoSuchFile:
|
||||||
|
return "SSH_FX_NO_SUCH_FILE"
|
||||||
|
case sshFxPermissionDenied:
|
||||||
|
return "SSH_FX_PERMISSION_DENIED"
|
||||||
|
case sshFxFailure:
|
||||||
|
return "SSH_FX_FAILURE"
|
||||||
|
case sshFxBadMessage:
|
||||||
|
return "SSH_FX_BAD_MESSAGE"
|
||||||
|
case sshFxNoConnection:
|
||||||
|
return "SSH_FX_NO_CONNECTION"
|
||||||
|
case sshFxConnectionLost:
|
||||||
|
return "SSH_FX_CONNECTION_LOST"
|
||||||
|
case sshFxOPUnsupported:
|
||||||
|
return "SSH_FX_OP_UNSUPPORTED"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexpectedPacketErr struct {
|
||||||
|
want, got uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *unexpectedPacketErr) Error() string {
|
||||||
|
return fmt.Sprintf("sftp: unexpected packet: want %v, got %v", fxp(u.want), fxp(u.got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unimplementedPacketErr(u uint8) error {
|
||||||
|
return fmt.Errorf("sftp: unimplemented packet type: got %v", fxp(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexpectedIDErr struct{ want, got uint32 }
|
||||||
|
|
||||||
|
func (u *unexpectedIDErr) Error() string {
|
||||||
|
return fmt.Sprintf("sftp: unexpected id: want %d, got %d", u.want, u.got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unimplementedSeekWhence(whence int) error {
|
||||||
|
return fmt.Errorf("sftp: unimplemented seek whence %d", whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unexpectedCount(want, got uint32) error {
|
||||||
|
return fmt.Errorf("sftp: unexpected count: want %d, got %d", want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexpectedVersionErr struct{ want, got uint32 }
|
||||||
|
|
||||||
|
func (u *unexpectedVersionErr) Error() string {
|
||||||
|
return fmt.Sprintf("sftp: unexpected server version: want %v, got %v", u.want, u.got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A StatusError is returned when an SFTP operation fails, and provides
|
||||||
|
// additional information about the failure.
|
||||||
|
type StatusError struct {
|
||||||
|
Code uint32
|
||||||
|
msg, lang string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatusError) Error() string {
|
||||||
|
return fmt.Sprintf("sftp: %q (%v)", s.msg, fx(s.Code))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FxCode returns the error code typed to match against the exported codes
|
||||||
|
func (s *StatusError) FxCode() fxerr {
|
||||||
|
return fxerr(s.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSupportedExtensionByName(extensionName string) (sshExtensionPair, error) {
|
||||||
|
for _, supportedExtension := range supportedSFTPExtensions {
|
||||||
|
if supportedExtension.Name == extensionName {
|
||||||
|
return supportedExtension, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sshExtensionPair{}, fmt.Errorf("unsupported extension: %s", extensionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSFTPExtensions allows to customize the supported server extensions.
|
||||||
|
// See the variable supportedSFTPExtensions for supported extensions.
|
||||||
|
// This method accepts a slice of sshExtensionPair names for example 'hardlink@openssh.com'.
|
||||||
|
// If an invalid extension is given an error will be returned and nothing will be changed
|
||||||
|
func SetSFTPExtensions(extensions ...string) error {
|
||||||
|
tempExtensions := []sshExtensionPair{}
|
||||||
|
for _, extension := range extensions {
|
||||||
|
sftpExtension, err := getSupportedExtensionByName(extension)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tempExtensions = append(tempExtensions, sftpExtension)
|
||||||
|
}
|
||||||
|
sftpExtensions = tempExtensions
|
||||||
|
return nil
|
||||||
|
}
|
103
sftp/stat_plan9.go
Normal file
103
sftp/stat_plan9.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var EBADF = syscall.NewError("fd out of range or not open")
|
||||||
|
|
||||||
|
func wrapPathError(filepath string, err error) error {
|
||||||
|
if errno, ok := err.(syscall.ErrorString); ok {
|
||||||
|
return &os.PathError{Path: filepath, Err: errno}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateErrno translates a syscall error number to a SFTP error code.
|
||||||
|
func translateErrno(errno syscall.ErrorString) uint32 {
|
||||||
|
switch errno {
|
||||||
|
case "":
|
||||||
|
return sshFxOk
|
||||||
|
case syscall.ENOENT:
|
||||||
|
return sshFxNoSuchFile
|
||||||
|
case syscall.EPERM:
|
||||||
|
return sshFxPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshFxFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateSyscallError(err error) (uint32, bool) {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case syscall.ErrorString:
|
||||||
|
return translateErrno(e), true
|
||||||
|
case *os.PathError:
|
||||||
|
debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
|
||||||
|
if errno, ok := e.Err.(syscall.ErrorString); ok {
|
||||||
|
return translateErrno(errno), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRegular returns true if the mode describes a regular file.
|
||||||
|
func isRegular(mode uint32) bool {
|
||||||
|
return mode&S_IFMT == syscall.S_IFREG
|
||||||
|
}
|
||||||
|
|
||||||
|
// toFileMode converts sftp filemode bits to the os.FileMode specification
|
||||||
|
func toFileMode(mode uint32) os.FileMode {
|
||||||
|
var fm = os.FileMode(mode & 0777)
|
||||||
|
|
||||||
|
switch mode & S_IFMT {
|
||||||
|
case syscall.S_IFBLK:
|
||||||
|
fm |= os.ModeDevice
|
||||||
|
case syscall.S_IFCHR:
|
||||||
|
fm |= os.ModeDevice | os.ModeCharDevice
|
||||||
|
case syscall.S_IFDIR:
|
||||||
|
fm |= os.ModeDir
|
||||||
|
case syscall.S_IFIFO:
|
||||||
|
fm |= os.ModeNamedPipe
|
||||||
|
case syscall.S_IFLNK:
|
||||||
|
fm |= os.ModeSymlink
|
||||||
|
case syscall.S_IFREG:
|
||||||
|
// nothing to do
|
||||||
|
case syscall.S_IFSOCK:
|
||||||
|
fm |= os.ModeSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
return fm
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromFileMode converts from the os.FileMode specification to sftp filemode bits
|
||||||
|
func fromFileMode(mode os.FileMode) uint32 {
|
||||||
|
ret := uint32(mode & os.ModePerm)
|
||||||
|
|
||||||
|
switch mode & os.ModeType {
|
||||||
|
case os.ModeDevice | os.ModeCharDevice:
|
||||||
|
ret |= syscall.S_IFCHR
|
||||||
|
case os.ModeDevice:
|
||||||
|
ret |= syscall.S_IFBLK
|
||||||
|
case os.ModeDir:
|
||||||
|
ret |= syscall.S_IFDIR
|
||||||
|
case os.ModeNamedPipe:
|
||||||
|
ret |= syscall.S_IFIFO
|
||||||
|
case os.ModeSymlink:
|
||||||
|
ret |= syscall.S_IFLNK
|
||||||
|
case 0:
|
||||||
|
ret |= syscall.S_IFREG
|
||||||
|
case os.ModeSocket:
|
||||||
|
ret |= syscall.S_IFSOCK
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan 9 doesn't have setuid, setgid or sticky, but a Plan 9 client should
|
||||||
|
// be able to send these bits to a POSIX server.
|
||||||
|
const (
|
||||||
|
s_ISUID = 04000
|
||||||
|
s_ISGID = 02000
|
||||||
|
s_ISVTX = 01000
|
||||||
|
)
|
124
sftp/stat_posix.go
Normal file
124
sftp/stat_posix.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
//go:build !plan9
|
||||||
|
// +build !plan9
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const EBADF = syscall.EBADF
|
||||||
|
|
||||||
|
func wrapPathError(filepath string, err error) error {
|
||||||
|
if errno, ok := err.(syscall.Errno); ok {
|
||||||
|
return &os.PathError{Path: filepath, Err: errno}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateErrno translates a syscall error number to a SFTP error code.
|
||||||
|
func translateErrno(errno syscall.Errno) uint32 {
|
||||||
|
switch errno {
|
||||||
|
case 0:
|
||||||
|
return sshFxOk
|
||||||
|
case syscall.ENOENT:
|
||||||
|
return sshFxNoSuchFile
|
||||||
|
case syscall.EACCES, syscall.EPERM:
|
||||||
|
return sshFxPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshFxFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateSyscallError(err error) (uint32, bool) {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case syscall.Errno:
|
||||||
|
return translateErrno(e), true
|
||||||
|
case *os.PathError:
|
||||||
|
debug("statusFromError,pathError: error is %T %#v", e.Err, e.Err)
|
||||||
|
if errno, ok := e.Err.(syscall.Errno); ok {
|
||||||
|
return translateErrno(errno), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRegular returns true if the mode describes a regular file.
|
||||||
|
func isRegular(mode uint32) bool {
|
||||||
|
return mode&S_IFMT == syscall.S_IFREG
|
||||||
|
}
|
||||||
|
|
||||||
|
// toFileMode converts sftp filemode bits to the os.FileMode specification
|
||||||
|
func toFileMode(mode uint32) os.FileMode {
|
||||||
|
var fm = os.FileMode(mode & 0777)
|
||||||
|
|
||||||
|
switch mode & S_IFMT {
|
||||||
|
case syscall.S_IFBLK:
|
||||||
|
fm |= os.ModeDevice
|
||||||
|
case syscall.S_IFCHR:
|
||||||
|
fm |= os.ModeDevice | os.ModeCharDevice
|
||||||
|
case syscall.S_IFDIR:
|
||||||
|
fm |= os.ModeDir
|
||||||
|
case syscall.S_IFIFO:
|
||||||
|
fm |= os.ModeNamedPipe
|
||||||
|
case syscall.S_IFLNK:
|
||||||
|
fm |= os.ModeSymlink
|
||||||
|
case syscall.S_IFREG:
|
||||||
|
// nothing to do
|
||||||
|
case syscall.S_IFSOCK:
|
||||||
|
fm |= os.ModeSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode&syscall.S_ISUID != 0 {
|
||||||
|
fm |= os.ModeSetuid
|
||||||
|
}
|
||||||
|
if mode&syscall.S_ISGID != 0 {
|
||||||
|
fm |= os.ModeSetgid
|
||||||
|
}
|
||||||
|
if mode&syscall.S_ISVTX != 0 {
|
||||||
|
fm |= os.ModeSticky
|
||||||
|
}
|
||||||
|
|
||||||
|
return fm
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromFileMode converts from the os.FileMode specification to sftp filemode bits
|
||||||
|
func fromFileMode(mode os.FileMode) uint32 {
|
||||||
|
ret := uint32(mode & os.ModePerm)
|
||||||
|
|
||||||
|
switch mode & os.ModeType {
|
||||||
|
case os.ModeDevice | os.ModeCharDevice:
|
||||||
|
ret |= syscall.S_IFCHR
|
||||||
|
case os.ModeDevice:
|
||||||
|
ret |= syscall.S_IFBLK
|
||||||
|
case os.ModeDir:
|
||||||
|
ret |= syscall.S_IFDIR
|
||||||
|
case os.ModeNamedPipe:
|
||||||
|
ret |= syscall.S_IFIFO
|
||||||
|
case os.ModeSymlink:
|
||||||
|
ret |= syscall.S_IFLNK
|
||||||
|
case 0:
|
||||||
|
ret |= syscall.S_IFREG
|
||||||
|
case os.ModeSocket:
|
||||||
|
ret |= syscall.S_IFSOCK
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode&os.ModeSetuid != 0 {
|
||||||
|
ret |= syscall.S_ISUID
|
||||||
|
}
|
||||||
|
if mode&os.ModeSetgid != 0 {
|
||||||
|
ret |= syscall.S_ISGID
|
||||||
|
}
|
||||||
|
if mode&os.ModeSticky != 0 {
|
||||||
|
ret |= syscall.S_ISVTX
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
s_ISUID = syscall.S_ISUID
|
||||||
|
s_ISGID = syscall.S_ISGID
|
||||||
|
s_ISVTX = syscall.S_ISVTX
|
||||||
|
)
|
9
sftp/syscall_fixed.go
Normal file
9
sftp/syscall_fixed.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// +build plan9 windows js,wasm
|
||||||
|
|
||||||
|
// Go defines S_IFMT on windows, plan9 and js/wasm as 0x1f000 instead of
|
||||||
|
// 0xf000. None of the the other S_IFxyz values include the "1" (in 0x1f000)
|
||||||
|
// which prevents them from matching the bitmask.
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
const S_IFMT = 0xf000
|
8
sftp/syscall_good.go
Normal file
8
sftp/syscall_good.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// +build !plan9,!windows
|
||||||
|
// +build !js !wasm
|
||||||
|
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const S_IFMT = syscall.S_IFMT
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/bagage/s3"
|
||||||
"golang.org/x/net/webdav"
|
"golang.org/x/net/webdav"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -15,7 +16,7 @@ func (wd WebDav) WithMC(mc *minio.Client) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
(&webdav.Handler{
|
(&webdav.Handler{
|
||||||
Prefix: wd.WithConfig.DavPath,
|
Prefix: wd.WithConfig.DavPath,
|
||||||
FileSystem: NewS3FS(mc),
|
FileSystem: s3.NewS3FS(mc),
|
||||||
LockSystem: webdav.NewMemLS(),
|
LockSystem: webdav.NewMemLS(),
|
||||||
Logger: func(r *http.Request, err error) {
|
Logger: func(r *http.Request, err error) {
|
||||||
log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
||||||
|
|
Loading…
Reference in a new issue