You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

server.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package lfs
  4. import (
  5. stdCtx "context"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/url"
  13. "path"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. actions_model "code.gitea.io/gitea/models/actions"
  18. auth_model "code.gitea.io/gitea/models/auth"
  19. git_model "code.gitea.io/gitea/models/git"
  20. "code.gitea.io/gitea/models/perm"
  21. access_model "code.gitea.io/gitea/models/perm/access"
  22. repo_model "code.gitea.io/gitea/models/repo"
  23. "code.gitea.io/gitea/models/unit"
  24. user_model "code.gitea.io/gitea/models/user"
  25. "code.gitea.io/gitea/modules/context"
  26. "code.gitea.io/gitea/modules/json"
  27. lfs_module "code.gitea.io/gitea/modules/lfs"
  28. "code.gitea.io/gitea/modules/log"
  29. "code.gitea.io/gitea/modules/setting"
  30. "code.gitea.io/gitea/modules/storage"
  31. "github.com/golang-jwt/jwt/v5"
  32. "github.com/minio/sha256-simd"
  33. )
  34. // requestContext contain variables from the HTTP request.
  35. type requestContext struct {
  36. User string
  37. Repo string
  38. Authorization string
  39. }
  40. // Claims is a JWT Token Claims
  41. type Claims struct {
  42. RepoID int64
  43. Op string
  44. UserID int64
  45. jwt.RegisteredClaims
  46. }
  47. // DownloadLink builds a URL to download the object.
  48. func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
  49. return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
  50. }
  51. // UploadLink builds a URL to upload the object.
  52. func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
  53. return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10))
  54. }
  55. // VerifyLink builds a URL for verifying the object.
  56. func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
  57. return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")
  58. }
  59. // CheckAcceptMediaType checks if the client accepts the LFS media type.
  60. func CheckAcceptMediaType(ctx *context.Context) {
  61. mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
  62. if mediaParts[0] != lfs_module.MediaType {
  63. log.Trace("Calling a LFS method without accepting the correct media type: %s", lfs_module.MediaType)
  64. writeStatus(ctx, http.StatusUnsupportedMediaType)
  65. return
  66. }
  67. }
  68. var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`)
  69. // DownloadHandler gets the content from the content store
  70. func DownloadHandler(ctx *context.Context) {
  71. rc := getRequestContext(ctx)
  72. p := lfs_module.Pointer{Oid: ctx.Params("oid")}
  73. meta := getAuthenticatedMeta(ctx, rc, p, false)
  74. if meta == nil {
  75. return
  76. }
  77. // Support resume download using Range header
  78. var fromByte, toByte int64
  79. toByte = meta.Size - 1
  80. statusCode := http.StatusOK
  81. if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
  82. match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr)
  83. if len(match) > 1 {
  84. statusCode = http.StatusPartialContent
  85. fromByte, _ = strconv.ParseInt(match[1], 10, 32)
  86. if fromByte >= meta.Size {
  87. writeStatus(ctx, http.StatusRequestedRangeNotSatisfiable)
  88. return
  89. }
  90. if match[2] != "" {
  91. _toByte, _ := strconv.ParseInt(match[2], 10, 32)
  92. if _toByte >= fromByte && _toByte < toByte {
  93. toByte = _toByte
  94. }
  95. }
  96. ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, toByte, meta.Size-fromByte))
  97. ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Range")
  98. }
  99. }
  100. contentStore := lfs_module.NewContentStore()
  101. content, err := contentStore.Get(meta.Pointer)
  102. if err != nil {
  103. writeStatus(ctx, http.StatusNotFound)
  104. return
  105. }
  106. defer content.Close()
  107. if fromByte > 0 {
  108. _, err = content.Seek(fromByte, io.SeekStart)
  109. if err != nil {
  110. log.Error("Whilst trying to read LFS OID[%s]: Unable to seek to %d Error: %v", meta.Oid, fromByte, err)
  111. writeStatus(ctx, http.StatusInternalServerError)
  112. return
  113. }
  114. }
  115. contentLength := toByte + 1 - fromByte
  116. ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
  117. ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
  118. filename := ctx.Params("filename")
  119. if len(filename) > 0 {
  120. decodedFilename, err := base64.RawURLEncoding.DecodeString(filename)
  121. if err == nil {
  122. ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"")
  123. ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
  124. }
  125. }
  126. ctx.Resp.WriteHeader(statusCode)
  127. if written, err := io.CopyN(ctx.Resp, content, contentLength); err != nil {
  128. log.Error("Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v", meta.Oid, written, err)
  129. }
  130. }
  131. // BatchHandler provides the batch api
  132. func BatchHandler(ctx *context.Context) {
  133. var br lfs_module.BatchRequest
  134. if err := decodeJSON(ctx.Req, &br); err != nil {
  135. log.Trace("Unable to decode BATCH request vars: Error: %v", err)
  136. writeStatus(ctx, http.StatusBadRequest)
  137. return
  138. }
  139. var isUpload bool
  140. if br.Operation == "upload" {
  141. isUpload = true
  142. } else if br.Operation == "download" {
  143. isUpload = false
  144. } else {
  145. log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
  146. writeStatus(ctx, http.StatusBadRequest)
  147. return
  148. }
  149. rc := getRequestContext(ctx)
  150. repository := getAuthenticatedRepository(ctx, rc, isUpload)
  151. if repository == nil {
  152. return
  153. }
  154. contentStore := lfs_module.NewContentStore()
  155. var responseObjects []*lfs_module.ObjectResponse
  156. for _, p := range br.Objects {
  157. if !p.IsValid() {
  158. responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
  159. Code: http.StatusUnprocessableEntity,
  160. Message: "Oid or size are invalid",
  161. }))
  162. continue
  163. }
  164. exists, err := contentStore.Exists(p)
  165. if err != nil {
  166. log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, rc.User, rc.Repo, err)
  167. writeStatus(ctx, http.StatusInternalServerError)
  168. return
  169. }
  170. meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
  171. if err != nil && err != git_model.ErrLFSObjectNotExist {
  172. log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
  173. writeStatus(ctx, http.StatusInternalServerError)
  174. return
  175. }
  176. if meta != nil && p.Size != meta.Size {
  177. responseObjects = append(responseObjects, buildObjectResponse(rc, p, false, false, &lfs_module.ObjectError{
  178. Code: http.StatusUnprocessableEntity,
  179. Message: fmt.Sprintf("Object %s is not %d bytes", p.Oid, p.Size),
  180. }))
  181. continue
  182. }
  183. var responseObject *lfs_module.ObjectResponse
  184. if isUpload {
  185. var err *lfs_module.ObjectError
  186. if !exists && setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize {
  187. err = &lfs_module.ObjectError{
  188. Code: http.StatusUnprocessableEntity,
  189. Message: fmt.Sprintf("Size must be less than or equal to %d", setting.LFS.MaxFileSize),
  190. }
  191. }
  192. if exists && meta == nil {
  193. accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
  194. if err != nil {
  195. log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
  196. writeStatus(ctx, http.StatusInternalServerError)
  197. return
  198. }
  199. if accessible {
  200. _, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
  201. if err != nil {
  202. log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
  203. writeStatus(ctx, http.StatusInternalServerError)
  204. return
  205. }
  206. } else {
  207. exists = false
  208. }
  209. }
  210. responseObject = buildObjectResponse(rc, p, false, !exists, err)
  211. } else {
  212. var err *lfs_module.ObjectError
  213. if !exists || meta == nil {
  214. err = &lfs_module.ObjectError{
  215. Code: http.StatusNotFound,
  216. Message: http.StatusText(http.StatusNotFound),
  217. }
  218. }
  219. responseObject = buildObjectResponse(rc, p, true, false, err)
  220. }
  221. responseObjects = append(responseObjects, responseObject)
  222. }
  223. respobj := &lfs_module.BatchResponse{Objects: responseObjects}
  224. ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
  225. enc := json.NewEncoder(ctx.Resp)
  226. if err := enc.Encode(respobj); err != nil {
  227. log.Error("Failed to encode representation as json. Error: %v", err)
  228. }
  229. }
  230. // UploadHandler receives data from the client and puts it into the content store
  231. func UploadHandler(ctx *context.Context) {
  232. rc := getRequestContext(ctx)
  233. p := lfs_module.Pointer{Oid: ctx.Params("oid")}
  234. var err error
  235. if p.Size, err = strconv.ParseInt(ctx.Params("size"), 10, 64); err != nil {
  236. writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
  237. }
  238. if !p.IsValid() {
  239. log.Trace("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
  240. writeStatus(ctx, http.StatusUnprocessableEntity)
  241. return
  242. }
  243. repository := getAuthenticatedRepository(ctx, rc, true)
  244. if repository == nil {
  245. return
  246. }
  247. contentStore := lfs_module.NewContentStore()
  248. exists, err := contentStore.Exists(p)
  249. if err != nil {
  250. log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, err)
  251. writeStatus(ctx, http.StatusInternalServerError)
  252. return
  253. }
  254. uploadOrVerify := func() error {
  255. if exists {
  256. accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
  257. if err != nil {
  258. log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
  259. return err
  260. }
  261. if !accessible {
  262. // The file exists but the user has no access to it.
  263. // The upload gets verified by hashing and size comparison to prove access to it.
  264. hash := sha256.New()
  265. written, err := io.Copy(hash, ctx.Req.Body)
  266. if err != nil {
  267. log.Error("Error creating hash. Error: %v", err)
  268. return err
  269. }
  270. if written != p.Size {
  271. return lfs_module.ErrSizeMismatch
  272. }
  273. if hex.EncodeToString(hash.Sum(nil)) != p.Oid {
  274. return lfs_module.ErrHashMismatch
  275. }
  276. }
  277. } else if err := contentStore.Put(p, ctx.Req.Body); err != nil {
  278. log.Error("Error putting LFS MetaObject [%s] into content store. Error: %v", p.Oid, err)
  279. return err
  280. }
  281. _, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
  282. return err
  283. }
  284. defer ctx.Req.Body.Close()
  285. if err := uploadOrVerify(); err != nil {
  286. if errors.Is(err, lfs_module.ErrSizeMismatch) || errors.Is(err, lfs_module.ErrHashMismatch) {
  287. log.Error("Upload does not match LFS MetaObject [%s]. Error: %v", p.Oid, err)
  288. writeStatusMessage(ctx, http.StatusUnprocessableEntity, err.Error())
  289. } else {
  290. log.Error("Error whilst uploadOrVerify LFS OID[%s]: %v", p.Oid, err)
  291. writeStatus(ctx, http.StatusInternalServerError)
  292. }
  293. if _, err = git_model.RemoveLFSMetaObjectByOid(ctx, repository.ID, p.Oid); err != nil {
  294. log.Error("Error whilst removing MetaObject for LFS OID[%s]: %v", p.Oid, err)
  295. }
  296. return
  297. }
  298. writeStatus(ctx, http.StatusOK)
  299. }
  300. // VerifyHandler verify oid and its size from the content store
  301. func VerifyHandler(ctx *context.Context) {
  302. var p lfs_module.Pointer
  303. if err := decodeJSON(ctx.Req, &p); err != nil {
  304. writeStatus(ctx, http.StatusUnprocessableEntity)
  305. return
  306. }
  307. rc := getRequestContext(ctx)
  308. meta := getAuthenticatedMeta(ctx, rc, p, true)
  309. if meta == nil {
  310. return
  311. }
  312. contentStore := lfs_module.NewContentStore()
  313. ok, err := contentStore.Verify(meta.Pointer)
  314. status := http.StatusOK
  315. if err != nil {
  316. log.Error("Error whilst verifying LFS OID[%s]: %v", p.Oid, err)
  317. status = http.StatusInternalServerError
  318. } else if !ok {
  319. status = http.StatusNotFound
  320. }
  321. writeStatus(ctx, status)
  322. }
  323. func decodeJSON(req *http.Request, v any) error {
  324. defer req.Body.Close()
  325. dec := json.NewDecoder(req.Body)
  326. return dec.Decode(v)
  327. }
  328. func getRequestContext(ctx *context.Context) *requestContext {
  329. return &requestContext{
  330. User: ctx.Params("username"),
  331. Repo: strings.TrimSuffix(ctx.Params("reponame"), ".git"),
  332. Authorization: ctx.Req.Header.Get("Authorization"),
  333. }
  334. }
  335. func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {
  336. if !p.IsValid() {
  337. log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
  338. writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")
  339. return nil
  340. }
  341. repository := getAuthenticatedRepository(ctx, rc, requireWrite)
  342. if repository == nil {
  343. return nil
  344. }
  345. meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
  346. if err != nil {
  347. log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
  348. writeStatus(ctx, http.StatusNotFound)
  349. return nil
  350. }
  351. return meta
  352. }
  353. func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {
  354. repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)
  355. if err != nil {
  356. log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
  357. writeStatus(ctx, http.StatusNotFound)
  358. return nil
  359. }
  360. if !authenticate(ctx, repository, rc.Authorization, false, requireWrite) {
  361. requireAuth(ctx)
  362. return nil
  363. }
  364. if requireWrite {
  365. context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
  366. } else {
  367. context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
  368. }
  369. if ctx.Written() {
  370. return nil
  371. }
  372. return repository
  373. }
  374. func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, download, upload bool, err *lfs_module.ObjectError) *lfs_module.ObjectResponse {
  375. rep := &lfs_module.ObjectResponse{Pointer: pointer}
  376. if err != nil {
  377. rep.Error = err
  378. } else {
  379. rep.Actions = make(map[string]*lfs_module.Link)
  380. header := make(map[string]string)
  381. if len(rc.Authorization) > 0 {
  382. header["Authorization"] = rc.Authorization
  383. }
  384. if download {
  385. var link *lfs_module.Link
  386. if setting.LFS.Storage.MinioConfig.ServeDirect {
  387. // If we have a signed url (S3, object storage), redirect to this directly.
  388. u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid)
  389. if u != nil && err == nil {
  390. // Presigned url does not need the Authorization header
  391. // https://github.com/go-gitea/gitea/issues/21525
  392. delete(header, "Authorization")
  393. link = &lfs_module.Link{Href: u.String(), Header: header}
  394. }
  395. }
  396. if link == nil {
  397. link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header}
  398. }
  399. rep.Actions["download"] = link
  400. }
  401. if upload {
  402. rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header}
  403. verifyHeader := make(map[string]string)
  404. for key, value := range header {
  405. verifyHeader[key] = value
  406. }
  407. // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
  408. verifyHeader["Accept"] = lfs_module.MediaType
  409. rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
  410. }
  411. }
  412. return rep
  413. }
  414. func writeStatus(ctx *context.Context, status int) {
  415. writeStatusMessage(ctx, status, http.StatusText(status))
  416. }
  417. func writeStatusMessage(ctx *context.Context, status int, message string) {
  418. ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType)
  419. ctx.Resp.WriteHeader(status)
  420. er := lfs_module.ErrorResponse{Message: message}
  421. enc := json.NewEncoder(ctx.Resp)
  422. if err := enc.Encode(er); err != nil {
  423. log.Error("Failed to encode error response as json. Error: %v", err)
  424. }
  425. }
  426. // authenticate uses the authorization string to determine whether
  427. // or not to proceed. This server assumes an HTTP Basic auth format.
  428. func authenticate(ctx *context.Context, repository *repo_model.Repository, authorization string, requireSigned, requireWrite bool) bool {
  429. accessMode := perm.AccessModeRead
  430. if requireWrite {
  431. accessMode = perm.AccessModeWrite
  432. }
  433. if ctx.Data["IsActionsToken"] == true {
  434. taskID := ctx.Data["ActionsTaskID"].(int64)
  435. task, err := actions_model.GetTaskByID(ctx, taskID)
  436. if err != nil {
  437. log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err)
  438. return false
  439. }
  440. if task.RepoID != repository.ID {
  441. return false
  442. }
  443. if task.IsForkPullRequest {
  444. return accessMode <= perm.AccessModeRead
  445. }
  446. return accessMode <= perm.AccessModeWrite
  447. }
  448. // ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
  449. perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
  450. if err != nil {
  451. log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
  452. return false
  453. }
  454. canRead := perm.CanAccess(accessMode, unit.TypeCode)
  455. if canRead && (!requireSigned || ctx.IsSigned) {
  456. return true
  457. }
  458. user, err := parseToken(ctx, authorization, repository, accessMode)
  459. if err != nil {
  460. // Most of these are Warn level - the true internal server errors are logged in parseToken already
  461. log.Warn("Authentication failure for provided token with Error: %v", err)
  462. return false
  463. }
  464. ctx.Doer = user
  465. return true
  466. }
  467. func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
  468. if !strings.Contains(tokenSHA, ".") {
  469. return nil, nil
  470. }
  471. token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
  472. if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
  473. return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
  474. }
  475. return setting.LFS.JWTSecretBytes, nil
  476. })
  477. if err != nil {
  478. return nil, nil
  479. }
  480. claims, claimsOk := token.Claims.(*Claims)
  481. if !token.Valid || !claimsOk {
  482. return nil, fmt.Errorf("invalid token claim")
  483. }
  484. if claims.RepoID != target.ID {
  485. return nil, fmt.Errorf("invalid token claim")
  486. }
  487. if mode == perm.AccessModeWrite && claims.Op != "upload" {
  488. return nil, fmt.Errorf("invalid token claim")
  489. }
  490. u, err := user_model.GetUserByID(ctx, claims.UserID)
  491. if err != nil {
  492. log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
  493. return nil, err
  494. }
  495. return u, nil
  496. }
  497. func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
  498. if authorization == "" {
  499. return nil, fmt.Errorf("no token")
  500. }
  501. parts := strings.SplitN(authorization, " ", 2)
  502. if len(parts) != 2 {
  503. return nil, fmt.Errorf("no token")
  504. }
  505. tokenSHA := parts[1]
  506. switch strings.ToLower(parts[0]) {
  507. case "bearer":
  508. fallthrough
  509. case "token":
  510. return handleLFSToken(ctx, tokenSHA, target, mode)
  511. }
  512. return nil, fmt.Errorf("token not found")
  513. }
  514. func requireAuth(ctx *context.Context) {
  515. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
  516. writeStatus(ctx, http.StatusUnauthorized)
  517. }