share.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package dataprovider
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "strings"
  7. "time"
  8. "github.com/alexedwards/argon2id"
  9. "golang.org/x/crypto/bcrypt"
  10. "github.com/drakkan/sftpgo/v2/logger"
  11. "github.com/drakkan/sftpgo/v2/util"
  12. )
  13. // ShareScope defines the supported share scopes
  14. type ShareScope int
  15. // Supported share scopes
  16. const (
  17. ShareScopeRead ShareScope = iota + 1
  18. ShareScopeWrite
  19. )
  20. const (
  21. redactedPassword = "[**redacted**]"
  22. )
  23. // Share defines files and or directories shared with external users
  24. type Share struct {
  25. // Database unique identifier
  26. ID int64 `json:"-"`
  27. // Unique ID used to access this object
  28. ShareID string `json:"id"`
  29. Name string `json:"name"`
  30. Description string `json:"description,omitempty"`
  31. Scope ShareScope `json:"scope"`
  32. // Paths to files or directories, for ShareScopeWrite it must be exactly one directory
  33. Paths []string `json:"paths"`
  34. // Username who shared this object
  35. Username string `json:"username"`
  36. CreatedAt int64 `json:"created_at"`
  37. UpdatedAt int64 `json:"updated_at"`
  38. // 0 means never used
  39. LastUseAt int64 `json:"last_use_at,omitempty"`
  40. // ExpiresAt expiration date/time as unix timestamp in milliseconds, 0 means no expiration
  41. ExpiresAt int64 `json:"expires_at,omitempty"`
  42. // Optional password to protect the share
  43. Password string `json:"password"`
  44. // Limit the available access tokens, 0 means no limit
  45. MaxTokens int `json:"max_tokens,omitempty"`
  46. // Used tokens
  47. UsedTokens int `json:"used_tokens,omitempty"`
  48. // Limit the share availability to these IPs/CIDR networks
  49. AllowFrom []string `json:"allow_from,omitempty"`
  50. // set for restores, we don't have to validate the expiration date
  51. // otherwise we fail to restore existing shares and we have to insert
  52. // all the previous values with no modifications
  53. IsRestore bool `json:"-"`
  54. }
  55. // GetScopeAsString returns the share's scope as string.
  56. // Used in web pages
  57. func (s *Share) GetScopeAsString() string {
  58. switch s.Scope {
  59. case ShareScopeRead:
  60. return "Read"
  61. default:
  62. return "Write"
  63. }
  64. }
  65. // IsExpired returns true if the share is expired
  66. func (s *Share) IsExpired() bool {
  67. if s.ExpiresAt > 0 {
  68. return s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
  69. }
  70. return false
  71. }
  72. // GetInfoString returns share's info as string.
  73. func (s *Share) GetInfoString() string {
  74. var result strings.Builder
  75. if s.ExpiresAt > 0 {
  76. t := util.GetTimeFromMsecSinceEpoch(s.ExpiresAt)
  77. result.WriteString(fmt.Sprintf("Expiration: %v. ", t.Format("2006-01-02 15:04"))) // YYYY-MM-DD HH:MM
  78. }
  79. if s.LastUseAt > 0 {
  80. t := util.GetTimeFromMsecSinceEpoch(s.LastUseAt)
  81. result.WriteString(fmt.Sprintf("Last use: %v. ", t.Format("2006-01-02 15:04")))
  82. }
  83. if s.MaxTokens > 0 {
  84. result.WriteString(fmt.Sprintf("Usage: %v/%v. ", s.UsedTokens, s.MaxTokens))
  85. } else {
  86. result.WriteString(fmt.Sprintf("Used tokens: %v. ", s.UsedTokens))
  87. }
  88. if len(s.AllowFrom) > 0 {
  89. result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v. ", len(s.AllowFrom)))
  90. }
  91. if s.Password != "" {
  92. result.WriteString("Password protected.")
  93. }
  94. return result.String()
  95. }
  96. // GetAllowedFromAsString returns the allowed IP as comma separated string
  97. func (s *Share) GetAllowedFromAsString() string {
  98. return strings.Join(s.AllowFrom, ",")
  99. }
  100. func (s *Share) getACopy() Share {
  101. allowFrom := make([]string, len(s.AllowFrom))
  102. copy(allowFrom, s.AllowFrom)
  103. return Share{
  104. ID: s.ID,
  105. ShareID: s.ShareID,
  106. Name: s.Name,
  107. Description: s.Description,
  108. Scope: s.Scope,
  109. Paths: s.Paths,
  110. Username: s.Username,
  111. CreatedAt: s.CreatedAt,
  112. UpdatedAt: s.UpdatedAt,
  113. LastUseAt: s.LastUseAt,
  114. ExpiresAt: s.ExpiresAt,
  115. Password: s.Password,
  116. MaxTokens: s.MaxTokens,
  117. UsedTokens: s.UsedTokens,
  118. AllowFrom: allowFrom,
  119. }
  120. }
  121. // RenderAsJSON implements the renderer interface used within plugins
  122. func (s *Share) RenderAsJSON(reload bool) ([]byte, error) {
  123. if reload {
  124. share, err := provider.shareExists(s.ShareID, s.Username)
  125. if err != nil {
  126. providerLog(logger.LevelError, "unable to reload share before rendering as json: %v", err)
  127. return nil, err
  128. }
  129. share.HideConfidentialData()
  130. return json.Marshal(share)
  131. }
  132. s.HideConfidentialData()
  133. return json.Marshal(s)
  134. }
  135. // HideConfidentialData hides share confidential data
  136. func (s *Share) HideConfidentialData() {
  137. if s.Password != "" {
  138. s.Password = redactedPassword
  139. }
  140. }
  141. // HasRedactedPassword returns true if this share has a redacted password
  142. func (s *Share) HasRedactedPassword() bool {
  143. return s.Password == redactedPassword
  144. }
  145. func (s *Share) hashPassword() error {
  146. if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
  147. if config.PasswordHashing.Algo == HashingAlgoBcrypt {
  148. hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)
  149. if err != nil {
  150. return err
  151. }
  152. s.Password = string(hashed)
  153. } else {
  154. hashed, err := argon2id.CreateHash(s.Password, argon2Params)
  155. if err != nil {
  156. return err
  157. }
  158. s.Password = hashed
  159. }
  160. }
  161. return nil
  162. }
  163. func (s *Share) validatePaths() error {
  164. var paths []string
  165. for _, p := range s.Paths {
  166. p = strings.TrimSpace(p)
  167. if p != "" {
  168. paths = append(paths, p)
  169. }
  170. }
  171. s.Paths = paths
  172. if len(s.Paths) == 0 {
  173. return util.NewValidationError("at least a shared path is required")
  174. }
  175. for idx := range s.Paths {
  176. s.Paths[idx] = util.CleanPath(s.Paths[idx])
  177. }
  178. s.Paths = util.RemoveDuplicates(s.Paths)
  179. if s.Scope == ShareScopeWrite && len(s.Paths) != 1 {
  180. return util.NewValidationError("the write share scope requires exactly one path")
  181. }
  182. // check nested paths
  183. if len(s.Paths) > 1 {
  184. for idx := range s.Paths {
  185. for innerIdx := range s.Paths {
  186. if idx == innerIdx {
  187. continue
  188. }
  189. if isVirtualDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true) {
  190. return util.NewGenericError("shared paths cannot be nested")
  191. }
  192. }
  193. }
  194. }
  195. return nil
  196. }
  197. func (s *Share) validate() error {
  198. if s.ShareID == "" {
  199. return util.NewValidationError("share_id is mandatory")
  200. }
  201. if s.Name == "" {
  202. return util.NewValidationError("name is mandatory")
  203. }
  204. if s.Scope != ShareScopeRead && s.Scope != ShareScopeWrite {
  205. return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope))
  206. }
  207. if err := s.validatePaths(); err != nil {
  208. return err
  209. }
  210. if s.ExpiresAt > 0 {
  211. if !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
  212. return util.NewValidationError("expiration must be in the future")
  213. }
  214. } else {
  215. s.ExpiresAt = 0
  216. }
  217. if s.MaxTokens < 0 {
  218. return util.NewValidationError("invalid max tokens")
  219. }
  220. if s.Username == "" {
  221. return util.NewValidationError("username is mandatory")
  222. }
  223. if s.HasRedactedPassword() {
  224. return util.NewValidationError("cannot save a share with a redacted password")
  225. }
  226. if err := s.hashPassword(); err != nil {
  227. return err
  228. }
  229. s.AllowFrom = util.RemoveDuplicates(s.AllowFrom)
  230. for _, IPMask := range s.AllowFrom {
  231. _, _, err := net.ParseCIDR(IPMask)
  232. if err != nil {
  233. return util.NewValidationError(fmt.Sprintf("could not parse allow from entry %#v : %v", IPMask, err))
  234. }
  235. }
  236. return nil
  237. }
  238. // CheckCredentials verifies the share credentials if a password if set
  239. func (s *Share) CheckCredentials(username, password string) (bool, error) {
  240. if s.Password == "" {
  241. return true, nil
  242. }
  243. if username == "" || password == "" {
  244. return false, ErrInvalidCredentials
  245. }
  246. if username != s.Username {
  247. return false, ErrInvalidCredentials
  248. }
  249. if strings.HasPrefix(s.Password, bcryptPwdPrefix) {
  250. if err := bcrypt.CompareHashAndPassword([]byte(s.Password), []byte(password)); err != nil {
  251. return false, ErrInvalidCredentials
  252. }
  253. return true, nil
  254. }
  255. match, err := argon2id.ComparePasswordAndHash(password, s.Password)
  256. if !match || err != nil {
  257. return false, ErrInvalidCredentials
  258. }
  259. return match, err
  260. }
  261. // GetRelativePath returns the specified absolute path as relative to the share base path
  262. func (s *Share) GetRelativePath(name string) string {
  263. if len(s.Paths) == 0 {
  264. return ""
  265. }
  266. return util.CleanPath(strings.TrimPrefix(name, s.Paths[0]))
  267. }
  268. // IsUsable checks if the share is usable from the specified IP
  269. func (s *Share) IsUsable(ip string) (bool, error) {
  270. if s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens {
  271. return false, util.NewRecordNotFoundError("max share usage exceeded")
  272. }
  273. if s.ExpiresAt > 0 {
  274. if s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
  275. return false, util.NewRecordNotFoundError("share expired")
  276. }
  277. }
  278. if len(s.AllowFrom) == 0 {
  279. return true, nil
  280. }
  281. parsedIP := net.ParseIP(ip)
  282. if parsedIP == nil {
  283. return false, ErrLoginNotAllowedFromIP
  284. }
  285. for _, ipMask := range s.AllowFrom {
  286. _, network, err := net.ParseCIDR(ipMask)
  287. if err != nil {
  288. continue
  289. }
  290. if network.Contains(parsedIP) {
  291. return true, nil
  292. }
  293. }
  294. return false, ErrLoginNotAllowedFromIP
  295. }