github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/setting/setting.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2014 The Gogs Authors. All rights reserved. 7 8 package setting 9 10 import ( 11 "encoding/base64" 12 "fmt" 13 "math" 14 "net" 15 "net/url" 16 "os" 17 "os/exec" 18 "path" 19 "path/filepath" 20 "runtime" 21 "strconv" 22 "strings" 23 "text/template" 24 "time" 25 26 "github.com/gitbundle/modules/generate" 27 "github.com/gitbundle/modules/json" 28 "github.com/gitbundle/modules/log" 29 "github.com/gitbundle/modules/user" 30 "github.com/gitbundle/modules/util" 31 32 gossh "golang.org/x/crypto/ssh" 33 ini "gopkg.in/ini.v1" 34 ) 35 36 // Scheme describes protocol types 37 type Scheme string 38 39 // enumerates all the scheme types 40 const ( 41 HTTP Scheme = "http" 42 HTTPS Scheme = "https" 43 FCGI Scheme = "fcgi" 44 FCGIUnix Scheme = "fcgi+unix" 45 HTTPUnix Scheme = "http+unix" 46 ) 47 48 // LandingPage describes the default page 49 type LandingPage string 50 51 // enumerates all the landing page types 52 const ( 53 LandingPageHome LandingPage = "/" 54 LandingPageExplore LandingPage = "/explore" 55 LandingPageOrganizations LandingPage = "/explore/organizations" 56 LandingPageLogin LandingPage = "/user/login" 57 ) 58 59 // enumerates all the types of captchas 60 const ( 61 ImageCaptcha = "image" 62 ReCaptcha = "recaptcha" 63 HCaptcha = "hcaptcha" 64 ) 65 66 // settings 67 var ( 68 // AppVer is the version of the current build of GitBundle. It is set in main.go from main.Version. 69 AppVer string 70 // AppBuiltWith represents a human readable version go runtime build version and build tags. (See main.go formatBuiltWith().) 71 AppBuiltWith string 72 // AppStartTime store time gitbundle has started 73 AppStartTime time.Time 74 // AppName is the Application name, used in the page title. 75 // It maps to ini:"APP_NAME" 76 AppName string 77 // AppURL is the Application ROOT_URL. It always has a '/' suffix 78 // It maps to ini:"ROOT_URL" 79 AppURL string 80 // AppSubURL represents the sub-url mounting point for gitbundle. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. 81 // This value is empty if site does not have sub-url. 82 AppSubURL string 83 // AppPath represents the path to the gitbundle binary 84 AppPath string 85 // AppWorkPath is the "working directory" of GitBundle. It maps to the environment variable GITBUNDLE_WORK_DIR. 86 // If that is not set it is the default set here by the linker or failing that the directory of AppPath. 87 // 88 // AppWorkPath is used as the base path for several other paths. 89 AppWorkPath string 90 // AppDataPath is the default path for storing data. 91 // It maps to ini:"APP_DATA_PATH" and defaults to AppWorkPath + "/data" 92 AppDataPath string 93 // LocalURL is the url for locally running applications to contact GitBundle. It always has a '/' suffix 94 // It maps to ini:"LOCAL_ROOT_URL" 95 LocalURL string 96 // AssetVersion holds a opaque value that is used for cache-busting assets 97 AssetVersion string 98 99 // Server settings 100 Protocol Scheme 101 Domain string 102 HTTPAddr string 103 HTTPPort string 104 RedirectOtherPort bool 105 PortToRedirect string 106 OfflineMode bool 107 CertFile string 108 KeyFile string 109 StaticRootPath string 110 StaticCacheTime time.Duration 111 EnableGzip bool 112 LandingPageURL LandingPage 113 LandingPageCustom string 114 UnixSocketPermission uint32 115 EnablePprof bool 116 PprofDataPath string 117 EnableAcme bool 118 AcmeTOS bool 119 AcmeLiveDirectory string 120 AcmeEmail string 121 AcmeURL string 122 AcmeCARoot string 123 SSLMinimumVersion string 124 SSLMaximumVersion string 125 SSLCurvePreferences []string 126 SSLCipherSuites []string 127 GracefulRestartable bool 128 GracefulHammerTime time.Duration 129 StartupTimeout time.Duration 130 PerWriteTimeout = 30 * time.Second 131 PerWritePerKbTimeout = 10 * time.Second 132 StaticURLPrefix string 133 AbsoluteAssetURL string 134 135 SSH = struct { 136 Disabled bool `ini:"DISABLE_SSH"` 137 StartBuiltinServer bool `ini:"START_SSH_SERVER"` 138 BuiltinServerUser string `ini:"BUILTIN_SSH_SERVER_USER"` 139 Domain string `ini:"SSH_DOMAIN"` 140 Port int `ini:"SSH_PORT"` 141 User string `ini:"SSH_USER"` 142 ListenHost string `ini:"SSH_LISTEN_HOST"` 143 ListenPort int `ini:"SSH_LISTEN_PORT"` 144 RootPath string `ini:"SSH_ROOT_PATH"` 145 ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"` 146 ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"` 147 ServerMACs []string `ini:"SSH_SERVER_MACS"` 148 ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"` 149 KeyTestPath string `ini:"SSH_KEY_TEST_PATH"` 150 KeygenPath string `ini:"SSH_KEYGEN_PATH"` 151 AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"` 152 AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"` 153 AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"` 154 AuthorizedKeysCommandTemplateTemplate *template.Template `ini:"-"` 155 MinimumKeySizeCheck bool `ini:"-"` 156 MinimumKeySizes map[string]int `ini:"-"` 157 CreateAuthorizedKeysFile bool `ini:"SSH_CREATE_AUTHORIZED_KEYS_FILE"` 158 CreateAuthorizedPrincipalsFile bool `ini:"SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE"` 159 ExposeAnonymous bool `ini:"SSH_EXPOSE_ANONYMOUS"` 160 AuthorizedPrincipalsAllow []string `ini:"SSH_AUTHORIZED_PRINCIPALS_ALLOW"` 161 AuthorizedPrincipalsEnabled bool `ini:"-"` 162 TrustedUserCAKeys []string `ini:"SSH_TRUSTED_USER_CA_KEYS"` 163 TrustedUserCAKeysFile string `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"` 164 TrustedUserCAKeysParsed []gossh.PublicKey `ini:"-"` 165 PerWriteTimeout time.Duration `ini:"SSH_PER_WRITE_TIMEOUT"` 166 PerWritePerKbTimeout time.Duration `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"` 167 }{ 168 Disabled: false, 169 StartBuiltinServer: false, 170 Domain: "", 171 Port: 22, 172 ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"}, 173 ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, 174 ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"}, 175 KeygenPath: "ssh-keygen", 176 MinimumKeySizeCheck: true, 177 MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2047}, 178 ServerHostKeys: []string{"ssh/gitbundle.rsa", "ssh/gogs.rsa"}, 179 AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}", 180 PerWriteTimeout: PerWriteTimeout, 181 PerWritePerKbTimeout: PerWritePerKbTimeout, 182 } 183 184 // Security settings 185 InstallLock bool 186 SecretKey string 187 LogInRememberDays int 188 CookieUserName string 189 CookieRememberName string 190 ReverseProxyAuthUser string 191 ReverseProxyAuthEmail string 192 ReverseProxyLimit int 193 ReverseProxyTrustedProxies []string 194 MinPasswordLength int 195 ImportLocalPaths bool 196 DisableGitHooks bool 197 DisableWebhooks bool 198 OnlyAllowPushIfGiteaEnvironmentSet bool 199 PasswordComplexity []string 200 PasswordHashAlgo string 201 PasswordCheckPwn bool 202 SuccessfulTokensCacheSize int 203 204 Camo = struct { 205 Enabled bool 206 ServerURL string `ini:"SERVER_URL"` 207 HMACKey string `ini:"HMAC_KEY"` 208 Allways bool 209 }{} 210 211 // UI settings 212 UI = struct { 213 ExplorePagingNum int 214 IssuePagingNum int 215 RepoSearchPagingNum int 216 MembersPagingNum int 217 FeedMaxCommitNum int 218 FeedPagingNum int 219 PackagesPagingNum int 220 GraphMaxCommitNum int 221 CodeCommentLines int 222 ReactionMaxUserNum int 223 // ThemeColorMetaTag string 224 MaxDisplayFileSize int64 225 ShowUserEmail bool 226 DefaultShowFullName bool 227 DefaultTheme string 228 Themes []string 229 Reactions []string 230 ReactionsMap map[string]bool `ini:"-"` 231 CustomEmojis []string 232 CustomEmojisMap map[string]string `ini:"-"` 233 SearchRepoDescription bool 234 UseServiceWorker bool 235 236 Notification struct { 237 MinTimeout time.Duration 238 TimeoutStep time.Duration 239 MaxTimeout time.Duration 240 EventSourceUpdateTime time.Duration 241 } `ini:"ui.notification"` 242 243 SVG struct { 244 Enabled bool `ini:"ENABLE_RENDER"` 245 } `ini:"ui.svg"` 246 247 CSV struct { 248 MaxFileSize int64 249 } `ini:"ui.csv"` 250 251 Admin struct { 252 UserPagingNum int 253 RepoPagingNum int 254 NoticePagingNum int 255 OrgPagingNum int 256 } `ini:"ui.admin"` 257 User struct { 258 RepoPagingNum int 259 } `ini:"ui.user"` 260 Meta struct { 261 Author string 262 Description string 263 Keywords string 264 } `ini:"ui.meta"` 265 }{ 266 ExplorePagingNum: 20, 267 IssuePagingNum: 10, 268 RepoSearchPagingNum: 10, 269 MembersPagingNum: 20, 270 FeedMaxCommitNum: 5, 271 FeedPagingNum: 20, 272 PackagesPagingNum: 20, 273 GraphMaxCommitNum: 100, 274 CodeCommentLines: 4, 275 ReactionMaxUserNum: 10, 276 // ThemeColorMetaTag: `#6cc644`, 277 MaxDisplayFileSize: 8388608, 278 DefaultTheme: `dark`, 279 Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, 280 CustomEmojis: []string{`git`, `gitbundle`, `codeberg`, `gitlab`, `github`, `gogs`}, 281 CustomEmojisMap: map[string]string{"git": ":git:", "gitbundle": ":gitbundle:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, 282 Notification: struct { 283 MinTimeout time.Duration 284 TimeoutStep time.Duration 285 MaxTimeout time.Duration 286 EventSourceUpdateTime time.Duration 287 }{ 288 MinTimeout: 10 * time.Second, 289 TimeoutStep: 10 * time.Second, 290 MaxTimeout: 60 * time.Second, 291 EventSourceUpdateTime: 10 * time.Second, 292 }, 293 SVG: struct { 294 Enabled bool `ini:"ENABLE_RENDER"` 295 }{ 296 Enabled: true, 297 }, 298 CSV: struct { 299 MaxFileSize int64 300 }{ 301 MaxFileSize: 524288, 302 }, 303 Admin: struct { 304 UserPagingNum int 305 RepoPagingNum int 306 NoticePagingNum int 307 OrgPagingNum int 308 }{ 309 UserPagingNum: 50, 310 RepoPagingNum: 50, 311 NoticePagingNum: 25, 312 OrgPagingNum: 50, 313 }, 314 User: struct { 315 RepoPagingNum int 316 }{ 317 RepoPagingNum: 15, 318 }, 319 Meta: struct { 320 Author string 321 Description string 322 Keywords string 323 }{ 324 Author: "GitBundle", 325 Description: "A Bundled Git Service", 326 Keywords: "go,git,self-hosted,gitbundle", 327 }, 328 } 329 330 // Markdown settings 331 Markdown = struct { 332 EnableHardLineBreakInComments bool 333 EnableHardLineBreakInDocuments bool 334 CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` 335 FileExtensions []string 336 }{ 337 EnableHardLineBreakInComments: true, 338 EnableHardLineBreakInDocuments: false, 339 FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), 340 } 341 342 // Admin settings 343 Admin struct { 344 DisableRegularOrgCreation bool 345 DefaultEmailNotification string 346 } 347 348 // Log settings 349 LogLevel log.Level 350 StacktraceLogLevel string 351 LogRootPath string 352 EnableSSHLog bool 353 EnableXORMLog bool 354 355 DisableRouterLog bool 356 357 EnableAccessLog bool 358 AccessLogTemplate string 359 360 // Time settings 361 TimeFormat string 362 // UILocation is the location on the UI, so that we can display the time on UI. 363 DefaultUILocation = time.Local 364 365 CSRFCookieName = "_csrf" 366 CSRFCookieHTTPOnly = true 367 368 ManifestData string 369 370 // API settings 371 API = struct { 372 EnableSwagger bool 373 SwaggerURL string 374 MaxResponseItems int 375 DefaultPagingNum int 376 DefaultGitTreesPerPage int 377 DefaultMaxBlobSize int64 378 }{ 379 EnableSwagger: true, 380 SwaggerURL: "", 381 MaxResponseItems: 50, 382 DefaultPagingNum: 30, 383 DefaultGitTreesPerPage: 1000, 384 DefaultMaxBlobSize: 10485760, 385 } 386 387 OAuth2 = struct { 388 Enable bool 389 AccessTokenExpirationTime int64 390 RefreshTokenExpirationTime int64 391 InvalidateRefreshTokens bool 392 JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` 393 JWTSecretBase64 string `ini:"JWT_SECRET"` 394 JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` 395 MaxTokenLength int 396 }{ 397 Enable: true, 398 AccessTokenExpirationTime: 3600, 399 RefreshTokenExpirationTime: 730, 400 InvalidateRefreshTokens: false, 401 JWTSigningAlgorithm: "RS256", 402 JWTSigningPrivateKeyFile: "jwt/private.pem", 403 MaxTokenLength: math.MaxInt16, 404 } 405 406 // FIXME: DEPRECATED to be removed in v1.18.0 407 U2F = struct { 408 AppID string 409 }{} 410 411 // Metrics settings 412 Metrics = struct { 413 Enabled bool 414 Token string 415 EnabledIssueByLabel bool 416 EnabledIssueByRepository bool 417 }{ 418 Enabled: false, 419 Token: "", 420 EnabledIssueByLabel: false, 421 EnabledIssueByRepository: false, 422 } 423 424 // I18n settings 425 Langs []string 426 Names []string 427 428 // Highlight settings are loaded in modules/template/highlight.go 429 430 // Other settings 431 ShowFooterBranding bool 432 ShowFooterVersion bool 433 ShowFooterTemplateLoadTime bool 434 435 // Global setting objects 436 Cfg *ini.File 437 CustomPath string // Custom directory path 438 CustomConf string 439 PIDFile = "/run/gitbundle.pid" 440 WritePIDFile bool 441 RunUser string 442 IsWindows bool 443 HasRobotsTxt bool 444 InternalToken string // internal access token 445 ) 446 447 func getAppPath() (string, error) { 448 var appPath string 449 var err error 450 if IsWindows && filepath.IsAbs(os.Args[0]) { 451 appPath = filepath.Clean(os.Args[0]) 452 } else { 453 appPath, err = exec.LookPath(os.Args[0]) 454 } 455 456 if err != nil { 457 return "", err 458 } 459 appPath, err = filepath.Abs(appPath) 460 if err != nil { 461 return "", err 462 } 463 // Note: we don't use path.Dir here because it does not handle case 464 // which path starts with two "/" in Windows: "//psf/Home/..." 465 return strings.ReplaceAll(appPath, "\\", "/"), err 466 } 467 468 func getWorkPath(appPath string) string { 469 workPath := AppWorkPath 470 471 if giteaWorkPath, ok := os.LookupEnv("GITBUNDLE_WORK_DIR"); ok { 472 workPath = giteaWorkPath 473 } 474 if len(workPath) == 0 { 475 i := strings.LastIndex(appPath, "/") 476 if i == -1 { 477 workPath = appPath 478 } else { 479 workPath = appPath[:i] 480 } 481 } 482 workPath = strings.ReplaceAll(workPath, "\\", "/") 483 if !filepath.IsAbs(workPath) { 484 log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath) 485 486 absPath, err := filepath.Abs(workPath) 487 if err != nil { 488 log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath) 489 workPath = filepath.Join(appPath, workPath) 490 } else { 491 workPath = absPath 492 } 493 } 494 return strings.ReplaceAll(workPath, "\\", "/") 495 } 496 497 func init() { 498 IsWindows = runtime.GOOS == "windows" 499 // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically 500 // By default set this logger at Info - we'll change it later but we need to start with something. 501 log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "info", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout), IsProd) 502 503 var err error 504 if AppPath, err = getAppPath(); err != nil { 505 log.Fatal("Failed to get app path: %v", err) 506 } 507 AppWorkPath = getWorkPath(AppPath) 508 } 509 510 func forcePathSeparator(path string) { 511 if strings.Contains(path, "\\") { 512 log.Fatal("Do not use '\\' or '\\\\' in paths, instead, please use '/' in all places") 513 } 514 } 515 516 // IsRunUserMatchCurrentUser returns false if configured run user does not match 517 // actual user that runs the app. The first return value is the actual user name. 518 // This check is ignored under Windows since SSH remote login is not the main 519 // method to login on Windows. 520 func IsRunUserMatchCurrentUser(runUser string) (string, bool) { 521 if IsWindows || SSH.StartBuiltinServer { 522 return "", true 523 } 524 525 currentUser := user.CurrentUsername() 526 return currentUser, runUser == currentUser 527 } 528 529 func createPIDFile(pidPath string) { 530 currentPid := os.Getpid() 531 if err := os.MkdirAll(filepath.Dir(pidPath), os.ModePerm); err != nil { 532 log.Fatal("Failed to create PID folder: %v", err) 533 } 534 535 file, err := os.Create(pidPath) 536 if err != nil { 537 log.Fatal("Failed to create PID file: %v", err) 538 } 539 defer file.Close() 540 if _, err := file.WriteString(strconv.FormatInt(int64(currentPid), 10)); err != nil { 541 log.Fatal("Failed to write PID information: %v", err) 542 } 543 } 544 545 // SetCustomPathAndConf will set CustomPath and CustomConf with reference to the 546 // GITBUNDLE_CUSTOM environment variable and with provided overrides before stepping 547 // back to the default 548 func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) { 549 if len(providedWorkPath) != 0 { 550 AppWorkPath = filepath.ToSlash(providedWorkPath) 551 } 552 if giteaCustom, ok := os.LookupEnv("GITBUNDLE_CUSTOM"); ok { 553 CustomPath = giteaCustom 554 } 555 if len(providedCustom) != 0 { 556 CustomPath = providedCustom 557 } 558 if len(CustomPath) == 0 { 559 CustomPath = path.Join(AppWorkPath, "custom") 560 } else if !filepath.IsAbs(CustomPath) { 561 CustomPath = path.Join(AppWorkPath, CustomPath) 562 } 563 564 if len(providedConf) != 0 { 565 CustomConf = providedConf 566 } 567 if len(CustomConf) == 0 { 568 CustomConf = path.Join(CustomPath, "conf/app.ini") 569 } else if !filepath.IsAbs(CustomConf) { 570 CustomConf = path.Join(CustomPath, CustomConf) 571 log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf) 572 } 573 } 574 575 // LoadFromExisting initializes setting options from an existing config file (app.ini) 576 func LoadFromExisting() { 577 loadFromConf(false, "") 578 } 579 580 // LoadAllowEmpty initializes setting options, it's also fine that if the config file (app.ini) doesn't exist 581 func LoadAllowEmpty() { 582 loadFromConf(true, "") 583 } 584 585 // LoadForTest initializes setting options for tests 586 func LoadForTest(extraConfigs ...string) { 587 loadFromConf(true, strings.Join(extraConfigs, "\n")) 588 if err := PrepareAppDataPath(); err != nil { 589 log.Fatal("Can not prepare APP_DATA_PATH: %v", err) 590 } 591 } 592 593 func deprecatedSetting(oldSection, oldKey, newSection, newKey string) { 594 if Cfg.Section(oldSection).HasKey(oldKey) { 595 log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.18.0", oldSection, oldKey, newSection, newKey) 596 } 597 } 598 599 // loadFromConf initializes configuration context. 600 // NOTE: do not print any log except error. 601 func loadFromConf(allowEmpty bool, extraConfig string) { 602 Cfg = ini.Empty() 603 604 if WritePIDFile && len(PIDFile) > 0 { 605 createPIDFile(PIDFile) 606 } 607 608 isFile, err := util.IsFile(CustomConf) 609 if err != nil { 610 log.Error("Unable to check if %s is a file. Error: %v", CustomConf, err) 611 } 612 if isFile { 613 if err := Cfg.Append(CustomConf); err != nil { 614 log.Fatal("Failed to load custom conf '%s': %v", CustomConf, err) 615 } 616 } else if !allowEmpty { 617 log.Fatal("Unable to find configuration file: %q.\nEnsure you are running in the correct environment or set the correct configuration file with -c.", CustomConf) 618 } // else: no config file, a config file might be created at CustomConf later (might not) 619 620 if extraConfig != "" { 621 if err = Cfg.Append([]byte(extraConfig)); err != nil { 622 log.Fatal("Unable to append more config: %v", err) 623 } 624 } 625 626 Cfg.NameMapper = ini.SnackCase 627 628 homeDir, err := util.HomeDir() 629 if err != nil { 630 log.Fatal("Failed to get home directory: %v", err) 631 } 632 homeDir = strings.ReplaceAll(homeDir, "\\", "/") 633 634 LogLevel = getLogLevel(Cfg.Section("log"), "LEVEL", log.INFO) 635 StacktraceLogLevel = getStacktraceLogLevel(Cfg.Section("log"), "STACKTRACE_LEVEL", "None") 636 LogRootPath = Cfg.Section("log").Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log")) 637 forcePathSeparator(LogRootPath) 638 639 sec := Cfg.Section("server") 640 AppName = Cfg.Section("").Key("APP_NAME").MustString("GitBundle: A modern git service") 641 642 Domain = sec.Key("DOMAIN").MustString("localhost") 643 HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") 644 HTTPPort = sec.Key("HTTP_PORT").MustString("3000") 645 646 Protocol = HTTP 647 protocolCfg := sec.Key("PROTOCOL").String() 648 switch protocolCfg { 649 case "https": 650 Protocol = HTTPS 651 // FIXME: DEPRECATED to be removed in v1.18.0 652 if sec.HasKey("ENABLE_ACME") { 653 EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) 654 } else { 655 deprecatedSetting("server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME") 656 EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) 657 } 658 if EnableAcme { 659 AcmeURL = sec.Key("ACME_URL").MustString("") 660 AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") 661 // FIXME: DEPRECATED to be removed in v1.18.0 662 if sec.HasKey("ACME_ACCEPTTOS") { 663 AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false) 664 } else { 665 deprecatedSetting("server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS") 666 AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false) 667 } 668 if !AcmeTOS { 669 log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).") 670 } 671 // FIXME: DEPRECATED to be removed in v1.18.0 672 if sec.HasKey("ACME_DIRECTORY") { 673 AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https") 674 } else { 675 deprecatedSetting("server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY") 676 AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https") 677 } 678 // FIXME: DEPRECATED to be removed in v1.18.0 679 if sec.HasKey("ACME_EMAIL") { 680 AcmeEmail = sec.Key("ACME_EMAIL").MustString("") 681 } else { 682 deprecatedSetting("server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL") 683 AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") 684 } 685 } else { 686 CertFile = sec.Key("CERT_FILE").String() 687 KeyFile = sec.Key("KEY_FILE").String() 688 if len(CertFile) > 0 && !filepath.IsAbs(CertFile) { 689 CertFile = filepath.Join(CustomPath, CertFile) 690 } 691 if len(KeyFile) > 0 && !filepath.IsAbs(KeyFile) { 692 KeyFile = filepath.Join(CustomPath, KeyFile) 693 } 694 } 695 SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("") 696 SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("") 697 SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",") 698 SSLCipherSuites = sec.Key("SSL_CIPHER_SUITES").Strings(",") 699 case "fcgi": 700 Protocol = FCGI 701 case "fcgi+unix", "unix", "http+unix": 702 switch protocolCfg { 703 case "fcgi+unix": 704 Protocol = FCGIUnix 705 case "unix": 706 log.Warn("unix PROTOCOL value is deprecated, please use http+unix") 707 fallthrough 708 case "http+unix": 709 Protocol = HTTPUnix 710 } 711 UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666") 712 UnixSocketPermissionParsed, err := strconv.ParseUint(UnixSocketPermissionRaw, 8, 32) 713 if err != nil || UnixSocketPermissionParsed > 0o777 { 714 log.Fatal("Failed to parse unixSocketPermission: %s", UnixSocketPermissionRaw) 715 } 716 717 UnixSocketPermission = uint32(UnixSocketPermissionParsed) 718 if !filepath.IsAbs(HTTPAddr) { 719 HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) 720 } 721 } 722 GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true) 723 GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second) 724 StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second) 725 PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout) 726 PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) 727 728 defaultAppURL := string(Protocol) + "://" + Domain 729 if (Protocol == HTTP && HTTPPort != "80") || (Protocol == HTTPS && HTTPPort != "443") { 730 defaultAppURL += ":" + HTTPPort 731 } 732 AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL + "/") 733 // This should be TrimRight to ensure that there is only a single '/' at the end of AppURL. 734 AppURL = strings.TrimRight(AppURL, "/") + "/" 735 736 // Check if has app suburl. 737 appURL, err := url.Parse(AppURL) 738 if err != nil { 739 log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err) 740 } 741 // Suburl should start with '/' and end without '/', such as '/{subpath}'. 742 // This value is empty if site does not have sub-url. 743 AppSubURL = strings.TrimSuffix(appURL.Path, "/") 744 StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") 745 746 // Check if Domain differs from AppURL domain than update it to AppURL's domain 747 urlHostname := appURL.Hostname() 748 if urlHostname != Domain && net.ParseIP(urlHostname) == nil && urlHostname != "" { 749 Domain = urlHostname 750 } 751 752 AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) 753 AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) 754 755 manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) 756 ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) 757 758 var defaultLocalURL string 759 switch Protocol { 760 case HTTPUnix: 761 defaultLocalURL = "http://unix/" 762 case FCGI: 763 defaultLocalURL = AppURL 764 case FCGIUnix: 765 defaultLocalURL = AppURL 766 default: 767 defaultLocalURL = string(Protocol) + "://" 768 if HTTPAddr == "0.0.0.0" { 769 defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/" 770 } else { 771 defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/" 772 } 773 } 774 LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) 775 LocalURL = strings.TrimRight(LocalURL, "/") + "/" 776 RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) 777 PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") 778 OfflineMode = sec.Key("OFFLINE_MODE").MustBool() 779 DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() 780 if len(StaticRootPath) == 0 { 781 StaticRootPath = AppWorkPath 782 } 783 StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath) 784 StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) 785 AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) 786 if !filepath.IsAbs(AppDataPath) { 787 log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath) 788 AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) 789 } 790 791 EnableGzip = sec.Key("ENABLE_GZIP").MustBool() 792 EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false) 793 PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof")) 794 if !filepath.IsAbs(PprofDataPath) { 795 PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath) 796 } 797 798 landingPage := sec.Key("LANDING_PAGE").MustString("home") 799 switch landingPage { 800 case "explore": 801 LandingPageURL = LandingPageExplore 802 case "organizations": 803 LandingPageURL = LandingPageOrganizations 804 case "login": 805 LandingPageURL = LandingPageLogin 806 case "": 807 case "home": 808 LandingPageURL = LandingPageHome 809 default: 810 LandingPageURL = LandingPage(landingPage) 811 } 812 813 if len(SSH.Domain) == 0 { 814 SSH.Domain = Domain 815 } 816 SSH.RootPath = path.Join(homeDir, ".ssh") 817 serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",") 818 if len(serverCiphers) > 0 { 819 SSH.ServerCiphers = serverCiphers 820 } 821 serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",") 822 if len(serverKeyExchanges) > 0 { 823 SSH.ServerKeyExchanges = serverKeyExchanges 824 } 825 serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",") 826 if len(serverMACs) > 0 { 827 SSH.ServerMACs = serverMACs 828 } 829 SSH.KeyTestPath = os.TempDir() 830 if err = Cfg.Section("server").MapTo(&SSH); err != nil { 831 log.Fatal("Failed to map SSH settings: %v", err) 832 } 833 for i, key := range SSH.ServerHostKeys { 834 if !filepath.IsAbs(key) { 835 SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key) 836 } 837 } 838 839 SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").MustString("ssh-keygen") 840 SSH.Port = sec.Key("SSH_PORT").MustInt(22) 841 SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port) 842 843 // When disable SSH, start builtin server value is ignored. 844 if SSH.Disabled { 845 SSH.StartBuiltinServer = false 846 } 847 848 SSH.TrustedUserCAKeysFile = sec.Key("SSH_TRUSTED_USER_CA_KEYS_FILENAME").MustString(filepath.Join(SSH.RootPath, "gitbundle-trusted-user-ca-keys.pem")) 849 850 for _, caKey := range SSH.TrustedUserCAKeys { 851 pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(caKey)) 852 if err != nil { 853 log.Fatal("Failed to parse TrustedUserCaKeys: %s %v", caKey, err) 854 } 855 856 SSH.TrustedUserCAKeysParsed = append(SSH.TrustedUserCAKeysParsed, pubKey) 857 } 858 if len(SSH.TrustedUserCAKeys) > 0 { 859 // Set the default as email,username otherwise we can leave it empty 860 sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("username,email") 861 } else { 862 sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("off") 863 } 864 865 SSH.AuthorizedPrincipalsAllow, SSH.AuthorizedPrincipalsEnabled = parseAuthorizedPrincipalsAllow(sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").Strings(",")) 866 867 SSH.MinimumKeySizeCheck = sec.Key("MINIMUM_KEY_SIZE_CHECK").MustBool(SSH.MinimumKeySizeCheck) 868 minimumKeySizes := Cfg.Section("ssh.minimum_key_sizes").Keys() 869 for _, key := range minimumKeySizes { 870 if key.MustInt() != -1 { 871 SSH.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt() 872 } else { 873 delete(SSH.MinimumKeySizes, strings.ToLower(key.Name())) 874 } 875 } 876 877 SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(true) 878 SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true) 879 880 SSH.AuthorizedPrincipalsBackup = false 881 SSH.CreateAuthorizedPrincipalsFile = false 882 if SSH.AuthorizedPrincipalsEnabled { 883 SSH.AuthorizedPrincipalsBackup = sec.Key("SSH_AUTHORIZED_PRINCIPALS_BACKUP").MustBool(true) 884 SSH.CreateAuthorizedPrincipalsFile = sec.Key("SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE").MustBool(true) 885 } 886 887 SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false) 888 SSH.AuthorizedKeysCommandTemplate = sec.Key("SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE").MustString(SSH.AuthorizedKeysCommandTemplate) 889 890 SSH.AuthorizedKeysCommandTemplateTemplate = template.Must(template.New("").Parse(SSH.AuthorizedKeysCommandTemplate)) 891 892 SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout) 893 SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) 894 895 if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil { 896 log.Fatal("Failed to OAuth2 settings: %v", err) 897 return 898 } 899 900 if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { 901 OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) 902 } 903 904 sec = Cfg.Section("admin") 905 Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") 906 907 sec = Cfg.Section("security") 908 InstallLock = sec.Key("INSTALL_LOCK").MustBool(false) 909 SecretKey = sec.Key("SECRET_KEY").MustString("") // NOTE: This is invalid as we need length = 32 !#@FDEWREWR&*( 910 LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7) 911 CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome") 912 CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible") 913 914 ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER") 915 ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL") 916 917 ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1) 918 ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",") 919 if len(ReverseProxyTrustedProxies) == 0 { 920 ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"} 921 } 922 923 MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(6) 924 ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false) 925 DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true) 926 DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false) 927 OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITBUNDLE_ENVIRONMENT_SET").MustBool(true) 928 PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2") 929 CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) 930 PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) 931 SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) 932 933 InternalToken = loadInternalToken(sec) 934 if InstallLock && InternalToken == "" { 935 // if GitBundle has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate 936 generateSaveInternalToken() 937 } 938 939 cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") 940 if len(cfgdata) == 0 { 941 cfgdata = []string{"off"} 942 } 943 PasswordComplexity = make([]string, 0, len(cfgdata)) 944 for _, name := range cfgdata { 945 name := strings.ToLower(strings.Trim(name, `"`)) 946 if name != "" { 947 PasswordComplexity = append(PasswordComplexity, name) 948 } 949 } 950 951 newAttachmentService() 952 newLFSService() 953 954 timeFormatKey := Cfg.Section("time").Key("FORMAT").MustString("") 955 if timeFormatKey != "" { 956 TimeFormat = map[string]string{ 957 "ANSIC": time.ANSIC, 958 "UnixDate": time.UnixDate, 959 "RubyDate": time.RubyDate, 960 "RFC822": time.RFC822, 961 "RFC822Z": time.RFC822Z, 962 "RFC850": time.RFC850, 963 "RFC1123": time.RFC1123, 964 "RFC1123Z": time.RFC1123Z, 965 "RFC3339": time.RFC3339, 966 "RFC3339Nano": time.RFC3339Nano, 967 "Kitchen": time.Kitchen, 968 "Stamp": time.Stamp, 969 "StampMilli": time.StampMilli, 970 "StampMicro": time.StampMicro, 971 "StampNano": time.StampNano, 972 }[timeFormatKey] 973 // When the TimeFormatKey does not exist in the previous map e.g.'2006-01-02 15:04:05' 974 if len(TimeFormat) == 0 { 975 TimeFormat = timeFormatKey 976 TestTimeFormat, _ := time.Parse(TimeFormat, TimeFormat) 977 if TestTimeFormat.Format(time.RFC3339) != "2006-01-02T15:04:05Z" { 978 log.Warn("Provided TimeFormat: %s does not create a fully specified date and time.", TimeFormat) 979 log.Warn("In order to display dates and times correctly please check your time format has 2006, 01, 02, 15, 04 and 05") 980 } 981 log.Trace("Custom TimeFormat: %s", TimeFormat) 982 } 983 } 984 985 zone := Cfg.Section("time").Key("DEFAULT_UI_LOCATION").String() 986 if zone != "" { 987 DefaultUILocation, err = time.LoadLocation(zone) 988 if err != nil { 989 log.Fatal("Load time zone failed: %v", err) 990 } else { 991 log.Info("Default UI Location is %v", zone) 992 } 993 } 994 if DefaultUILocation == nil { 995 DefaultUILocation = time.Local 996 } 997 998 RunUser = Cfg.Section("").Key("RUN_USER").MustString(user.CurrentUsername()) 999 // The following is a purposefully undocumented option. Please do not run GitBundle as root. It will only cause future headaches. 1000 // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. 1001 unsafeAllowRunAsRoot := Cfg.Section("").Key("I_AM_BEING_UNSAFE_RUNNING_AS_ROOT").MustBool(false) 1002 // Does not check run user when the install lock is off. 1003 if InstallLock { 1004 currentUser, match := IsRunUserMatchCurrentUser(RunUser) 1005 if !match { 1006 log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser) 1007 } 1008 } 1009 1010 // check if we run as root 1011 if os.Getuid() == 0 { 1012 if !unsafeAllowRunAsRoot { 1013 // Special thanks to VLC which inspired the wording of this messaging. 1014 log.Fatal("GitBundle is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission") 1015 } 1016 log.Critical("You are running GitBundle using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.") 1017 } 1018 1019 SSH.BuiltinServerUser = Cfg.Section("server").Key("BUILTIN_SSH_SERVER_USER").MustString(RunUser) 1020 SSH.User = Cfg.Section("server").Key("SSH_USER").MustString(SSH.BuiltinServerUser) 1021 1022 newRepository() 1023 1024 newPictureService() 1025 1026 newPackages() 1027 1028 if err = Cfg.Section("ui").MapTo(&UI); err != nil { 1029 log.Fatal("Failed to map UI settings: %v", err) 1030 } else if err = Cfg.Section("markdown").MapTo(&Markdown); err != nil { 1031 log.Fatal("Failed to map Markdown settings: %v", err) 1032 } else if err = Cfg.Section("admin").MapTo(&Admin); err != nil { 1033 log.Fatal("Fail to map Admin settings: %v", err) 1034 } else if err = Cfg.Section("api").MapTo(&API); err != nil { 1035 log.Fatal("Failed to map API settings: %v", err) 1036 } else if err = Cfg.Section("metrics").MapTo(&Metrics); err != nil { 1037 log.Fatal("Failed to map Metrics settings: %v", err) 1038 } else if err = Cfg.Section("camo").MapTo(&Camo); err != nil { 1039 log.Fatal("Failed to map Camo settings: %v", err) 1040 } 1041 1042 if Camo.Enabled { 1043 if Camo.ServerURL == "" || Camo.HMACKey == "" { 1044 log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`) 1045 } 1046 } 1047 1048 u := *appURL 1049 u.Path = path.Join(u.Path, "api", "swagger") 1050 API.SwaggerURL = u.String() 1051 1052 newGit() 1053 1054 newMirror() 1055 1056 Langs = Cfg.Section("i18n").Key("LANGS").Strings(",") 1057 if len(Langs) == 0 { 1058 Langs = defaultI18nLangs() 1059 } 1060 Names = Cfg.Section("i18n").Key("NAMES").Strings(",") 1061 if len(Names) == 0 { 1062 Names = defaultI18nNames() 1063 } 1064 1065 ShowFooterBranding = Cfg.Section("other").Key("SHOW_FOOTER_BRANDING").MustBool(false) 1066 ShowFooterVersion = Cfg.Section("other").Key("SHOW_FOOTER_VERSION").MustBool(true) 1067 ShowFooterTemplateLoadTime = Cfg.Section("other").Key("SHOW_FOOTER_TEMPLATE_LOAD_TIME").MustBool(true) 1068 1069 UI.ShowUserEmail = Cfg.Section("ui").Key("SHOW_USER_EMAIL").MustBool(true) 1070 UI.DefaultShowFullName = Cfg.Section("ui").Key("DEFAULT_SHOW_FULL_NAME").MustBool(false) 1071 UI.SearchRepoDescription = Cfg.Section("ui").Key("SEARCH_REPO_DESCRIPTION").MustBool(true) 1072 UI.UseServiceWorker = Cfg.Section("ui").Key("USE_SERVICE_WORKER").MustBool(false) 1073 1074 // Rewrite the default themes, make sure the user defined themes disabled 1075 UI.Themes = []string{`dark`} 1076 1077 HasRobotsTxt, err = util.IsFile(path.Join(CustomPath, "robots.txt")) 1078 if err != nil { 1079 log.Error("Unable to check if %s is a file. Error: %v", path.Join(CustomPath, "robots.txt"), err) 1080 } 1081 1082 newMarkup() 1083 1084 UI.ReactionsMap = make(map[string]bool) 1085 for _, reaction := range UI.Reactions { 1086 UI.ReactionsMap[reaction] = true 1087 } 1088 UI.CustomEmojisMap = make(map[string]string) 1089 for _, emoji := range UI.CustomEmojis { 1090 UI.CustomEmojisMap[emoji] = ":" + emoji + ":" 1091 } 1092 1093 // FIXME: DEPRECATED to be removed in v1.18.0 1094 U2F.AppID = strings.TrimSuffix(AppURL, "/") 1095 if Cfg.Section("U2F").HasKey("APP_ID") { 1096 log.Error("Deprecated setting `[U2F]` `APP_ID` present. This fallback will be removed in v1.18.0") 1097 U2F.AppID = Cfg.Section("U2F").Key("APP_ID").MustString(strings.TrimSuffix(AppURL, "/")) 1098 } else if Cfg.Section("u2f").HasKey("APP_ID") { 1099 log.Error("Deprecated setting `[u2]` `APP_ID` present. This fallback will be removed in v1.18.0") 1100 U2F.AppID = Cfg.Section("u2f").Key("APP_ID").MustString(strings.TrimSuffix(AppURL, "/")) 1101 } 1102 } 1103 1104 func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) { 1105 anything := false 1106 email := false 1107 username := false 1108 for _, value := range values { 1109 v := strings.ToLower(strings.TrimSpace(value)) 1110 switch v { 1111 case "off": 1112 return []string{"off"}, false 1113 case "email": 1114 email = true 1115 case "username": 1116 username = true 1117 case "anything": 1118 anything = true 1119 } 1120 } 1121 if anything { 1122 return []string{"anything"}, true 1123 } 1124 1125 authorizedPrincipalsAllow := []string{} 1126 if username { 1127 authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "username") 1128 } 1129 if email { 1130 authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "email") 1131 } 1132 1133 return authorizedPrincipalsAllow, true 1134 } 1135 1136 func loadInternalToken(sec *ini.Section) string { 1137 uri := sec.Key("INTERNAL_TOKEN_URI").String() 1138 if uri == "" { 1139 return sec.Key("INTERNAL_TOKEN").String() 1140 } 1141 tempURI, err := url.Parse(uri) 1142 if err != nil { 1143 log.Fatal("Failed to parse INTERNAL_TOKEN_URI (%s): %v", uri, err) 1144 } 1145 switch tempURI.Scheme { 1146 case "file": 1147 buf, err := os.ReadFile(tempURI.RequestURI()) 1148 if err != nil && !os.IsNotExist(err) { 1149 log.Fatal("Failed to open InternalTokenURI (%s): %v", uri, err) 1150 } 1151 // No token in the file, generate one and store it. 1152 if len(buf) == 0 { 1153 token, err := generate.NewInternalToken() 1154 if err != nil { 1155 log.Fatal("Error generate internal token: %v", err) 1156 } 1157 err = os.WriteFile(tempURI.RequestURI(), []byte(token), 0o600) 1158 if err != nil { 1159 log.Fatal("Error writing to InternalTokenURI (%s): %v", uri, err) 1160 } 1161 return token 1162 } 1163 return strings.TrimSpace(string(buf)) 1164 default: 1165 log.Fatal("Unsupported URI-Scheme %q (INTERNAL_TOKEN_URI = %q)", tempURI.Scheme, uri) 1166 } 1167 return "" 1168 } 1169 1170 // generateSaveInternalToken generates and saves the internal token to app.ini 1171 func generateSaveInternalToken() { 1172 token, err := generate.NewInternalToken() 1173 if err != nil { 1174 log.Fatal("Error generate internal token: %v", err) 1175 } 1176 1177 InternalToken = token 1178 CreateOrAppendToCustomConf(func(cfg *ini.File) { 1179 cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) 1180 }) 1181 } 1182 1183 // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash 1184 func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { 1185 parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) 1186 if err != nil { 1187 log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) 1188 } 1189 1190 if err == nil && parsedPrefix.Hostname() == "" { 1191 if staticURLPrefix == "" { 1192 return strings.TrimSuffix(appURL, "/") 1193 } 1194 1195 // StaticURLPrefix is just a path 1196 return util.URLJoin(appURL, strings.TrimSuffix(staticURLPrefix, "/")) 1197 } 1198 1199 return strings.TrimSuffix(staticURLPrefix, "/") 1200 } 1201 1202 // MakeManifestData generates web app manifest JSON 1203 func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { 1204 type manifestIcon struct { 1205 Src string `json:"src"` 1206 Type string `json:"type"` 1207 Sizes string `json:"sizes"` 1208 } 1209 1210 type manifestJSON struct { 1211 Name string `json:"name"` 1212 ShortName string `json:"short_name"` 1213 StartURL string `json:"start_url"` 1214 Icons []manifestIcon `json:"icons"` 1215 } 1216 1217 bytes, err := json.Marshal(&manifestJSON{ 1218 Name: appName, 1219 ShortName: appName, 1220 StartURL: appURL, 1221 Icons: []manifestIcon{ 1222 { 1223 Src: absoluteAssetURL + "/assets/img/logo.png", 1224 Type: "image/png", 1225 Sizes: "512x512", 1226 }, 1227 { 1228 Src: absoluteAssetURL + "/assets/img/logo.svg", 1229 Type: "image/svg+xml", 1230 Sizes: "512x512", 1231 }, 1232 }, 1233 }) 1234 if err != nil { 1235 log.Error("unable to marshal manifest JSON. Error: %v", err) 1236 return make([]byte, 0) 1237 } 1238 1239 return bytes 1240 } 1241 1242 // CreateOrAppendToCustomConf creates or updates the custom config. 1243 // Use the callback to set individual values. 1244 func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) { 1245 cfg := ini.Empty() 1246 isFile, err := util.IsFile(CustomConf) 1247 if err != nil { 1248 log.Error("Unable to check if %s is a file. Error: %v", CustomConf, err) 1249 } 1250 if isFile { 1251 if err := cfg.Append(CustomConf); err != nil { 1252 log.Error("failed to load custom conf %s: %v", CustomConf, err) 1253 return 1254 } 1255 } 1256 1257 callback(cfg) 1258 1259 log.Info("Settings saved to: %q", CustomConf) 1260 1261 if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { 1262 log.Fatal("failed to create '%s': %v", CustomConf, err) 1263 return 1264 } 1265 if err := cfg.SaveTo(CustomConf); err != nil { 1266 log.Fatal("error saving to custom config: %v", err) 1267 } 1268 1269 // Change permissions to be more restrictive 1270 fi, err := os.Stat(CustomConf) 1271 if err != nil { 1272 log.Error("Failed to determine current conf file permissions: %v", err) 1273 return 1274 } 1275 1276 if fi.Mode().Perm() > 0o600 { 1277 if err = os.Chmod(CustomConf, 0o600); err != nil { 1278 log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") 1279 } 1280 } 1281 } 1282 1283 // NewServices initializes the services 1284 func NewServices() { 1285 InitDBConfig() 1286 newService() 1287 newOAuth2Client() 1288 NewLogServices(false) 1289 newCacheService() 1290 newSessionService() 1291 newCORSService() 1292 newMailService() 1293 newRegisterMailService() 1294 newNotifyMailService() 1295 newProxyService() 1296 newWebhookService() 1297 newMigrationsService() 1298 newIndexerService() 1299 newTaskService() 1300 NewQueueService() 1301 newProject() 1302 newMimeTypeMap() 1303 newFederationService() 1304 newNsqService() 1305 newRedisService() 1306 newDeploymentService() 1307 } 1308 1309 // NewServicesForInstall initializes the services for install 1310 func NewServicesForInstall() { 1311 newService() 1312 newMailService() 1313 }