2020-04-05 06:20:50 +00:00
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2016-12-26 01:16:37 +00:00
package lfs
import (
"encoding/base64"
"fmt"
"io"
"net/http"
2017-11-08 13:04:19 +00:00
"path"
2016-12-26 01:16:37 +00:00
"regexp"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2020-09-08 15:45:10 +00:00
"code.gitea.io/gitea/modules/storage"
2017-11-08 13:04:19 +00:00
2021-07-24 15:13:56 +00:00
"github.com/golang-jwt/jwt"
2021-03-01 21:08:10 +00:00
jsoniter "github.com/json-iterator/go"
2016-12-26 01:16:37 +00:00
)
const (
2019-05-24 21:21:00 +00:00
metaMediaType = "application/vnd.git-lfs+json"
2016-12-26 01:16:37 +00:00
)
// RequestVars contain variables from the HTTP request. Variables from routing, json body decoding, and
// some headers are stored.
type RequestVars struct {
Oid string
Size int64
User string
Password string
Repo string
Authorization string
}
// BatchVars contains multiple RequestVars processed in one batch operation.
// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
type BatchVars struct {
Transfers [ ] string ` json:"transfers,omitempty" `
Operation string ` json:"operation" `
Objects [ ] * RequestVars ` json:"objects" `
}
// BatchResponse contains multiple object metadata Representation structures
// for use with the batch API.
type BatchResponse struct {
Transfer string ` json:"transfer,omitempty" `
Objects [ ] * Representation ` json:"objects" `
}
2017-03-15 00:52:01 +00:00
// Representation is object metadata as seen by clients of the lfs server.
2016-12-26 01:16:37 +00:00
type Representation struct {
Oid string ` json:"oid" `
Size int64 ` json:"size" `
Actions map [ string ] * link ` json:"actions" `
Error * ObjectError ` json:"error,omitempty" `
}
// ObjectError defines the JSON structure returned to the client in case of an error
type ObjectError struct {
Code int ` json:"code" `
Message string ` json:"message" `
}
2020-03-09 19:56:18 +00:00
// Claims is a JWT Token Claims
type Claims struct {
RepoID int64
Op string
UserID int64
jwt . StandardClaims
}
2016-12-26 01:16:37 +00:00
// ObjectLink builds a URL linking to the object.
func ( v * RequestVars ) ObjectLink ( ) string {
2017-12-08 12:21:37 +00:00
return setting . AppURL + path . Join ( v . User , v . Repo + ".git" , "info/lfs/objects" , v . Oid )
2017-11-08 13:04:19 +00:00
}
// VerifyLink builds a URL for verifying the object.
func ( v * RequestVars ) VerifyLink ( ) string {
2017-12-08 12:21:37 +00:00
return setting . AppURL + path . Join ( v . User , v . Repo + ".git" , "info/lfs/verify" )
2016-12-26 01:16:37 +00:00
}
// link provides a structure used to build a hypermedia representation of an HTTP link.
type link struct {
Href string ` json:"href" `
Header map [ string ] string ` json:"header,omitempty" `
ExpiresAt time . Time ` json:"expires_at,omitempty" `
}
2018-05-01 01:46:04 +00:00
var oidRegExp = regexp . MustCompile ( ` ^[A-Fa-f0-9]+$ ` )
2018-07-19 15:39:19 +00:00
func isOidValid ( oid string ) bool {
return oidRegExp . MatchString ( oid )
}
2016-12-26 01:16:37 +00:00
// ObjectOidHandler is the main request routing entry point into LFS server functions
func ObjectOidHandler ( ctx * context . Context ) {
if ! setting . LFS . StartServer {
2020-03-09 19:56:18 +00:00
log . Debug ( "Attempt to access LFS server but LFS server is disabled" )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
if ctx . Req . Method == "GET" || ctx . Req . Method == "HEAD" {
if MetaMatcher ( ctx . Req ) {
2017-10-30 12:11:56 +00:00
getMetaHandler ( ctx )
2016-12-26 01:16:37 +00:00
return
}
2019-05-24 21:21:00 +00:00
getContentHandler ( ctx )
return
} else if ctx . Req . Method == "PUT" {
2016-12-26 01:16:37 +00:00
PutHandler ( ctx )
return
}
2020-03-09 19:56:18 +00:00
log . Warn ( "Unhandled LFS method: %s for %s/%s OID[%s]" , ctx . Req . Method , ctx . Params ( "username" ) , ctx . Params ( "reponame" ) , ctx . Params ( "oid" ) )
writeStatus ( ctx , 404 )
2016-12-26 01:16:37 +00:00
}
2017-10-30 12:11:56 +00:00
func getAuthenticatedRepoAndMeta ( ctx * context . Context , rv * RequestVars , requireWrite bool ) ( * models . LFSMetaObject , * models . Repository ) {
2018-07-19 15:39:19 +00:00
if ! isOidValid ( rv . Oid ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Attempt to access invalid LFS OID[%s] in %s/%s" , rv . Oid , rv . User , rv . Repo )
2018-07-19 15:39:19 +00:00
writeStatus ( ctx , 404 )
return nil , nil
}
2017-12-02 07:34:39 +00:00
repository , err := models . GetRepositoryByOwnerAndName ( rv . User , rv . Repo )
2016-12-26 01:16:37 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , rv . User , rv . Repo , err )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
2017-10-30 12:11:56 +00:00
return nil , nil
2016-12-26 01:16:37 +00:00
}
2017-10-30 12:11:56 +00:00
if ! authenticate ( ctx , repository , rv . Authorization , requireWrite ) {
requireAuth ( ctx )
return nil , nil
}
2016-12-26 01:16:37 +00:00
2017-10-30 12:11:56 +00:00
meta , err := repository . GetLFSMetaObjectByOid ( rv . Oid )
2016-12-26 01:16:37 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to get LFS OID[%s] Error: %v" , rv . Oid , err )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
2017-10-30 12:11:56 +00:00
return nil , nil
2016-12-26 01:16:37 +00:00
}
2017-10-30 12:11:56 +00:00
return meta , repository
}
// getContentHandler gets the content from the content store
func getContentHandler ( ctx * context . Context ) {
rv := unpack ( ctx )
meta , _ := getAuthenticatedRepoAndMeta ( ctx , rv , false )
if meta == nil {
2020-03-09 19:56:18 +00:00
// Status already written in getAuthenticatedRepoAndMeta
2016-12-26 01:16:37 +00:00
return
}
// Support resume download using Range header
2020-05-11 08:37:59 +00:00
var fromByte , toByte int64
toByte = meta . Size - 1
2016-12-26 01:16:37 +00:00
statusCode := 200
if rangeHdr := ctx . Req . Header . Get ( "Range" ) ; rangeHdr != "" {
2020-05-11 08:37:59 +00:00
regex := regexp . MustCompile ( ` bytes=(\d+)\-(\d*).* ` )
2016-12-26 01:16:37 +00:00
match := regex . FindStringSubmatch ( rangeHdr )
2019-06-12 19:41:28 +00:00
if len ( match ) > 1 {
2016-12-26 01:16:37 +00:00
statusCode = 206
fromByte , _ = strconv . ParseInt ( match [ 1 ] , 10 , 32 )
2020-05-11 08:37:59 +00:00
2021-04-06 19:25:31 +00:00
if fromByte >= meta . Size {
writeStatus ( ctx , http . StatusRequestedRangeNotSatisfiable )
return
}
2020-05-11 08:37:59 +00:00
if match [ 2 ] != "" {
_toByte , _ := strconv . ParseInt ( match [ 2 ] , 10 , 32 )
if _toByte >= fromByte && _toByte < toByte {
toByte = _toByte
}
}
ctx . Resp . Header ( ) . Set ( "Content-Range" , fmt . Sprintf ( "bytes %d-%d/%d" , fromByte , toByte , meta . Size - fromByte ) )
2020-08-13 17:18:18 +00:00
ctx . Resp . Header ( ) . Set ( "Access-Control-Expose-Headers" , "Content-Range" )
2016-12-26 01:16:37 +00:00
}
}
2020-09-08 15:45:10 +00:00
contentStore := & ContentStore { ObjectStorage : storage . LFS }
2021-04-06 19:25:31 +00:00
content , err := contentStore . Get ( meta )
2016-12-26 01:16:37 +00:00
if err != nil {
2021-04-06 19:25:31 +00:00
// Errors are logged in contentStore.Get
writeStatus ( ctx , http . StatusNotFound )
2016-12-26 01:16:37 +00:00
return
}
2020-03-09 19:56:18 +00:00
defer content . Close ( )
2016-12-26 01:16:37 +00:00
2021-04-06 19:25:31 +00:00
if fromByte > 0 {
_ , err = content . Seek ( fromByte , io . SeekStart )
if err != nil {
log . Error ( "Whilst trying to read LFS OID[%s]: Unable to seek to %d Error: %v" , meta . Oid , fromByte , err )
writeStatus ( ctx , http . StatusInternalServerError )
return
}
}
2020-05-11 08:37:59 +00:00
contentLength := toByte + 1 - fromByte
ctx . Resp . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( contentLength , 10 ) )
2016-12-26 01:16:37 +00:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , "application/octet-stream" )
filename := ctx . Params ( "filename" )
if len ( filename ) > 0 {
decodedFilename , err := base64 . RawURLEncoding . DecodeString ( filename )
if err == nil {
ctx . Resp . Header ( ) . Set ( "Content-Disposition" , "attachment; filename=\"" + string ( decodedFilename ) + "\"" )
2020-08-13 17:18:18 +00:00
ctx . Resp . Header ( ) . Set ( "Access-Control-Expose-Headers" , "Content-Disposition" )
2016-12-26 01:16:37 +00:00
}
}
ctx . Resp . WriteHeader ( statusCode )
2020-05-11 08:37:59 +00:00
if written , err := io . CopyN ( ctx . Resp , content , contentLength ) ; err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v" , meta . Oid , written , err )
}
2016-12-26 01:16:37 +00:00
logRequest ( ctx . Req , statusCode )
}
2017-10-30 12:11:56 +00:00
// getMetaHandler retrieves metadata about the object
func getMetaHandler ( ctx * context . Context ) {
2016-12-26 01:16:37 +00:00
rv := unpack ( ctx )
2017-10-30 12:11:56 +00:00
meta , _ := getAuthenticatedRepoAndMeta ( ctx , rv , false )
if meta == nil {
2020-03-09 19:56:18 +00:00
// Status already written in getAuthenticatedRepoAndMeta
2016-12-26 01:16:37 +00:00
return
}
ctx . Resp . Header ( ) . Set ( "Content-Type" , metaMediaType )
if ctx . Req . Method == "GET" {
2021-03-01 21:08:10 +00:00
json := jsoniter . ConfigCompatibleWithStandardLibrary
2016-12-26 01:16:37 +00:00
enc := json . NewEncoder ( ctx . Resp )
2020-03-09 19:56:18 +00:00
if err := enc . Encode ( Represent ( rv , meta , true , false ) ) ; err != nil {
log . Error ( "Failed to encode representation as json. Error: %v" , err )
}
2016-12-26 01:16:37 +00:00
}
logRequest ( ctx . Req , 200 )
}
// PostHandler instructs the client how to upload data
func PostHandler ( ctx * context . Context ) {
if ! setting . LFS . StartServer {
2020-03-09 19:56:18 +00:00
log . Debug ( "Attempt to access LFS server but LFS server is disabled" )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
if ! MetaMatcher ( ctx . Req ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Attempt to POST without accepting the correct media type: %s" , metaMediaType )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 400 )
return
}
rv := unpack ( ctx )
2017-12-02 07:34:39 +00:00
repository , err := models . GetRepositoryByOwnerAndName ( rv . User , rv . Repo )
2016-12-26 01:16:37 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , rv . User , rv . Repo , err )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
if ! authenticate ( ctx , repository , rv . Authorization , true ) {
requireAuth ( ctx )
2018-05-01 01:46:04 +00:00
return
}
2018-07-19 15:39:19 +00:00
if ! isOidValid ( rv . Oid ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Invalid LFS OID[%s] attempt to POST in %s/%s" , rv . Oid , rv . User , rv . Repo )
2018-05-01 01:46:04 +00:00
writeStatus ( ctx , 404 )
return
2016-12-26 01:16:37 +00:00
}
2020-02-28 04:46:57 +00:00
if setting . LFS . MaxFileSize > 0 && rv . Size > setting . LFS . MaxFileSize {
2020-03-09 19:56:18 +00:00
log . Info ( "Denied LFS OID[%s] upload of size %d to %s/%s because of LFS_MAX_FILE_SIZE=%d" , rv . Oid , rv . Size , rv . User , rv . Repo , setting . LFS . MaxFileSize )
2020-02-28 04:46:57 +00:00
writeStatus ( ctx , 413 )
return
}
2016-12-26 01:16:37 +00:00
meta , err := models . NewLFSMetaObject ( & models . LFSMetaObject { Oid : rv . Oid , Size : rv . Size , RepositoryID : repository . ID } )
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v" , rv . Oid , rv . Size , rv . User , rv . Repo , err )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
ctx . Resp . Header ( ) . Set ( "Content-Type" , metaMediaType )
sentStatus := 202
2020-09-08 15:45:10 +00:00
contentStore := & ContentStore { ObjectStorage : storage . LFS }
exist , err := contentStore . Exists ( meta )
if err != nil {
log . Error ( "Unable to check if LFS OID[%s] exist on %s / %s. Error: %v" , rv . Oid , rv . User , rv . Repo , err )
writeStatus ( ctx , 500 )
return
}
if meta . Existing && exist {
2016-12-26 01:16:37 +00:00
sentStatus = 200
}
ctx . Resp . WriteHeader ( sentStatus )
2021-03-01 21:08:10 +00:00
json := jsoniter . ConfigCompatibleWithStandardLibrary
2016-12-26 01:16:37 +00:00
enc := json . NewEncoder ( ctx . Resp )
2020-03-09 19:56:18 +00:00
if err := enc . Encode ( Represent ( rv , meta , meta . Existing , true ) ) ; err != nil {
log . Error ( "Failed to encode representation as json. Error: %v" , err )
}
2016-12-26 01:16:37 +00:00
logRequest ( ctx . Req , sentStatus )
}
// BatchHandler provides the batch api
func BatchHandler ( ctx * context . Context ) {
if ! setting . LFS . StartServer {
2020-03-09 19:56:18 +00:00
log . Debug ( "Attempt to access LFS server but LFS server is disabled" )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
if ! MetaMatcher ( ctx . Req ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Attempt to BATCH without accepting the correct media type: %s" , metaMediaType )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 400 )
return
}
bv := unpackbatch ( ctx )
var responseObjects [ ] * Representation
// Create a response object
for _ , object := range bv . Objects {
2018-07-19 15:39:19 +00:00
if ! isOidValid ( object . Oid ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Invalid LFS OID[%s] attempt to BATCH in %s/%s" , object . Oid , object . User , object . Repo )
2018-07-19 15:39:19 +00:00
continue
}
2017-12-02 07:34:39 +00:00
repository , err := models . GetRepositoryByOwnerAndName ( object . User , object . Repo )
2016-12-26 01:16:37 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , object . User , object . Repo , err )
2016-12-26 01:16:37 +00:00
writeStatus ( ctx , 404 )
return
}
requireWrite := false
if bv . Operation == "upload" {
requireWrite = true
}
if ! authenticate ( ctx , repository , object . Authorization , requireWrite ) {
requireAuth ( ctx )
return
}
2020-09-08 15:45:10 +00:00
contentStore := & ContentStore { ObjectStorage : storage . LFS }
2017-10-30 12:11:56 +00:00
meta , err := repository . GetLFSMetaObjectByOid ( object . Oid )
2020-09-08 15:45:10 +00:00
if err == nil { // Object is found and exists
exist , err := contentStore . Exists ( meta )
if err != nil {
log . Error ( "Unable to check if LFS OID[%s] exist on %s / %s. Error: %v" , object . Oid , object . User , object . Repo , err )
writeStatus ( ctx , 500 )
return
}
if exist {
responseObjects = append ( responseObjects , Represent ( object , meta , true , false ) )
continue
}
2016-12-26 01:16:37 +00:00
}
2020-03-03 20:57:27 +00:00
if requireWrite && setting . LFS . MaxFileSize > 0 && object . Size > setting . LFS . MaxFileSize {
2020-03-09 19:56:18 +00:00
log . Info ( "Denied LFS OID[%s] upload of size %d to %s/%s because of LFS_MAX_FILE_SIZE=%d" , object . Oid , object . Size , object . User , object . Repo , setting . LFS . MaxFileSize )
2020-03-03 20:57:27 +00:00
writeStatus ( ctx , 413 )
return
}
2018-07-19 15:39:19 +00:00
// Object is not found
meta , err = models . NewLFSMetaObject ( & models . LFSMetaObject { Oid : object . Oid , Size : object . Size , RepositoryID : repository . ID } )
if err == nil {
2020-09-08 15:45:10 +00:00
exist , err := contentStore . Exists ( meta )
if err != nil {
log . Error ( "Unable to check if LFS OID[%s] exist on %s / %s. Error: %v" , object . Oid , object . User , object . Repo , err )
writeStatus ( ctx , 500 )
return
}
responseObjects = append ( responseObjects , Represent ( object , meta , meta . Existing , ! exist ) )
2020-03-09 19:56:18 +00:00
} else {
log . Error ( "Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v" , object . Oid , object . Size , object . User , object . Repo , err )
2016-12-26 01:16:37 +00:00
}
}
ctx . Resp . Header ( ) . Set ( "Content-Type" , metaMediaType )
respobj := & BatchResponse { Objects : responseObjects }
2021-03-01 21:08:10 +00:00
json := jsoniter . ConfigCompatibleWithStandardLibrary
2016-12-26 01:16:37 +00:00
enc := json . NewEncoder ( ctx . Resp )
2020-03-09 19:56:18 +00:00
if err := enc . Encode ( respobj ) ; err != nil {
log . Error ( "Failed to encode representation as json. Error: %v" , err )
}
2016-12-26 01:16:37 +00:00
logRequest ( ctx . Req , 200 )
}
// PutHandler receives data from the client and puts it into the content store
func PutHandler ( ctx * context . Context ) {
rv := unpack ( ctx )
2017-10-30 12:11:56 +00:00
meta , repository := getAuthenticatedRepoAndMeta ( ctx , rv , true )
if meta == nil {
2020-03-09 19:56:18 +00:00
// Status already written in getAuthenticatedRepoAndMeta
2016-12-26 01:16:37 +00:00
return
}
2020-09-08 15:45:10 +00:00
contentStore := & ContentStore { ObjectStorage : storage . LFS }
2021-01-26 15:36:53 +00:00
defer ctx . Req . Body . Close ( )
if err := contentStore . Put ( meta , ctx . Req . Body ) ; err != nil {
2020-03-09 19:56:18 +00:00
// Put will log the error itself
2016-12-26 01:16:37 +00:00
ctx . Resp . WriteHeader ( 500 )
2020-03-09 19:56:18 +00:00
if err == errSizeMismatch || err == errHashMismatch {
fmt . Fprintf ( ctx . Resp , ` { "message":"%s"} ` , err )
} else {
fmt . Fprintf ( ctx . Resp , ` { "message":"Internal Server Error"} ` )
}
2019-10-28 18:31:55 +00:00
if _ , err = repository . RemoveLFSMetaObjectByOid ( rv . Oid ) ; err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Whilst removing metaobject for LFS OID[%s] due to preceding error there was another Error: %v" , rv . Oid , err )
2017-10-30 12:11:56 +00:00
}
2016-12-26 01:16:37 +00:00
return
}
logRequest ( ctx . Req , 200 )
}
2017-11-08 13:04:19 +00:00
// VerifyHandler verify oid and its size from the content store
func VerifyHandler ( ctx * context . Context ) {
if ! setting . LFS . StartServer {
2020-03-09 19:56:18 +00:00
log . Debug ( "Attempt to access LFS server but LFS server is disabled" )
2017-11-08 13:04:19 +00:00
writeStatus ( ctx , 404 )
return
}
2019-05-24 21:21:00 +00:00
if ! MetaMatcher ( ctx . Req ) {
2020-03-09 19:56:18 +00:00
log . Info ( "Attempt to VERIFY without accepting the correct media type: %s" , metaMediaType )
2017-11-08 13:04:19 +00:00
writeStatus ( ctx , 400 )
return
}
rv := unpack ( ctx )
meta , _ := getAuthenticatedRepoAndMeta ( ctx , rv , true )
if meta == nil {
2020-03-09 19:56:18 +00:00
// Status already written in getAuthenticatedRepoAndMeta
2017-11-08 13:04:19 +00:00
return
}
2020-09-08 15:45:10 +00:00
contentStore := & ContentStore { ObjectStorage : storage . LFS }
2017-11-08 13:04:19 +00:00
ok , err := contentStore . Verify ( meta )
if err != nil {
2020-03-09 19:56:18 +00:00
// Error will be logged in Verify
2017-11-08 13:04:19 +00:00
ctx . Resp . WriteHeader ( 500 )
2020-03-09 19:56:18 +00:00
fmt . Fprintf ( ctx . Resp , ` { "message":"Internal Server Error"} ` )
2017-11-08 13:04:19 +00:00
return
}
if ! ok {
writeStatus ( ctx , 422 )
return
}
logRequest ( ctx . Req , 200 )
}
2016-12-26 01:16:37 +00:00
// Represent takes a RequestVars and Meta and turns it into a Representation suitable
// for json encoding
func Represent ( rv * RequestVars , meta * models . LFSMetaObject , download , upload bool ) * Representation {
rep := & Representation {
Oid : meta . Oid ,
Size : meta . Size ,
Actions : make ( map [ string ] * link ) ,
}
header := make ( map [ string ] string )
if rv . Authorization == "" {
//https://github.com/github/git-lfs/issues/1088
header [ "Authorization" ] = "Authorization: Basic dummy"
} else {
header [ "Authorization" ] = rv . Authorization
}
if download {
rep . Actions [ "download" ] = & link { Href : rv . ObjectLink ( ) , Header : header }
}
if upload {
rep . Actions [ "upload" ] = & link { Href : rv . ObjectLink ( ) , Header : header }
}
2017-11-08 13:04:19 +00:00
if upload && ! download {
// Force client side verify action while gitea lacks proper server side verification
2019-05-24 21:21:00 +00:00
verifyHeader := make ( map [ string ] string )
for k , v := range header {
verifyHeader [ k ] = v
}
// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
verifyHeader [ "Accept" ] = metaMediaType
rep . Actions [ "verify" ] = & link { Href : rv . VerifyLink ( ) , Header : verifyHeader }
2017-11-08 13:04:19 +00:00
}
2016-12-26 01:16:37 +00:00
return rep
}
// MetaMatcher provides a mux.MatcherFunc that only allows requests that contain
// an Accept header with the metaMediaType
2021-01-26 15:36:53 +00:00
func MetaMatcher ( r * http . Request ) bool {
2016-12-26 01:16:37 +00:00
mediaParts := strings . Split ( r . Header . Get ( "Accept" ) , ";" )
mt := mediaParts [ 0 ]
return mt == metaMediaType
}
func unpack ( ctx * context . Context ) * RequestVars {
r := ctx . Req
rv := & RequestVars {
User : ctx . Params ( "username" ) ,
Repo : strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" ) ,
Oid : ctx . Params ( "oid" ) ,
Authorization : r . Header . Get ( "Authorization" ) ,
}
if r . Method == "POST" { // Maybe also check if +json
var p RequestVars
2021-01-26 15:36:53 +00:00
bodyReader := r . Body
2019-10-10 17:42:28 +00:00
defer bodyReader . Close ( )
2021-03-01 21:08:10 +00:00
json := jsoniter . ConfigCompatibleWithStandardLibrary
2019-10-10 17:42:28 +00:00
dec := json . NewDecoder ( bodyReader )
2016-12-26 01:16:37 +00:00
err := dec . Decode ( & p )
if err != nil {
2020-03-09 19:56:18 +00:00
// The error is logged as a WARN here because this may represent misbehaviour rather than a true error
log . Warn ( "Unable to decode POST request vars for LFS OID[%s] in %s/%s: Error: %v" , rv . Oid , rv . User , rv . Repo , err )
2016-12-26 01:16:37 +00:00
return rv
}
rv . Oid = p . Oid
rv . Size = p . Size
}
return rv
}
// TODO cheap hack, unify with unpack
func unpackbatch ( ctx * context . Context ) * BatchVars {
r := ctx . Req
var bv BatchVars
2021-01-26 15:36:53 +00:00
bodyReader := r . Body
2019-10-10 17:42:28 +00:00
defer bodyReader . Close ( )
2021-03-01 21:08:10 +00:00
json := jsoniter . ConfigCompatibleWithStandardLibrary
2019-10-10 17:42:28 +00:00
dec := json . NewDecoder ( bodyReader )
2016-12-26 01:16:37 +00:00
err := dec . Decode ( & bv )
if err != nil {
2020-03-09 19:56:18 +00:00
// The error is logged as a WARN here because this may represent misbehaviour rather than a true error
log . Warn ( "Unable to decode BATCH request vars in %s/%s: Error: %v" , ctx . Params ( "username" ) , strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" ) , err )
2016-12-26 01:16:37 +00:00
return & bv
}
for i := 0 ; i < len ( bv . Objects ) ; i ++ {
bv . Objects [ i ] . User = ctx . Params ( "username" )
bv . Objects [ i ] . Repo = strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" )
bv . Objects [ i ] . Authorization = r . Header . Get ( "Authorization" )
}
return & bv
}
func writeStatus ( ctx * context . Context , status int ) {
message := http . StatusText ( status )
mediaParts := strings . Split ( ctx . Req . Header . Get ( "Accept" ) , ";" )
mt := mediaParts [ 0 ]
if strings . HasSuffix ( mt , "+json" ) {
message = ` { "message":" ` + message + ` "} `
}
ctx . Resp . WriteHeader ( status )
fmt . Fprint ( ctx . Resp , message )
logRequest ( ctx . Req , status )
}
2021-01-26 15:36:53 +00:00
func logRequest ( r * http . Request , status int ) {
2016-12-26 01:16:37 +00:00
log . Debug ( "LFS request - Method: %s, URL: %s, Status %d" , r . Method , r . URL , status )
}
// authenticate uses the authorization string to determine whether
// or not to proceed. This server assumes an HTTP Basic auth format.
func authenticate ( ctx * context . Context , repository * models . Repository , authorization string , requireWrite bool ) bool {
accessMode := models . AccessModeRead
if requireWrite {
accessMode = models . AccessModeWrite
}
2019-01-31 13:36:57 +00:00
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
2018-11-28 11:26:14 +00:00
perm , err := models . GetUserRepoPermission ( repository , ctx . User )
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v" , ctx . User , repository )
2018-11-28 11:26:14 +00:00
return false
2016-12-26 01:16:37 +00:00
}
2019-01-31 13:36:57 +00:00
canRead := perm . CanAccess ( accessMode , models . UnitTypeCode )
if canRead {
return true
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
user , repo , opStr , err := parseToken ( authorization )
2016-12-26 01:16:37 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
// Most of these are Warn level - the true internal server errors are logged in parseToken already
log . Warn ( "Authentication failure for provided token with Error: %v" , err )
2016-12-26 01:16:37 +00:00
return false
}
2018-01-27 16:48:15 +00:00
ctx . User = user
if opStr == "basic" {
2018-11-28 11:26:14 +00:00
perm , err = models . GetUserRepoPermission ( repository , ctx . User )
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v" , ctx . User , repository )
2018-11-28 11:26:14 +00:00
return false
}
return perm . CanAccess ( accessMode , models . UnitTypeCode )
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
if repository . ID == repo . ID {
if requireWrite && opStr != "upload" {
return false
}
return true
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
return false
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
func parseToken ( authorization string ) ( * models . User , * models . Repository , string , error ) {
if authorization == "" {
return nil , nil , "unknown" , fmt . Errorf ( "No token" )
}
if strings . HasPrefix ( authorization , "Bearer " ) {
2020-03-09 19:56:18 +00:00
token , err := jwt . ParseWithClaims ( authorization [ 7 : ] , & Claims { } , func ( t * jwt . Token ) ( interface { } , error ) {
2018-01-27 16:48:15 +00:00
if _ , ok := t . Method . ( * jwt . SigningMethodHMAC ) ; ! ok {
return nil , fmt . Errorf ( "unexpected signing method: %v" , t . Header [ "alg" ] )
}
return setting . LFS . JWTSecretBytes , nil
} )
if err != nil {
2020-03-09 19:56:18 +00:00
// The error here is WARN level because it is caused by bad authorization rather than an internal server error
2018-01-27 16:48:15 +00:00
return nil , nil , "unknown" , err
2016-12-26 01:16:37 +00:00
}
2020-03-09 19:56:18 +00:00
claims , claimsOk := token . Claims . ( * Claims )
2018-01-27 16:48:15 +00:00
if ! token . Valid || ! claimsOk {
return nil , nil , "unknown" , fmt . Errorf ( "Token claim invalid" )
}
2020-03-09 19:56:18 +00:00
r , err := models . GetRepositoryByID ( claims . RepoID )
2018-01-27 16:48:15 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to GetRepositoryById[%d]: Error: %v" , claims . RepoID , err )
return nil , nil , claims . Op , err
2018-01-27 16:48:15 +00:00
}
2020-03-09 19:56:18 +00:00
u , err := models . GetUserByID ( claims . UserID )
2018-01-27 16:48:15 +00:00
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to GetUserById[%d]: Error: %v" , claims . UserID , err )
return nil , r , claims . Op , err
2018-01-27 16:48:15 +00:00
}
2020-03-09 19:56:18 +00:00
return u , r , claims . Op , nil
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
if strings . HasPrefix ( authorization , "Basic " ) {
c , err := base64 . StdEncoding . DecodeString ( strings . TrimPrefix ( authorization , "Basic " ) )
if err != nil {
return nil , nil , "basic" , err
}
cs := string ( c )
i := strings . IndexByte ( cs , ':' )
if i < 0 {
return nil , nil , "basic" , fmt . Errorf ( "Basic auth invalid" )
}
user , password := cs [ : i ] , cs [ i + 1 : ]
u , err := models . GetUserByName ( user )
if err != nil {
2020-03-09 19:56:18 +00:00
log . Error ( "Unable to GetUserByName[%d]: Error: %v" , user , err )
2018-01-27 16:48:15 +00:00
return nil , nil , "basic" , err
}
2019-01-30 21:18:54 +00:00
if ! u . IsPasswordSet ( ) || ! u . ValidatePassword ( password ) {
2018-01-27 16:48:15 +00:00
return nil , nil , "basic" , fmt . Errorf ( "Basic auth failed" )
}
return u , nil , "basic" , nil
2016-12-26 01:16:37 +00:00
}
2018-01-27 16:48:15 +00:00
return nil , nil , "unknown" , fmt . Errorf ( "Token not found" )
2016-12-26 01:16:37 +00:00
}
func requireAuth ( ctx * context . Context ) {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
writeStatus ( ctx , 401 )
}