mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	# The problem
There were many "path tricks":
* By default, Gitea uses its program directory as its work path
* Gitea tries to use the "work path" to guess its "custom path" and
"custom conf (app.ini)"
* Users might want to use other directories as work path
* The non-default work path should be passed to Gitea by GITEA_WORK_DIR
or "--work-path"
* But some Gitea processes are started without these values
    * The "serv" process started by OpenSSH server
    * The CLI sub-commands started by site admin
* The paths are guessed by SetCustomPathAndConf again and again
* The default values of "work path / custom path / custom conf" can be
changed when compiling
# The solution
* Use `InitWorkPathAndCommonConfig` to handle these path tricks, and use
test code to cover its behaviors.
* When Gitea's web server runs, write the WORK_PATH to "app.ini", this
value must be the most correct one, because if this value is not right,
users would find that the web UI doesn't work and then they should be
able to fix it.
* Then all other sub-commands can use the WORK_PATH in app.ini to
initialize their paths.
* By the way, when Gitea starts for git protocol, it shouldn't output
any log, otherwise the git protocol gets broken and client blocks
forever.
The "work path" priority is: WORK_PATH in app.ini > cmd arg --work-path
> env var GITEA_WORK_DIR > builtin default
The "app.ini" searching order is: cmd arg --config > cmd arg "work path
/ custom path" > env var "work path / custom path" > builtin default
## ⚠️ BREAKING
If your instance's "work path / custom path / custom conf" doesn't meet
the requirements (eg: work path must be absolute), Gitea will report a
fatal error and exit. You need to set these values according to the
error log.
----
Close #24818
Close #24222
Close #21606
Close #21498
Close #25107
Close #24981
Maybe close #24503
Replace #23301
Replace #22754
And maybe more
		
	
		
			
				
	
	
		
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			358 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) {
 | |
| 		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)
 | |
| 	}
 | |
| }
 |