mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
324 lines
12 KiB
Go
324 lines
12 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package setting
|
|
|
|
import (
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/log"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
const (
|
|
PublicURLAuto = "auto"
|
|
PublicURLLegacy = "legacy"
|
|
PublicURLNever = "never"
|
|
)
|
|
|
|
// Server settings
|
|
var (
|
|
// AppURL is the Application ROOT_URL. It always has a '/' suffix
|
|
// It maps to ini:"ROOT_URL"
|
|
AppURL string
|
|
|
|
// PublicURLDetection controls how to use the HTTP request headers to detect public URL
|
|
PublicURLDetection string
|
|
|
|
// AppSubURL represents the sub-url mounting point for the forge, parsed from "ROOT_URL"
|
|
// It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'.
|
|
// This value is empty if site does not have sub-url.
|
|
AppSubURL string
|
|
|
|
// UseSubURLPath makes the forge handle requests with sub-path like "/sub-path/owner/repo/...",
|
|
// to make it easier to debug sub-path related problems without a reverse proxy.
|
|
UseSubURLPath bool
|
|
|
|
// 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 the forge. It always has a '/' suffix
|
|
// It maps to ini:"LOCAL_ROOT_URL" in [server]
|
|
LocalURL string
|
|
|
|
// appTempPathInternal is the temporary path for the app, it is only an internal variable
|
|
// DO NOT use it directly, always use AppDataTempDir
|
|
appTempPathInternal string
|
|
|
|
Protocol Scheme
|
|
UseProxyProtocol bool
|
|
ProxyProtocolTLSBridging bool
|
|
ProxyProtocolHeaderTimeout time.Duration
|
|
ProxyProtocolAcceptUnknown bool
|
|
Domain string
|
|
HTTPAddr string
|
|
HTTPPort string
|
|
LocalUseProxyProtocol bool
|
|
RedirectOtherPort bool
|
|
RedirectorUseProxyProtocol bool
|
|
PortToRedirect string
|
|
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 // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute
|
|
)
|
|
|
|
func loadServerFrom(rootCfg ConfigProvider) {
|
|
sec := rootCfg.Section("server")
|
|
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Hanzo Git")
|
|
|
|
Domain = sec.Key("DOMAIN").MustString("localhost")
|
|
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
|
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
|
|
|
// 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)
|
|
}
|
|
|
|
protocolCfg := sec.Key("PROTOCOL").String()
|
|
if protocolCfg != "https" && EnableAcme {
|
|
log.Fatal("ACME could only be used with HTTPS protocol")
|
|
}
|
|
|
|
switch protocolCfg {
|
|
case "", "http":
|
|
Protocol = HTTP
|
|
case "https":
|
|
Protocol = HTTPS
|
|
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
|
|
default: // "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)
|
|
}
|
|
default:
|
|
log.Fatal("Invalid PROTOCOL %q", protocolCfg)
|
|
}
|
|
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)
|
|
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
|
|
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
|
|
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
|
|
}
|
|
|
|
// Check validity of AppURL
|
|
appURL, err := url.Parse(AppURL)
|
|
if err != nil {
|
|
log.Fatal("Invalid ROOT_URL %q: %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(), "/") + "/"
|
|
|
|
// AppSubURL 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, "/")
|
|
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
|
|
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
|
|
}
|
|
|
|
var defaultLocalURL string
|
|
switch Protocol {
|
|
case HTTPUnix:
|
|
defaultLocalURL = "http://unix/"
|
|
case FCGI:
|
|
defaultLocalURL = AppURL
|
|
case FCGIUnix:
|
|
defaultLocalURL = AppURL
|
|
case HTTP, HTTPS:
|
|
defaultLocalURL = string(Protocol) + "://"
|
|
if HTTPAddr == "0.0.0.0" {
|
|
defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/"
|
|
} else {
|
|
defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/"
|
|
}
|
|
default:
|
|
log.Fatal("Invalid PROTOCOL %q", Protocol)
|
|
}
|
|
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)
|
|
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(filepath.Join(AppWorkPath, "data"))
|
|
if !filepath.IsAbs(AppDataPath) {
|
|
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
|
|
}
|
|
if IsInTesting && HasInstallLock(rootCfg) {
|
|
// FIXME: in testing, the "app data" directory is not correctly initialized before loading settings
|
|
if _, err := os.Stat(AppDataPath); err != nil {
|
|
_ = os.MkdirAll(AppDataPath, os.ModePerm)
|
|
}
|
|
}
|
|
|
|
appTempPathInternal = sec.Key("APP_TEMP_PATH").String()
|
|
if appTempPathInternal != "" {
|
|
if _, err := os.Stat(appTempPathInternal); err != nil {
|
|
log.Fatal("APP_TEMP_PATH %q is not accessible: %v", appTempPathInternal, err)
|
|
}
|
|
}
|
|
|
|
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
|
|
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
|
|
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
|
|
|
|
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
|
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
|
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
|
if !filepath.IsAbs(PprofDataPath) {
|
|
PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
|
|
}
|
|
checkOverlappedPath("[server].PPROF_DATA_PATH", 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)
|
|
}
|
|
}
|