add support for different bandwidth limits based on client IP
This commit is contained in:
parent
c153330ab8
commit
0bb141960f
18 changed files with 575 additions and 56 deletions
|
@ -31,7 +31,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
|
|||
- [Data At Rest Encryption](./docs/dare.md).
|
||||
- Dynamic user modification before login via external programs/HTTP API.
|
||||
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
|
||||
- Bandwidth throttling, with distinct settings for upload and download.
|
||||
- Bandwidth throttling, with distinct settings for upload and download and overrides based on the client IP address.
|
||||
- Per-protocol [rate limiting](./docs/rate-limiting.md) is supported and can be optionally connected to the built-in defender to automatically block hosts that repeatedly exceed the configured limit.
|
||||
- Per user maximum concurrent sessions.
|
||||
- Per user and global IP filters: login can be restricted to specific ranges of IP addresses or to a specific IP address.
|
||||
|
|
|
@ -47,6 +47,7 @@ func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprov
|
|||
if util.IsStringInSlice(protocol, supportedProtocols) {
|
||||
connID = fmt.Sprintf("%v_%v", protocol, id)
|
||||
}
|
||||
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
|
||||
return &BaseConnection{
|
||||
ID: connID,
|
||||
User: user,
|
||||
|
|
|
@ -1619,22 +1619,48 @@ func checkEmptyFiltersStruct(user *User) {
|
|||
}
|
||||
}
|
||||
|
||||
func validateFilters(user *User) error {
|
||||
checkEmptyFiltersStruct(user)
|
||||
func validateIPFilters(user *User) error {
|
||||
user.Filters.DeniedIP = util.RemoveDuplicates(user.Filters.DeniedIP)
|
||||
for _, IPMask := range user.Filters.DeniedIP {
|
||||
_, _, err := net.ParseCIDR(IPMask)
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err))
|
||||
return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v: %v", IPMask, err))
|
||||
}
|
||||
}
|
||||
user.Filters.AllowedIP = util.RemoveDuplicates(user.Filters.AllowedIP)
|
||||
for _, IPMask := range user.Filters.AllowedIP {
|
||||
_, _, err := net.ParseCIDR(IPMask)
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err))
|
||||
return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v: %v", IPMask, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBandwidthLimitFilters(user *User) error {
|
||||
for idx, bandwidthLimit := range user.Filters.BandwidthLimits {
|
||||
user.Filters.BandwidthLimits[idx].Sources = util.RemoveDuplicates(bandwidthLimit.Sources)
|
||||
if err := bandwidthLimit.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if bandwidthLimit.DownloadBandwidth < 0 {
|
||||
user.Filters.BandwidthLimits[idx].DownloadBandwidth = 0
|
||||
}
|
||||
if bandwidthLimit.UploadBandwidth < 0 {
|
||||
user.Filters.BandwidthLimits[idx].UploadBandwidth = 0
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFilters(user *User) error {
|
||||
checkEmptyFiltersStruct(user)
|
||||
if err := validateIPFilters(user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateBandwidthLimitFilters(user); err != nil {
|
||||
return err
|
||||
}
|
||||
user.Filters.DeniedLoginMethods = util.RemoveDuplicates(user.Filters.DeniedLoginMethods)
|
||||
if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
|
||||
return util.NewValidationError("invalid denied_login_methods")
|
||||
|
@ -1664,6 +1690,7 @@ func validateFilters(user *User) error {
|
|||
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
|
||||
}
|
||||
}
|
||||
|
||||
return validateFiltersPatternExtensions(user)
|
||||
}
|
||||
|
||||
|
@ -1728,6 +1755,12 @@ func validateBaseParams(user *User) error {
|
|||
if !filepath.IsAbs(user.HomeDir) {
|
||||
return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir))
|
||||
}
|
||||
if user.DownloadBandwidth < 0 {
|
||||
user.DownloadBandwidth = 0
|
||||
}
|
||||
if user.UploadBandwidth < 0 {
|
||||
user.UploadBandwidth = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -883,6 +883,28 @@ func (u *User) GetSignature() string {
|
|||
return base64.StdEncoding.EncodeToString(signature[:])
|
||||
}
|
||||
|
||||
// GetBandwidthForIP returns the upload and download bandwidth for the specified IP
|
||||
func (u *User) GetBandwidthForIP(clientIP, connectionID string) (int64, int64) {
|
||||
if len(u.Filters.BandwidthLimits) > 0 {
|
||||
ip := net.ParseIP(clientIP)
|
||||
if ip != nil {
|
||||
for _, bwLimit := range u.Filters.BandwidthLimits {
|
||||
for _, source := range bwLimit.Sources {
|
||||
_, ipNet, err := net.ParseCIDR(source)
|
||||
if err == nil {
|
||||
if ipNet.Contains(ip) {
|
||||
logger.Debug(logSender, connectionID, "override bandwidth limit for ip %#v, upload limit: %v KB/s, download limit: %v KB/s",
|
||||
clientIP, bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth)
|
||||
return bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return u.UploadBandwidth, u.DownloadBandwidth
|
||||
}
|
||||
|
||||
// IsLoginFromAddrAllowed returns true if the login is allowed from the specified remoteAddr.
|
||||
// If AllowedIP is defined only the specified IP/Mask can login.
|
||||
// If DeniedIP is defined the specified IP/Mask cannot login.
|
||||
|
@ -1150,7 +1172,7 @@ func (u *User) getACopy() User {
|
|||
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
|
||||
filters.WebClient = make([]string, len(u.Filters.WebClient))
|
||||
copy(filters.WebClient, u.Filters.WebClient)
|
||||
filters.RecoveryCodes = make([]sdk.RecoveryCode, 0)
|
||||
filters.RecoveryCodes = make([]sdk.RecoveryCode, 0, len(u.Filters.RecoveryCodes))
|
||||
for _, code := range u.Filters.RecoveryCodes {
|
||||
if code.Secret == nil {
|
||||
code.Secret = kms.NewEmptySecret()
|
||||
|
@ -1160,6 +1182,17 @@ func (u *User) getACopy() User {
|
|||
Used: code.Used,
|
||||
})
|
||||
}
|
||||
filters.BandwidthLimits = make([]sdk.BandwidthLimit, 0, len(u.Filters.BandwidthLimits))
|
||||
for _, limit := range u.Filters.BandwidthLimits {
|
||||
bwLimit := sdk.BandwidthLimit{
|
||||
UploadBandwidth: limit.UploadBandwidth,
|
||||
DownloadBandwidth: limit.DownloadBandwidth,
|
||||
Sources: make([]string, 0, len(limit.Sources)),
|
||||
}
|
||||
bwLimit.Sources = make([]string, len(limit.Sources))
|
||||
copy(bwLimit.Sources, limit.Sources)
|
||||
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
|
||||
}
|
||||
|
||||
return User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
|
|
14
go.mod
14
go.mod
|
@ -7,8 +7,8 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go v1.42.20
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.4
|
||||
github.com/aws/aws-sdk-go v1.42.22
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.5
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
github.com/fclairamb/ftpserverlib v0.16.0
|
||||
github.com/fclairamb/go-log v0.1.0
|
||||
|
@ -52,10 +52,10 @@ require (
|
|||
go.uber.org/automaxprocs v1.4.0
|
||||
gocloud.dev v0.24.0
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
google.golang.org/api v0.61.0
|
||||
google.golang.org/api v0.62.0
|
||||
google.golang.org/grpc v1.42.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
@ -129,7 +129,7 @@ require (
|
|||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
|
@ -140,5 +140,5 @@ replace (
|
|||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20211107071448-34ff70e85dfb
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20211203175531-87c7ca02d2a9
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20211203175337-bdbe03411e23
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20211210172952-3f0f9446f73f
|
||||
)
|
||||
|
|
26
go.sum
26
go.sum
|
@ -33,6 +33,7 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
|
|||
cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
|
||||
cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
|
@ -136,8 +137,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.42.20 h1:nQkkmTWK5N2Ao1iVzoOx1HTIxwbSWErxyZ1eiwLJWc4=
|
||||
github.com/aws/aws-sdk-go v1.42.20/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.42.22 h1:EwcM7/+Ytg6xK+jbeM2+f9OELHqPiEiEKetT/GgAr7I=
|
||||
github.com/aws/aws-sdk-go v1.42.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
|
@ -192,8 +193,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.4 h1:VuiBJKut2Imgrzl+TNk+U5+GxLOh3hnIFxU0EzjTCnI=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.4/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.5 h1:tfPdGHO5YpmrpN2ikJZYpaSGgU8WALwwjH3s+msiTQ0=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.5/go.mod h1:q4ZRgO6CQpwNyEvEwSxwNrOSVchsmzrBnAv3HuZ3Abc=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
|
@ -224,8 +225,8 @@ github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHP
|
|||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20211107071448-34ff70e85dfb h1:cT/w4XStm7m022JgVqmrXZLcZ4UjoUER1VW5/5gd6ec=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20211107071448-34ff70e85dfb/go.mod h1:fBiQ19WDhtvKArMu0Pifg71k+0xqRYn+F0d9AsjkZw8=
|
||||
github.com/drakkan/net v0.0.0-20211203175337-bdbe03411e23 h1:Ocx33vj+hqabIb/Adh03t4OWIB589lVpWvOODTADbBc=
|
||||
github.com/drakkan/net v0.0.0-20211203175337-bdbe03411e23/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
github.com/drakkan/net v0.0.0-20211210172952-3f0f9446f73f h1:8XuTk84FMb6DGc5MxmPkswjO5m4XFqGoZomO/Q8CUwQ=
|
||||
github.com/drakkan/net v0.0.0-20211210172952-3f0f9446f73f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639 h1:8tfGdb4kg/YCvAbIrsMazgoNtnqdOqQVDKW12uUCuuU=
|
||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
@ -990,8 +991,9 @@ golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1119,8 +1121,9 @@ google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv
|
|||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
|
||||
google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
|
||||
google.golang.org/api v0.61.0 h1:TXXKS1slM3b2bZNJwD5DV/Tp6/M2cLzLOLh9PjDhrw8=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc=
|
||||
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -1196,9 +1199,11 @@ google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEc
|
|||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf h1:PSEM+IQFb9xdsj2CGhfqUTfsZvF8DScCVP1QZb2IiTQ=
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
@ -1225,6 +1230,7 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
|
|||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
|
|
|
@ -502,6 +502,101 @@ func TestBasicUserHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserBandwidthLimit(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.UploadBandwidth = 128
|
||||
u.DownloadBandwidth = 96
|
||||
u.Filters.BandwidthLimits = []sdk.BandwidthLimit{
|
||||
{
|
||||
Sources: []string{"1"},
|
||||
},
|
||||
}
|
||||
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Contains(t, string(resp), "Validation error: could not parse bandwidth limit source")
|
||||
u.Filters.BandwidthLimits = []sdk.BandwidthLimit{
|
||||
{
|
||||
Sources: []string{"127.0.0.0/8", "::1/128"},
|
||||
UploadBandwidth: 256,
|
||||
},
|
||||
{
|
||||
Sources: []string{"10.0.0.0/8"},
|
||||
UploadBandwidth: 512,
|
||||
DownloadBandwidth: 256,
|
||||
},
|
||||
}
|
||||
user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Len(t, user.Filters.BandwidthLimits, 2)
|
||||
assert.Equal(t, u.Filters.BandwidthLimits, user.Filters.BandwidthLimits)
|
||||
|
||||
connID := xid.New().String()
|
||||
localAddr := "127.0.0.1"
|
||||
up, down := user.GetBandwidthForIP("127.0.1.1", connID)
|
||||
assert.Equal(t, int64(256), up)
|
||||
assert.Equal(t, int64(0), down)
|
||||
conn := common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "127.0.1.1", user)
|
||||
assert.Equal(t, int64(256), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(0), conn.User.DownloadBandwidth)
|
||||
up, down = user.GetBandwidthForIP("10.1.2.3", connID)
|
||||
assert.Equal(t, int64(512), up)
|
||||
assert.Equal(t, int64(256), down)
|
||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "10.2.1.4:1234", user)
|
||||
assert.Equal(t, int64(512), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(256), conn.User.DownloadBandwidth)
|
||||
up, down = user.GetBandwidthForIP("192.168.1.2", connID)
|
||||
assert.Equal(t, int64(128), up)
|
||||
assert.Equal(t, int64(96), down)
|
||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "172.16.0.1", user)
|
||||
assert.Equal(t, int64(128), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(96), conn.User.DownloadBandwidth)
|
||||
up, down = user.GetBandwidthForIP("invalid", connID)
|
||||
assert.Equal(t, int64(128), up)
|
||||
assert.Equal(t, int64(96), down)
|
||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "172.16.0", user)
|
||||
assert.Equal(t, int64(128), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(96), conn.User.DownloadBandwidth)
|
||||
|
||||
user.Filters.BandwidthLimits = []sdk.BandwidthLimit{
|
||||
{
|
||||
Sources: []string{"10.0.0.0/24"},
|
||||
UploadBandwidth: 256,
|
||||
DownloadBandwidth: 512,
|
||||
},
|
||||
}
|
||||
user, resp, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err, string(resp))
|
||||
if assert.Len(t, user.Filters.BandwidthLimits, 1) {
|
||||
bwLimit := user.Filters.BandwidthLimits[0]
|
||||
assert.Equal(t, []string{"10.0.0.0/24"}, bwLimit.Sources)
|
||||
assert.Equal(t, int64(256), bwLimit.UploadBandwidth)
|
||||
assert.Equal(t, int64(512), bwLimit.DownloadBandwidth)
|
||||
}
|
||||
up, down = user.GetBandwidthForIP("10.1.2.3", connID)
|
||||
assert.Equal(t, int64(128), up)
|
||||
assert.Equal(t, int64(96), down)
|
||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "172.16.0.2", user)
|
||||
assert.Equal(t, int64(128), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(96), conn.User.DownloadBandwidth)
|
||||
up, down = user.GetBandwidthForIP("10.0.0.26", connID)
|
||||
assert.Equal(t, int64(256), up)
|
||||
assert.Equal(t, int64(512), down)
|
||||
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "10.0.0.28", user)
|
||||
assert.Equal(t, int64(256), conn.User.UploadBandwidth)
|
||||
assert.Equal(t, int64(512), conn.User.DownloadBandwidth)
|
||||
|
||||
// this works if we remove the omitempty tag from BandwidthLimits
|
||||
/*user.Filters.BandwidthLimits = nil
|
||||
user, resp, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Len(t, user.Filters.BandwidthLimits, 0)*/
|
||||
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserTimestamps(t *testing.T) {
|
||||
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err, string(resp))
|
||||
|
@ -12915,6 +13010,39 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Validation error: invalid TLS username")
|
||||
form.Set("tls_username", string(sdk.TLSUsernameNone))
|
||||
// invalid upload_bandwidth_source0
|
||||
form.Set("bandwidth_limit_sources0", "192.168.1.0/24, 192.168.2.0/25")
|
||||
form.Set("upload_bandwidth_source0", "a")
|
||||
form.Set("download_bandwidth_source0", "0")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "invalid upload_bandwidth_source")
|
||||
// invalid download_bandwidth_source0
|
||||
form.Set("upload_bandwidth_source0", "256")
|
||||
form.Set("download_bandwidth_source0", "a")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "invalid download_bandwidth_source")
|
||||
form.Set("download_bandwidth_source0", "512")
|
||||
form.Set("download_bandwidth_source1", "1024")
|
||||
form.Set("bandwidth_limit_sources1", "1.1.1")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source")
|
||||
form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
|
||||
form.Set("upload_bandwidth_source1", "-1")
|
||||
form.Set(csrfFormToken, "invalid form token")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
|
@ -12999,6 +13127,21 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
assert.True(t, util.IsStringInSlice("*.rar", filter.DeniedPatterns))
|
||||
}
|
||||
}
|
||||
if assert.Len(t, newUser.Filters.BandwidthLimits, 2) {
|
||||
for _, bwLimit := range newUser.Filters.BandwidthLimits {
|
||||
if len(bwLimit.Sources) == 2 {
|
||||
assert.Equal(t, "192.168.1.0/24", bwLimit.Sources[0])
|
||||
assert.Equal(t, "192.168.2.0/25", bwLimit.Sources[1])
|
||||
assert.Equal(t, int64(256), bwLimit.UploadBandwidth)
|
||||
assert.Equal(t, int64(512), bwLimit.DownloadBandwidth)
|
||||
} else {
|
||||
assert.Equal(t, []string{"127.0.0.1/32"}, bwLimit.Sources)
|
||||
assert.Equal(t, int64(0), bwLimit.UploadBandwidth)
|
||||
assert.Equal(t, int64(1024), bwLimit.DownloadBandwidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
|
@ -13018,6 +13161,13 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
user.Filters.BandwidthLimits = []sdk.BandwidthLimit{
|
||||
{
|
||||
Sources: []string{"10.8.0.0/16", "192.168.1.0/25"},
|
||||
UploadBandwidth: 256,
|
||||
DownloadBandwidth: 512,
|
||||
},
|
||||
}
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
setBearerForReq(req, apiToken)
|
||||
|
@ -13053,6 +13203,14 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, user.Filters.TOTPConfig.Enabled)
|
||||
if assert.Len(t, user.Filters.BandwidthLimits, 1) {
|
||||
if assert.Len(t, user.Filters.BandwidthLimits[0].Sources, 2) {
|
||||
assert.Equal(t, "10.8.0.0/16", user.Filters.BandwidthLimits[0].Sources[0])
|
||||
assert.Equal(t, "192.168.1.0/25", user.Filters.BandwidthLimits[0].Sources[1])
|
||||
}
|
||||
assert.Equal(t, int64(256), user.Filters.BandwidthLimits[0].UploadBandwidth)
|
||||
assert.Equal(t, int64(512), user.Filters.BandwidthLimits[0].DownloadBandwidth)
|
||||
}
|
||||
|
||||
dbUser, err := dataprovider.UserExists(user.Username)
|
||||
assert.NoError(t, err)
|
||||
|
@ -13178,6 +13336,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, updateUser.Filters.DeniedLoginMethods))
|
||||
assert.True(t, util.IsStringInSlice(common.ProtocolFTP, updateUser.Filters.DeniedProtocols))
|
||||
assert.True(t, util.IsStringInSlice("*.zip", updateUser.Filters.FilePatterns[0].DeniedPatterns))
|
||||
assert.Len(t, updateUser.Filters.BandwidthLimits, 0)
|
||||
req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, apiToken)
|
||||
|
|
|
@ -732,6 +732,41 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
|||
return permissions
|
||||
}
|
||||
|
||||
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
|
||||
var result []sdk.BandwidthLimit
|
||||
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "bandwidth_limit_sources") {
|
||||
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
|
||||
if len(sources) > 0 {
|
||||
bwLimit := sdk.BandwidthLimit{
|
||||
Sources: sources,
|
||||
}
|
||||
idx := strings.TrimPrefix(k, "bandwidth_limit_sources")
|
||||
ul := r.Form.Get(fmt.Sprintf("upload_bandwidth_source%v", idx))
|
||||
dl := r.Form.Get(fmt.Sprintf("download_bandwidth_source%v", idx))
|
||||
if ul != "" {
|
||||
bandwidthUL, err := strconv.ParseInt(ul, 10, 64)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid upload_bandwidth_source%v %#v: %w", idx, ul, err)
|
||||
}
|
||||
bwLimit.UploadBandwidth = bandwidthUL
|
||||
}
|
||||
if dl != "" {
|
||||
bandwidthDL, err := strconv.ParseInt(dl, 10, 64)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid download_bandwidth_source%v %#v: %w", idx, ul, err)
|
||||
}
|
||||
bwLimit.DownloadBandwidth = bandwidthDL
|
||||
}
|
||||
result = append(result, bwLimit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
|
||||
var result []sdk.PatternsFilter
|
||||
|
||||
|
@ -786,8 +821,13 @@ func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
|
|||
return result
|
||||
}
|
||||
|
||||
func getFiltersFromUserPostFields(r *http.Request) sdk.UserFilters {
|
||||
func getFiltersFromUserPostFields(r *http.Request) (sdk.UserFilters, error) {
|
||||
var filters sdk.UserFilters
|
||||
bwLimits, err := getBandwidthLimitsFromPostFields(r)
|
||||
if err != nil {
|
||||
return filters, err
|
||||
}
|
||||
filters.BandwidthLimits = bwLimits
|
||||
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
|
||||
filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
|
||||
|
@ -807,7 +847,7 @@ func getFiltersFromUserPostFields(r *http.Request) sdk.UserFilters {
|
|||
}
|
||||
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
||||
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
return filters
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
|
||||
|
@ -1143,6 +1183,10 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
filters, err := getFiltersFromUserPostFields(r)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user = dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: r.Form.Get("username"),
|
||||
|
@ -1160,7 +1204,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
DownloadBandwidth: bandwidthDL,
|
||||
Status: status,
|
||||
ExpirationDate: expirationDateMillis,
|
||||
Filters: getFiltersFromUserPostFields(r),
|
||||
Filters: filters,
|
||||
AdditionalInfo: r.Form.Get("additional_info"),
|
||||
Description: r.Form.Get("description"),
|
||||
},
|
||||
|
|
|
@ -1492,6 +1492,9 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
|
|||
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
return compareUserFilePatternsFilters(expected, actual)
|
||||
}
|
||||
|
||||
|
@ -1507,6 +1510,31 @@ func checkFilterMatch(expected []string, actual []string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func compareUserBandwidthLimitFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.BandwidthLimits) != len(actual.Filters.BandwidthLimits) {
|
||||
return errors.New("bandwidth filters mismatch")
|
||||
}
|
||||
|
||||
for idx, l := range expected.Filters.BandwidthLimits {
|
||||
if actual.Filters.BandwidthLimits[idx].UploadBandwidth != l.UploadBandwidth {
|
||||
return errors.New("bandwidth filters upload_bandwidth mismatch")
|
||||
}
|
||||
if actual.Filters.BandwidthLimits[idx].DownloadBandwidth != l.DownloadBandwidth {
|
||||
return errors.New("bandwidth filters download_bandwidth mismatch")
|
||||
}
|
||||
if len(actual.Filters.BandwidthLimits[idx].Sources) != len(l.Sources) {
|
||||
return errors.New("bandwidth filters sources mismatch")
|
||||
}
|
||||
for _, source := range actual.Filters.BandwidthLimits[idx].Sources {
|
||||
if !util.IsStringInSlice(source, l.Sources) {
|
||||
return errors.New("bandwidth filters source mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareUserFilePatternsFilters(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if len(expected.Filters.FilePatterns) != len(actual.Filters.FilePatterns) {
|
||||
return errors.New("file patterns mismatch")
|
||||
|
|
|
@ -4282,6 +4282,22 @@ components:
|
|||
example: false
|
||||
description: If true, the check password hook, if defined, will not be executed
|
||||
description: User specific hook overrides
|
||||
BandwidthLimit:
|
||||
type: object
|
||||
properties:
|
||||
sources:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: 'Source networks in CIDR notation as defined in RFC 4632 and RFC 4291 for example `192.0.2.0/24` or `2001:db8::/32`. The limit applies if the defined networks contain the client IP'
|
||||
upload_bandwidth:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 'Maximum upload bandwidth as KB/s, 0 means unlimited'
|
||||
download_bandwidth:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 'Maximum download bandwidth as KB/s, 0 means unlimited'
|
||||
UserFilters:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -4347,6 +4363,10 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RecoveryCode'
|
||||
bandwidth_limits:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BandwidthLimit'
|
||||
description: Additional user options
|
||||
Secret:
|
||||
type: object
|
||||
|
|
50
sdk/user.go
50
sdk/user.go
|
@ -1,6 +1,8 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
|
@ -125,6 +127,34 @@ type TOTPConfig struct {
|
|||
Protocols []string `json:"protocols,omitempty"`
|
||||
}
|
||||
|
||||
// BandwidthLimit defines a per-source bandwidth limit
|
||||
type BandwidthLimit struct {
|
||||
// Source networks in CIDR notation as defined in RFC 4632 and RFC 4291
|
||||
// for example "192.0.2.0/24" or "2001:db8::/32". The limit applies if the
|
||||
// defined networks contain the client IP
|
||||
Sources []string `json:"sources"`
|
||||
// Maximum upload bandwidth as KB/s
|
||||
UploadBandwidth int64 `json:"upload_bandwidth,omitempty"`
|
||||
// Maximum download bandwidth as KB/s
|
||||
DownloadBandwidth int64 `json:"download_bandwidth,omitempty"`
|
||||
}
|
||||
|
||||
// Validate returns an error if the bandwidth limit is not valid
|
||||
func (l *BandwidthLimit) Validate() error {
|
||||
for _, source := range l.Sources {
|
||||
_, _, err := net.ParseCIDR(source)
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not parse bandwidth limit source %#v: %v", source, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSourcesAsString returns the sources as comma separated string
|
||||
func (l *BandwidthLimit) GetSourcesAsString() string {
|
||||
return strings.Join(l.Sources, ",")
|
||||
}
|
||||
|
||||
// UserFilters defines additional restrictions for a user
|
||||
// TODO: rename to UserOptions in v3
|
||||
type UserFilters struct {
|
||||
|
@ -173,6 +203,8 @@ type UserFilters struct {
|
|||
// UserType is an hint for authentication plugins.
|
||||
// It is ignored when using SFTPGo internal authentication
|
||||
UserType string `json:"user_type,omitempty"`
|
||||
// Per-source bandwidth limits
|
||||
BandwidthLimits []BandwidthLimit `json:"bandwidth_limits,omitempty"`
|
||||
}
|
||||
|
||||
// BaseUser defines the shared user fields
|
||||
|
@ -209,17 +241,19 @@ type BaseUser struct {
|
|||
// List of the granted permissions
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
// Used quota as bytes
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
UsedQuotaSize int64 `json:"used_quota_size,omitempty"`
|
||||
// Used quota as number of files
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
UsedQuotaFiles int `json:"used_quota_files,omitempty"`
|
||||
// Last quota update as unix timestamp in milliseconds
|
||||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
// Maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
UploadBandwidth int64 `json:"upload_bandwidth"`
|
||||
// Maximum download bandwidth as KB/s, 0 means unlimited
|
||||
DownloadBandwidth int64 `json:"download_bandwidth"`
|
||||
LastQuotaUpdate int64 `json:"last_quota_update,omitempty"`
|
||||
// Maximum upload bandwidth as KB/s, 0 means unlimited.
|
||||
// This is the default if no per-source limit match
|
||||
UploadBandwidth int64 `json:"upload_bandwidth,omitempty"`
|
||||
// Maximum download bandwidth as KB/s, 0 means unlimited.
|
||||
// This is the default if no per-source limit match
|
||||
DownloadBandwidth int64 `json:"download_bandwidth,omitempty"`
|
||||
// Last login as unix timestamp in milliseconds
|
||||
LastLogin int64 `json:"last_login"`
|
||||
LastLogin int64 `json:"last_login,omitempty"`
|
||||
// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
// last update time as unix timestamp in milliseconds
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true)
|
||||
}
|
||||
],
|
||||
"scrollX": false,
|
||||
|
|
|
@ -228,15 +228,15 @@ function deleteAction() {
|
|||
"columnDefs": [
|
||||
{
|
||||
"targets": [1],
|
||||
"render": $.fn.dataTable.render.ellipsis(50, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(50, true)
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"render": $.fn.dataTable.render.ellipsis(60, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(60, true)
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true)
|
||||
}
|
||||
],
|
||||
"scrollX": false,
|
||||
|
|
|
@ -364,6 +364,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idUID" class="col-sm-2 col-form-label">UID</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idUID" name="uid" placeholder="" value="{{.User.UID}}"
|
||||
min="0" max="2147483647">
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idGID" class="col-sm-2 col-form-label">GID</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idGID" name="gid" placeholder="" value="{{.User.GID}}"
|
||||
min="0" max="2147483647">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label>
|
||||
<div class="col-sm-3">
|
||||
|
@ -425,17 +439,85 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idUID" class="col-sm-2 col-form-label">UID</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idUID" name="uid" placeholder="" value="{{.User.UID}}"
|
||||
min="0" max="2147483647">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Per-source bandwidth limits
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idGID" class="col-sm-2 col-form-label">GID</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idGID" name="gid" placeholder="" value="{{.User.GID}}"
|
||||
min="0" max="2147483647">
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_bwlimits_outer">
|
||||
{{range $idx, $bwLimit := .User.Filters.BandwidthLimits -}}
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources{{$idx}}" name="bandwidth_limit_sources{{$idx}}" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock{{$idx}}">{{$bwLimit.GetSourcesAsString}}</textarea>
|
||||
<small id="bwLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource{{$idx}}" name="upload_bandwidth_source{{$idx}}"
|
||||
placeholder="" value="{{$bwLimit.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock{{$idx}}">
|
||||
<small id="ulHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource{{$idx}}" name="download_bandwidth_source{{$idx}}"
|
||||
placeholder="" value="{{$bwLimit.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock{{$idx}}">
|
||||
<small id="dlHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources0" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock0"></textarea>
|
||||
<small id="bwLimitSourcesHelpBlock0" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource0" name="upload_bandwidth_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="ulHelpBlock0">
|
||||
<small id="ulHelpBlock0" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource0" name="download_bandwidth_source0"
|
||||
placeholder="" value="" min="0" aria-describedby="dlHelpBlock0">
|
||||
<small id="dlHelpBlock0" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_bwlimit_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new bandwidth limit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -757,6 +839,49 @@
|
|||
$(this).closest(".form_field_vfolder_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_bwlimit_field_btn", function () {
|
||||
var index = $(".form_field_bwlimits_outer").find(".form_field_bwlimits_outer_row").length;
|
||||
while (document.getElementById("idBandwidthLimitSources"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_bwlimits_outer").append(`
|
||||
<div class="row form_field_bwlimits_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources${index}" rows="4" placeholder=""
|
||||
aria-describedby="bwLimitSourcesHelpBlock${index}"></textarea>
|
||||
<small id="bwLimitSourcesHelpBlock${index}" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idUploadBandwidthSource${index}" name="upload_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="ulHelpBlock${index}">
|
||||
<small id="ulHelpBlock${index}" class="form-text text-muted">
|
||||
UL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="number" class="form-control" id="idDownloadBandwidthSource${index}" name="download_bandwidth_source${index}"
|
||||
placeholder="" value="" min="0" aria-describedby="dlHelpBlock${index}">
|
||||
<small id="dlHelpBlock${index}" class="form-text text-muted">
|
||||
DL (KB/s). 0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_bwlimit_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_bwlimits_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
||||
var index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
||||
while (document.getElementById("idPatternPath"+index) != null){
|
||||
|
|
|
@ -255,11 +255,11 @@
|
|||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(40, true)
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"render": $.fn.dataTable.render.ellipsis(70, true),
|
||||
"render": $.fn.dataTable.render.ellipsis(70, true)
|
||||
}
|
||||
],
|
||||
"scrollX": false,
|
||||
|
|
|
@ -386,6 +386,27 @@
|
|||
<script type="text/javascript">
|
||||
var spinnerDone = false;
|
||||
|
||||
var escapeHTML = function ( t ) {
|
||||
return t
|
||||
.replace( /&/g, '&' )
|
||||
.replace( /</g, '<' )
|
||||
.replace( />/g, '>' )
|
||||
.replace( /"/g, '"' );
|
||||
};
|
||||
|
||||
function shortenData(d, cutoff) {
|
||||
if ( typeof d !== 'string' ) {
|
||||
return d;
|
||||
}
|
||||
|
||||
if ( d.length <= cutoff ) {
|
||||
return d;
|
||||
}
|
||||
|
||||
var shortened = d.substr(0, cutoff-1);
|
||||
return shortened+'…';
|
||||
}
|
||||
|
||||
function getIconForFile(filename) {
|
||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
switch (extension) {
|
||||
|
@ -651,10 +672,17 @@
|
|||
try {
|
||||
var f = files[index];
|
||||
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||
var lastModified;
|
||||
try {
|
||||
lastModified = f.lastModified;
|
||||
} catch (e) {
|
||||
console.log("unable to get last modified time from file: "+e.message);
|
||||
lastModified = "";
|
||||
}
|
||||
response = await fetch(uploadPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-SFTPGO-MTIME': f.lastModified,
|
||||
'X-SFTPGO-MTIME': lastModified,
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
|
@ -886,14 +914,22 @@
|
|||
"data": "name",
|
||||
"render": function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
var title = "";
|
||||
var cssClass = "";
|
||||
var shortened = shortenData(data, 70);
|
||||
if (shortened != data){
|
||||
title = escapeHTML(data);
|
||||
cssClass = "ellipsis";
|
||||
}
|
||||
|
||||
if (row["type"] == "1") {
|
||||
return `<i class="fas fa-folder"></i> <a href="${row['url']}">${data}</a>`;
|
||||
return `<i class="fas fa-folder"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||
}
|
||||
if (row["size"] == "") {
|
||||
return `<i class="fas fa-external-link-alt"></i> <a href="${row['url']}">${data}</a>`;
|
||||
return `<i class="fas fa-external-link-alt"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||
}
|
||||
var icon = getIconForFile(data);
|
||||
return `<i class="${icon}"></i> <a href="${row['url']}">${data}</a>`;
|
||||
return `<i class="${icon}"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -492,7 +492,7 @@ func GetRealIP(r *http.Request) string {
|
|||
} else if clientIP := r.Header.Get(cfConnectingIP); clientIP != "" {
|
||||
ip = clientIP
|
||||
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
|
||||
i := strings.Index(xff, ", ")
|
||||
i := strings.Index(xff, ",")
|
||||
if i == -1 {
|
||||
i = len(xff)
|
||||
}
|
||||
|
|
|
@ -460,7 +460,7 @@ func TestRemoteAddress(t *testing.T) {
|
|||
assert.Equal(t, remoteAddr2, ip)
|
||||
|
||||
req.RemoteAddr = remoteAddr2
|
||||
req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", "12.34.56.78", "172.16.2.4"))
|
||||
req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v,%v", "12.34.56.78", "172.16.2.4"))
|
||||
ip = server.checkRemoteAddress(req)
|
||||
assert.Equal(t, "12.34.56.78", ip)
|
||||
assert.Equal(t, ip, req.RemoteAddr)
|
||||
|
|
Loading…
Reference in a new issue