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  }