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 17KB

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