share.go 9.3 KB

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