access.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package share
  2. /*
  3. Share access control
  4. Decides whether a request to /share/... may open a share:
  5. 1. The share must exist and not have expired
  6. 2. The visitor must be in the share's audience (anyone / signed in /
  7. same group / listed groups / listed users)
  8. 3. A password protected share must have been unlocked. Unlocking sets a
  9. signed, HttpOnly cookie scoped to /share/ that is bound to the share
  10. and to its current password, so changing the password signs everyone out.
  11. */
  12. import (
  13. "crypto/hmac"
  14. "crypto/rand"
  15. "crypto/sha256"
  16. "encoding/hex"
  17. "net"
  18. "net/http"
  19. "os"
  20. "strconv"
  21. "strings"
  22. "sync"
  23. "time"
  24. "imuslab.com/arozos/mod/share/shareEntry"
  25. "imuslab.com/arozos/mod/utils"
  26. )
  27. const (
  28. shareMetaTable = "share_meta"
  29. shareCookieSecretKey = "cookie_secret"
  30. shareSessionCookiePrefix = "ao_share_"
  31. shareSessionLifetime = 12 * time.Hour
  32. //Password guessing limit, per share and client address
  33. maxPasswordAttempts = 10
  34. passwordAttemptWindow = 10 * time.Minute
  35. )
  36. // accessGuard holds the state needed for password sessions
  37. type accessGuard struct {
  38. secret []byte
  39. mu sync.Mutex
  40. attempts map[string]*passwordAttempts
  41. }
  42. type passwordAttempts struct {
  43. count int
  44. windowStart time.Time
  45. }
  46. // newAccessGuard loads (or creates) the cookie signing secret. Storing it in
  47. // the database keeps unlocked shares unlocked across restarts.
  48. func (s *Manager) newAccessGuard() *accessGuard {
  49. guard := &accessGuard{attempts: map[string]*passwordAttempts{}}
  50. if s.options.ShareEntryTable == nil || s.options.ShareEntryTable.Database == nil {
  51. guard.secret = randomSecret()
  52. return guard
  53. }
  54. db := s.options.ShareEntryTable.Database
  55. db.NewTable(shareMetaTable)
  56. secretHex := ""
  57. if err := db.Read(shareMetaTable, shareCookieSecretKey, &secretHex); err == nil {
  58. if secret, err := hex.DecodeString(secretHex); err == nil && len(secret) == 32 {
  59. guard.secret = secret
  60. return guard
  61. }
  62. }
  63. guard.secret = randomSecret()
  64. db.Write(shareMetaTable, shareCookieSecretKey, hex.EncodeToString(guard.secret))
  65. return guard
  66. }
  67. func randomSecret() []byte {
  68. secret := make([]byte, 32)
  69. if _, err := rand.Read(secret); err != nil {
  70. //crypto/rand failing is not recoverable in a meaningful way
  71. panic(err)
  72. }
  73. return secret
  74. }
  75. func sessionCookieName(so *shareEntry.ShareOption) string {
  76. return shareSessionCookiePrefix + so.UUID
  77. }
  78. // sign returns the session signature for the given share and expiry time
  79. func (g *accessGuard) sign(so *shareEntry.ShareOption, expires int64) string {
  80. mac := hmac.New(sha256.New, g.secret)
  81. mac.Write([]byte(so.UUID + "\n" + so.PasswordHash + "\n" + strconv.FormatInt(expires, 10)))
  82. return hex.EncodeToString(mac.Sum(nil))
  83. }
  84. // issueSession sets the cookie that unlocks a password protected share
  85. func (g *accessGuard) issueSession(w http.ResponseWriter, r *http.Request, so *shareEntry.ShareOption) {
  86. expiresAt := time.Now().Add(shareSessionLifetime)
  87. if so.ExpireAt > 0 && so.ExpireAt < expiresAt.Unix() {
  88. expiresAt = time.Unix(so.ExpireAt, 0)
  89. }
  90. expires := expiresAt.Unix()
  91. http.SetCookie(w, &http.Cookie{
  92. Name: sessionCookieName(so),
  93. Value: strconv.FormatInt(expires, 10) + "." + g.sign(so, expires),
  94. Path: "/share/",
  95. Expires: expiresAt,
  96. HttpOnly: true,
  97. Secure: r.TLS != nil,
  98. SameSite: http.SameSiteLaxMode,
  99. })
  100. }
  101. // hasValidSession reports whether the request carries an unexpired session
  102. // for the share's current password.
  103. func (g *accessGuard) hasValidSession(r *http.Request, so *shareEntry.ShareOption) bool {
  104. cookie, err := r.Cookie(sessionCookieName(so))
  105. if err != nil {
  106. return false
  107. }
  108. expiresStr, signature, found := strings.Cut(cookie.Value, ".")
  109. if !found {
  110. return false
  111. }
  112. expires, err := strconv.ParseInt(expiresStr, 10, 64)
  113. if err != nil || time.Now().Unix() >= expires {
  114. return false
  115. }
  116. return hmac.Equal([]byte(signature), []byte(g.sign(so, expires)))
  117. }
  118. // allowAttempt reports whether the client may try another password. Failed
  119. // attempts are counted by recordFailure.
  120. func (g *accessGuard) allowAttempt(key string, now time.Time) bool {
  121. g.mu.Lock()
  122. defer g.mu.Unlock()
  123. record, ok := g.attempts[key]
  124. if !ok || now.Sub(record.windowStart) > passwordAttemptWindow {
  125. return true
  126. }
  127. return record.count < maxPasswordAttempts
  128. }
  129. func (g *accessGuard) recordFailure(key string, now time.Time) {
  130. g.mu.Lock()
  131. defer g.mu.Unlock()
  132. //Drop stale records so the map cannot grow without bound
  133. for k, record := range g.attempts {
  134. if now.Sub(record.windowStart) > passwordAttemptWindow {
  135. delete(g.attempts, k)
  136. }
  137. }
  138. record, ok := g.attempts[key]
  139. if !ok {
  140. record = &passwordAttempts{windowStart: now}
  141. g.attempts[key] = record
  142. }
  143. record.count++
  144. }
  145. func (g *accessGuard) clearFailures(key string) {
  146. g.mu.Lock()
  147. defer g.mu.Unlock()
  148. delete(g.attempts, key)
  149. }
  150. func attemptKey(r *http.Request, so *shareEntry.ShareOption) string {
  151. host, _, err := net.SplitHostPort(r.RemoteAddr)
  152. if err != nil {
  153. host = r.RemoteAddr
  154. }
  155. return so.UUID + "|" + host
  156. }
  157. // lookupActiveShare returns the share with the given id if it exists and has not expired
  158. func (s *Manager) lookupActiveShare(id string) *shareEntry.ShareOption {
  159. so := s.GetShareObjectFromUUID(id)
  160. if so == nil || so.IsExpired(time.Now()) {
  161. return nil
  162. }
  163. return so
  164. }
  165. /*
  166. authorizeShareAccess checks the audience and the password of a share. It
  167. writes the rejection itself and returns false if the request must stop.
  168. raw is true for requests that fetch content (download, preview, zip,
  169. upload) rather than a page, which get a status code instead of a redirect
  170. to the login page or the password form.
  171. */
  172. func (s *Manager) authorizeShareAccess(w http.ResponseWriter, r *http.Request, so *shareEntry.ShareOption, raw bool) bool {
  173. if !s.checkAudience(w, r, so, raw) {
  174. return false
  175. }
  176. if so.HasPassword() && !s.guard.hasValidSession(r, so) {
  177. if raw {
  178. w.WriteHeader(http.StatusUnauthorized)
  179. w.Write([]byte("401 - Password required"))
  180. } else {
  181. s.servePasswordPage(w, r, so, false, http.StatusOK)
  182. }
  183. return false
  184. }
  185. return true
  186. }
  187. // checkAudience checks that the visitor is someone the share is for
  188. func (s *Manager) checkAudience(w http.ResponseWriter, r *http.Request, so *shareEntry.ShareOption, raw bool) bool {
  189. rejectUnauthorized := func() {
  190. if raw {
  191. w.WriteHeader(http.StatusUnauthorized)
  192. w.Write([]byte("401 - Unauthorized"))
  193. } else {
  194. http.Redirect(w, r, utils.ConstructRelativePathFromRequestURL(r.RequestURI, "login.html")+"?redirect=/share/"+so.UUID, http.StatusTemporaryRedirect)
  195. }
  196. }
  197. rejectForbidden := func() {
  198. if raw {
  199. w.WriteHeader(http.StatusForbidden)
  200. w.Write([]byte("403 - Forbidden"))
  201. } else {
  202. ServePermissionDeniedPage(w)
  203. }
  204. }
  205. switch so.Permission {
  206. case shareEntry.PermissionAnyone:
  207. return true
  208. case shareEntry.PermissionSignedIn:
  209. if !s.options.AuthAgent.CheckAuth(r) {
  210. rejectUnauthorized()
  211. return false
  212. }
  213. return true
  214. case shareEntry.PermissionSameGroup, shareEntry.PermissionGroups, shareEntry.PermissionUsers:
  215. thisuserinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  216. if err != nil {
  217. //User not logged in. Redirect to login page
  218. rejectUnauthorized()
  219. return false
  220. }
  221. //The owner can always open their own share
  222. if thisuserinfo.Username == so.Owner {
  223. return true
  224. }
  225. thisUsersGroupByName := thisuserinfo.GetUserPermissionGroupNames()
  226. allowed := false
  227. switch so.Permission {
  228. case shareEntry.PermissionSameGroup:
  229. //Every group of the owner (at share time) must be one of the user's groups
  230. allowed = true
  231. for _, allowedpg := range so.Accessibles {
  232. if !utils.StringInArray(thisUsersGroupByName, allowedpg) {
  233. allowed = false
  234. }
  235. }
  236. case shareEntry.PermissionGroups:
  237. for _, thisUserPg := range thisUsersGroupByName {
  238. if utils.StringInArray(so.Accessibles, thisUserPg) {
  239. allowed = true
  240. }
  241. }
  242. case shareEntry.PermissionUsers:
  243. allowed = utils.StringInArray(so.Accessibles, thisuserinfo.Username)
  244. }
  245. if !allowed {
  246. rejectForbidden()
  247. return false
  248. }
  249. return true
  250. }
  251. //Unsupported mode. Show notfound
  252. http.NotFound(w, r)
  253. return false
  254. }
  255. /*
  256. handlePasswordSubmit handles POST /share/auth/{uuid} from the password page.
  257. On success the share is unlocked and the visitor is sent back to it.
  258. */
  259. func (s *Manager) handlePasswordSubmit(w http.ResponseWriter, r *http.Request, id string) {
  260. so := s.lookupActiveShare(id)
  261. if so == nil {
  262. s.serveNotFoundPage(w, r, id)
  263. return
  264. }
  265. if r.Method != http.MethodPost {
  266. http.Redirect(w, r, "/share/"+so.UUID+"/", http.StatusSeeOther)
  267. return
  268. }
  269. if !s.checkAudience(w, r, so, false) {
  270. return
  271. }
  272. if !so.HasPassword() {
  273. http.Redirect(w, r, "/share/"+so.UUID+"/", http.StatusSeeOther)
  274. return
  275. }
  276. now := time.Now()
  277. key := attemptKey(r, so)
  278. if !s.guard.allowAttempt(key, now) {
  279. s.servePasswordPage(w, r, so, true, http.StatusTooManyRequests)
  280. return
  281. }
  282. r.ParseForm()
  283. if !so.CheckPassword(r.PostForm.Get("password")) {
  284. s.guard.recordFailure(key, now)
  285. s.servePasswordPage(w, r, so, true, http.StatusUnauthorized)
  286. return
  287. }
  288. s.guard.clearFailures(key)
  289. s.guard.issueSession(w, r, so)
  290. http.Redirect(w, r, "/share/"+so.UUID+"/", http.StatusSeeOther)
  291. }
  292. // servePasswordPage renders the password form. It deliberately shows nothing
  293. // about the shared file.
  294. func (s *Manager) servePasswordPage(w http.ResponseWriter, r *http.Request, so *shareEntry.ShareOption, failed bool, status int) {
  295. errorMessage := ""
  296. if failed {
  297. errorMessage = "Incorrect password, please try again."
  298. if status == http.StatusTooManyRequests {
  299. errorMessage = "Too many attempts. Please wait a few minutes and try again."
  300. }
  301. }
  302. content, err := utils.Templateload("./system/share/password.html", map[string]string{
  303. "hostname": s.options.HostName,
  304. "reqid": so.UUID,
  305. "error": errorMessage,
  306. "reqtime": strconv.Itoa(int(time.Now().Unix())),
  307. })
  308. if err != nil {
  309. w.WriteHeader(http.StatusUnauthorized)
  310. w.Write([]byte("401 - This share is password protected"))
  311. return
  312. }
  313. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  314. w.Header().Set("Cache-Control", "no-store")
  315. w.WriteHeader(status)
  316. w.Write([]byte(content))
  317. }
  318. // serveNotFoundPage renders the "share not found" page, also used for expired shares
  319. func (s *Manager) serveNotFoundPage(w http.ResponseWriter, r *http.Request, id string) {
  320. content, err := utils.Templateload("./system/share/notfound.html", map[string]string{
  321. "hostname": s.options.HostName,
  322. "reqid": id,
  323. "reqtime": strconv.Itoa(int(time.Now().Unix())),
  324. })
  325. if err != nil {
  326. http.NotFound(w, r)
  327. return
  328. }
  329. w.Header().Set("Content-Type", "text/html")
  330. w.WriteHeader(http.StatusNotFound)
  331. w.Write([]byte(content))
  332. }
  333. func ServePermissionDeniedPage(w http.ResponseWriter) {
  334. w.WriteHeader(http.StatusForbidden)
  335. pageContent := []byte("Permissioned Denied")
  336. if utils.FileExists("system/share/permissionDenied.html") {
  337. content, err := os.ReadFile("system/share/permissionDenied.html")
  338. if err == nil {
  339. pageContent = content
  340. }
  341. }
  342. w.Write([]byte(pageContent))
  343. }