Files
opds-proxy/main.go
Evan Buss 9da7ea1bbc feat: environment configuration
Environment variables can now be used to configure any
config property including the feeds list.

This makes it easier to use in environments without access
to config files like GCR. Some may prefer not to have a separate
config file as well.

Also added build metadata to the docker image and binaries.
2024-08-11 18:47:46 +00:00

166 lines
4.1 KiB
Go

package main
import (
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/evan-buss/opds-proxy/internal/envextended"
"github.com/gorilla/securecookie"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/v2"
flag "github.com/spf13/pflag"
)
// Version information set at build time
var version = "dev"
var commit = "unknown"
var date = "unknown"
type ProxyConfig struct {
Port string `koanf:"port"`
Auth AuthConfig `koanf:"auth"`
Feeds []FeedConfig `koanf:"feeds" `
DebugMode bool `koanf:"debug"`
}
type AuthConfig struct {
HashKey string `koanf:"hash_key"`
BlockKey string `koanf:"block_key"`
}
type FeedConfig struct {
Name string `koanf:"name"`
Url string `koanf:"url"`
Auth *FeedConfigAuth `koanf:"auth"`
}
type FeedConfigAuth struct {
Username string `koanf:"username"`
Password string `koanf:"password"`
LocalOnly bool `koanf:"local_only"`
}
func main() {
var k = koanf.New(".")
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.StringP("port", "p", "8080", "port to listen on")
fs.StringP("config", "c", "config.yml", "config file to load")
fs.Bool("generate-keys", false, "generate cookie signing keys and exit")
fs.BoolP("version", "v", false, "print version and exit")
fs.Usage = func() {
fmt.Println("Usage: opds-proxy [flags]")
fmt.Println(fs.FlagUsages())
os.Exit(0)
}
if err := fs.Parse(os.Args[1:]); err != nil {
log.Fatalf("error parsing flags: %v", err)
}
if showVersion, _ := fs.GetBool("version"); showVersion {
fmt.Println("opds-proxy")
fmt.Printf(" Version: %s\n", version)
fmt.Printf(" Commit: %s\n", commit)
fmt.Printf(" Build Date: %s\n", date)
os.Exit(0)
}
if generate, _ := fs.GetBool("generate-keys"); generate {
displayKeys()
os.Exit(0)
}
// YAML Config
configPath, _ := fs.GetString("config")
if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil && !os.IsNotExist(err) {
log.Fatalf("error loading config file: %v", err)
}
// Environment Variables Config
if err := k.Load(envextended.ProviderWithValue("OPDS", ".", envCallback), json.Parser()); err != nil {
log.Fatalf("error loading environment variables: %v", err)
}
// CLI Flags Config
if err := k.Load(posflag.Provider(fs, ".", k), nil); err != nil {
log.Fatalf("error loading CLI flags: %v", err)
}
config := ProxyConfig{}
k.Unmarshal("", &config)
if config.Auth.HashKey == "" || config.Auth.BlockKey == "" {
log.Println("Generating new cookie signing credentials")
hashKey, blockKey := displayKeys()
config.Auth.HashKey = hashKey
config.Auth.BlockKey = blockKey
}
if err := config.Validate(); err != nil {
log.Fatalf("invalid configuration: %v", err)
}
server, err := NewServer(&config)
if err != nil {
log.Fatalf("error creating server: %v", err)
}
if err = server.Serve(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error serving: %v", err)
}
}
func displayKeys() (string, string) {
hashKey := hex.EncodeToString(securecookie.GenerateRandomKey(32))
blockKey := hex.EncodeToString(securecookie.GenerateRandomKey(32))
log.Println("Set these values in your config file to persist authentication between server restarts.")
fmt.Println("auth:")
fmt.Printf(" hash_key: %s\n", hashKey)
fmt.Printf(" block_key: %s\n", blockKey)
return hashKey, blockKey
}
func envCallback(key string, value string) (string, interface{}) {
key = strings.TrimPrefix(key, "OPDS__")
key = strings.ReplaceAll(key, "__", ".")
key = strings.ToLower(key)
return key, value
}
func (c *ProxyConfig) Validate() error {
if c.Port == "" {
return errors.New("port is required")
}
if c.Auth.HashKey == "" || c.Auth.BlockKey == "" {
return errors.New("auth.hash_key and auth.block_key are required")
}
if len(c.Feeds) == 0 {
return errors.New("at least one feed must be defined")
}
for _, feed := range c.Feeds {
if feed.Name == "" {
return errors.New("feed.name is required")
}
if feed.Url == "" {
return errors.New("feed.url is required")
}
}
return nil
}