git » gofer » master » tree

[master] / config / config.go

// Package config implements the gofer configuration.
package config

import (
	"fmt"
	"io/ioutil"
	"net/url"
	"regexp"

	"gopkg.in/yaml.v3"
)

type Config struct {
	ControlAddr string `yaml:"control_addr,omitempty"`

	// Map address -> config.
	HTTP  map[string]HTTP  `yaml:",omitempty"`
	HTTPS map[string]HTTPS `yaml:",omitempty"`
	Raw   map[string]Raw   `yaml:",omitempty"`

	ReqLog map[string]ReqLog `yaml:",omitempty"`
}

type HTTP struct {
	Routes map[string]Route

	Auth map[string]string `yaml:",omitempty"`

	SetHeader map[string]map[string]string `yaml:",omitempty"`

	ReqLog map[string]string `yaml:",omitempty"`
}

type HTTPS struct {
	HTTP      `yaml:",inline"`
	Certs     string    `yaml:",omitempty"`
	AutoCerts AutoCerts `yaml:"autocerts,omitempty"`
}

type AutoCerts struct {
	Hosts    []string `yaml:",omitempty"`
	CacheDir string   `yaml:",omitempty"`
	Email    string   `yaml:",omitempty"`
	AcmeURL  string   `yaml:",omitempty"`
}

type Route struct {
	Dir      string   `yaml:",omitempty"`
	File     string   `yaml:",omitempty"`
	Proxy    *URL     `yaml:",omitempty"`
	Redirect *URL     `yaml:",omitempty"`
	CGI      []string `yaml:",omitempty"`
	Status   int      `yaml:",omitempty"`
	DirOpts  DirOpts  `yaml:",omitempty"`
}

type DirOpts struct {
	Listing map[string]bool `yaml:",omitempty"`
	Exclude []Regexp        `yaml:",omitempty"`
}

type Raw struct {
	Certs  string `yaml:",omitempty"`
	To     string `yaml:",omitempty"`
	ToTLS  bool   `yaml:"to_tls,omitempty"`
	ReqLog string `yaml:",omitempty"`
}

type ReqLog struct {
	File    string `yaml:",omitempty"`
	BufSize int    `yaml:",omitempty"`
	Format  string `yaml:",omitempty"`
}

func (c Config) String() string {
	d, err := yaml.Marshal(&c)
	if err != nil {
		return fmt.Sprintf("<error: %v>", err)
	}
	return string(d)
}

func (c Config) Check() []error {
	errs := []error{}
	for addr, h := range c.HTTP {
		errs = append(errs, h.Check(c, addr)...)

	}

	for addr, h := range c.HTTPS {
		errs = append(errs, h.Check(c, addr)...)

		// For HTTPS, either Certs or AutoCerts must be set.
		if h.Certs == "" && len(h.AutoCerts.Hosts) == 0 {
			errs = append(errs,
				fmt.Errorf("%q: certs or autocerts must be set", addr))
		}
	}

	for addr, r := range c.Raw {
		if _, ok := c.ReqLog[r.ReqLog]; r.ReqLog != "" && !ok {
			errs = append(errs,
				fmt.Errorf("%q: unknown reqlog %q", addr, r.ReqLog))
		}
	}
	return errs
}

func (h HTTP) Check(c Config, addr string) []error {
	errs := []error{}

	if len(h.Routes) == 0 {
		errs = append(errs, fmt.Errorf("%q: missing routes", addr))
	}

	for path, r := range h.Routes {
		if len(r.DirOpts.Listing)+len(r.DirOpts.Exclude) > 0 && r.Dir == "" {
			errs = append(errs,
				fmt.Errorf("%q: %q: diropts is set on non-dir route",
					addr, path))
		}

		nSet := nTrue(
			r.Dir != "",
			r.File != "",
			r.Proxy != nil,
			r.Redirect != nil,
			len(r.CGI) > 0,
			r.Status > 0)
		if nSet > 1 {
			errs = append(errs,
				fmt.Errorf("%q: %q: too many actions set", addr, path))
		} else if nSet == 0 {
			errs = append(errs,
				fmt.Errorf("%q: %q: action missing", addr, path))
		}
	}

	for path, name := range h.ReqLog {
		if _, ok := c.ReqLog[name]; !ok {
			errs = append(errs,
				fmt.Errorf("%q: %q: unknown reqlog %q", addr, path, name))
		}
	}

	return errs
}

func Load(filename string) (*Config, error) {
	contents, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("error reading config file: %v", err)
	}
	return LoadString(string(contents))
}

func LoadString(contents string) (*Config, error) {
	conf := &Config{}
	err := yaml.Unmarshal([]byte(contents), conf)
	return conf, err
}

// Wrapper to simplify regexp in configuration.
type Regexp struct {
	orig string
	*regexp.Regexp
}

func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var s string
	if err := unmarshal(&s); err != nil {
		return err
	}

	rx, err := regexp.Compile("^(?:" + s + ")$")
	if err != nil {
		return err
	}

	re.orig = s
	re.Regexp = rx
	return nil
}

func (re Regexp) MarshalYAML() (interface{}, error) {
	return re.orig, nil
}

// Wrapper to simplify URLs in configuration.
type URL url.URL

func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var s string
	if err := unmarshal(&s); err != nil {
		return err
	}

	x, err := url.Parse(s)
	if err != nil {
		return err
	}

	*u = URL(*x)
	return nil
}

func (u *URL) MarshalYAML() (interface{}, error) {
	if u == nil {
		return "", nil
	}
	return u.String(), nil
}

func (u *URL) URL() url.URL {
	return url.URL(*u)
}

func (u URL) String() string {
	p := u.URL()
	return p.String()
}

func nTrue(bs ...bool) int {
	n := 0
	for _, b := range bs {
		if b {
			n++
		}
	}
	return n
}