diff --git a/cmd/hook.go b/cmd/hook.go index 1a4943736..cb8b5112f 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -7,14 +7,22 @@ package cmd import ( "bufio" "bytes" + "crypto/tls" "os" "os/exec" "path/filepath" + "strings" "github.com/Unknwon/com" "github.com/urfave/cli" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/httplib" + "github.com/gogits/gogs/modules/setting" + http "github.com/gogits/gogs/routers/repo" ) var ( @@ -56,7 +64,7 @@ func runHookPreReceive(c *cli.Context) error { if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { return nil } - setup(c, "hooks/pre-receive.log") + setup(c, "hooks/pre-receive.log", false) buf := bytes.NewBuffer(nil) scanner := bufio.NewScanner(os.Stdin) @@ -65,12 +73,12 @@ func runHookPreReceive(c *cli.Context) error { buf.WriteByte('\n') } - customHooksPath := os.Getenv(_ENV_REPO_CUSTOM_HOOKS_PATH) + customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive") if !com.IsFile(customHooksPath) { return nil } - hookCmd := exec.Command(filepath.Join(customHooksPath, "pre-receive")) + hookCmd := exec.Command(customHooksPath) hookCmd.Stdout = os.Stdout hookCmd.Stdin = buf hookCmd.Stderr = os.Stderr @@ -84,7 +92,7 @@ func runHookUpdate(c *cli.Context) error { if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { return nil } - setup(c, "hooks/update.log") + setup(c, "hooks/update.log", false) args := c.Args() if len(args) != 3 { @@ -93,22 +101,12 @@ func runHookUpdate(c *cli.Context) error { fail("First argument 'refName' is empty", "First argument 'refName' is empty") } - uuid := os.Getenv(_ENV_UPDATE_TASK_UUID) - if err := models.AddUpdateTask(&models.UpdateTask{ - UUID: uuid, - RefName: args[0], - OldCommitID: args[1], - NewCommitID: args[2], - }); err != nil { - fail("Internal error", "Fail to add update task '%s': %v", uuid, err) - } - - customHooksPath := os.Getenv(_ENV_REPO_CUSTOM_HOOKS_PATH) + customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "update") if !com.IsFile(customHooksPath) { return nil } - hookCmd := exec.Command(filepath.Join(customHooksPath, "update"), args...) + hookCmd := exec.Command(customHooksPath, args...) hookCmd.Stdout = os.Stdout hookCmd.Stdin = os.Stdin hookCmd.Stderr = os.Stderr @@ -122,16 +120,67 @@ func runHookPostReceive(c *cli.Context) error { if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { return nil } - setup(c, "hooks/post-receive.log") + setup(c, "hooks/post-receive.log", true) + + isWiki := strings.Contains(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/") + + buf := bytes.NewBuffer(nil) + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + buf.Write(scanner.Bytes()) + buf.WriteByte('\n') - customHooksPath := os.Getenv(_ENV_REPO_CUSTOM_HOOKS_PATH) + // TODO: support news feeds for wiki + if isWiki { + continue + } + + fields := bytes.Fields(scanner.Bytes()) + if len(fields) != 3 { + continue + } + + options := models.PushUpdateOptions{ + OldCommitID: string(fields[0]), + NewCommitID: string(fields[1]), + RefFullName: string(fields[2]), + PusherID: com.StrTo(os.Getenv(http.ENV_AUTH_USER_ID)).MustInt64(), + PusherName: os.Getenv(http.ENV_AUTH_USER_NAME), + RepoUserName: os.Getenv(http.ENV_REPO_OWNER_NAME), + RepoName: os.Getenv(http.ENV_REPO_NAME), + } + if err := models.PushUpdate(options); err != nil { + log.Error(2, "PushUpdate: %v", err) + } + + // Ask for running deliver hook and test pull request tasks. + reqURL := setting.LocalURL + options.RepoUserName + "/" + options.RepoName + "/tasks/trigger?branch=" + + strings.TrimPrefix(options.RefFullName, git.BRANCH_PREFIX) + + "&secret=" + os.Getenv(http.ENV_REPO_OWNER_SALT_MD5) + + "&pusher=" + os.Getenv(http.ENV_AUTH_USER_ID) + log.Trace("Trigger task: %s", reqURL) + + resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: true, + }).Response() + if err == nil { + resp.Body.Close() + if resp.StatusCode/100 != 2 { + log.Error(2, "Fail to trigger task: not 2xx response code") + } + } else { + log.Error(2, "Fail to trigger task: %v", err) + } + } + + customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "post-receive") if !com.IsFile(customHooksPath) { return nil } - hookCmd := exec.Command(filepath.Join(customHooksPath, "post-receive")) + hookCmd := exec.Command(customHooksPath) hookCmd.Stdout = os.Stdout - hookCmd.Stdin = os.Stdin + hookCmd.Stdin = buf hookCmd.Stderr = os.Stderr if err := hookCmd.Run(); err != nil { fail("Internal error", "Fail to execute custom post-receive hook: %v", err) diff --git a/cmd/serv.go b/cmd/serv.go index b15f4e412..57a674e56 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -5,7 +5,6 @@ package cmd import ( - "crypto/tls" "fmt" "os" "os/exec" @@ -14,21 +13,16 @@ import ( "time" "github.com/Unknwon/com" - "github.com/gogits/git-module" - gouuid "github.com/satori/go.uuid" "github.com/urfave/cli" log "gopkg.in/clog.v1" "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/httplib" "github.com/gogits/gogs/modules/setting" + http "github.com/gogits/gogs/routers/repo" ) const ( - _ACCESS_DENIED_MESSAGE = "Repository does not exist or you do not have access" - _ENV_UPDATE_TASK_UUID = "UPDATE_TASK_UUID" - _ENV_REPO_CUSTOM_HOOKS_PATH = "REPO_CUSTOM_HOOKS_PATH" + _ACCESS_DENIED_MESSAGE = "Repository does not exist or you do not have access" ) var Serv = cli.Command{ @@ -41,7 +35,20 @@ var Serv = cli.Command{ }, } -func setup(c *cli.Context, logPath string) { +func fail(userMessage, logMessage string, args ...interface{}) { + fmt.Fprintln(os.Stderr, "Gogs:", userMessage) + + if len(logMessage) > 0 { + if !setting.ProdMode { + fmt.Fprintf(os.Stderr, logMessage+"\n", args...) + } + log.Fatal(3, logMessage, args...) + } + + os.Exit(1) +} + +func setup(c *cli.Context, logPath string, connectDB bool) { if c.IsSet("config") { setting.CustomConf = c.String("config") } else if c.GlobalIsSet("config") { @@ -49,8 +56,13 @@ func setup(c *cli.Context, logPath string) { } setting.NewContext() - setting.NewService() + + level := log.TRACE + if setting.ProdMode { + level = log.ERROR + } log.New(log.FILE, log.FileConfig{ + Level: level, Filename: filepath.Join(setting.LogRootPath, logPath), FileRotationConfig: log.FileRotationConfig{ Rotate: true, @@ -60,6 +72,10 @@ func setup(c *cli.Context, logPath string) { }) log.Delete(log.CONSOLE) // Remove primary logger + if !connectDB { + return + } + models.LoadConfigs() if setting.UseSQLite3 { @@ -67,7 +83,9 @@ func setup(c *cli.Context, logPath string) { os.Chdir(workDir) } - models.SetEngine() + if err := models.SetEngine(); err != nil { + fail("Internal error", "SetEngine: %v", err) + } } func parseSSHCmd(cmd string) (string, string) { @@ -104,68 +122,8 @@ var ( } ) -func fail(userMessage, logMessage string, args ...interface{}) { - fmt.Fprintln(os.Stderr, "Gogs:", userMessage) - - if len(logMessage) > 0 { - if !setting.ProdMode { - fmt.Fprintf(os.Stderr, logMessage+"\n", args...) - } - log.Fatal(3, logMessage, args...) - } - - log.Shutdown() - os.Exit(1) -} - -func handleUpdateTask(uuid string, user, repoUser *models.User, reponame string, isWiki bool) { - task, err := models.GetUpdateTaskByUUID(uuid) - if err != nil { - if models.IsErrUpdateTaskNotExist(err) { - log.Trace("No update task is presented: %s", uuid) - return - } - log.Fatal(2, "GetUpdateTaskByUUID: %v", err) - } else if err = models.DeleteUpdateTaskByUUID(uuid); err != nil { - log.Fatal(2, "DeleteUpdateTaskByUUID: %v", err) - } - - if isWiki { - return - } - - if err = models.PushUpdate(models.PushUpdateOptions{ - RefFullName: task.RefName, - OldCommitID: task.OldCommitID, - NewCommitID: task.NewCommitID, - PusherID: user.ID, - PusherName: user.Name, - RepoUserName: repoUser.Name, - RepoName: reponame, - }); err != nil { - log.Error(2, "Update: %v", err) - } - - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + repoUser.Name + "/" + reponame + "/tasks/trigger?branch=" + - strings.TrimPrefix(task.RefName, git.BRANCH_PREFIX) + "&secret=" + base.EncodeMD5(repoUser.Salt) + "&pusher=" + com.ToStr(user.ID) - log.Trace("Trigger task: %s", reqURL) - - resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - }).Response() - if err == nil { - resp.Body.Close() - if resp.StatusCode/100 != 2 { - log.Error(2, "Fail to trigger task: not 2xx response code") - } - } else { - log.Error(2, "Fail to trigger task: %v", err) - } -} - func runServ(c *cli.Context) error { - setup(c, "serv.log") + setup(c, "serv.log", true) if setting.SSH.Disabled { println("Gogs: SSH has been disabled") @@ -189,31 +147,26 @@ func runServ(c *cli.Context) error { if len(repoFields) != 2 { fail("Invalid repository path", "Invalid repository path: %v", args) } - username := strings.ToLower(repoFields[0]) - reponame := strings.ToLower(strings.TrimSuffix(repoFields[1], ".git")) + ownerName := strings.ToLower(repoFields[0]) + repoName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git") + repoName = strings.TrimSuffix(repoName, ".wiki") - isWiki := false - if strings.HasSuffix(reponame, ".wiki") { - isWiki = true - reponame = reponame[:len(reponame)-5] - } - - repoOwner, err := models.GetUserByName(username) + owner, err := models.GetUserByName(ownerName) if err != nil { if models.IsErrUserNotExist(err) { - fail("Repository owner does not exist", "Unregistered owner: %s", username) + fail("Repository owner does not exist", "Unregistered owner: %s", ownerName) } - fail("Internal error", "Fail to get repository owner '%s': %v", username, err) + fail("Internal error", "Fail to get repository owner '%s': %v", ownerName, err) } - repo, err := models.GetRepositoryByName(repoOwner.ID, reponame) + repo, err := models.GetRepositoryByName(owner.ID, repoName) if err != nil { if models.IsErrRepoNotExist(err) { - fail(_ACCESS_DENIED_MESSAGE, "Repository does not exist: %s/%s", repoOwner.Name, reponame) + fail(_ACCESS_DENIED_MESSAGE, "Repository does not exist: %s/%s", owner.Name, repoName) } fail("Internal error", "Fail to get repository: %v", err) } - repo.Owner = repoOwner + repo.Owner = owner requestMode, ok := allowedCommands[verb] if !ok { @@ -262,6 +215,7 @@ func runServ(c *cli.Context) error { } } } else { + setting.NewService() // Check if the key can access to the repository in case of it is a deploy key (a deploy keys != user key). // A deploy key doesn't represent a signed in user, so in a site with Service.RequireSignInView activated // we should give read access only in repositories where this deploy key is in use. In other case, a server @@ -271,9 +225,18 @@ func runServ(c *cli.Context) error { } } - uuid := gouuid.NewV4().String() - os.Setenv(_ENV_UPDATE_TASK_UUID, uuid) - os.Setenv(_ENV_REPO_CUSTOM_HOOKS_PATH, filepath.Join(repo.RepoPath(), "custom_hooks")) + // Update user key activity. + if key.ID > 0 { + key, err := models.GetPublicKeyByID(key.ID) + if err != nil { + fail("Internal error", "GetPublicKeyByID: %v", err) + } + + key.Updated = time.Now() + if err = models.UpdatePublicKey(key); err != nil { + fail("Internal error", "UpdatePublicKey: %v", err) + } + } // Special handle for Windows. if setting.IsWindows { @@ -287,6 +250,9 @@ func runServ(c *cli.Context) error { } else { gitCmd = exec.Command(verb, repoFullName) } + if requestMode == models.ACCESS_MODE_WRITE { + gitCmd.Env = append(os.Environ(), http.ComposeHookEnvs(repo.RepoPath(), owner.Name, owner.Salt, repo.Name, user)...) + } gitCmd.Dir = setting.RepoRootPath gitCmd.Stdout = os.Stdout gitCmd.Stdin = os.Stdin @@ -295,22 +261,5 @@ func runServ(c *cli.Context) error { fail("Internal error", "Fail to execute git command: %v", err) } - if requestMode == models.ACCESS_MODE_WRITE { - handleUpdateTask(uuid, user, repoOwner, reponame, isWiki) - } - - // Update user key activity. - if key.ID > 0 { - key, err := models.GetPublicKeyByID(key.ID) - if err != nil { - fail("Internal error", "GetPublicKeyByID: %v", err) - } - - key.Updated = time.Now() - if err = models.UpdatePublicKey(key); err != nil { - fail("Internal error", "UpdatePublicKey: %v", err) - } - } - return nil } diff --git a/cmd/web.go b/cmd/web.go index aa4d84b0d..ea7d155fe 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -623,8 +623,8 @@ func runWeb(ctx *cli.Context) error { }, ignSignIn, context.RepoAssignment(true), context.RepoRef()) m.Group("/:reponame", func() { - m.Any("/*", ignSignInAndCsrf, repo.HTTP) m.Head("/tasks/trigger", repo.TriggerTask) + m.Route("\\.git/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP) }) }) // ***** END: Repository ***** diff --git a/gogs.go b/gogs.go index 2a911fdff..73fecd558 100644 --- a/gogs.go +++ b/gogs.go @@ -16,7 +16,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.9.151.0216" +const APP_VER = "0.9.152.0216" func init() { setting.AppVer = APP_VER diff --git a/models/access.go b/models/access.go index 43a51775e..0b114376f 100644 --- a/models/access.go +++ b/models/access.go @@ -86,7 +86,7 @@ func AccessLevel(u *User, repo *Repository) (AccessMode, error) { func hasAccess(e Engine, u *User, repo *Repository, testMode AccessMode) (bool, error) { mode, err := accessLevel(e, u, repo) - return testMode <= mode, err + return mode >= testMode, err } // HasAccess returns true if someone has the request access level. User can be nil! diff --git a/models/action.go b/models/action.go index 371035238..ab8d9167f 100644 --- a/models/action.go +++ b/models/action.go @@ -468,7 +468,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { } if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil { - log.Error(4, "UpdateIssuesCommit: %v", err) + log.Error(2, "UpdateIssuesCommit: %v", err) } } diff --git a/models/models.go b/models/models.go index 7b32f8bdd..301102254 100644 --- a/models/models.go +++ b/models/models.go @@ -66,7 +66,7 @@ func init() { new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser), new(Label), new(IssueLabel), new(Milestone), new(Mirror), new(Release), new(LoginSource), new(Webhook), - new(UpdateTask), new(HookTask), + new(HookTask), new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), new(Notice), new(EmailAddress)) @@ -254,7 +254,6 @@ func GetStatistic() (stats Statistic) { stats.Counter.Label, _ = x.Count(new(Label)) stats.Counter.HookTask, _ = x.Count(new(HookTask)) stats.Counter.Team, _ = x.Count(new(Team)) - stats.Counter.UpdateTask, _ = x.Count(new(UpdateTask)) stats.Counter.Attachment, _ = x.Count(new(Attachment)) return } diff --git a/models/update.go b/models/update.go index 18d6c9dca..507150bef 100644 --- a/models/update.go +++ b/models/update.go @@ -15,38 +15,6 @@ import ( git "github.com/gogits/git-module" ) -type UpdateTask struct { - ID int64 `xorm:"pk autoincr"` - UUID string `xorm:"index"` - RefName string - OldCommitID string - NewCommitID string -} - -func AddUpdateTask(task *UpdateTask) error { - _, err := x.Insert(task) - return err -} - -// GetUpdateTaskByUUID returns update task by given UUID. -func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) { - task := &UpdateTask{ - UUID: uuid, - } - has, err := x.Get(task) - if err != nil { - return nil, err - } else if !has { - return nil, ErrUpdateTaskNotExist{uuid} - } - return task, nil -} - -func DeleteUpdateTaskByUUID(uuid string) error { - _, err := x.Delete(&UpdateTask{UUID: uuid}) - return err -} - // CommitToPushCommit transforms a git.Commit to PushCommit type. func CommitToPushCommit(commit *git.Commit) *PushCommit { return &PushCommit{ @@ -74,13 +42,13 @@ func ListToPushCommits(l *list.List) *PushCommits { } type PushUpdateOptions struct { + OldCommitID string + NewCommitID string + RefFullName string PusherID int64 PusherName string RepoUserName string RepoName string - RefFullName string - OldCommitID string - NewCommitID string } // PushUpdate must be called for any push actions in order to diff --git a/modules/context/context.go b/modules/context/context.go index e90d5f000..792148561 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -112,7 +112,7 @@ func (ctx *Context) NotFound() { // or error context description for logging purpose of 500 server error. func (ctx *Context) NotFoundOrServerError(title string, errck func(error) bool, err error) { if errck(err) { - ctx.Handle(404, title, err) + ctx.NotFound() return } diff --git a/routers/repo/http.go b/routers/repo/http.go index 805b51891..f2f1110bd 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -1,4 +1,4 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -8,21 +8,18 @@ import ( "bytes" "compress/gzip" "fmt" - "io" - "io/ioutil" "net/http" "os" "os/exec" "path" "regexp" - "runtime" "strconv" "strings" "time" + "github.com/Unknwon/com" log "gopkg.in/clog.v1" - - git "github.com/gogits/git-module" + "gopkg.in/macaron.v1" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" @@ -30,59 +27,54 @@ import ( "github.com/gogits/gogs/modules/setting" ) -func HTTP(ctx *context.Context) { - username := ctx.Params(":username") - reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git") - - var isPull bool - service := ctx.Query("service") - if service == "git-receive-pack" || - strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") { - isPull = false - } else if service == "git-upload-pack" || - strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { - isPull = true - } else { - isPull = (ctx.Req.Method == "GET") - } +const ( + ENV_AUTH_USER_ID = "AUTH_USER_ID" + ENV_AUTH_USER_NAME = "AUTH_USER_NAME" + ENV_REPO_OWNER_NAME = "REPO_OWNER_NAME" + ENV_REPO_OWNER_SALT_MD5 = "REPO_OWNER_SALT_MD5" + ENV_REPO_NAME = "REPO_NAME" + ENV_REPO_CUSTOM_HOOKS_PATH = "REPO_CUSTOM_HOOKS_PATH" +) - isWiki := false - if strings.HasSuffix(reponame, ".wiki") { - isWiki = true - reponame = reponame[:len(reponame)-5] - } +type HTTPContext struct { + *context.Context + OwnerName string + OwnerSalt string + RepoName string + AuthUser *models.User +} - repoUser, err := models.GetUserByName(username) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.Handle(http.StatusNotFound, "GetUserByName", nil) - } else { - ctx.Handle(http.StatusInternalServerError, "GetUserByName", err) +func HTTPContexter() macaron.Handler { + return func(ctx *context.Context) { + ownerName := ctx.Params(":username") + repoName := strings.TrimSuffix(ctx.Params(":reponame"), ".git") + repoName = strings.TrimSuffix(repoName, ".wiki") + + isPull := ctx.Query("service") == "git-upload-pack" || + strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") || + ctx.Req.Method == "GET" + + owner, err := models.GetUserByName(ownerName) + if err != nil { + ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err) + return } - return - } - repo, err := models.GetRepositoryByName(repoUser.ID, reponame) - if err != nil { - if models.IsErrRepoNotExist(err) { - ctx.Handle(http.StatusNotFound, "GetRepositoryByName", nil) - } else { - ctx.Handle(http.StatusInternalServerError, "GetRepositoryByName", err) + repo, err := models.GetRepositoryByName(owner.ID, repoName) + if err != nil { + ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoNotExist, err) + return } - return - } - // Only public pull don't need auth. - isPublicPull := !repo.IsPrivate && isPull - var ( - askAuth = !isPublicPull || setting.Service.RequireSignInView - authUser *models.User - authUsername string - authPasswd string - ) + // Authentication is not required for pulling from public repositories. + if isPull && !repo.IsPrivate && !setting.Service.RequireSignInView { + ctx.Map(&HTTPContext{ + Context: ctx, + }) + return + } - // check access - if askAuth { + // Handle HTTP Basic Authentication authHead := ctx.Req.Header.Get("Authorization") if len(authHead) == 0 { ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"") @@ -91,174 +83,78 @@ func HTTP(ctx *context.Context) { } auths := strings.Fields(authHead) - // currently check basic auth - // TODO: support digit auth - // FIXME: middlewares/context.go did basic auth check already, - // maybe could use that one. if len(auths) != 2 || auths[0] != "Basic" { - ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth") + ctx.Error(http.StatusUnauthorized) return } - authUsername, authPasswd, err = base.BasicAuthDecode(auths[1]) + authUsername, authPassword, err := base.BasicAuthDecode(auths[1]) if err != nil { - ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth") + ctx.Error(http.StatusUnauthorized) return } - authUser, err = models.UserSignIn(authUsername, authPasswd) - if err != nil { - if !models.IsErrUserNotExist(err) { - ctx.Handle(http.StatusInternalServerError, "UserSignIn error: %v", err) - return - } + authUser, err := models.UserSignIn(authUsername, authPassword) + if err != nil && !models.IsErrUserNotExist(err) { + ctx.Handle(http.StatusInternalServerError, "UserSignIn: %v", err) + return + } - // Assume username now is a token. + // If username and password combination failed, try again using username as a token. + if authUser == nil { token, err := models.GetAccessTokenBySHA(authUsername) if err != nil { - if models.IsErrAccessTokenNotExist(err) || models.IsErrAccessTokenEmpty(err) { - ctx.HandleText(http.StatusUnauthorized, "invalid token") - } else { - ctx.Handle(http.StatusInternalServerError, "GetAccessTokenBySha", err) - } + ctx.NotFoundOrServerError("GetAccessTokenBySHA", models.IsErrAccessTokenNotExist, err) return } token.Updated = time.Now() - if err = models.UpdateAccessToken(token); err != nil { - ctx.Handle(http.StatusInternalServerError, "UpdateAccessToken", err) - } + authUser, err = models.GetUserByID(token.UID) if err != nil { + // Once we found token, we're supposed to find its related user, + // thus any error is unexpected. ctx.Handle(http.StatusInternalServerError, "GetUserByID", err) return } } - if !isPublicPull { - var tp = models.ACCESS_MODE_WRITE - if isPull { - tp = models.ACCESS_MODE_READ - } - - has, err := models.HasAccess(authUser, repo, tp) - if err != nil { - ctx.Handle(http.StatusInternalServerError, "HasAccess", err) - return - } else if !has { - if tp == models.ACCESS_MODE_READ { - has, err = models.HasAccess(authUser, repo, models.ACCESS_MODE_WRITE) - if err != nil { - ctx.Handle(http.StatusInternalServerError, "HasAccess2", err) - return - } else if !has { - ctx.HandleText(http.StatusForbidden, "User permission denied") - return - } - } else { - ctx.HandleText(http.StatusForbidden, "User permission denied") - return - } - } - - if !isPull && repo.IsMirror { - ctx.HandleText(http.StatusForbidden, "mirror repository is read-only") - return - } + mode := models.ACCESS_MODE_WRITE + if isPull { + mode = models.ACCESS_MODE_READ } - } - - callback := func(rpc string, input *os.File) { - if rpc != "receive-pack" || isWiki { + has, err := models.HasAccess(authUser, repo, mode) + if err != nil { + ctx.Handle(http.StatusInternalServerError, "HasAccess", err) + return + } else if !has { + ctx.HandleText(http.StatusForbidden, "User permission denied") return } - var ( - head = make([]byte, 4) // 00+size - n int - err error - ) - for { - n, err = input.Read(head) - if err != nil && err != io.EOF { - log.Error(4, "read head: %v", err) - return - } else if n < 4 { - break - } - - if head[0] == '0' && head[1] == '0' { - size, err := strconv.ParseInt(string(head[2:4]), 16, 32) - if err != nil { - log.Error(4, "parse size: %v", err) - return - } - - if size == 0 { - //fmt.Println(string(input[lastLine:])) - break - } - - line := make([]byte, size) - n, err = input.Read(line) - if err != nil { - log.Error(4, "read line: %v", err) - return - } else if n < int(size) { - log.Error(4, "didn't read enough bytes: expect %d got %d", size, n) - break - } - - idx := bytes.IndexRune(line, '\000') - if idx > -1 { - line = line[:idx] - } - - fields := strings.Fields(string(line)) - if len(fields) >= 3 { - oldCommitId := fields[0] - newCommitId := fields[1] - refFullName := fields[2] - - // FIXME: handle error. - if err = models.PushUpdate(models.PushUpdateOptions{ - RefFullName: refFullName, - OldCommitID: oldCommitId, - NewCommitID: newCommitId, - PusherID: authUser.ID, - PusherName: authUser.Name, - RepoUserName: username, - RepoName: reponame, - }); err == nil { - go models.AddTestPullRequestTask(authUser, repo.ID, strings.TrimPrefix(refFullName, git.BRANCH_PREFIX), true) - } - - } - } else { - break - } + if !isPull && repo.IsMirror { + ctx.HandleText(http.StatusForbidden, "Mirror repository is read-only") + return } - } - - HTTPBackend(ctx, &serviceConfig{ - UploadPack: true, - ReceivePack: true, - OnSucceed: callback, - })(ctx.Resp, ctx.Req.Request) - - runtime.GC() -} -type serviceConfig struct { - UploadPack bool - ReceivePack bool - OnSucceed func(rpc string, input *os.File) + ctx.Map(&HTTPContext{ + Context: ctx, + OwnerName: ownerName, + OwnerSalt: owner.Salt, + RepoName: repoName, + AuthUser: authUser, + }) + } } type serviceHandler struct { - cfg *serviceConfig w http.ResponseWriter r *http.Request dir string file string + + authUser *models.User + ownerName string + ownerSalt string + repoName string } func (h *serviceHandler) setHeaderNoCache() { @@ -277,7 +173,6 @@ func (h *serviceHandler) setHeaderCacheForever() { func (h *serviceHandler) sendFile(contentType string) { reqFile := path.Join(h.dir, h.file) - fi, err := os.Stat(reqFile) if os.IsNotExist(err) { h.w.WriteHeader(http.StatusNotFound) @@ -290,152 +185,57 @@ func (h *serviceHandler) sendFile(contentType string) { http.ServeFile(h.w, h.r, reqFile) } -type route struct { - reg *regexp.Regexp - method string - handler func(serviceHandler) -} - -var routes = []route{ - {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack}, - {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack}, - {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs}, - {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile}, - {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile}, - {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile}, - {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks}, - {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile}, - {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject}, - {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile}, - {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile}, -} - -// FIXME: use process module -func gitCommand(dir string, args ...string) []byte { - cmd := exec.Command("git", args...) - cmd.Dir = dir - out, err := cmd.Output() - if err != nil { - log.Error(4, fmt.Sprintf("Git: %v - %s", err, out)) - } - return out -} - -func getGitConfig(option, dir string) string { - out := string(gitCommand(dir, "config", option)) - return out[0 : len(out)-1] -} - -func getConfigSetting(service, dir string) bool { - service = strings.Replace(service, "-", "", -1) - setting := getGitConfig("http."+service, dir) - - if service == "uploadpack" { - return setting != "false" +func ComposeHookEnvs(repoPath, ownerName, ownerSalt, repoName string, authUser *models.User) []string { + envs := []string{ + "SSH_ORIGINAL_COMMAND=1", + ENV_AUTH_USER_ID + "=" + com.ToStr(authUser.ID), + ENV_AUTH_USER_NAME + "=" + authUser.Name, + ENV_REPO_OWNER_NAME + "=" + ownerName, + ENV_REPO_OWNER_SALT_MD5 + "=" + base.EncodeMD5(ownerSalt), + ENV_REPO_NAME + "=" + repoName, + ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(repoPath, "custom_hooks"), } - - return setting == "true" -} - -func hasAccess(service string, h serviceHandler, checkContentType bool) bool { - if checkContentType { - if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) { - return false - } - } - - if !(service == "upload-pack" || service == "receive-pack") { - return false - } - if service == "receive-pack" { - return h.cfg.ReceivePack - } - if service == "upload-pack" { - return h.cfg.UploadPack - } - - return getConfigSetting(service, h.dir) + return envs } func serviceRPC(h serviceHandler, service string) { defer h.r.Body.Close() - if !hasAccess(service, h, true) { + if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) { h.w.WriteHeader(http.StatusUnauthorized) return } h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) var ( - reqBody = h.r.Body - tmpFilename string - br io.Reader - err error + reqBody = h.r.Body + err error ) // Handle GZIP. if h.r.Header.Get("Content-Encoding") == "gzip" { reqBody, err = gzip.NewReader(reqBody) if err != nil { - log.Error(2, "Git: fail to create gzip reader: %v", err) + log.Error(2, "HTTP.Get: fail to create gzip reader: %v", err) h.w.WriteHeader(http.StatusInternalServerError) return } } - if h.cfg.OnSucceed != nil { - tmpfile, err := ioutil.TempFile("", "gogs") - if err != nil { - log.Error(2, "Git: fail to create temporary file: %v", err) - h.w.WriteHeader(http.StatusInternalServerError) - return - } - defer os.Remove(tmpfile.Name()) - - _, err = io.Copy(tmpfile, reqBody) - if err != nil { - log.Error(2, "Git: fail to save request body: %v", err) - h.w.WriteHeader(http.StatusInternalServerError) - return - } - tmpfile.Close() - - tmpFilename = tmpfile.Name() - tmpfile, err = os.Open(tmpFilename) - if err != nil { - log.Error(2, "Git: fail to open temporary file: %v", err) - h.w.WriteHeader(http.StatusInternalServerError) - return - } - defer tmpfile.Close() - - br = tmpfile - } else { - br = reqBody - } - var stderr bytes.Buffer cmd := exec.Command("git", service, "--stateless-rpc", h.dir) + if service == "receive-pack" { + cmd.Env = append(os.Environ(), ComposeHookEnvs(h.dir, h.ownerName, h.ownerSalt, h.repoName, h.authUser)...) + } cmd.Dir = h.dir cmd.Stdout = h.w cmd.Stderr = &stderr - cmd.Stdin = br - if err := cmd.Run(); err != nil { - log.Error(2, "Git: fail to serve RPC '%s': %v - %s", service, err, stderr) + cmd.Stdin = reqBody + if err = cmd.Run(); err != nil { + log.Error(2, "HTTP.serviceRPC: fail to serve RPC '%s': %v - %s", service, err, stderr) h.w.WriteHeader(http.StatusInternalServerError) return } - - if h.cfg.OnSucceed != nil { - input, err := os.Open(tmpFilename) - if err != nil { - log.Error(2, "Git: fail to open temporary file: %v", err) - h.w.WriteHeader(http.StatusInternalServerError) - return - } - defer input.Close() - h.cfg.OnSucceed(service, input) - } } func serviceUploadPack(h serviceHandler) { @@ -451,7 +251,18 @@ func getServiceType(r *http.Request) string { if !strings.HasPrefix(serviceType, "git-") { return "" } - return strings.Replace(serviceType, "git-", "", 1) + return strings.TrimPrefix(serviceType, "git-") +} + +// FIXME: use process module +func gitCommand(dir string, args ...string) []byte { + cmd := exec.Command("git", args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + log.Error(2, fmt.Sprintf("Git: %v - %s", err, out)) + } + return out } func updateServerInfo(dir string) []byte { @@ -468,19 +279,19 @@ func packetWrite(str string) []byte { func getInfoRefs(h serviceHandler) { h.setHeaderNoCache() - if hasAccess(getServiceType(h.r), h, false) { - service := getServiceType(h.r) - refs := gitCommand(h.dir, service, "--stateless-rpc", "--advertise-refs", ".") - - h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) - h.w.WriteHeader(http.StatusOK) - h.w.Write(packetWrite("# service=git-" + service + "\n")) - h.w.Write([]byte("0000")) - h.w.Write(refs) - } else { + service := getServiceType(h.r) + if service != "upload-pack" && service != "receive-pack" { updateServerInfo(h.dir) h.sendFile("text/plain; charset=utf-8") + return } + + refs := gitCommand(h.dir, service, "--stateless-rpc", "--advertise-refs", ".") + h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) + h.w.WriteHeader(http.StatusOK) + h.w.Write(packetWrite("# service=git-" + service + "\n")) + h.w.Write([]byte("0000")) + h.w.Write(refs) } func getTextFile(h serviceHandler) { @@ -508,55 +319,79 @@ func getIdxFile(h serviceHandler) { h.sendFile("application/x-git-packed-objects-toc") } -func getGitRepoPath(subdir string) (string, error) { - if !strings.HasSuffix(subdir, ".git") { - subdir += ".git" +var routes = []struct { + reg *regexp.Regexp + method string + handler func(serviceHandler) +}{ + {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack}, + {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack}, + {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs}, + {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks}, + {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile}, +} + +func getGitRepoPath(dir string) (string, error) { + if !strings.HasSuffix(dir, ".git") { + dir += ".git" } - fpath := path.Join(setting.RepoRootPath, subdir) - if _, err := os.Stat(fpath); os.IsNotExist(err) { + filename := path.Join(setting.RepoRootPath, dir) + if _, err := os.Stat(filename); os.IsNotExist(err) { return "", err } - return fpath, nil + return filename, nil } -func HTTPBackend(ctx *context.Context, cfg *serviceConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - for _, route := range routes { - r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name - if m := route.reg.FindStringSubmatch(r.URL.Path); m != nil { - if setting.Repository.DisableHTTPGit { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte("Interacting with repositories by HTTP protocol is not allowed")) - return - } - - if route.method != r.Method { - if r.Proto == "HTTP/1.1" { - w.WriteHeader(http.StatusMethodNotAllowed) - w.Write([]byte("Method Not Allowed")) - } else { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Bad Request")) - } - return - } - - file := strings.Replace(r.URL.Path, m[1]+"/", "", 1) - dir, err := getGitRepoPath(m[1]) - if err != nil { - log.Error(4, "Git: getGitRepoPath: %v", err) - ctx.Handle(http.StatusNotFound, "HTTPBackend", err) - return - } - - route.handler(serviceHandler{cfg, w, r, dir, file}) - return - } +func HTTP(ctx *HTTPContext) { + for _, route := range routes { + reqPath := strings.ToLower(ctx.Req.URL.Path) + m := route.reg.FindStringSubmatch(reqPath) + if m == nil { + continue + } + + // We perform check here because routes matched in cmd/web.go is wider than needed, + // but we only want to output this message only if user is really trying to access + // Git HTTP endpoints. + if setting.Repository.DisableHTTPGit { + ctx.HandleText(http.StatusForbidden, "Interacting with repositories by HTTP protocol is not disabled") + return + } + + if route.method != ctx.Req.Method { + ctx.NotFound() + return } - ctx.Handle(http.StatusNotFound, "HTTPBackend", nil) + file := strings.TrimPrefix(reqPath, m[1]+"/") + dir, err := getGitRepoPath(m[1]) + if err != nil { + log.Warn("HTTP.getGitRepoPath: %v", err) + ctx.NotFound() + return + } + + route.handler(serviceHandler{ + w: ctx.Resp, + r: ctx.Req.Request, + dir: dir, + file: file, + + authUser: ctx.AuthUser, + ownerName: ctx.OwnerName, + ownerSalt: ctx.OwnerSalt, + repoName: ctx.RepoName, + }) return } + + ctx.NotFound() } diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 8aa37bb24..db8d70dbf 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -740,7 +740,7 @@ func TriggerTask(ctx *context.Context) { return } - log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) + log.Trace("TriggerTask '%s/%s' by '%s'", repo.Name, branch, pusher.Name) go models.HookQueue.Add(repo.ID) go models.AddTestPullRequestTask(pusher, repo.ID, branch, true) diff --git a/templates/.VERSION b/templates/.VERSION index 8fcefd697..71394c9fc 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.9.151.0216 \ No newline at end of file +0.9.152.0216 \ No newline at end of file