mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-19 21:51:36 +00:00
This reverts commit 351d0a0c85.
This option does not have any effect at the moment and may potentially
confuse users. It can be easily reintroduced later (by reverting this
commit) once we start logging at any level other than `info`.
345 lines
11 KiB
Go
345 lines
11 KiB
Go
package git_pages
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/c2h5oh/datasize"
|
|
"github.com/creasty/defaults"
|
|
"github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
// For an unknown reason, the standard `time.Duration` type doesn't implement the standard
|
|
// `encoding.{TextMarshaler,TextUnmarshaler}` interfaces.
|
|
type Duration time.Duration
|
|
|
|
func (t Duration) String() string {
|
|
return fmt.Sprint(time.Duration(t))
|
|
}
|
|
|
|
func (t *Duration) UnmarshalText(data []byte) (err error) {
|
|
u, err := time.ParseDuration(string(data))
|
|
if err == nil {
|
|
*t = Duration(u)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *Duration) MarshalText() ([]byte, error) {
|
|
return []byte(t.String()), nil
|
|
}
|
|
|
|
// For a known but upsetting reason, the standard `url.URL` type doesn't implement the standard
|
|
// `encoding.{TextMarshaler,TextUnmarshaler}` interfaces.
|
|
type URL struct {
|
|
url.URL
|
|
}
|
|
|
|
func (t *URL) String() string {
|
|
return fmt.Sprint(&t.URL)
|
|
}
|
|
|
|
func (t *URL) UnmarshalText(data []byte) (err error) {
|
|
u, err := url.Parse(string(data))
|
|
if err == nil {
|
|
*t = URL{*u}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *URL) MarshalText() ([]byte, error) {
|
|
return []byte(t.String()), nil
|
|
}
|
|
|
|
type Config struct {
|
|
Insecure bool `toml:"-" env:"insecure"`
|
|
Features []string `toml:"features"`
|
|
LogFormat string `toml:"log-format" default:"text"`
|
|
Server ServerConfig `toml:"server"`
|
|
Wildcard []WildcardConfig `toml:"wildcard"`
|
|
Fallback FallbackConfig `toml:"fallback"`
|
|
Storage StorageConfig `toml:"storage"`
|
|
Limits LimitsConfig `toml:"limits"`
|
|
Audit AuditConfig `toml:"audit"`
|
|
Observability ObservabilityConfig `toml:"observability"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Pages string `toml:"pages" default:"tcp/localhost:3000"`
|
|
Caddy string `toml:"caddy" default:"tcp/localhost:3001"`
|
|
Metrics string `toml:"metrics" default:"tcp/localhost:3002"`
|
|
}
|
|
|
|
type WildcardConfig struct {
|
|
Domain string `toml:"domain"`
|
|
CloneURL string `toml:"clone-url"` // URL template, not an exact URL
|
|
IndexRepos []string `toml:"index-repos" default:"[]"`
|
|
IndexRepoBranch string `toml:"index-repo-branch" default:"pages"`
|
|
Authorization string `toml:"authorization"`
|
|
}
|
|
|
|
type FallbackConfig struct {
|
|
ProxyTo *URL `toml:"proxy-to"`
|
|
Insecure bool `toml:"insecure"`
|
|
}
|
|
|
|
type CacheConfig struct {
|
|
MaxSize datasize.ByteSize `toml:"max-size"`
|
|
MaxAge Duration `toml:"max-age"`
|
|
MaxStale Duration `toml:"max-stale"`
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
Type string `toml:"type" default:"fs"`
|
|
FS FSConfig `toml:"fs" default:"{\"Root\":\"./data\"}"`
|
|
S3 S3Config `toml:"s3"`
|
|
}
|
|
|
|
type FSConfig struct {
|
|
Root string `toml:"root"`
|
|
}
|
|
|
|
type S3Config struct {
|
|
Endpoint string `toml:"endpoint"`
|
|
Insecure bool `toml:"insecure"`
|
|
AccessKeyID string `toml:"access-key-id"`
|
|
SecretAccessKey string `toml:"secret-access-key"`
|
|
Region string `toml:"region"`
|
|
Bucket string `toml:"bucket"`
|
|
BlobCache CacheConfig `toml:"blob-cache" default:"{\"MaxSize\":\"256MB\"}"`
|
|
SiteCache CacheConfig `toml:"site-cache" default:"{\"MaxAge\":\"60s\",\"MaxStale\":\"1h\",\"MaxSize\":\"16MB\"}"`
|
|
}
|
|
|
|
type LimitsConfig struct {
|
|
// Maximum size of a single published site. Also used to limit the size of archive
|
|
// uploads and other similar overconsumption conditions.
|
|
MaxSiteSize datasize.ByteSize `toml:"max-site-size" default:"128M"`
|
|
// Maximum size of a single site manifest, computed over its binary Protobuf
|
|
// serialization.
|
|
MaxManifestSize datasize.ByteSize `toml:"max-manifest-size" default:"1M"`
|
|
// Maximum size of a file that will still be inlined into the site manifest.
|
|
MaxInlineFileSize datasize.ByteSize `toml:"max-inline-file-size" default:"256B"`
|
|
// Maximum size of a Git object that will be cached in memory during Git operations.
|
|
GitLargeObjectThreshold datasize.ByteSize `toml:"git-large-object-threshold" default:"1M"`
|
|
// Maximum number of symbolic link traversals before the path is considered unreachable.
|
|
MaxSymlinkDepth uint `toml:"max-symlink-depth" default:"16"`
|
|
// Maximum time that an update operation (PUT or POST request) could take before being
|
|
// interrupted.
|
|
UpdateTimeout Duration `toml:"update-timeout" default:"60s"`
|
|
// Soft limit on Go heap size, expressed as a fraction of total available RAM.
|
|
MaxHeapSizeRatio float64 `toml:"max-heap-size-ratio" default:"0.5"`
|
|
// List of domains unconditionally forbidden for uploads.
|
|
ForbiddenDomains []string `toml:"forbidden-domains" default:"[]"`
|
|
// List of allowed repository URL prefixes. Setting this option prohibits uploading archives.
|
|
AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes" default:"[]"`
|
|
// List of allowed custom headers. Header name must be in the MIME canonical form,
|
|
// e.g. `Foo-Bar`. Setting this option permits including this custom header in `_headers`,
|
|
// unless it is fundamentally unsafe.
|
|
AllowedCustomHeaders []string `toml:"allowed-custom-headers" default:"[\"X-Clacks-Overhead\"]"`
|
|
}
|
|
|
|
type AuditConfig struct {
|
|
// Globally unique machine identifier (0 to 63 inclusive).
|
|
NodeID int `toml:"node-id"`
|
|
// Whether audit reports should be stored whenever an audit event occurs.
|
|
Collect bool `toml:"collect"`
|
|
// If not empty, includes the principal's IP address in audit reports, with the value specifying
|
|
// the source of the IP address. If the value is "X-Forwarded-For", the last item of the
|
|
// corresponding header field (assumed to be comma-separated) is used. If the value is
|
|
// "RemoteAddr", the connecting host's address is used. Any other value is disallowed.
|
|
IncludeIPs string `toml:"include-ip"`
|
|
// Endpoint to notify with a `GET /<notify-url>?<id>` whenever an audit event occurs.
|
|
NotifyURL *URL `toml:"notify-url"`
|
|
}
|
|
|
|
type ObservabilityConfig struct {
|
|
// Minimum duration for an HTTP request transaction to be unconditionally sampled.
|
|
SlowResponseThreshold Duration `toml:"slow-response-threshold" default:"500ms"`
|
|
}
|
|
|
|
func (config *Config) TOML() string {
|
|
result, err := toml.Marshal(config)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(result)
|
|
}
|
|
|
|
func (config *Config) Feature(name string) bool {
|
|
return slices.Contains(config.Features, name)
|
|
}
|
|
|
|
type walkConfigState struct {
|
|
config reflect.Value
|
|
scopeType reflect.Type
|
|
index []int
|
|
segments []string
|
|
}
|
|
|
|
func walkConfigScope(scopeState walkConfigState, onKey func(string, reflect.Value) error) (err error) {
|
|
for _, field := range reflect.VisibleFields(scopeState.scopeType) {
|
|
fieldState := walkConfigState{config: scopeState.config}
|
|
fieldState.scopeType = field.Type
|
|
fieldState.index = append(scopeState.index, field.Index...)
|
|
var tagValue, ok = "", false
|
|
if tagValue, ok = field.Tag.Lookup("env"); !ok {
|
|
if tagValue, ok = field.Tag.Lookup("toml"); !ok {
|
|
continue // implicit skip
|
|
}
|
|
} else if tagValue == "-" {
|
|
continue // explicit skip
|
|
}
|
|
fieldSegment := strings.ReplaceAll(strings.ToUpper(tagValue), "-", "_")
|
|
fieldState.segments = append(scopeState.segments, fieldSegment)
|
|
switch field.Type.Kind() {
|
|
case reflect.Struct:
|
|
err = walkConfigScope(fieldState, onKey)
|
|
default:
|
|
err = onKey(
|
|
strings.Join(fieldState.segments, "_"),
|
|
scopeState.config.FieldByIndex(fieldState.index),
|
|
)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func walkConfig(config *Config, onKey func(string, reflect.Value) error) error {
|
|
state := walkConfigState{
|
|
config: reflect.ValueOf(config).Elem(),
|
|
scopeType: reflect.TypeOf(config).Elem(),
|
|
index: []int{},
|
|
segments: []string{"PAGES"},
|
|
}
|
|
return walkConfigScope(state, onKey)
|
|
}
|
|
|
|
func setConfigValue(reflValue reflect.Value, repr string) (err error) {
|
|
valueAny := reflValue.Interface()
|
|
switch valueCast := valueAny.(type) {
|
|
case string:
|
|
reflValue.SetString(repr)
|
|
case []string:
|
|
reflValue.Set(reflect.ValueOf(strings.Split(repr, ",")))
|
|
case bool:
|
|
if valueCast, err = strconv.ParseBool(repr); err == nil {
|
|
reflValue.SetBool(valueCast)
|
|
}
|
|
case int:
|
|
var parsed int64
|
|
if parsed, err = strconv.ParseInt(repr, 10, strconv.IntSize); err == nil {
|
|
reflValue.SetInt(parsed)
|
|
}
|
|
case uint:
|
|
var parsed uint64
|
|
if parsed, err = strconv.ParseUint(repr, 10, strconv.IntSize); err == nil {
|
|
reflValue.SetUint(parsed)
|
|
}
|
|
case float64:
|
|
if valueCast, err = strconv.ParseFloat(repr, 64); err == nil {
|
|
reflValue.SetFloat(valueCast)
|
|
}
|
|
case datasize.ByteSize:
|
|
if valueCast, err = datasize.ParseString(repr); err == nil {
|
|
reflValue.Set(reflect.ValueOf(valueCast))
|
|
}
|
|
case Duration:
|
|
var parsed time.Duration
|
|
if parsed, err = time.ParseDuration(repr); err == nil {
|
|
reflValue.Set(reflect.ValueOf(Duration(parsed)))
|
|
}
|
|
case *URL:
|
|
if repr == "" {
|
|
reflValue.Set(reflect.ValueOf(nil))
|
|
} else {
|
|
var parsed *url.URL
|
|
if parsed, err = url.Parse(repr); err == nil {
|
|
reflValue.Set(reflect.ValueOf(&URL{*parsed}))
|
|
}
|
|
}
|
|
case []WildcardConfig:
|
|
var parsed []*WildcardConfig
|
|
decoder := json.NewDecoder(bytes.NewReader([]byte(repr)))
|
|
decoder.DisallowUnknownFields()
|
|
if err = decoder.Decode(&parsed); err == nil {
|
|
var assigned []WildcardConfig
|
|
for _, wildcard := range parsed {
|
|
defaults.MustSet(wildcard)
|
|
assigned = append(assigned, *wildcard)
|
|
}
|
|
reflValue.Set(reflect.ValueOf(assigned))
|
|
}
|
|
default:
|
|
panic("unhandled config value type")
|
|
}
|
|
return err
|
|
}
|
|
|
|
func PrintConfigEnvVars() {
|
|
config := Config{}
|
|
defaults.MustSet(&config)
|
|
|
|
walkConfig(&config, func(envName string, reflValue reflect.Value) (err error) {
|
|
value := reflValue.Interface()
|
|
reprBefore := fmt.Sprint(value)
|
|
fmt.Printf("%s %T = %q\n", envName, value, reprBefore)
|
|
// make sure that the value, at least, roundtrips
|
|
setConfigValue(reflValue, reprBefore)
|
|
reprAfter := fmt.Sprint(value)
|
|
if reprBefore != reprAfter {
|
|
panic("failed to roundtrip config value")
|
|
}
|
|
return
|
|
})
|
|
}
|
|
|
|
func Configure(tomlPath string) (config *Config, err error) {
|
|
// start with an all-default configuration
|
|
config = new(Config)
|
|
defaults.MustSet(config)
|
|
|
|
// inject values from `config.toml`
|
|
if tomlPath != "" {
|
|
var file *os.File
|
|
file, err = os.Open(tomlPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
decoder := toml.NewDecoder(file)
|
|
decoder.DisallowUnknownFields()
|
|
decoder.EnableUnmarshalerInterface()
|
|
if err = decoder.Decode(&config); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// inject values from the environment, overriding everything else
|
|
err = walkConfig(config, func(envName string, reflValue reflect.Value) error {
|
|
if envValue, found := os.LookupEnv(envName); found {
|
|
return setConfigValue(reflValue, envValue)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// defaults for wildcards aren't set by `defaults.MustSet` call above because the structs
|
|
// for them haven't been created yet
|
|
for i := range config.Wildcard {
|
|
defaults.MustSet(&config.Wildcard[i])
|
|
}
|
|
|
|
return
|
|
}
|