summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/auth/user_form.go1
-rw-r--r--modules/base/tool.go13
-rw-r--r--modules/lfs/LICENSE20
-rw-r--r--modules/lfs/content_store.go94
-rw-r--r--modules/lfs/server.go549
-rw-r--r--modules/setting/setting.go90
6 files changed, 767 insertions, 0 deletions
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 0bdd7c1532..f020f365d2 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -23,6 +23,7 @@ type InstallForm struct {
AppName string `binding:"Required" locale:"install.app_name"`
RepoRootPath string `binding:"Required"`
+ LFSRootPath string
RunUser string `binding:"Required"`
Domain string `binding:"Required"`
SSHPort int
diff --git a/modules/base/tool.go b/modules/base/tool.go
index cb2eafd309..f4249f6d6d 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -12,6 +12,7 @@ import (
"encoding/hex"
"fmt"
"html/template"
+ "io"
"math"
"math/big"
"net/http"
@@ -103,6 +104,18 @@ func GetRandomString(n int) (string, error) {
return string(buffer), nil
}
+// GetRandomBytesAsBase64 generates a random base64 string from n bytes
+func GetRandomBytesAsBase64(n int) string {
+ bytes := make([]byte, 32)
+ _, err := io.ReadFull(rand.Reader, bytes)
+
+ if err != nil {
+ log.Fatal(4, "Error reading random bytes: %s", err)
+ }
+
+ return base64.RawURLEncoding.EncodeToString(bytes)
+}
+
func randomInt(max *big.Int) (int, error) {
rand, err := rand.Int(rand.Reader, max)
if err != nil {
diff --git a/modules/lfs/LICENSE b/modules/lfs/LICENSE
new file mode 100644
index 0000000000..0a94a80c8c
--- /dev/null
+++ b/modules/lfs/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2016 The Gitea Authors
+Copyright (c) GitHub, Inc. and LFS Test Server contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go
new file mode 100644
index 0000000000..2ca44512f1
--- /dev/null
+++ b/modules/lfs/content_store.go
@@ -0,0 +1,94 @@
+package lfs
+
+import (
+ "code.gitea.io/gitea/models"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+var (
+ errHashMismatch = errors.New("Content hash does not match OID")
+ errSizeMismatch = errors.New("Content size does not match")
+)
+
+// ContentStore provides a simple file system based storage.
+type ContentStore struct {
+ BasePath string
+}
+
+// Get takes a Meta object and retreives the content from the store, returning
+// it as an io.Reader. If fromByte > 0, the reader starts from that byte
+func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) {
+ path := filepath.Join(s.BasePath, transformKey(meta.Oid))
+
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ if fromByte > 0 {
+ _, err = f.Seek(fromByte, os.SEEK_CUR)
+ }
+ return f, err
+}
+
+// Put takes a Meta object and an io.Reader and writes the content to the store.
+func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
+ path := filepath.Join(s.BasePath, transformKey(meta.Oid))
+ tmpPath := path + ".tmp"
+
+ dir := filepath.Dir(path)
+ if err := os.MkdirAll(dir, 0750); err != nil {
+ return err
+ }
+
+ file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640)
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tmpPath)
+
+ hash := sha256.New()
+ hw := io.MultiWriter(hash, file)
+
+ written, err := io.Copy(hw, r)
+ if err != nil {
+ file.Close()
+ return err
+ }
+ file.Close()
+
+ if written != meta.Size {
+ return errSizeMismatch
+ }
+
+ shaStr := hex.EncodeToString(hash.Sum(nil))
+ if shaStr != meta.Oid {
+ return errHashMismatch
+ }
+
+ if err := os.Rename(tmpPath, path); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Exists returns true if the object exists in the content store.
+func (s *ContentStore) Exists(meta *models.LFSMetaObject) bool {
+ path := filepath.Join(s.BasePath, transformKey(meta.Oid))
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return false
+ }
+ return true
+}
+
+func transformKey(key string) string {
+ if len(key) < 5 {
+ return key
+ }
+
+ return filepath.Join(key[0:2], key[2:4], key[4:len(key)])
+}
diff --git a/modules/lfs/server.go b/modules/lfs/server.go
new file mode 100644
index 0000000000..f82cb70364
--- /dev/null
+++ b/modules/lfs/server.go
@@ -0,0 +1,549 @@
+package lfs
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "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"
+ "github.com/dgrijalva/jwt-go"
+ "gopkg.in/macaron.v1"
+)
+
+const (
+ contentMediaType = "application/vnd.git-lfs"
+ metaMediaType = contentMediaType + "+json"
+)
+
+// 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"`
+}
+
+// Representation is object medata as seen by clients of the lfs server.
+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"`
+}
+
+// ObjectLink builds a URL linking to the object.
+func (v *RequestVars) ObjectLink() string {
+ return fmt.Sprintf("%s%s/%s/info/lfs/objects/%s", setting.AppURL, v.User, v.Repo, v.Oid)
+}
+
+// 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"`
+}
+
+// ObjectOidHandler is the main request routing entry point into LFS server functions
+func ObjectOidHandler(ctx *context.Context) {
+
+ if !setting.LFS.StartServer {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if ctx.Req.Method == "GET" || ctx.Req.Method == "HEAD" {
+ if MetaMatcher(ctx.Req) {
+ GetMetaHandler(ctx)
+ return
+ }
+ if ContentMatcher(ctx.Req) || len(ctx.Params("filename")) > 0 {
+ GetContentHandler(ctx)
+ return
+ }
+ } else if ctx.Req.Method == "PUT" && ContentMatcher(ctx.Req) {
+ PutHandler(ctx)
+ return
+ }
+
+}
+
+// GetContentHandler gets the content from the content store
+func GetContentHandler(ctx *context.Context) {
+
+ rv := unpack(ctx)
+
+ meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ repository, err := models.GetRepositoryByID(meta.RepositoryID)
+
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !authenticate(ctx, repository, rv.Authorization, false) {
+ requireAuth(ctx)
+ return
+ }
+
+ // Support resume download using Range header
+ var fromByte int64
+ statusCode := 200
+ if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
+ regex := regexp.MustCompile(`bytes=(\d+)\-.*`)
+ match := regex.FindStringSubmatch(rangeHdr)
+ if match != nil && len(match) > 1 {
+ statusCode = 206
+ fromByte, _ = strconv.ParseInt(match[1], 10, 32)
+ ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, meta.Size-1, int64(meta.Size)-fromByte))
+ }
+ }
+
+ contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
+ content, err := contentStore.Get(meta, fromByte)
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10))
+ 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)+"\"")
+ }
+ }
+
+ ctx.Resp.WriteHeader(statusCode)
+ io.Copy(ctx.Resp, content)
+ content.Close()
+ logRequest(ctx.Req, statusCode)
+}
+
+// GetMetaHandler retrieves metadata about the object
+func GetMetaHandler(ctx *context.Context) {
+
+ rv := unpack(ctx)
+
+ meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ repository, err := models.GetRepositoryByID(meta.RepositoryID)
+
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !authenticate(ctx, repository, rv.Authorization, false) {
+ requireAuth(ctx)
+ return
+ }
+
+ ctx.Resp.Header().Set("Content-Type", metaMediaType)
+
+ if ctx.Req.Method == "GET" {
+ enc := json.NewEncoder(ctx.Resp)
+ enc.Encode(Represent(rv, meta, true, false))
+ }
+
+ logRequest(ctx.Req, 200)
+}
+
+// PostHandler instructs the client how to upload data
+func PostHandler(ctx *context.Context) {
+
+ if !setting.LFS.StartServer {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !MetaMatcher(ctx.Req) {
+ writeStatus(ctx, 400)
+ return
+ }
+
+ rv := unpack(ctx)
+
+ repositoryString := rv.User + "/" + rv.Repo
+ repository, err := models.GetRepositoryByRef(repositoryString)
+
+ if err != nil {
+ log.Debug("Could not find repository: %s - %s", repositoryString, err)
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !authenticate(ctx, repository, rv.Authorization, true) {
+ requireAuth(ctx)
+ }
+
+ meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: rv.Oid, Size: rv.Size, RepositoryID: repository.ID})
+
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ ctx.Resp.Header().Set("Content-Type", metaMediaType)
+
+ sentStatus := 202
+ contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
+ if meta.Existing && contentStore.Exists(meta) {
+ sentStatus = 200
+ }
+ ctx.Resp.WriteHeader(sentStatus)
+
+ enc := json.NewEncoder(ctx.Resp)
+ enc.Encode(Represent(rv, meta, meta.Existing, true))
+ logRequest(ctx.Req, sentStatus)
+}
+
+// BatchHandler provides the batch api
+func BatchHandler(ctx *context.Context) {
+
+ if !setting.LFS.StartServer {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !MetaMatcher(ctx.Req) {
+ writeStatus(ctx, 400)
+ return
+ }
+
+ bv := unpackbatch(ctx)
+
+ var responseObjects []*Representation
+
+ // Create a response object
+ for _, object := range bv.Objects {
+
+ repositoryString := object.User + "/" + object.Repo
+ repository, err := models.GetRepositoryByRef(repositoryString)
+
+ if err != nil {
+ log.Debug("Could not find repository: %s - %s", repositoryString, err)
+ writeStatus(ctx, 404)
+ return
+ }
+
+ requireWrite := false
+ if bv.Operation == "upload" {
+ requireWrite = true
+ }
+
+ if !authenticate(ctx, repository, object.Authorization, requireWrite) {
+ requireAuth(ctx)
+ return
+ }
+
+ meta, err := models.GetLFSMetaObjectByOid(object.Oid)
+
+ contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
+ if err == nil && contentStore.Exists(meta) { // Object is found and exists
+ responseObjects = append(responseObjects, Represent(object, meta, true, false))
+ continue
+ }
+
+ // Object is not found
+ meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Oid: object.Oid, Size: object.Size, RepositoryID: repository.ID})
+
+ if err == nil {
+ responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, true))
+ }
+ }
+
+ ctx.Resp.Header().Set("Content-Type", metaMediaType)
+
+ respobj := &BatchResponse{Objects: responseObjects}
+
+ enc := json.NewEncoder(ctx.Resp)
+ enc.Encode(respobj)
+ 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)
+
+ meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
+
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ repository, err := models.GetRepositoryByID(meta.RepositoryID)
+
+ if err != nil {
+ writeStatus(ctx, 404)
+ return
+ }
+
+ if !authenticate(ctx, repository, rv.Authorization, true) {
+ requireAuth(ctx)
+ return
+ }
+
+ contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
+ if err := contentStore.Put(meta, ctx.Req.Body().ReadCloser()); err != nil {
+ models.RemoveLFSMetaObjectByOid(rv.Oid)
+ ctx.Resp.WriteHeader(500)
+ fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
+ return
+ }
+
+ logRequest(ctx.Req, 200)
+}
+
+// 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)
+ header["Accept"] = contentMediaType
+
+ 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}
+ }
+
+ return rep
+}
+
+// ContentMatcher provides a mux.MatcherFunc that only allows requests that contain
+// an Accept header with the contentMediaType
+func ContentMatcher(r macaron.Request) bool {
+ mediaParts := strings.Split(r.Header.Get("Accept"), ";")
+ mt := mediaParts[0]
+ return mt == contentMediaType
+}
+
+// MetaMatcher provides a mux.MatcherFunc that only allows requests that contain
+// an Accept header with the metaMediaType
+func MetaMatcher(r macaron.Request) bool {
+ 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
+ dec := json.NewDecoder(r.Body().ReadCloser())
+ err := dec.Decode(&p)
+ if err != nil {
+ 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
+
+ dec := json.NewDecoder(r.Body().ReadCloser())
+ err := dec.Decode(&bv)
+ if err != nil {
+ 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)
+}
+
+func logRequest(r macaron.Request, status int) {
+ 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
+ }
+
+ if !repository.IsPrivate && !requireWrite {
+ return true
+ }
+
+ if ctx.IsSigned {
+ accessCheck, _ := models.HasAccess(ctx.User, repository, accessMode)
+ return accessCheck
+ }
+
+ if authorization == "" {
+ return false
+ }
+
+ if authenticateToken(repository, authorization, requireWrite) {
+ return true
+ }
+
+ if !strings.HasPrefix(authorization, "Basic ") {
+ return false
+ }
+
+ c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authorization, "Basic "))
+ if err != nil {
+ return false
+ }
+ cs := string(c)
+ i := strings.IndexByte(cs, ':')
+ if i < 0 {
+ return false
+ }
+ user, password := cs[:i], cs[i+1:]
+
+ userModel, err := models.GetUserByName(user)
+ if err != nil {
+ return false
+ }
+
+ if !userModel.ValidatePassword(password) {
+ return false
+ }
+
+ accessCheck, _ := models.HasAccess(userModel, repository, accessMode)
+ return accessCheck
+}
+
+func authenticateToken(repository *models.Repository, authorization string, requireWrite bool) bool {
+ if !strings.HasPrefix(authorization, "Bearer ") {
+ return false
+ }
+
+ token, err := jwt.Parse(authorization[7:], func(t *jwt.Token) (interface{}, error) {
+ 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 {
+ return false
+ }
+ claims, claimsOk := token.Claims.(jwt.MapClaims)
+ if !token.Valid || !claimsOk {
+ return false
+ }
+
+ opStr, ok := claims["op"].(string)
+ if !ok {
+ return false
+ }
+
+ if requireWrite && opStr != "upload" {
+ return false
+ }
+
+ repoID, ok := claims["repo"].(float64)
+ if !ok {
+ return false
+ }
+
+ if repository.ID != int64(repoID) {
+ return false
+ }
+
+ return true
+}
+
+func requireAuth(ctx *context.Context) {
+ ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
+ writeStatus(ctx, 401)
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 81fcb4b150..105d33371e 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -5,7 +5,10 @@
package setting
import (
+ "crypto/rand"
+ "encoding/base64"
"fmt"
+ "io"
"net/mail"
"net/url"
"os"
@@ -17,6 +20,7 @@ import (
"strings"
"time"
+ "code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/user"
"github.com/Unknwon/com"
@@ -89,6 +93,13 @@ var (
MinimumKeySizes map[string]int `ini:"-"`
}
+ LFS struct {
+ StartServer bool `ini:"LFS_START_SERVER"`
+ ContentPath string `ini:"LFS_CONTENT_PATH"`
+ JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
+ JWTSecretBytes []byte `ini:"-"`
+ }
+
// Security settings
InstallLock bool
SecretKey string
@@ -583,6 +594,85 @@ please consider changing to GITEA_CUSTOM`)
}
}
+ if err = Cfg.Section("server").MapTo(&LFS); err != nil {
+ log.Fatal(4, "Fail to map LFS settings: %v", err)
+ }
+
+ if LFS.StartServer {
+
+ if err := os.MkdirAll(LFS.ContentPath, 0700); err != nil {
+ log.Fatal(4, "Fail to create '%s': %v", LFS.ContentPath, err)
+ }
+
+ LFS.JWTSecretBytes = make([]byte, 32)
+ n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64))
+
+ if err != nil || n != 32 {
+ //Generate new secret and save to config
+
+ _, err := io.ReadFull(rand.Reader, LFS.JWTSecretBytes)
+
+ if err != nil {
+ log.Fatal(4, "Error reading random bytes: %s", err)
+ }
+
+ LFS.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(LFS.JWTSecretBytes)
+
+ // Save secret
+ cfg := ini.Empty()
+ if com.IsFile(CustomConf) {
+ // Keeps custom settings if there is already something.
+ if err := cfg.Append(CustomConf); err != nil {
+ log.Error(4, "Fail to load custom conf '%s': %v", CustomConf, err)
+ }
+ }
+
+ cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
+
+ os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm)
+ if err := cfg.SaveTo(CustomConf); err != nil {
+ log.Fatal(4, "Error saving generated JWT Secret to custom config: %v", err)
+ return
+ }
+ }
+
+ //Disable LFS client hooks if installed for the current OS user
+ //Needs at least git v2.1.2
+
+ binVersion, err := git.BinVersion()
+ if err != nil {
+ log.Fatal(4, "Error retrieving git version: %s", err)
+ }
+
+ splitVersion := strings.SplitN(binVersion, ".", 3)
+
+ majorVersion, err := strconv.ParseUint(splitVersion[0], 10, 64)
+ if err != nil {
+ log.Fatal(4, "Error parsing git major version: %s", err)
+ }
+ minorVersion, err := strconv.ParseUint(splitVersion[1], 10, 64)
+ if err != nil {
+ log.Fatal(4, "Error parsing git minor version: %s", err)
+ }
+ revisionVersion, err := strconv.ParseUint(splitVersion[2], 10, 64)
+ if err != nil {
+ log.Fatal(4, "Error parsing git revision version: %s", err)
+ }
+
+ if !((majorVersion > 2) || (majorVersion == 2 && minorVersion > 1) ||
+ (majorVersion == 2 && minorVersion == 1 && revisionVersion >= 2)) {
+
+ LFS.StartServer = false
+ log.Error(4, "LFS server support needs at least Git v2.1.2")
+
+ } else {
+
+ git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=",
+ "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
+
+ }
+ }
+
sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")