diff options
author | Unknwon <u@gogs.io> | 2019-10-24 01:51:46 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-24 01:51:46 -0700 |
commit | 01c8df01ec0608f1f25b2f1444adabb98fa5ee8a (patch) | |
tree | f8a7e5dd8d2a8c51e1ce2cabb9d33571a93314dd /internal/cmd | |
parent | 613139e7bef81d3573e7988a47eb6765f3de347a (diff) |
internal: move packages under this directory (#5836)
* Rename pkg -> internal
* Rename routes -> route
* Move route -> internal/route
* Rename models -> db
* Move db -> internal/db
* Fix route2 -> route
* Move cmd -> internal/cmd
* Bump version
Diffstat (limited to 'internal/cmd')
-rw-r--r-- | internal/cmd/admin.go | 183 | ||||
-rw-r--r-- | internal/cmd/backup.go | 137 | ||||
-rw-r--r-- | internal/cmd/cert.go | 163 | ||||
-rw-r--r-- | internal/cmd/cert_stub.go | 28 | ||||
-rw-r--r-- | internal/cmd/cmd.go | 42 | ||||
-rw-r--r-- | internal/cmd/hook.go | 277 | ||||
-rw-r--r-- | internal/cmd/import.go | 113 | ||||
-rw-r--r-- | internal/cmd/restore.go | 148 | ||||
-rw-r--r-- | internal/cmd/serv.go | 272 | ||||
-rw-r--r-- | internal/cmd/web.go | 757 |
10 files changed, 2120 insertions, 0 deletions
diff --git a/internal/cmd/admin.go b/internal/cmd/admin.go new file mode 100644 index 00000000..de8b6ba6 --- /dev/null +++ b/internal/cmd/admin.go @@ -0,0 +1,183 @@ +// Copyright 2016 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. + +package cmd + +import ( + "fmt" + "reflect" + "runtime" + + "github.com/urfave/cli" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/setting" +) + +var ( + Admin = cli.Command{ + Name: "admin", + Usage: "Perform admin operations on command line", + Description: `Allow using internal logic of Gogs without hacking into the source code +to make automatic initialization process more smoothly`, + Subcommands: []cli.Command{ + subcmdCreateUser, + subcmdDeleteInactivateUsers, + subcmdDeleteRepositoryArchives, + subcmdDeleteMissingRepositories, + subcmdGitGcRepos, + subcmdRewriteAuthorizedKeys, + subcmdSyncRepositoryHooks, + subcmdReinitMissingRepositories, + }, + } + + subcmdCreateUser = cli.Command{ + Name: "create-user", + Usage: "Create a new user in database", + Action: runCreateUser, + Flags: []cli.Flag{ + stringFlag("name", "", "Username"), + stringFlag("password", "", "User password"), + stringFlag("email", "", "User email address"), + boolFlag("admin", "User is an admin"), + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdDeleteInactivateUsers = cli.Command{ + Name: "delete-inactive-users", + Usage: "Delete all inactive accounts", + Action: adminDashboardOperation( + db.DeleteInactivateUsers, + "All inactivate accounts have been deleted successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdDeleteRepositoryArchives = cli.Command{ + Name: "delete-repository-archives", + Usage: "Delete all repositories archives", + Action: adminDashboardOperation( + db.DeleteRepositoryArchives, + "All repositories archives have been deleted successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdDeleteMissingRepositories = cli.Command{ + Name: "delete-missing-repositories", + Usage: "Delete all repository records that lost Git files", + Action: adminDashboardOperation( + db.DeleteMissingRepositories, + "All repositories archives have been deleted successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdGitGcRepos = cli.Command{ + Name: "collect-garbage", + Usage: "Do garbage collection on repositories", + Action: adminDashboardOperation( + db.GitGcRepos, + "All repositories have done garbage collection successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdRewriteAuthorizedKeys = cli.Command{ + Name: "rewrite-authorized-keys", + Usage: "Rewrite '.ssh/authorized_keys' file (caution: non-Gogs keys will be lost)", + Action: adminDashboardOperation( + db.RewriteAuthorizedKeys, + "All public keys have been rewritten successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdSyncRepositoryHooks = cli.Command{ + Name: "resync-hooks", + Usage: "Resync pre-receive, update and post-receive hooks", + Action: adminDashboardOperation( + db.SyncRepositoryHooks, + "All repositories' pre-receive, update and post-receive hooks have been resynced successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } + + subcmdReinitMissingRepositories = cli.Command{ + Name: "reinit-missing-repositories", + Usage: "Reinitialize all repository records that lost Git files", + Action: adminDashboardOperation( + db.ReinitMissingRepositories, + "All repository records that lost Git files have been reinitialized successfully", + ), + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } +) + +func runCreateUser(c *cli.Context) error { + if !c.IsSet("name") { + return fmt.Errorf("Username is not specified") + } else if !c.IsSet("password") { + return fmt.Errorf("Password is not specified") + } else if !c.IsSet("email") { + return fmt.Errorf("Email is not specified") + } + + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + + setting.NewContext() + db.LoadConfigs() + db.SetEngine() + + if err := db.CreateUser(&db.User{ + Name: c.String("name"), + Email: c.String("email"), + Passwd: c.String("password"), + IsActive: true, + IsAdmin: c.Bool("admin"), + }); err != nil { + return fmt.Errorf("CreateUser: %v", err) + } + + fmt.Printf("New user '%s' has been successfully created!\n", c.String("name")) + return nil +} + +func adminDashboardOperation(operation func() error, successMessage string) func(*cli.Context) error { + return func(c *cli.Context) error { + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + + setting.NewContext() + db.LoadConfigs() + db.SetEngine() + + if err := operation(); err != nil { + functionName := runtime.FuncForPC(reflect.ValueOf(operation).Pointer()).Name() + return fmt.Errorf("%s: %v", functionName, err) + } + + fmt.Printf("%s\n", successMessage) + return nil + } +} diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go new file mode 100644 index 00000000..e918d486 --- /dev/null +++ b/internal/cmd/backup.go @@ -0,0 +1,137 @@ +// 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. + +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "time" + + "github.com/unknwon/cae/zip" + "github.com/unknwon/com" + "github.com/urfave/cli" + log "gopkg.in/clog.v1" + "gopkg.in/ini.v1" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/setting" +) + +var Backup = cli.Command{ + Name: "backup", + Usage: "Backup files and database", + Description: `Backup dumps and compresses all related files and database into zip file, +which can be used for migrating Gogs to another server. The output format is meant to be +portable among all supported database engines.`, + Action: runBackup, + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + boolFlag("verbose, v", "Show process details"), + stringFlag("tempdir, t", os.TempDir(), "Temporary directory path"), + stringFlag("target", "./", "Target directory path to save backup archive"), + stringFlag("archive-name", fmt.Sprintf("gogs-backup-%s.zip", time.Now().Format("20060102150405")), "Name of backup archive"), + boolFlag("database-only", "Only dump database"), + boolFlag("exclude-repos", "Exclude repositories"), + }, +} + +const _CURRENT_BACKUP_FORMAT_VERSION = 1 +const _ARCHIVE_ROOT_DIR = "gogs-backup" + +func runBackup(c *cli.Context) error { + zip.Verbose = c.Bool("verbose") + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + setting.NewContext() + db.LoadConfigs() + db.SetEngine() + + tmpDir := c.String("tempdir") + if !com.IsExist(tmpDir) { + log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir) + } + rootDir, err := ioutil.TempDir(tmpDir, "gogs-backup-") + if err != nil { + log.Fatal(0, "Fail to create backup root directory '%s': %v", rootDir, err) + } + log.Info("Backup root directory: %s", rootDir) + + // Metadata + metaFile := path.Join(rootDir, "metadata.ini") + metadata := ini.Empty() + metadata.Section("").Key("VERSION").SetValue(com.ToStr(_CURRENT_BACKUP_FORMAT_VERSION)) + metadata.Section("").Key("DATE_TIME").SetValue(time.Now().String()) + metadata.Section("").Key("GOGS_VERSION").SetValue(setting.AppVer) + if err = metadata.SaveTo(metaFile); err != nil { + log.Fatal(0, "Fail to save metadata '%s': %v", metaFile, err) + } + + archiveName := path.Join(c.String("target"), c.String("archive-name")) + log.Info("Packing backup files to: %s", archiveName) + + z, err := zip.Create(archiveName) + if err != nil { + log.Fatal(0, "Fail to create backup archive '%s': %v", archiveName, err) + } + if err = z.AddFile(_ARCHIVE_ROOT_DIR+"/metadata.ini", metaFile); err != nil { + log.Fatal(0, "Fail to include 'metadata.ini': %v", err) + } + + // Database + dbDir := path.Join(rootDir, "db") + if err = db.DumpDatabase(dbDir); err != nil { + log.Fatal(0, "Fail to dump database: %v", err) + } + if err = z.AddDir(_ARCHIVE_ROOT_DIR+"/db", dbDir); err != nil { + log.Fatal(0, "Fail to include 'db': %v", err) + } + + // Custom files + if !c.Bool("database-only") { + if err = z.AddDir(_ARCHIVE_ROOT_DIR+"/custom", setting.CustomPath); err != nil { + log.Fatal(0, "Fail to include 'custom': %v", err) + } + } + + // Data files + if !c.Bool("database-only") { + for _, dir := range []string{"attachments", "avatars", "repo-avatars"} { + dirPath := path.Join(setting.AppDataPath, dir) + if !com.IsDir(dirPath) { + continue + } + + if err = z.AddDir(path.Join(_ARCHIVE_ROOT_DIR+"/data", dir), dirPath); err != nil { + log.Fatal(0, "Fail to include 'data': %v", err) + } + } + } + + // Repositories + if !c.Bool("exclude-repos") && !c.Bool("database-only") { + reposDump := path.Join(rootDir, "repositories.zip") + log.Info("Dumping repositories in '%s'", setting.RepoRootPath) + if err = zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil { + log.Fatal(0, "Fail to dump repositories: %v", err) + } + log.Info("Repositories dumped to: %s", reposDump) + + if err = z.AddFile(_ARCHIVE_ROOT_DIR+"/repositories.zip", reposDump); err != nil { + log.Fatal(0, "Fail to include 'repositories.zip': %v", err) + } + } + + if err = z.Close(); err != nil { + log.Fatal(0, "Fail to save backup archive '%s': %v", archiveName, err) + } + + os.RemoveAll(rootDir) + log.Info("Backup succeed! Archive is located at: %s", archiveName) + log.Shutdown() + return nil +} diff --git a/internal/cmd/cert.go b/internal/cmd/cert.go new file mode 100644 index 00000000..7c91c2c6 --- /dev/null +++ b/internal/cmd/cert.go @@ -0,0 +1,163 @@ +// +build cert + +// Copyright 2009 The Go Authors. All rights reserved. +// Copyright 2014 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. + +package cmd + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "log" + "math/big" + "net" + "os" + "strings" + "time" + + "github.com/urfave/cli" +) + +var Cert = cli.Command{ + Name: "cert", + Usage: "Generate self-signed certificate", + Description: `Generate a self-signed X.509 certificate for a TLS server. +Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, + Action: runCert, + Flags: []cli.Flag{ + stringFlag("host", "", "Comma-separated hostnames and IPs to generate a certificate for"), + stringFlag("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521"), + intFlag("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set"), + stringFlag("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011"), + durationFlag("duration", 365*24*time.Hour, "Duration that certificate is valid for"), + boolFlag("ca", "whether this cert should be its own Certificate Authority"), + }, +} + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} + +func pemBlockForKey(priv interface{}) *pem.Block { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + if err != nil { + log.Fatalf("Unable to marshal ECDSA private key: %v\n", err) + } + return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + default: + return nil + } +} + +func runCert(ctx *cli.Context) error { + if len(ctx.String("host")) == 0 { + log.Fatal("Missing required --host parameter") + } + + var priv interface{} + var err error + switch ctx.String("ecdsa-curve") { + case "": + priv, err = rsa.GenerateKey(rand.Reader, ctx.Int("rsa-bits")) + case "P224": + priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + case "P256": + priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "P384": + priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "P521": + priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + default: + log.Fatalf("Unrecognized elliptic curve: %q", ctx.String("ecdsa-curve")) + } + if err != nil { + log.Fatalf("Failed to generate private key: %s", err) + } + + var notBefore time.Time + if len(ctx.String("start-date")) == 0 { + notBefore = time.Now() + } else { + notBefore, err = time.Parse("Jan 2 15:04:05 2006", ctx.String("start-date")) + if err != nil { + log.Fatalf("Failed to parse creation date: %s", err) + } + } + + notAfter := notBefore.Add(ctx.Duration("duration")) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + log.Fatalf("Failed to generate serial number: %s", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + CommonName: "Gogs", + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + hosts := strings.Split(ctx.String("host"), ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + if ctx.Bool("ca") { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) + if err != nil { + log.Fatalf("Failed to create certificate: %s", err) + } + + certOut, err := os.Create("cert.pem") + if err != nil { + log.Fatalf("Failed to open cert.pem for writing: %s", err) + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + log.Println("Written cert.pem") + + keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatalf("Failed to open key.pem for writing: %v\n", err) + } + pem.Encode(keyOut, pemBlockForKey(priv)) + keyOut.Close() + log.Println("Written key.pem") + + return nil +} diff --git a/internal/cmd/cert_stub.go b/internal/cmd/cert_stub.go new file mode 100644 index 00000000..6164c83e --- /dev/null +++ b/internal/cmd/cert_stub.go @@ -0,0 +1,28 @@ +// +build !cert + +// Copyright 2009 The Go Authors. All rights reserved. +// Copyright 2014 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. +package cmd + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +var Cert = cli.Command{ + Name: "cert", + Usage: "Generate self-signed certificate", + Description: `Please use build tags "cert" to rebuild Gogs in order to have this ability`, + Action: runCert, +} + +func runCert(ctx *cli.Context) error { + fmt.Println("Command cert not available, please use build tags 'cert' to rebuild.") + os.Exit(1) + + return nil +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 00000000..29afa625 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,42 @@ +// Copyright 2015 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. + +package cmd + +import ( + "time" + + "github.com/urfave/cli" +) + +func stringFlag(name, value, usage string) cli.StringFlag { + return cli.StringFlag{ + Name: name, + Value: value, + Usage: usage, + } +} + +func boolFlag(name, usage string) cli.BoolFlag { + return cli.BoolFlag{ + Name: name, + Usage: usage, + } +} + +func intFlag(name string, value int, usage string) cli.IntFlag { + return cli.IntFlag{ + Name: name, + Value: value, + Usage: usage, + } +} + +func durationFlag(name string, value time.Duration, usage string) cli.DurationFlag { + return cli.DurationFlag{ + Name: name, + Value: value, + Usage: usage, + } +} diff --git a/internal/cmd/hook.go b/internal/cmd/hook.go new file mode 100644 index 00000000..a6c79c3b --- /dev/null +++ b/internal/cmd/hook.go @@ -0,0 +1,277 @@ +// Copyright 2014 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. + +package cmd + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/unknwon/com" + "github.com/urfave/cli" + log "gopkg.in/clog.v1" + + "github.com/gogs/git-module" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/httplib" + "gogs.io/gogs/internal/mailer" + "gogs.io/gogs/internal/setting" + "gogs.io/gogs/internal/template" +) + +var ( + Hook = cli.Command{ + Name: "hook", + Usage: "Delegate commands to corresponding Git hooks", + Description: "All sub-commands should only be called by Git", + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + Subcommands: []cli.Command{ + subcmdHookPreReceive, + subcmdHookUpadte, + subcmdHookPostReceive, + }, + } + + subcmdHookPreReceive = cli.Command{ + Name: "pre-receive", + Usage: "Delegate pre-receive Git hook", + Description: "This command should only be called by Git", + Action: runHookPreReceive, + } + subcmdHookUpadte = cli.Command{ + Name: "update", + Usage: "Delegate update Git hook", + Description: "This command should only be called by Git", + Action: runHookUpdate, + } + subcmdHookPostReceive = cli.Command{ + Name: "post-receive", + Usage: "Delegate post-receive Git hook", + Description: "This command should only be called by Git", + Action: runHookPostReceive, + } +) + +func runHookPreReceive(c *cli.Context) error { + if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { + return nil + } + setup(c, "hooks/pre-receive.log", true) + + isWiki := strings.Contains(os.Getenv(db.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') + + if isWiki { + continue + } + + fields := bytes.Fields(scanner.Bytes()) + if len(fields) != 3 { + continue + } + oldCommitID := string(fields[0]) + newCommitID := string(fields[1]) + branchName := strings.TrimPrefix(string(fields[2]), git.BRANCH_PREFIX) + + // Branch protection + repoID := com.StrTo(os.Getenv(db.ENV_REPO_ID)).MustInt64() + protectBranch, err := db.GetProtectBranchOfRepoByName(repoID, branchName) + if err != nil { + if errors.IsErrBranchNotExist(err) { + continue + } + fail("Internal error", "GetProtectBranchOfRepoByName [repo_id: %d, branch: %s]: %v", repoID, branchName, err) + } + if !protectBranch.Protected { + continue + } + + // Whitelist users can bypass require pull request check + bypassRequirePullRequest := false + + // Check if user is in whitelist when enabled + userID := com.StrTo(os.Getenv(db.ENV_AUTH_USER_ID)).MustInt64() + if protectBranch.EnableWhitelist { + if !db.IsUserInProtectBranchWhitelist(repoID, userID, branchName) { + fail(fmt.Sprintf("Branch '%s' is protected and you are not in the push whitelist", branchName), "") + } + + bypassRequirePullRequest = true + } + + // Check if branch allows direct push + if !bypassRequirePullRequest && protectBranch.RequirePullRequest { + fail(fmt.Sprintf("Branch '%s' is protected and commits must be merged through pull request", branchName), "") + } + + // check and deletion + if newCommitID == git.EMPTY_SHA { + fail(fmt.Sprintf("Branch '%s' is protected from deletion", branchName), "") + } + + // Check force push + output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID). + RunInDir(db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME))) + if err != nil { + fail("Internal error", "Fail to detect force push: %v", err) + } else if len(output) > 0 { + fail(fmt.Sprintf("Branch '%s' is protected from force push", branchName), "") + } + } + + customHooksPath := filepath.Join(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive") + if !com.IsFile(customHooksPath) { + return nil + } + + var hookCmd *exec.Cmd + if setting.IsWindows { + hookCmd = exec.Command("bash.exe", "custom_hooks/pre-receive") + } else { + hookCmd = exec.Command(customHooksPath) + } + hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME)) + hookCmd.Stdout = os.Stdout + hookCmd.Stdin = buf + hookCmd.Stderr = os.Stderr + if err := hookCmd.Run(); err != nil { + fail("Internal error", "Fail to execute custom pre-receive hook: %v", err) + } + return nil +} + +func runHookUpdate(c *cli.Context) error { + if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { + return nil + } + setup(c, "hooks/update.log", false) + + args := c.Args() + if len(args) != 3 { + fail("Arguments received are not equal to three", "Arguments received are not equal to three") + } else if len(args[0]) == 0 { + fail("First argument 'refName' is empty", "First argument 'refName' is empty") + } + + customHooksPath := filepath.Join(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), "update") + if !com.IsFile(customHooksPath) { + return nil + } + + var hookCmd *exec.Cmd + if setting.IsWindows { + hookCmd = exec.Command("bash.exe", append([]string{"custom_hooks/update"}, args...)...) + } else { + hookCmd = exec.Command(customHooksPath, args...) + } + hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME)) + hookCmd.Stdout = os.Stdout + hookCmd.Stdin = os.Stdin + hookCmd.Stderr = os.Stderr + if err := hookCmd.Run(); err != nil { + fail("Internal error", "Fail to execute custom pre-receive hook: %v", err) + } + return nil +} + +func runHookPostReceive(c *cli.Context) error { + if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { + return nil + } + setup(c, "hooks/post-receive.log", true) + + // Post-receive hook does more than just gather Git information, + // so we need to setup additional services for email notifications. + setting.NewPostReceiveHookServices() + mailer.NewContext() + mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"), + path.Join(setting.CustomPath, "templates/mail"), template.NewFuncMap()) + + isWiki := strings.Contains(os.Getenv(db.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 := db.PushUpdateOptions{ + OldCommitID: string(fields[0]), + NewCommitID: string(fields[1]), + RefFullName: string(fields[2]), + PusherID: com.StrTo(os.Getenv(db.ENV_AUTH_USER_ID)).MustInt64(), + PusherName: os.Getenv(db.ENV_AUTH_USER_NAME), + RepoUserName: os.Getenv(db.ENV_REPO_OWNER_NAME), + RepoName: os.Getenv(db.ENV_REPO_NAME), + } + if err := db.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=" + + template.EscapePound(strings.TrimPrefix(options.RefFullName, git.BRANCH_PREFIX)) + + "&secret=" + os.Getenv(db.ENV_REPO_OWNER_SALT_MD5) + + "&pusher=" + os.Getenv(db.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(db.ENV_REPO_CUSTOM_HOOKS_PATH), "post-receive") + if !com.IsFile(customHooksPath) { + return nil + } + + var hookCmd *exec.Cmd + if setting.IsWindows { + hookCmd = exec.Command("bash.exe", "custom_hooks/post-receive") + } else { + hookCmd = exec.Command(customHooksPath) + } + hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME)) + hookCmd.Stdout = os.Stdout + 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) + } + return nil +} diff --git a/internal/cmd/import.go b/internal/cmd/import.go new file mode 100644 index 00000000..4ee43b5b --- /dev/null +++ b/internal/cmd/import.go @@ -0,0 +1,113 @@ +// Copyright 2016 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. + +package cmd + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/unknwon/com" + "github.com/urfave/cli" + + "gogs.io/gogs/internal/setting" +) + +var ( + Import = cli.Command{ + Name: "import", + Usage: "Import portable data as local Gogs data", + Description: `Allow user import data from other Gogs installations to local instance +without manually hacking the data files`, + Subcommands: []cli.Command{ + subcmdImportLocale, + }, + } + + subcmdImportLocale = cli.Command{ + Name: "locale", + Usage: "Import locale files to local repository", + Action: runImportLocale, + Flags: []cli.Flag{ + stringFlag("source", "", "Source directory that stores new locale files"), + stringFlag("target", "", "Target directory that stores old locale files"), + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, + } +) + +func runImportLocale(c *cli.Context) error { + if !c.IsSet("source") { + return fmt.Errorf("Source directory is not specified") + } else if !c.IsSet("target") { + return fmt.Errorf("Target directory is not specified") + } + if !com.IsDir(c.String("source")) { + return fmt.Errorf("Source directory does not exist or is not a directory") + } else if !com.IsDir(c.String("target")) { + return fmt.Errorf("Target directory does not exist or is not a directory") + } + + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + + setting.NewContext() + + now := time.Now() + + line := make([]byte, 0, 100) + badChars := []byte(`="`) + escapedQuotes := []byte(`\"`) + regularQuotes := []byte(`"`) + // Cut out en-US. + for _, lang := range setting.Langs[1:] { + name := fmt.Sprintf("locale_%s.ini", lang) + source := filepath.Join(c.String("source"), name) + target := filepath.Join(c.String("target"), name) + if !com.IsFile(source) { + continue + } + + // Crowdin surrounds double quotes for strings contain quotes inside, + // this breaks INI parser, we need to fix that. + sr, err := os.Open(source) + if err != nil { + return fmt.Errorf("Open: %v", err) + } + + tw, err := os.Create(target) + if err != nil { + if err != nil { + return fmt.Errorf("Open: %v", err) + } + } + + scanner := bufio.NewScanner(sr) + for scanner.Scan() { + line = scanner.Bytes() + idx := bytes.Index(line, badChars) + if idx > -1 && line[len(line)-1] == '"' { + // We still want the "=" sign + line = append(line[:idx+1], line[idx+2:len(line)-1]...) + line = bytes.Replace(line, escapedQuotes, regularQuotes, -1) + } + tw.Write(line) + tw.WriteString("\n") + } + sr.Close() + tw.Close() + + // Modification time of files from Crowdin often ahead of current, + // so we need to set back to current. + os.Chtimes(target, now, now) + } + + fmt.Println("Locale files has been successfully imported!") + return nil +} diff --git a/internal/cmd/restore.go b/internal/cmd/restore.go new file mode 100644 index 00000000..cd5595df --- /dev/null +++ b/internal/cmd/restore.go @@ -0,0 +1,148 @@ +// 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. + +package cmd + +import ( + "os" + "path" + + "github.com/mcuadros/go-version" + "github.com/unknwon/cae/zip" + "github.com/unknwon/com" + "github.com/urfave/cli" + log "gopkg.in/clog.v1" + "gopkg.in/ini.v1" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/setting" +) + +var Restore = cli.Command{ + Name: "restore", + Usage: "Restore files and database from backup", + Description: `Restore imports all related files and database from a backup archive. +The backup version must lower or equal to current Gogs version. You can also import +backup from other database engines, which is useful for database migrating. + +If corresponding files or database tables are not presented in the archive, they will +be skipped and remain unchanged.`, + Action: runRestore, + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + boolFlag("verbose, v", "Show process details"), + stringFlag("tempdir, t", os.TempDir(), "Temporary directory path"), + stringFlag("from", "", "Path to backup archive"), + boolFlag("database-only", "Only import database"), + boolFlag("exclude-repos", "Exclude repositories"), + }, +} + +// lastSupportedVersionOfFormat returns the last supported version of the backup archive +// format that is able to import. +var lastSupportedVersionOfFormat = map[int]string{} + +func runRestore(c *cli.Context) error { + zip.Verbose = c.Bool("verbose") + + tmpDir := c.String("tempdir") + if !com.IsExist(tmpDir) { + log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir) + } + + log.Info("Restore backup from: %s", c.String("from")) + if err := zip.ExtractTo(c.String("from"), tmpDir); err != nil { + log.Fatal(0, "Failed to extract backup archive: %v", err) + } + archivePath := path.Join(tmpDir, _ARCHIVE_ROOT_DIR) + defer os.RemoveAll(archivePath) + + // Check backup version + metaFile := path.Join(archivePath, "metadata.ini") + if !com.IsExist(metaFile) { + log.Fatal(0, "File 'metadata.ini' is missing") + } + metadata, err := ini.Load(metaFile) + if err != nil { + log.Fatal(0, "Failed to load metadata '%s': %v", metaFile, err) + } + backupVersion := metadata.Section("").Key("GOGS_VERSION").MustString("999.0") + if version.Compare(setting.AppVer, backupVersion, "<") { + log.Fatal(0, "Current Gogs version is lower than backup version: %s < %s", setting.AppVer, backupVersion) + } + formatVersion := metadata.Section("").Key("VERSION").MustInt() + if formatVersion == 0 { + log.Fatal(0, "Failed to determine the backup format version from metadata '%s': %s", metaFile, "VERSION is not presented") + } + if formatVersion != _CURRENT_BACKUP_FORMAT_VERSION { + log.Fatal(0, "Backup format version found is %d but this binary only supports %d\nThe last known version that is able to import your backup is %s", + formatVersion, _CURRENT_BACKUP_FORMAT_VERSION, lastSupportedVersionOfFormat[formatVersion]) + } + + // If config file is not present in backup, user must set this file via flag. + // Otherwise, it's optional to set config file flag. + configFile := path.Join(archivePath, "custom/conf/app.ini") + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } else if !com.IsExist(configFile) { + log.Fatal(0, "'--config' is not specified and custom config file is not found in backup") + } else { + setting.CustomConf = configFile + } + setting.NewContext() + db.LoadConfigs() + db.SetEngine() + + // Database + dbDir := path.Join(archivePath, "db") + if err = db.ImportDatabase(dbDir, c.Bool("verbose")); err != nil { + log.Fatal(0, "Failed to import database: %v", err) + } + + // Custom files + if !c.Bool("database-only") { + if com.IsExist(setting.CustomPath) { + if err = os.Rename(setting.CustomPath, setting.CustomPath+".bak"); err != nil { + log.Fatal(0, "Failed to backup current 'custom': %v", err) + } + } + if err = os.Rename(path.Join(archivePath, "custom"), setting.CustomPath); err != nil { + log.Fatal(0, "Failed to import 'custom': %v", err) + } + } + + // Data files + if !c.Bool("database-only") { + os.MkdirAll(setting.AppDataPath, os.ModePerm) + for _, dir := range []string{"attachments", "avatars", "repo-avatars"} { + // Skip if backup archive does not have corresponding data + srcPath := path.Join(archivePath, "data", dir) + if !com.IsDir(srcPath) { + continue + } + + dirPath := path.Join(setting.AppDataPath, dir) + if com.IsExist(dirPath) { + if err = os.Rename(dirPath, dirPath+".bak"); err != nil { + log.Fatal(0, "Failed to backup current 'data': %v", err) + } + } + if err = os.Rename(srcPath, dirPath); err != nil { + log.Fatal(0, "Failed to import 'data': %v", err) + } + } + } + + // Repositories + reposPath := path.Join(archivePath, "repositories.zip") + if !c.Bool("exclude-repos") && !c.Bool("database-only") && com.IsExist(reposPath) { + if err := zip.ExtractTo(reposPath, path.Dir(setting.RepoRootPath)); err != nil { + log.Fatal(0, "Failed to extract 'repositories.zip': %v", err) + } + } + + log.Info("Restore succeed!") + log.Shutdown() + return nil +} diff --git a/internal/cmd/serv.go b/internal/cmd/serv.go new file mode 100644 index 00000000..870cf62e --- /dev/null +++ b/internal/cmd/serv.go @@ -0,0 +1,272 @@ +// Copyright 2014 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. + +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/unknwon/com" + "github.com/urfave/cli" + log "gopkg.in/clog.v1" + + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/setting" +) + +const ( + _ACCESS_DENIED_MESSAGE = "Repository does not exist or you do not have access" +) + +var Serv = cli.Command{ + Name: "serv", + Usage: "This command should only be called by SSH shell", + Description: `Serv provide access auth for repositories`, + Action: runServ, + Flags: []cli.Flag{ + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, +} + +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") { + setting.CustomConf = c.GlobalString("config") + } + + setting.NewContext() + + 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, + Daily: true, + MaxDays: 3, + }, + }) + log.Delete(log.CONSOLE) // Remove primary logger + + if !connectDB { + return + } + + db.LoadConfigs() + + if setting.UseSQLite3 { + workDir, _ := setting.WorkDir() + os.Chdir(workDir) + } + + if err := db.SetEngine(); err != nil { + fail("Internal error", "SetEngine: %v", err) + } +} + +func parseSSHCmd(cmd string) (string, string) { + ss := strings.SplitN(cmd, " ", 2) + if len(ss) != 2 { + return "", "" + } + return ss[0], strings.Replace(ss[1], "'/", "'", 1) +} + +func checkDeployKey(key *db.PublicKey, repo *db.Repository) { + // Check if this deploy key belongs to current repository. + if !db.HasDeployKey(key.ID, repo.ID) { + fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID) + } + + // Update deploy key activity. + deployKey, err := db.GetDeployKeyByRepo(key.ID, repo.ID) + if err != nil { + fail("Internal error", "GetDeployKey: %v", err) + } + + deployKey.Updated = time.Now() + if err = db.UpdateDeployKey(deployKey); err != nil { + fail("Internal error", "UpdateDeployKey: %v", err) + } +} + +var ( + allowedCommands = map[string]db.AccessMode{ + "git-upload-pack": db.ACCESS_MODE_READ, + "git-upload-archive": db.ACCESS_MODE_READ, + "git-receive-pack": db.ACCESS_MODE_WRITE, + } +) + +func runServ(c *cli.Context) error { + setup(c, "serv.log", true) + + if setting.SSH.Disabled { + println("Gogs: SSH has been disabled") + return nil + } + + if len(c.Args()) < 1 { + fail("Not enough arguments", "Not enough arguments") + } + + sshCmd := os.Getenv("SSH_ORIGINAL_COMMAND") + if len(sshCmd) == 0 { + println("Hi there, You've successfully authenticated, but Gogs does not provide shell access.") + println("If this is unexpected, please log in with password and setup Gogs under another user.") + return nil + } + + verb, args := parseSSHCmd(sshCmd) + repoFullName := strings.ToLower(strings.Trim(args, "'")) + repoFields := strings.SplitN(repoFullName, "/", 2) + if len(repoFields) != 2 { + fail("Invalid repository path", "Invalid repository path: %v", args) + } + ownerName := strings.ToLower(repoFields[0]) + repoName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git") + repoName = strings.TrimSuffix(repoName, ".wiki") + + owner, err := db.GetUserByName(ownerName) + if err != nil { + if errors.IsUserNotExist(err) { + fail("Repository owner does not exist", "Unregistered owner: %s", ownerName) + } + fail("Internal error", "Fail to get repository owner '%s': %v", ownerName, err) + } + + repo, err := db.GetRepositoryByName(owner.ID, repoName) + if err != nil { + if errors.IsRepoNotExist(err) { + fail(_ACCESS_DENIED_MESSAGE, "Repository does not exist: %s/%s", owner.Name, repoName) + } + fail("Internal error", "Fail to get repository: %v", err) + } + repo.Owner = owner + + requestMode, ok := allowedCommands[verb] + if !ok { + fail("Unknown git command", "Unknown git command '%s'", verb) + } + + // Prohibit push to mirror repositories. + if requestMode > db.ACCESS_MODE_READ && repo.IsMirror { + fail("Mirror repository is read-only", "") + } + + // Allow anonymous (user is nil) clone for public repositories. + var user *db.User + + key, err := db.GetPublicKeyByID(com.StrTo(strings.TrimPrefix(c.Args()[0], "key-")).MustInt64()) + if err != nil { + fail("Invalid key ID", "Invalid key ID '%s': %v", c.Args()[0], err) + } + + if requestMode == db.ACCESS_MODE_WRITE || repo.IsPrivate { + // Check deploy key or user key. + if key.IsDeployKey() { + if key.Mode < requestMode { + fail("Key permission denied", "Cannot push with deployment key: %d", key.ID) + } + checkDeployKey(key, repo) + } else { + user, err = db.GetUserByKeyID(key.ID) + if err != nil { + fail("Internal error", "Fail to get user by key ID '%d': %v", key.ID, err) + } + + mode, err := db.UserAccessMode(user.ID, repo) + if err != nil { + fail("Internal error", "Fail to check access: %v", err) + } + + if mode < requestMode { + clientMessage := _ACCESS_DENIED_MESSAGE + if mode >= db.ACCESS_MODE_READ { + clientMessage = "You do not have sufficient authorization for this action" + } + fail(clientMessage, + "User '%s' does not have level '%v' access to repository '%s'", + user.Name, requestMode, repoFullName) + } + } + } 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 + // or system using an active deploy key can get read access to all the repositories in a Gogs service. + if key.IsDeployKey() && setting.Service.RequireSignInView { + checkDeployKey(key, repo) + } + } + + // Update user key activity. + if key.ID > 0 { + key, err := db.GetPublicKeyByID(key.ID) + if err != nil { + fail("Internal error", "GetPublicKeyByID: %v", err) + } + + key.Updated = time.Now() + if err = db.UpdatePublicKey(key); err != nil { + fail("Internal error", "UpdatePublicKey: %v", err) + } + } + + // 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 == db.ACCESS_MODE_WRITE { + gitCmd.Env = append(os.Environ(), db.ComposeHookEnvs(db.ComposeHookEnvsOptions{ + AuthUser: user, + OwnerName: owner.Name, + OwnerSalt: owner.Salt, + RepoID: repo.ID, + RepoName: repo.Name, + RepoPath: repo.RepoPath(), + })...) + } + 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 +} diff --git a/internal/cmd/web.go b/internal/cmd/web.go new file mode 100644 index 00000000..709bc0df --- /dev/null +++ b/internal/cmd/web.go @@ -0,0 +1,757 @@ +// Copyright 2014 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. + +package cmd + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/fcgi" + "os" + "path" + "strings" + + "github.com/go-macaron/binding" + "github.com/go-macaron/cache" + "github.com/go-macaron/captcha" + "github.com/go-macaron/csrf" + "github.com/go-macaron/gzip" + "github.com/go-macaron/i18n" + "github.com/go-macaron/session" + "github.com/go-macaron/toolbox" + "github.com/mcuadros/go-version" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/unknwon/com" + "github.com/urfave/cli" + log "gopkg.in/clog.v1" + "gopkg.in/macaron.v1" + + "gogs.io/gogs/internal/bindata" + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/form" + "gogs.io/gogs/internal/mailer" + "gogs.io/gogs/internal/route" + "gogs.io/gogs/internal/route/admin" + apiv1 "gogs.io/gogs/internal/route/api/v1" + "gogs.io/gogs/internal/route/dev" + "gogs.io/gogs/internal/route/org" + "gogs.io/gogs/internal/route/repo" + "gogs.io/gogs/internal/route/user" + "gogs.io/gogs/internal/setting" + "gogs.io/gogs/internal/template" +) + +var Web = cli.Command{ + Name: "web", + Usage: "Start web server", + Description: `Gogs web server is the only thing you need to run, +and it takes care of all the other things for you`, + Action: runWeb, + Flags: []cli.Flag{ + stringFlag("port, p", "3000", "Temporary port number to prevent conflict"), + stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), + }, +} + +// checkVersion checks if binary matches the version of templates files. +func checkVersion() { + // Templates. + data, err := ioutil.ReadFile(setting.StaticRootPath + "/templates/.VERSION") + if err != nil { + log.Fatal(2, "Fail to read 'templates/.VERSION': %v", err) + } + tplVer := strings.TrimSpace(string(data)) + if tplVer != setting.AppVer { + if version.Compare(tplVer, setting.AppVer, ">") { + log.Fatal(2, "Binary version is lower than template file version, did you forget to recompile Gogs?") + } else { + log.Fatal(2, "Binary version is higher than template file version, did you forget to update template files?") + } + } +} + +// newMacaron initializes Macaron instance. +func newMacaron() *macaron.Macaron { + m := macaron.New() + if !setting.DisableRouterLog { + m.Use(macaron.Logger()) + } + m.Use(macaron.Recovery()) + if setting.EnableGzip { + m.Use(gzip.Gziper()) + } + if setting.Protocol == setting.SCHEME_FCGI { + m.SetURLPrefix(setting.AppSubURL) + } + m.Use(macaron.Static( + path.Join(setting.StaticRootPath, "public"), + macaron.StaticOptions{ + SkipLogging: setting.DisableRouterLog, + }, + )) + m.Use(macaron.Static( + setting.AvatarUploadPath, + macaron.StaticOptions{ + Prefix: db.USER_AVATAR_URL_PREFIX, + SkipLogging: setting.DisableRouterLog, + }, + )) + m.Use(macaron.Static( + setting.RepositoryAvatarUploadPath, + macaron.StaticOptions{ + Prefix: db.REPO_AVATAR_URL_PREFIX, + SkipLogging: setting.DisableRouterLog, + }, + )) + + funcMap := template.NewFuncMap() + m.Use(macaron.Renderer(macaron.RenderOptions{ + Directory: path.Join(setting.StaticRootPath, "templates"), + AppendDirectories: []string{path.Join(setting.CustomPath, "templates")}, + Funcs: funcMap, + IndentJSON: macaron.Env != macaron.PROD, + })) + mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"), + path.Join(setting.CustomPath, "templates/mail"), funcMap) + + localeNames, err := bindata.AssetDir("conf/locale") + if err != nil { + log.Fatal(4, "Fail to list locale files: %v", err) + } + localFiles := make(map[string][]byte) + for _, name := range localeNames { + localFiles[name] = bindata.MustAsset("conf/locale/" + name) + } + m.Use(i18n.I18n(i18n.Options{ + SubURL: setting.AppSubURL, + Files: localFiles, + CustomDirectory: path.Join(setting.CustomPath, "conf/locale"), + Langs: setting.Langs, + Names: setting.Names, + DefaultLang: "en-US", + Redirect: true, + })) + m.Use(cache.Cacher(cache.Options{ + Adapter: setting.CacheAdapter, + AdapterConfig: setting.CacheConn, + Interval: setting.CacheInterval, + })) + m.Use(captcha.Captchaer(captcha.Options{ + SubURL: setting.AppSubURL, + })) + m.Use(session.Sessioner(setting.SessionConfig)) + m.Use(csrf.Csrfer(csrf.Options{ + Secret: setting.SecretKey, + Cookie: setting.CSRFCookieName, + SetCookie: true, + Header: "X-Csrf-Token", + CookiePath: setting.AppSubURL, + })) + m.Use(toolbox.Toolboxer(m, toolbox.Options{ + HealthCheckFuncs: []*toolbox.HealthCheckFuncDesc{ + &toolbox.HealthCheckFuncDesc{ + Desc: "Database connection", + Func: db.Ping, + }, + }, + })) + m.Use(context.Contexter()) + return m +} + +func runWeb(c *cli.Context) error { + if c.IsSet("config") { + setting.CustomConf = c.String("config") + } + route.GlobalInit() + checkVersion() + + m := newMacaron() + + reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true}) + ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView}) + ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true}) + reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) + + bindIgnErr := binding.BindIgnErr + + m.SetAutoHead(true) + + // FIXME: not all route need go through same middlewares. + // Especially some AJAX requests, we can reduce middleware number to improve performance. + // Routers. + m.Get("/", ignSignIn, route.Home) + m.Group("/explore", func() { + m.Get("", func(c *context.Context) { + c.Redirect(setting.AppSubURL + "/explore/repos") + }) + m.Get("/repos", route.ExploreRepos) + m.Get("/users", route.ExploreUsers) + m.Get("/organizations", route.ExploreOrganizations) + }, ignSignIn) + m.Combo("/install", route.InstallInit).Get(route.Install). + Post(bindIgnErr(form.Install{}), route.InstallPost) + m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues) + + // ***** START: User ***** + m.Group("/user", func() { + m.Group("/login", func() { + m.Combo("").Get(user.Login). + Post(bindIgnErr(form.SignIn{}), user.LoginPost) + m.Combo("/two_factor").Get(user.LoginTwoFactor).Post(user.LoginTwoFactorPost) + m.Combo("/two_factor_recovery_code").Get(user.LoginTwoFactorRecoveryCode).Post(user.LoginTwoFactorRecoveryCodePost) + }) + + m.Get("/sign_up", user.SignUp) + m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost) + m.Get("/reset_password", user.ResetPasswd) + m.Post("/reset_password", user.ResetPasswdPost) + }, reqSignOut) + + m.Group("/user/settings", func() { + m.Get("", user.Settings) + m.Post("", bindIgnErr(form.UpdateProfile{}), user.SettingsPost) + m.Combo("/avatar").Get(user.SettingsAvatar). + Post(binding.MultipartForm(form.Avatar{}), user.SettingsAvatarPost) + m.Post("/avatar/delete", user.SettingsDeleteAvatar) + m.Combo("/email").Get(user.SettingsEmails). + Post(bindIgnErr(form.AddEmail{}), user.SettingsEmailPost) + m.Post("/email/delete", user.DeleteEmail) + m.Get("/password", user.SettingsPassword) + m.Post("/password", bindIgnErr(form.ChangePassword{}), user.SettingsPasswordPost) + m.Combo("/ssh").Get(user.SettingsSSHKeys). + Post(bindIgnErr(form.AddSSHKey{}), user.SettingsSSHKeysPost) + m.Post("/ssh/delete", user.DeleteSSHKey) + m.Group("/security", func() { + m.Get("", user.SettingsSecurity) + m.Combo("/two_factor_enable").Get(user.SettingsTwoFactorEnable). + Post(user.SettingsTwoFactorEnablePost) + m.Combo("/two_factor_recovery_codes").Get(user.SettingsTwoFactorRecoveryCodes). + Post(user.SettingsTwoFactorRecoveryCodesPost) + m.Post("/two_factor_disable", user.SettingsTwoFactorDisable) + }) + m.Group("/repositories", func() { + m.Get("", user.SettingsRepos) + m.Post("/leave", user.SettingsLeaveRepo) + }) + m.Group("/organizations", func() { + m.Get("", user.SettingsOrganizations) + m.Post("/leave", user.SettingsLeaveOrganization) + }) + m.Combo("/applications").Get(user.SettingsApplications). + Post(bindIgnErr(form.NewAccessToken{}), user.SettingsApplicationsPost) + m.Post("/applications/delete", user.SettingsDeleteApplication) + m.Route("/delete", "GET,POST", user.SettingsDelete) + }, reqSignIn, func(c *context.Context) { + c.Data["PageIsUserSettings"] = true + }) + + m.Group("/user", func() { + m.Any("/activate", user.Activate) + m.Any("/activate_email", user.ActivateEmail) + m.Get("/email2user", user.Email2User) + m.Get("/forget_password", user.ForgotPasswd) + m.Post("/forget_password", user.ForgotPasswdPost) + m.Post("/logout", user.SignOut) + }) + // ***** END: User ***** + + reqAdmin := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true}) + + // ***** START: Admin ***** + m.Group("/admin", func() { + m.Get("", admin.Dashboard) + m.Get("/config", admin.Config) + m.Post("/config/test_mail", admin.SendTestMail) + m.Get("/monitor", admin.Monitor) + + m.Group("/users", func() { + m.Get("", admin.Users) + m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(form.AdminCrateUser{}), admin.NewUserPost) + m.Combo("/:userid").Get(admin.EditUser).Post(bindIgnErr(form.AdminEditUser{}), admin.EditUserPost) + m.Post("/:userid/delete", admin.DeleteUser) + }) + + m.Group("/orgs", func() { + m.Get("", admin.Organizations) + }) + + m.Group("/repos", func() { + m.Get("", admin.Repos) + m.Post("/delete", admin.DeleteRepo) + }) + + m.Group("/auths", func() { + m.Get("", admin.Authentications) + m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(form.Authentication{}), admin.NewAuthSourcePost) + m.Combo("/:authid").Get(admin.EditAuthSource). + Post(bindIgnErr(form.Authentication{}), admin.EditAuthSourcePost) + m.Post("/:authid/delete", admin.DeleteAuthSource) + }) + + m.Group("/notices", func() { + m.Get("", admin.Notices) + m.Post("/delete", admin.DeleteNotices) + m.Get("/empty", admin.EmptyNotices) + }) + }, reqAdmin) + // ***** END: Admin ***** + + m.Group("", func() { + m.Group("/:username", func() { + m.Get("", user.Profile) + m.Get("/followers", user.Followers) + m.Get("/following", user.Following) + m.Get("/stars", user.Stars) + }, context.InjectParamsUser()) + + m.Get("/attachments/:uuid", func(c *context.Context) { + attach, err := db.GetAttachmentByUUID(c.Params(":uuid")) + if err != nil { + c.NotFoundOrServerError("GetAttachmentByUUID", db.IsErrAttachmentNotExist, err) + return + } else if !com.IsFile(attach.LocalPath()) { + c.NotFound() + return + } + + fr, err := os.Open(attach.LocalPath()) + if err != nil { + c.Handle(500, "Open", err) + return + } + defer fr.Close() + + c.Header().Set("Cache-Control", "public,max-age=86400") + fmt.Println("attach.Name:", attach.Name) + c.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, attach.Name)) + if err = repo.ServeData(c, attach.Name, fr); err != nil { + c.Handle(500, "ServeData", err) + return + } + }) + m.Post("/issues/attachments", repo.UploadIssueAttachment) + m.Post("/releases/attachments", repo.UploadReleaseAttachment) + }, ignSignIn) + + m.Group("/:username", func() { + m.Post("/action/:action", user.Action) + }, reqSignIn, context.InjectParamsUser()) + + if macaron.Env == macaron.DEV { + m.Get("/template/*", dev.TemplatePreview) + } + + reqRepoAdmin := context.RequireRepoAdmin() + reqRepoWriter := context.RequireRepoWriter() + + // ***** START: Organization ***** + m.Group("/org", func() { + m.Group("", func() { + m.Get("/create", org.Create) + m.Post("/create", bindIgnErr(form.CreateOrg{}), org.CreatePost) + }, func(c *context.Context) { + if !c.User.CanCreateOrganization() { + c.NotFound() + } + }) + + m.Group("/:org", func() { + m.Get("/dashboard", user.Dashboard) + m.Get("/^:type(issues|pulls)$", user.Issues) + m.Get("/members", org.Members) + m.Get("/members/action/:action", org.MembersAction) + + m.Get("/teams", org.Teams) + }, context.OrgAssignment(true)) + + m.Group("/:org", func() { + m.Get("/teams/:team", org.TeamMembers) + m.Get("/teams/:team/repositories", org.TeamRepositories) + m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction) + m.Route("/teams/:team/action/repo/:action", "GET,POST", org.TeamsRepoAction) + }, context.OrgAssignment(true, false, true)) + + m.Group("/:org", func() { + m.Get("/teams/new", org.NewTeam) + m.Post("/teams/new", bindIgnErr(form.CreateTeam{}), org.NewTeamPost) + m.Get("/teams/:team/edit", org.EditTeam) + m.Post("/teams/:team/edit", bindIgnErr(form.CreateTeam{}), org.EditTeamPost) + m.Post("/teams/:team/delete", org.DeleteTeam) + + m.Group("/settings", func() { + m.Combo("").Get(org.Settings). + Post(bindIgnErr(form.UpdateOrgSetting{}), org.SettingsPost) + m.Post("/avatar", binding.MultipartForm(form.Avatar{}), org.SettingsAvatar) + m.Post("/avatar/delete", org.SettingsDeleteAvatar) + + m.Group("/hooks", func() { + m.Get("", org.Webhooks) + m.Post("/delete", org.DeleteWebhook) + m.Get("/:type/new", repo.WebhooksNew) + m.Post("/gogs/new", bindIgnErr(form.NewWebhook{}), repo.WebHooksNewPost) + m.Post("/slack/new", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksNewPost) + m.Post("/discord/new", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksNewPost) + m.Get("/:id", repo.WebHooksEdit) + m.Post("/gogs/:id", bindIgnErr(form.NewWebhook{}), repo.WebHooksEditPost) + m.Post("/slack/:id", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksEditPost) + m.Post("/discord/:id", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/:id", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksEditPost) + }) + + m.Route("/delete", "GET,POST", org.SettingsDelete) + }) + + m.Route("/invitations/new", "GET,POST", org.Invitation) + }, context.OrgAssignment(true, true)) + }, reqSignIn) + // ***** END: Organization ***** + + // ***** START: Repository ***** + m.Group("/repo", func() { + m.Get("/create", repo.Create) + m.Post("/create", bindIgnErr(form.CreateRepo{}), repo.CreatePost) + m.Get("/migrate", repo.Migrate) + m.Post("/migrate", bindIgnErr(form.MigrateRepo{}), repo.MigratePost) + m.Combo("/fork/:repoid").Get(repo.Fork). + Post(bindIgnErr(form.CreateRepo{}), repo.ForkPost) + }, reqSignIn) + + m.Group("/:username/:reponame", func() { + m.Group("/settings", func() { + m.Combo("").Get(repo.Settings). + Post(bindIgnErr(form.RepoSetting{}), repo.SettingsPost) + m.Combo("/avatar").Get(repo.SettingsAvatar). + Post(binding.MultipartForm(form.Avatar{}), repo.SettingsAvatarPost) + m.Post("/avatar/delete", repo.SettingsDeleteAvatar) + m.Group("/collaboration", func() { + m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost) + m.Post("/access_mode", repo.ChangeCollaborationAccessMode) + m.Post("/delete", repo.DeleteCollaboration) + }) + m.Group("/branches", func() { + m.Get("", repo.SettingsBranches) + m.Post("/default_branch", repo.UpdateDefaultBranch) + m.Combo("/*").Get(repo.SettingsProtectedBranch). + Post(bindIgnErr(form.ProtectBranch{}), repo.SettingsProtectedBranchPost) + }, func(c *context.Context) { + if c.Repo.Repository.IsMirror { + c.NotFound() + return + } + }) + + m.Group("/hooks", func() { + m.Get("", repo.Webhooks) + m.Post("/delete", repo.DeleteWebhook) + m.Get("/:type/new", repo.WebhooksNew) + m.Post("/gogs/new", bindIgnErr(form.NewWebhook{}), repo.WebHooksNewPost) + m.Post("/slack/new", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksNewPost) + m.Post("/discord/new", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksNewPost) + m.Post("/gogs/:id", bindIgnErr(form.NewWebhook{}), repo.WebHooksEditPost) + m.Post("/slack/:id", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksEditPost) + m.Post("/discord/:id", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/:id", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksEditPost) + + m.Group("/:id", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/test", repo.TestWebhook) + m.Post("/redelivery", repo.RedeliveryWebhook) + }) + + m.Group("/git", func() { + m.Get("", repo.SettingsGitHooks) + m.Combo("/:name").Get(repo.SettingsGitHooksEdit). + Post(repo.SettingsGitHooksEditPost) + }, context.GitHookService()) + }) + + m.Group("/keys", func() { + m.Combo("").Get(repo.SettingsDeployKeys). + Post(bindIgnErr(form.AddSSHKey{}), repo.SettingsDeployKeysPost) + m.Post("/delete", repo.DeleteDeployKey) + }) + + }, func(c *context.Context) { + c.Data["PageIsSettings"] = true + }) + }, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef()) + + m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action) + m.Group("/:username/:reponame", func() { + m.Get("/issues", repo.RetrieveLabels, repo.Issues) + m.Get("/issues/:index", repo.ViewIssue) + m.Get("/labels/", repo.RetrieveLabels, repo.Labels) + m.Get("/milestones", repo.Milestones) + }, ignSignIn, context.RepoAssignment(true)) + m.Group("/:username/:reponame", func() { + // FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest. + // So they can apply their own enable/disable logic on routers. + m.Group("/issues", func() { + m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue). + Post(bindIgnErr(form.NewIssue{}), repo.NewIssuePost) + + m.Group("/:index", func() { + m.Post("/title", repo.UpdateIssueTitle) + m.Post("/content", repo.UpdateIssueContent) + m.Combo("/comments").Post(bindIgnErr(form.CreateComment{}), repo.NewComment) + }) + }) + m.Group("/comments/:id", func() { + m.Post("", repo.UpdateCommentContent) + m.Post("/delete", repo.DeleteComment) + }) + }, reqSignIn, context.RepoAssignment(true)) + m.Group("/:username/:reponame", func() { + m.Group("/wiki", func() { + m.Get("/?:page", repo.Wiki) + m.Get("/_pages", repo.WikiPages) + }, repo.MustEnableWiki, context.RepoRef()) + }, ignSignIn, context.RepoAssignment(false, true)) + + m.Group("/:username/:reponame", func() { + // FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest. + // So they can apply their own enable/disable logic on routers. + m.Group("/issues", func() { + m.Group("/:index", func() { + m.Post("/label", repo.UpdateIssueLabel) + m.Post("/milestone", repo.UpdateIssueMilestone) + m.Post("/assignee", repo.UpdateIssueAssignee) + }, reqRepoWriter) + }) + m.Group("/labels", func() { + m.Post("/new", bindIgnErr(form.CreateLabel{}), repo.NewLabel) + m.Post("/edit", bindIgnErr(form.CreateLabel{}), repo.UpdateLabel) + m.Post("/delete", repo.DeleteLabel) + m.Post("/initialize", bindIgnErr(form.InitializeLabels{}), repo.InitializeLabels) + }, reqRepoWriter, context.RepoRef()) + m.Group("/milestones", func() { + m.Combo("/new").Get(repo.NewMilestone). + Post(bindIgnErr(form.CreateMilestone{}), repo.NewMilestonePost) + m.Get("/:id/edit", repo.EditMilestone) + m.Post("/:id/edit", bindIgnErr(form.CreateMilestone{}), repo.EditMilestonePost) + m.Get("/:id/:action", repo.ChangeMilestonStatus) + m.Post("/delete", repo.DeleteMilestone) + }, reqRepoWriter, context.RepoRef()) + + m.Group("/releases", func() { + m.Get("/new", repo.NewRelease) + m.Post("/new", bindIgnErr(form.NewRelease{}), repo.NewReleasePost) + m.Post("/delete", repo.DeleteRelease) + m.Get("/edit/*", repo.EditRelease) + m.Post("/edit/*", bindIgnErr(form.EditRelease{}), repo.EditReleasePost) + }, repo.MustBeNotBare, reqRepoWriter, func(c *context.Context) { + c.Data["PageIsViewFiles"] = true + }) + + // FIXME: Should use c.Repo.PullRequest to unify template, currently we have inconsistent URL + // for PR in same repository. After select branch on the page, the URL contains redundant head user name. + // e.g. /org1/test-repo/compare/master...org1:develop + // which should be /org1/test-repo/compare/master...develop + m.Combo("/compare/*", repo.MustAllowPulls).Get(repo.CompareAndPullRequest). + Post(bindIgnErr(form.NewIssue{}), repo.CompareAndPullRequestPost) + + m.Group("", func() { + m.Combo("/_edit/*").Get(repo.EditFile). + Post(bindIgnErr(form.EditRepoFile{}), repo.EditFilePost) + m.Combo("/_new/*").Get(repo.NewFile). + Post(bindIgnErr(form.EditRepoFile{}), repo.NewFilePost) + m.Post("/_preview/*", bindIgnErr(form.EditPreviewDiff{}), repo.DiffPreviewPost) + m.Combo("/_delete/*").Get(repo.DeleteFile). + Post(bindIgnErr(form.DeleteRepoFile{}), repo.DeleteFilePost) + + m.Group("", func() { + m.Combo("/_upload/*").Get(repo.UploadFile). + Post(bindIgnErr(form.UploadRepoFile{}), repo.UploadFilePost) + m.Post("/upload-file", repo.UploadFileToServer) + m.Post("/upload-remove", bindIgnErr(form.RemoveUploadFile{}), repo.RemoveUploadFileFromServer) + }, func(c *context.Context) { + if !setting.Repository.Upload.Enabled { + c.NotFound() + return + } + }) + }, repo.MustBeNotBare, reqRepoWriter, context.RepoRef(), func(c *context.Context) { + if !c.Repo.CanEnableEditor() { + c.NotFound() + return + } + + c.Data["PageIsViewFiles"] = true + }) + }, reqSignIn, context.RepoAssignment()) + + m.Group("/:username/:reponame", func() { + m.Group("", func() { + m.Get("/releases", repo.MustBeNotBare, repo.Releases) + m.Get("/pulls", repo.RetrieveLabels, repo.Pulls) + m.Get("/pulls/:index", repo.ViewPull) + }, context.RepoRef()) + + m.Group("/branches", func() { + m.Get("", repo.Branches) + m.Get("/all", repo.AllBranches) + m.Post("/delete/*", reqSignIn, reqRepoWriter, repo.DeleteBranchPost) + }, repo.MustBeNotBare, func(c *context.Context) { + c.Data["PageIsViewFiles"] = true + }) + + m.Group("/wiki", func() { + m.Group("", func() { + m.Combo("/_new").Get(repo.NewWiki). + Post(bindIgnErr(form.NewWiki{}), repo.NewWikiPost) + m.Combo("/:page/_edit").Get(repo.EditWiki). + Post(bindIgnErr(form.NewWiki{}), repo.EditWikiPost) + m.Post("/:page/delete", repo.DeleteWikiPagePost) + }, reqSignIn, reqRepoWriter) + }, repo.MustEnableWiki, context.RepoRef()) + + m.Get("/archive/*", repo.MustBeNotBare, repo.Download) + + m.Group("/pulls/:index", func() { + m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) + m.Get("/files", context.RepoRef(), repo.ViewPullFiles) + m.Post("/merge", reqRepoWriter, repo.MergePullRequest) + }, repo.MustAllowPulls) + + m.Group("", func() { + m.Get("/src/*", repo.Home) + m.Get("/raw/*", repo.SingleDownload) + m.Get("/commits/*", repo.RefCommits) + m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.Diff) + m.Get("/forks", repo.Forks) + }, repo.MustBeNotBare, context.RepoRef()) + m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.MustBeNotBare, repo.RawDiff) + + m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.MustBeNotBare, context.RepoRef(), repo.CompareDiff) + }, ignSignIn, context.RepoAssignment()) + m.Group("/:username/:reponame", func() { + m.Get("/stars", repo.Stars) + m.Get("/watchers", repo.Watchers) + }, ignSignIn, context.RepoAssignment(), context.RepoRef()) + + m.Group("/:username", func() { + m.Get("/:reponame", ignSignIn, context.RepoAssignment(), context.RepoRef(), repo.Home) + + m.Group("/:reponame", func() { + m.Head("/tasks/trigger", repo.TriggerTask) + }) + // Use the regexp to match the repository name + // Duplicated route to enable different ways of accessing same set of URLs, + // e.g. with or without ".git" suffix. + m.Group("/:reponame([\\d\\w-_\\.]+\\.git$)", func() { + m.Get("", ignSignIn, context.RepoAssignment(), context.RepoRef(), repo.Home) + m.Options("/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP) + m.Route("/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP) + }) + m.Options("/:reponame/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP) + m.Route("/:reponame/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP) + }) + // ***** END: Repository ***** + + m.Group("/api", func() { + apiv1.RegisterRoutes(m) + }, ignSignIn) + + m.Group("/-", func() { + if setting.Prometheus.Enabled { + m.Get("/metrics", func(c *context.Context) { + if !setting.Prometheus.EnableBasicAuth { + return + } + + c.RequireBasicAuth(setting.Prometheus.BasicAuthUsername, setting.Prometheus.BasicAuthPassword) + }, promhttp.Handler()) + } + }) + + // robots.txt + m.Get("/robots.txt", func(c *context.Context) { + if setting.HasRobotsTxt { + c.ServeFileContent(path.Join(setting.CustomPath, "robots.txt")) + } else { + c.NotFound() + } + }) + + // Not found handler. + m.NotFound(route.NotFound) + + // Flag for port number in case first time run conflict. + if c.IsSet("port") { + setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, c.String("port"), 1) + setting.HTTPPort = c.String("port") + } + + var listenAddr string + if setting.Protocol == setting.SCHEME_UNIX_SOCKET { + listenAddr = fmt.Sprintf("%s", setting.HTTPAddr) + } else { + listenAddr = fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.HTTPPort) + } + log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL) + + var err error + switch setting.Protocol { + case setting.SCHEME_HTTP: + err = http.ListenAndServe(listenAddr, m) + case setting.SCHEME_HTTPS: + var tlsMinVersion uint16 + switch setting.TLSMinVersion { + case "SSL30": + tlsMinVersion = tls.VersionSSL30 + case "TLS12": + tlsMinVersion = tls.VersionTLS12 + case "TLS11": + tlsMinVersion = tls.VersionTLS11 + case "TLS10": + fallthrough + default: + tlsMinVersion = tls.VersionTLS10 + } + server := &http.Server{Addr: listenAddr, TLSConfig: &tls.Config{ + MinVersion: tlsMinVersion, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // Required for HTTP/2 support. + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + }, Handler: m} + err = server.ListenAndServeTLS(setting.CertFile, setting.KeyFile) + case setting.SCHEME_FCGI: + err = fcgi.Serve(nil, m) + case setting.SCHEME_UNIX_SOCKET: + os.Remove(listenAddr) + + var listener *net.UnixListener + listener, err = net.ListenUnix("unix", &net.UnixAddr{listenAddr, "unix"}) + if err != nil { + break // Handle error after switch + } + + // FIXME: add proper implementation of signal capture on all protocols + // execute this on SIGTERM or SIGINT: listener.Close() + if err = os.Chmod(listenAddr, os.FileMode(setting.UnixSocketPermission)); err != nil { + log.Fatal(4, "Failed to set permission of unix socket: %v", err) + } + err = http.Serve(listener, m) + default: + log.Fatal(4, "Invalid protocol: %s", setting.Protocol) + } + + if err != nil { + log.Fatal(4, "Failed to start server: %v", err) + } + + return nil +} |