diff options
Diffstat (limited to 'modules/setting/server.go')
-rw-r--r-- | modules/setting/server.go | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/modules/setting/server.go b/modules/setting/server.go new file mode 100644 index 00000000..5cc33f6f --- /dev/null +++ b/modules/setting/server.go @@ -0,0 +1,368 @@ +// 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 + // AppSlogan is the Application slogan. + // It maps to ini:"APP_SLOGAN" + AppSlogan string + // AppDisplayNameFormat defines how the AppDisplayName should be presented + // It maps to ini:"APP_DISPLAY_NAME_FORMAT" + AppDisplayNameFormat string + // AppDisplayName is the display name for the application, defined following AppDisplayNameFormat + AppDisplayName 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 + 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 + + 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 generateDisplayName() string { + appDisplayName := AppName + if AppSlogan != "" { + appDisplayName = strings.Replace(AppDisplayNameFormat, "{APP_NAME}", AppName, 1) + appDisplayName = strings.Replace(appDisplayName, "{APP_SLOGAN}", AppSlogan, 1) + } + return appDisplayName +} + +func loadServerFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("server") + AppName = rootCfg.Section("").Key("APP_NAME").MustString("Forgejo: Beyond coding. We Forge.") + AppSlogan = rootCfg.Section("").Key("APP_SLOGAN").MustString("") + AppDisplayNameFormat = rootCfg.Section("").Key("APP_DISPLAY_NAME_FORMAT").MustString("{APP_NAME}: {APP_SLOGAN}") + AppDisplayName = generateDisplayName() + 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 + + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + if sec.HasKey("ENABLE_ACME") { + EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) + } else { + deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0") + EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) + } + if EnableAcme { + AcmeURL = sec.Key("ACME_URL").MustString("") + AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") + + if sec.HasKey("ACME_ACCEPTTOS") { + AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false) + } else { + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS", "v1.19.0") + AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false) + } + if !AcmeTOS { + log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).") + } + + if sec.HasKey("ACME_DIRECTORY") { + AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https") + } else { + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY", "v1.19.0") + AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https") + } + + if sec.HasKey("ACME_EMAIL") { + AcmeEmail = sec.Key("ACME_EMAIL").MustString("") + } else { + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0") + 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(true) + 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) { + 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 "", "home": + LandingPageURL = LandingPageHome + default: + LandingPageURL = LandingPage(landingPage) + } +} |