4647660776
## ⚠️ Breaking The `log.<mode>.<logger>` style config has been dropped. If you used it, please check the new config manual & app.example.ini to make your instance output logs as expected. Although many legacy options still work, it's encouraged to upgrade to the new options. The SMTP logger is deleted because SMTP is not suitable to collect logs. If you have manually configured Gitea log options, please confirm the logger system works as expected after upgrading. ## Description Close #12082 and maybe more log-related issues, resolve some related FIXMEs in old code (which seems unfixable before) Just like rewriting queue #24505 : make code maintainable, clear legacy bugs, and add the ability to support more writers (eg: JSON, structured log) There is a new document (with examples): `logging-config.en-us.md` This PR is safer than the queue rewriting, because it's just for logging, it won't break other logic. ## The old problems The logging system is quite old and difficult to maintain: * Unclear concepts: Logger, NamedLogger, MultiChannelledLogger, SubLogger, EventLogger, WriterLogger etc * Some code is diffuclt to konw whether it is right: `log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs `log.DelLogger("console")` * The old system heavily depends on ini config system, it's difficult to create new logger for different purpose, and it's very fragile. * The "color" trick is difficult to use and read, many colors are unnecessary, and in the future structured log could help * It's difficult to add other log formats, eg: JSON format * The log outputer doesn't have full control of its goroutine, it's difficult to make outputer have advanced behaviors * The logs could be lost in some cases: eg: no Fatal error when using CLI. * Config options are passed by JSON, which is quite fragile. * INI package makes the KEY in `[log]` section visible in `[log.sub1]` and `[log.sub1.subA]`, this behavior is quite fragile and would cause more unclear problems, and there is no strong requirement to support `log.<mode>.<logger>` syntax. ## The new design See `logger.go` for documents. ## Screenshot <details> ![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff) ![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9) ![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee) </details> ## TODO * [x] add some new tests * [x] fix some tests * [x] test some sub-commands (manually ....) --------- Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: Giteabot <teabot@gitea.io>
357 lines
12 KiB
Go
357 lines
12 KiB
Go
// 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("Gitea: Git with a cup of tea")
|
|
|
|
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()
|
|
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)
|
|
}
|
|
}
|