// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package setting import ( "encoding/base64" "net" "net/url" "path" "path/filepath" "strconv" "strings" "time" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) // Scheme describes protocol types type Scheme string // enumerates all the scheme types const ( HTTP Scheme = "http" HTTPS Scheme = "https" FCGI Scheme = "fcgi" FCGIUnix Scheme = "fcgi+unix" HTTPUnix Scheme = "http+unix" ) // LandingPage describes the default page type LandingPage string // enumerates all the landing page types const ( LandingPageHome LandingPage = "/" LandingPageExplore LandingPage = "/explore" LandingPageOrganizations LandingPage = "/explore/organizations" LandingPageLogin LandingPage = "/user/login" ) var ( // AppName is the Application name, used in the page title. // It maps to ini:"APP_NAME" AppName string // AppURL is the Application ROOT_URL. It always has a '/' suffix // It maps to ini:"ROOT_URL" AppURL string // AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. // This value is empty if site does not have sub-url. AppSubURL string // AppDataPath is the default path for storing data. // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" AppDataPath string // LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string // AssetVersion holds a opaque value that is used for cache-busting assets AssetVersion string // Server settings Protocol Scheme UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` ProxyProtocolHeaderTimeout time.Duration ProxyProtocolAcceptUnknown bool Domain string HTTPAddr string HTTPPort string LocalUseProxyProtocol bool RedirectOtherPort bool RedirectorUseProxyProtocol bool PortToRedirect string OfflineMode bool CertFile string KeyFile string StaticRootPath string StaticCacheTime time.Duration EnableGzip bool LandingPageURL LandingPage LandingPageCustom string UnixSocketPermission uint32 EnablePprof bool PprofDataPath string EnableAcme bool AcmeTOS bool AcmeLiveDirectory string AcmeEmail string AcmeURL string AcmeCARoot string SSLMinimumVersion string SSLMaximumVersion string SSLCurvePreferences []string SSLCipherSuites []string GracefulRestartable bool GracefulHammerTime time.Duration StartupTimeout time.Duration PerWriteTimeout = 30 * time.Second PerWritePerKbTimeout = 10 * time.Second StaticURLPrefix string AbsoluteAssetURL string HasRobotsTxt bool ManifestData string ) // MakeManifestData generates web app manifest JSON func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { type manifestIcon struct { Src string `json:"src"` Type string `json:"type"` Sizes string `json:"sizes"` } type manifestJSON struct { Name string `json:"name"` ShortName string `json:"short_name"` StartURL string `json:"start_url"` Icons []manifestIcon `json:"icons"` } bytes, err := json.Marshal(&manifestJSON{ Name: appName, ShortName: appName, StartURL: appURL, Icons: []manifestIcon{ { Src: absoluteAssetURL + "/assets/img/logo.png", Type: "image/png", Sizes: "512x512", }, { Src: absoluteAssetURL + "/assets/img/logo.svg", Type: "image/svg+xml", Sizes: "512x512", }, }, }) if err != nil { log.Error("unable to marshal manifest JSON. Error: %v", err) return make([]byte, 0) } return bytes } // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) if err != nil { log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) } if err == nil && parsedPrefix.Hostname() == "" { if staticURLPrefix == "" { return strings.TrimSuffix(appURL, "/") } // StaticURLPrefix is just a path return util.URLJoin(appURL, strings.TrimSuffix(staticURLPrefix, "/")) } return strings.TrimSuffix(staticURLPrefix, "/") } func loadServerFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("server") AppName = rootCfg.Section("").Key("APP_NAME").MustString("Forgejo: Beyond coding. We Forge.") Domain = sec.Key("DOMAIN").MustString("localhost") HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HTTPPort = sec.Key("HTTP_PORT").MustString("3000") Protocol = HTTP protocolCfg := sec.Key("PROTOCOL").String() switch protocolCfg { case "https": Protocol = HTTPS // FIXME: DEPRECATED to be removed in v1.18.0 if sec.HasKey("ENABLE_ACME") { EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) } else { deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME") EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) } if EnableAcme { AcmeURL = sec.Key("ACME_URL").MustString("") AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") // FIXME: DEPRECATED to be removed in v1.18.0 if sec.HasKey("ACME_ACCEPTTOS") { AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false) } else { deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS") AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false) } if !AcmeTOS { log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).") } // FIXME: DEPRECATED to be removed in v1.18.0 if sec.HasKey("ACME_DIRECTORY") { AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https") } else { deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY") AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https") } // FIXME: DEPRECATED to be removed in v1.18.0 if sec.HasKey("ACME_EMAIL") { AcmeEmail = sec.Key("ACME_EMAIL").MustString("") } else { deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL") AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") } } else { CertFile = sec.Key("CERT_FILE").String() KeyFile = sec.Key("KEY_FILE").String() if len(CertFile) > 0 && !filepath.IsAbs(CertFile) { CertFile = filepath.Join(CustomPath, CertFile) } if len(KeyFile) > 0 && !filepath.IsAbs(KeyFile) { KeyFile = filepath.Join(CustomPath, KeyFile) } } SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("") SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("") SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",") SSLCipherSuites = sec.Key("SSL_CIPHER_SUITES").Strings(",") case "fcgi": Protocol = FCGI case "fcgi+unix", "unix", "http+unix": switch protocolCfg { case "fcgi+unix": Protocol = FCGIUnix case "unix": log.Warn("unix PROTOCOL value is deprecated, please use http+unix") fallthrough case "http+unix": Protocol = HTTPUnix } UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666") UnixSocketPermissionParsed, err := strconv.ParseUint(UnixSocketPermissionRaw, 8, 32) if err != nil || UnixSocketPermissionParsed > 0o777 { log.Fatal("Failed to parse unixSocketPermission: %s", UnixSocketPermissionRaw) } UnixSocketPermission = uint32(UnixSocketPermissionParsed) if !filepath.IsAbs(HTTPAddr) { HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) } } UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false) ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false) ProxyProtocolHeaderTimeout = sec.Key("PROXY_PROTOCOL_HEADER_TIMEOUT").MustDuration(5 * time.Second) ProxyProtocolAcceptUnknown = sec.Key("PROXY_PROTOCOL_ACCEPT_UNKNOWN").MustBool(false) GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true) GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second) StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second) PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout) PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) // Check validity of AppURL appURL, err := url.Parse(AppURL) if err != nil { log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err) } // Remove default ports from AppURL. // (scheme-based URL normalization, RFC 3986 section 6.2.3) if (appURL.Scheme == string(HTTP) && appURL.Port() == "80") || (appURL.Scheme == string(HTTPS) && appURL.Port() == "443") { appURL.Host = appURL.Hostname() } // This should be TrimRight to ensure that there is only a single '/' at the end of AppURL. AppURL = strings.TrimRight(appURL.String(), "/") + "/" // Suburl should start with '/' and end without '/', such as '/{subpath}'. // This value is empty if site does not have sub-url. AppSubURL = strings.TrimSuffix(appURL.Path, "/") StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") // Check if Domain differs from AppURL domain than update it to AppURL's domain urlHostname := appURL.Hostname() if urlHostname != Domain && net.ParseIP(urlHostname) == nil && urlHostname != "" { Domain = urlHostname } AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) var defaultLocalURL string switch Protocol { case HTTPUnix: defaultLocalURL = "http://unix/" case FCGI: defaultLocalURL = AppURL case FCGIUnix: defaultLocalURL = AppURL default: defaultLocalURL = string(Protocol) + "://" if HTTPAddr == "0.0.0.0" { defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/" } else { defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/" } } LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) LocalURL = strings.TrimRight(LocalURL, "/") + "/" LocalUseProxyProtocol = sec.Key("LOCAL_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol) RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol) OfflineMode = sec.Key("OFFLINE_MODE").MustBool() Log.DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() if len(StaticRootPath) == 0 { StaticRootPath = AppWorkPath } StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath) StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath) AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) } EnableGzip = sec.Key("ENABLE_GZIP").MustBool() EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false) PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof")) if !filepath.IsAbs(PprofDataPath) { PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath) } landingPage := sec.Key("LANDING_PAGE").MustString("home") switch landingPage { case "explore": LandingPageURL = LandingPageExplore case "organizations": LandingPageURL = LandingPageOrganizations case "login": LandingPageURL = LandingPageLogin case "": case "home": LandingPageURL = LandingPageHome default: LandingPageURL = LandingPage(landingPage) } HasRobotsTxt, err = util.IsFile(path.Join(CustomPath, "robots.txt")) if err != nil { log.Error("Unable to check if %s is a file. Error: %v", path.Join(CustomPath, "robots.txt"), err) } }