refactoring: SSH and HTTP push procees is now unified

We used to handle SSH and HTTP push separately which produces
duplicated code, but now with post-receive hook, the process
is unified to one single place and much cleaner.
Thus, UpdateTask struct is removed.

Narrow down the range of Git HTTP routes to reduce condufsing
HTTP Basic Authentication window popup on browser.

By detecting <old-commit, new-commit, ref-name> inside post-receive
hook, Git HTTP doesn't need to read the whole content body anymore,
which completely solve the RAM problem reported in #636.
master
Unknwon 2017-02-16 16:33:49 -05:00
parent 3b49a99b60
commit d521e716dd
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
12 changed files with 331 additions and 531 deletions

View File

@ -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)
customHooksPath := os.Getenv(_ENV_REPO_CUSTOM_HOOKS_PATH)
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')
// 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)

View File

@ -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,34 +225,6 @@ 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"))
// Special handle for Windows.
if setting.IsWindows {
verb = strings.Replace(verb, "-", " ", 1)
}
var gitCmd *exec.Cmd
verbs := strings.Split(verb, " ")
if len(verbs) == 2 {
gitCmd = exec.Command(verbs[0], verbs[1], repoFullName)
} else {
gitCmd = exec.Command(verb, repoFullName)
}
gitCmd.Dir = setting.RepoRootPath
gitCmd.Stdout = os.Stdout
gitCmd.Stdin = os.Stdin
gitCmd.Stderr = os.Stderr
if err = gitCmd.Run(); err != nil {
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)
@ -312,5 +238,28 @@ func runServ(c *cli.Context) error {
}
}
// Special handle for Windows.
if setting.IsWindows {
verb = strings.Replace(verb, "-", " ", 1)
}
var gitCmd *exec.Cmd
verbs := strings.Split(verb, " ")
if len(verbs) == 2 {
gitCmd = exec.Command(verbs[0], verbs[1], repoFullName)
} 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
gitCmd.Stderr = os.Stderr
if err = gitCmd.Run(); err != nil {
fail("Internal error", "Fail to execute git command: %v", err)
}
return nil
}

View File

@ -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 *****

View File

@ -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

View File

@ -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!

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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")
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"
)
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")
}
type HTTPContext struct {
*context.Context
OwnerName string
OwnerSalt string
RepoName string
AuthUser *models.User
}
isWiki := false
if strings.HasSuffix(reponame, ".wiki") {
isWiki = true
reponame = reponame[:len(reponame)-5]
}
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")
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)
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
}
ctx.Map(&HTTPContext{
Context: ctx,
OwnerName: ownerName,
OwnerSalt: owner.Salt,
RepoName: repoName,
AuthUser: authUser,
})
}
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)
}
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))
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 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"
}
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
}
ctx.Handle(http.StatusNotFound, "HTTPBackend", nil)
// 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
}
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()
}

View File

@ -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)

View File

@ -1 +1 @@
0.9.151.0216
0.9.152.0216