// 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 setting import ( "fmt" "net/url" "os" "os/exec" "path" "path/filepath" "runtime" "strings" "time" "gopkg.in/ini.v1" "github.com/Unknwon/com" "github.com/go-macaron/session" "github.com/gogits/gogs/modules/bindata" "github.com/gogits/gogs/modules/log" // "github.com/gogits/gogs/modules/ssh" "github.com/gogits/gogs/modules/user" ) type Scheme string const ( HTTP Scheme = "http" HTTPS Scheme = "https" FCGI Scheme = "fcgi" ) type LandingPage string const ( LANDING_PAGE_HOME LandingPage = "/" LANDING_PAGE_EXPLORE LandingPage = "/explore" ) var ( // Build information. BuildTime string BuildGitHash string // App settings. AppVer string AppName string AppUrl string AppSubUrl string AppDataPath = "data" // Server settings. Protocol Scheme Domain string HttpAddr, HttpPort string DisableSSH bool SSHPort int SSHDomain string OfflineMode bool DisableRouterLog bool CertFile, KeyFile string StaticRootPath string EnableGzip bool LandingPageUrl LandingPage // Security settings. InstallLock bool SecretKey string LogInRememberDays int CookieUserName string CookieRememberName string ReverseProxyAuthUser string // Database settings. UseSQLite3 bool UseMySQL bool UsePostgreSQL bool UseTiDB bool // Webhook settings. Webhook struct { QueueLength int DeliverTimeout int SkipTLSVerify bool Types []string PagingNum int } // Repository settings. Repository struct { AnsiCharset string ForcePrivate bool PullRequestQueueLength int } RepoRootPath string ScriptType string // UI settings. ExplorePagingNum int IssuePagingNum int FeedMaxCommitNum int AdminUserPagingNum int AdminRepoPagingNum int AdminNoticePagingNum int AdminOrgPagingNum int // Markdown sttings. Markdown struct { EnableHardLineBreak bool } // Picture settings. PictureService string AvatarUploadPath string GravatarSource string DisableGravatar bool // Log settings. LogRootPath string LogModes []string LogConfigs []string // Attachment settings. AttachmentPath string AttachmentAllowedTypes string AttachmentMaxSize int64 AttachmentMaxFiles int AttachmentEnabled bool // Time settings. TimeFormat string // Cache settings. CacheAdapter string CacheInternal int CacheConn string EnableRedis bool EnableMemcache bool // Session settings. SessionConfig session.Options // Git settings. Git struct { MaxGitDiffLines int GcArgs []string `delim:" "` } // Cron tasks. Cron struct { UpdateMirror struct { Enabled bool RunAtStart bool Schedule string } `ini:"cron.update_mirrors"` RepoHealthCheck struct { Enabled bool RunAtStart bool Schedule string Args []string `delim:" "` } `ini:"cron.repo_health_check"` CheckRepoStats struct { Enabled bool RunAtStart bool Schedule string } `ini:"cron.check_repo_stats"` } // I18n settings. Langs, Names []string dateLangs map[string]string // Other settings. ShowFooterBranding bool // Global setting objects. Cfg *ini.File CustomPath string // Custom directory path. CustomConf string ProdMode bool RunUser string IsWindows bool HasRobotsTxt bool ) func DateLang(lang string) string { name, ok := dateLangs[lang] if ok { return name } return "en" } func init() { IsWindows = runtime.GOOS == "windows" log.NewLogger(0, "console", `{"level": 0}`) } func ExecPath() (string, error) { file, err := exec.LookPath(os.Args[0]) if err != nil { return "", err } p, err := filepath.Abs(file) if err != nil { return "", err } return p, nil } // WorkDir returns absolute path of work directory. func WorkDir() (string, error) { wd := os.Getenv("GOGS_WORK_DIR") if len(wd) > 0 { return wd, nil } execPath, err := ExecPath() if err != nil { return execPath, err } // Note: we don't use path.Dir here because it does not handle case // which path starts with two "/" in Windows: "//psf/Home/..." execPath = strings.Replace(execPath, "\\", "/", -1) i := strings.LastIndex(execPath, "/") if i == -1 { return execPath, nil } return execPath[:i], nil } func forcePathSeparator(path string) { if strings.Contains(path, "\\") { log.Fatal(4, "Do not use '\\' or '\\\\' in paths, instead, please use '/' in all places") } } // NewContext initializes configuration context. // NOTE: do not print any log except error. func NewContext() { workDir, err := WorkDir() if err != nil { log.Fatal(4, "Fail to get work directory: %v", err) } Cfg, err = ini.Load(bindata.MustAsset("conf/app.ini")) if err != nil { log.Fatal(4, "Fail to parse 'conf/app.ini': %v", err) } CustomPath = os.Getenv("GOGS_CUSTOM") if len(CustomPath) == 0 { CustomPath = workDir + "/custom" } if len(CustomConf) == 0 { CustomConf = CustomPath + "/conf/app.ini" } if com.IsFile(CustomConf) { if err = Cfg.Append(CustomConf); err != nil { log.Fatal(4, "Fail to load custom conf '%s': %v", CustomConf, err) } } else { log.Warn("Custom config (%s) not found, ignore this if you're running first time", CustomConf) } Cfg.NameMapper = ini.AllCapsUnderscore LogRootPath = Cfg.Section("log").Key("ROOT_PATH").MustString(path.Join(workDir, "log")) forcePathSeparator(LogRootPath) sec := Cfg.Section("server") AppName = Cfg.Section("").Key("APP_NAME").MustString("Gogs: Go Git Service") AppUrl = sec.Key("ROOT_URL").MustString("http://localhost:3000/") if AppUrl[len(AppUrl)-1] != '/' { AppUrl += "/" } // Check if has app suburl. url, err := url.Parse(AppUrl) if err != nil { log.Fatal(4, "Invalid ROOT_URL(%s): %s", AppUrl, err) } AppSubUrl = strings.TrimSuffix(url.Path, "/") Protocol = HTTP if sec.Key("PROTOCOL").String() == "https" { Protocol = HTTPS CertFile = sec.Key("CERT_FILE").String() KeyFile = sec.Key("KEY_FILE").String() } else if sec.Key("PROTOCOL").String() == "fcgi" { Protocol = FCGI } Domain = sec.Key("DOMAIN").MustString("localhost") HttpAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HttpPort = sec.Key("HTTP_PORT").MustString("3000") DisableSSH = sec.Key("DISABLE_SSH").MustBool() SSHDomain = sec.Key("SSH_DOMAIN").MustString(Domain) SSHPort = sec.Key("SSH_PORT").MustInt(22) OfflineMode = sec.Key("OFFLINE_MODE").MustBool() DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir) EnableGzip = sec.Key("ENABLE_GZIP").MustBool() switch sec.Key("LANDING_PAGE").MustString("home") { case "explore": LandingPageUrl = LANDING_PAGE_EXPLORE default: LandingPageUrl = LANDING_PAGE_HOME } sec = Cfg.Section("security") InstallLock = sec.Key("INSTALL_LOCK").MustBool() SecretKey = sec.Key("SECRET_KEY").String() LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt() CookieUserName = sec.Key("COOKIE_USERNAME").String() CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").String() ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER") sec = Cfg.Section("attachment") AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) if !filepath.IsAbs(AttachmentPath) { AttachmentPath = path.Join(workDir, AttachmentPath) } AttachmentAllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png"), "|", ",", -1) AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(4) AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(5) AttachmentEnabled = sec.Key("ENABLE").MustBool(true) TimeFormat = map[string]string{ "ANSIC": time.ANSIC, "UnixDate": time.UnixDate, "RubyDate": time.RubyDate, "RFC822": time.RFC822, "RFC822Z": time.RFC822Z, "RFC850": time.RFC850, "RFC1123": time.RFC1123, "RFC1123Z": time.RFC1123Z, "RFC3339": time.RFC3339, "RFC3339Nano": time.RFC3339Nano, "Kitchen": time.Kitchen, "Stamp": time.Stamp, "StampMilli": time.StampMilli, "StampMicro": time.StampMicro, "StampNano": time.StampNano, }[Cfg.Section("time").Key("FORMAT").MustString("RFC1123")] RunUser = Cfg.Section("").Key("RUN_USER").String() curUser := user.CurrentUsername() // Does not check run user when the install lock is off. if InstallLock && RunUser != curUser { log.Fatal(4, "Expect user(%s) but current user is: %s", RunUser, curUser) } // Determine and create root git repository path. homeDir, err := com.HomeDir() if err != nil { log.Fatal(4, "Fail to get home directory: %v", err) } homeDir = strings.Replace(homeDir, "\\", "/", -1) sec = Cfg.Section("repository") RepoRootPath = sec.Key("ROOT").MustString(path.Join(homeDir, "gogs-repositories")) forcePathSeparator(RepoRootPath) if !filepath.IsAbs(RepoRootPath) { RepoRootPath = path.Join(workDir, RepoRootPath) } else { RepoRootPath = path.Clean(RepoRootPath) } ScriptType = sec.Key("SCRIPT_TYPE").MustString("bash") Repository.AnsiCharset = sec.Key("ANSI_CHARSET").String() Repository.ForcePrivate = sec.Key("FORCE_PRIVATE").MustBool() Repository.PullRequestQueueLength = sec.Key("PULL_REQUEST_QUEUE_LENGTH").MustInt(10000) // UI settings. sec = Cfg.Section("ui") ExplorePagingNum = sec.Key("EXPLORE_PAGING_NUM").MustInt(20) IssuePagingNum = sec.Key("ISSUE_PAGING_NUM").MustInt(10) FeedMaxCommitNum = sec.Key("FEED_MAX_COMMIT_NUM").MustInt(5) sec = Cfg.Section("ui.admin") AdminUserPagingNum = sec.Key("USER_PAGING_NUM").MustInt(50) AdminRepoPagingNum = sec.Key("REPO_PAGING_NUM").MustInt(50) AdminNoticePagingNum = sec.Key("NOTICE_PAGING_NUM").MustInt(50) AdminOrgPagingNum = sec.Key("ORG_PAGING_NUM").MustInt(50) sec = Cfg.Section("picture") PictureService = sec.Key("SERVICE").In("server", []string{"server"}) AvatarUploadPath = sec.Key("AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "avatars")) forcePathSeparator(AvatarUploadPath) if !filepath.IsAbs(AvatarUploadPath) { AvatarUploadPath = path.Join(workDir, AvatarUploadPath) } switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" case "gravatar": GravatarSource = "//1.gravatar.com/avatar/" default: GravatarSource = source } DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool() if OfflineMode { DisableGravatar = true } if err = Cfg.Section("markdown").MapTo(&Markdown); err != nil { log.Fatal(4, "Fail to map Markdown settings: %v", err) } else if err = Cfg.Section("git").MapTo(&Git); err != nil { log.Fatal(4, "Fail to map Git settings: %v", err) } else if Cfg.Section("cron").MapTo(&Cron); err != nil { log.Fatal(4, "Fail to map Cron settings: %v", err) } Langs = Cfg.Section("i18n").Key("LANGS").Strings(",") Names = Cfg.Section("i18n").Key("NAMES").Strings(",") dateLangs = Cfg.Section("i18n.datelang").KeysHash() ShowFooterBranding = Cfg.Section("other").Key("SHOW_FOOTER_BRANDING").MustBool() HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt")) } var Service struct { ActiveCodeLives int ResetPwdCodeLives int RegisterEmailConfirm bool DisableRegistration bool ShowRegistrationButton bool RequireSignInView bool EnableCacheAvatar bool EnableNotifyMail bool EnableReverseProxyAuth bool EnableReverseProxyAutoRegister bool DisableMinimumKeySizeCheck bool MinimumKeySizes map[string]int EnableCaptcha bool } func newService() { sec := Cfg.Section("service") Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180) Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180) Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool() Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!Service.DisableRegistration) Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.EnableCacheAvatar = sec.Key("ENABLE_CACHE_AVATAR").MustBool() Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.DisableMinimumKeySizeCheck = sec.Key("DISABLE_MINIMUM_KEY_SIZE_CHECK").MustBool() Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool() minimumKeySizes := Cfg.Section("service.minimum_key_sizes").Keys() Service.MinimumKeySizes = make(map[string]int) for _, key := range minimumKeySizes { Service.MinimumKeySizes[key.Name()] = key.MustInt() } } var logLevels = map[string]string{ "Trace": "0", "Debug": "1", "Info": "2", "Warn": "3", "Error": "4", "Critical": "5", } func newLogService() { log.Info("%s %s", AppName, AppVer) if len(BuildTime) > 0 { log.Info("Build Time: %s", BuildTime) log.Info("Build Git Hash: %s", BuildGitHash) } // Get and check log mode. LogModes = strings.Split(Cfg.Section("log").Key("MODE").MustString("console"), ",") LogConfigs = make([]string, len(LogModes)) for i, mode := range LogModes { mode = strings.TrimSpace(mode) sec, err := Cfg.GetSection("log." + mode) if err != nil { log.Fatal(4, "Unknown log mode: %s", mode) } validLevels := []string{"Trace", "Debug", "Info", "Warn", "Error", "Critical"} // Log level. levelName := Cfg.Section("log."+mode).Key("LEVEL").In( Cfg.Section("log").Key("LEVEL").In("Trace", validLevels), validLevels) level, ok := logLevels[levelName] if !ok { log.Fatal(4, "Unknown log level: %s", levelName) } // Generate log configuration. switch mode { case "console": LogConfigs[i] = fmt.Sprintf(`{"level":%s}`, level) case "file": logPath := sec.Key("FILE_NAME").MustString(path.Join(LogRootPath, "gogs.log")) os.MkdirAll(path.Dir(logPath), os.ModePerm) LogConfigs[i] = fmt.Sprintf( `{"level":%s,"filename":"%s","rotate":%v,"maxlines":%d,"maxsize":%d,"daily":%v,"maxdays":%d}`, level, logPath, sec.Key("LOG_ROTATE").MustBool(true), sec.Key("MAX_LINES").MustInt(1000000), 1<