package config
import (
"fmt"
"io"
"io/fs"
"log"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/vaughan0/go-ini"
)
// osFS implement the fs.FS interface for the host OS filesystem.
type osFS struct{}
func (osFS) Open(name string) (fs.File, error) { return os.Open(filepath.FromSlash(name)) }
func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(filepath.FromSlash(pattern)) }
var (
// FS is the filesystem to operate on. Can be exchanged for testing or embedding.
FS fs.GlobFS = osFS{}
internalIPNet []net.IPNet
)
// Loads the application configuration.
func LoadConfig() ini.File {
var (
config ini.File
err error
)
// Only loads from one of these locations
for _, path := range []string{
"config.ini",
"../config.ini",
"/etc/sr.ht/config.ini",
"/etc/sr.ht/*.ini",
} {
matches, err_ := FS.Glob(path)
if err_ != nil {
panic(err) // only happens on bad input
}
for _, f := range matches {
var r io.Reader
r, err = FS.Open(f)
if err != nil {
break
}
if config == nil {
config, err = ini.Load(r)
} else {
err = config.Load(r)
}
if err != nil {
break
}
}
if len(matches) > 0 {
break
}
}
if err != nil {
log.Fatalf("Failed to load config file: %v", err)
}
nets, ok := config.Get("sr.ht", "internal-ipnet")
if !ok {
nets = "127.0.0.0/8,::1/128," + // Loopback
"10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7," + // Private
"169.254.0.0/16,fe80::/64" // Link local
}
for _, n := range strings.Split(nets, ",") {
_, net, err := net.ParseCIDR(n)
if err != nil {
panic(fmt.Errorf("[sr.ht]internal-ipnet: %w", err))
}
internalIPNet = append(internalIPNet, *net)
}
return config
}
// Returns the URL (scheme, host, port) at which the web application for a
// given service can be found.
//
// Set "external" to true if the URL will be accessed by an external user (i.e.
// outside of the LAN).
func GetOrigin(conf ini.File, svc string, external bool) string {
if external {
origin, _ := conf.Get(svc, "origin")
return origin
}
origin, ok := conf.Get(svc, "internal-origin")
if ok {
return origin
}
origin, _ = conf.Get(svc, "origin")
return origin
}
// Returns the URL (scheme, host, port) at which the API for a given service
// can be found. Does not include the /query path!
//
// Set "external" to true if the URL will be accessed by an external user (i.e.
// outside of the LAN).
func GetAPI(conf ini.File, svc string, external bool) string {
var candidates []string
if external {
candidates = []string{
"api-origin",
"origin",
}
} else {
candidates = []string{
"api-internal-origin",
"internal-origin",
"api-origin",
"origin",
}
}
for _, name := range candidates {
origin, ok := conf.Get(svc, name)
if ok {
return origin
}
}
panic(fmt.Errorf("no suitable origin configured for requested API"))
}
const DefaultQueueSize = 512
// Returns the configured integer value for the given section and variable name.
// If nothing is configured, the default value will be returned.
// If an invalid integer is configured, this function will panic.
func GetInt(conf ini.File, section, name string, defValue int) int {
value := defValue
if s, ok := conf.Get(section, name); ok {
var err error
if value, err = strconv.Atoi(s); err != nil {
panic(fmt.Errorf("[%s]%s: %w", section, name, err))
}
}
return value
}
// Returns the configured boolean value for the given section and variable name.
// If nothing is configured, the default value will be returned.
// If an invalid boolean is configured, this function will panic.
func GetBool(conf ini.File, section, name string, defValue bool) bool {
value := defValue
if s, ok := conf.Get(section, name); ok {
switch s {
case "true", "yes", "on", "1":
value = true
case "false", "no", "off", "0":
value = false
default:
panic(fmt.Errorf("[%s]%s: invalid boolean value", section, name))
}
}
return value
}
// Returns true if the given IP address is part of the internal networks as
// per the configuration of [sr.ht]internal-ipnet.
func IsInternalIP(ip net.IP) bool {
for _, net := range internalIPNet {
if net.Contains(ip) {
return true
}
}
return false
}