config.go

  1package config
  2
  3import (
  4	"fmt"
  5	"log"
  6	"net"
  7	"net/url"
  8	"os"
  9	"path/filepath"
 10	"strings"
 11
 12	"github.com/caarlos0/env/v6"
 13	"github.com/charmbracelet/soft-serve/proto"
 14	"github.com/charmbracelet/soft-serve/server/db"
 15	"github.com/charmbracelet/soft-serve/server/db/sqlite"
 16	"github.com/gliderlabs/ssh"
 17)
 18
 19// Callbacks provides an interface that can be used to run callbacks on different events.
 20type Callbacks interface {
 21	Tui(action string)
 22	Push(repo string)
 23	Fetch(repo string)
 24}
 25
 26// SSHConfig is the SSH configuration for the server.
 27type SSHConfig struct {
 28	Key           string `env:"KEY"`
 29	KeyPath       string `env:"KEY_PATH" envDefault:"soft_serve"`
 30	Port          int    `env:"PORT" envDefault:"23231"`
 31	AllowKeyless  bool   `env:"ALLOW_KEYLESS" envDefault:"true"`
 32	AllowPassword bool   `env:"ALLOW_PASSWORD" envDefault:"false"`
 33	Password      string `env:"PASSWORD"`
 34	MaxTimeout    int    `env:"MAX_TIMEOUT" envDefault:"0"`
 35	IdleTimeout   int    `env:"IDLE_TIMEOUT" envDefault:"300"`
 36}
 37
 38// GitConfig is the Git daemon configuration for the server.
 39type GitConfig struct {
 40	Enabled        bool `env:"ENABLED" envDefault:"true"`
 41	Port           int  `env:"PORT" envDefault:"9418"`
 42	MaxTimeout     int  `env:"MAX_TIMEOUT" envDefault:"0"`
 43	IdleTimeout    int  `env:"IDLE_TIMEOUT" envDefault:"3"`
 44	MaxConnections int  `env:"SOFT_SERVE_GIT_MAX_CONNECTIONS" envDefault:"32"`
 45}
 46
 47// DBConfig is the database configuration for the server.
 48type DBConfig struct {
 49	Driver   string `env:"DRIVER" envDefault:"sqlite"`
 50	User     string `env:"USER"`
 51	Password string `env:"PASSWORD"`
 52	Host     string `env:"HOST"`
 53	Port     string `env:"PORT"`
 54	Name     string `env:"NAME"`
 55	SSLMode  bool   `env:"SSL_MODE" envDefault:"false"`
 56}
 57
 58// HTTPConfig is the HTTP server config.
 59type HTTPConfig struct {
 60	Enabled bool   `env:"ENABLED" envDefault:"true"`
 61	Port    int    `env:"PORT" envDefault:"8080"`
 62	Domain  string `env:"DOMAIN" envDefault:"localhost"` // used for go get
 63}
 64
 65// URL returns a database URL for the configuration.
 66func (d *DBConfig) URL() *url.URL {
 67	switch d.Driver {
 68	case "sqlite":
 69		return &url.URL{
 70			Scheme: "sqlite",
 71			Path:   filepath.Join(d.Name),
 72		}
 73	default:
 74		ssl := "disable"
 75		if d.SSLMode {
 76			ssl = "require"
 77		}
 78		var user *url.Userinfo
 79		if d.User != "" && d.Password != "" {
 80			user = url.UserPassword(d.User, d.Password)
 81		} else if d.User != "" {
 82			user = url.User(d.User)
 83		}
 84		return &url.URL{
 85			Scheme:   d.Driver,
 86			Host:     net.JoinHostPort(d.Host, d.Port),
 87			User:     user,
 88			Path:     d.Name,
 89			RawQuery: fmt.Sprintf("sslmode=%s", ssl),
 90		}
 91	}
 92}
 93
 94// Config is the configuration for Soft Serve.
 95type Config struct {
 96	Host string `env:"HOST" envDefault:"localhost"`
 97
 98	SSH  SSHConfig  `env:"SSH" envPrefix:"SSH_"`
 99	Git  GitConfig  `env:"GIT" envPrefix:"GIT_"`
100	HTTP HTTPConfig `env:"HTTP" envPrefix:"HTTP_"`
101	Db   DBConfig   `env:"DB" envPrefix:"DB_"`
102
103	ServerName string            `env:"SERVER_NAME" envDefault:"Soft Serve"`
104	AnonAccess proto.AccessLevel `env:"ANON_ACCESS" envDefault:"read-only"`
105	DataPath   string            `env:"DATA_PATH" envDefault:"data"`
106
107	// Deprecated: use SOFT_SERVE_SSH_PORT instead.
108	Port int `env:"PORT"`
109	// Deprecated: use DataPath instead.
110	KeyPath string `env:"KEY_PATH"`
111	// Deprecated: use DataPath instead.
112	ReposPath string `env:"REPO_PATH"`
113
114	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n"`
115	Callbacks        Callbacks
116
117	db db.Store
118}
119
120// RepoPath returns the path to the repositories.
121func (c *Config) RepoPath() string {
122	return filepath.Join(c.DataPath, "repos")
123}
124
125// SSHPath returns the path to the SSH directory.
126func (c *Config) SSHPath() string {
127	return filepath.Join(c.DataPath, "ssh")
128}
129
130// PrivateKeyPath returns the path to the SSH key.
131func (c *Config) PrivateKeyPath() string {
132	return filepath.Join(c.SSHPath(), c.SSH.KeyPath)
133}
134
135// DBPath returns the path to the database.
136func (c *Config) DBPath() string {
137	return filepath.Join(c.DataPath, "db", "soft-serve.db")
138}
139
140// DefaultConfig returns a Config with the values populated with the defaults
141// or specified environment variables.
142func DefaultConfig() *Config {
143	var err error
144	var migrateWarn bool
145	var cfg Config
146	if err = env.Parse(&cfg, env.Options{
147		Prefix: "SOFT_SERVE_",
148	}); err != nil {
149		log.Fatalln(err)
150	}
151	if cfg.Port != 0 {
152		log.Printf("warning: SOFT_SERVE_PORT is deprecated, use SOFT_SERVE_SSH_PORT instead.")
153		migrateWarn = true
154	}
155	if cfg.KeyPath != "" {
156		log.Printf("warning: SOFT_SERVE_KEY_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
157		migrateWarn = true
158	}
159	if cfg.ReposPath != "" {
160		log.Printf("warning: SOFT_SERVE_REPO_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
161		migrateWarn = true
162	}
163	if migrateWarn {
164		log.Printf("warning: please run `soft serve migrate` to migrate your server and configuration.")
165	}
166	// initialize admin keys
167	for i, k := range cfg.InitialAdminKeys {
168		if bts, err := os.ReadFile(k); err == nil {
169			// k is a file path, read the file
170			k = string(bts)
171		}
172		pk := strings.TrimSpace(k)
173		if pk == "" {
174			// ignore empty keys
175			continue
176		}
177		if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)); err != nil {
178			// Fatal if the key is invalid
179			log.Fatalf("invalid initial admin key %q: %v", k, err)
180		}
181		// store the key in the config
182		cfg.InitialAdminKeys[i] = pk
183	}
184	// init data path and db
185	if err := os.MkdirAll(cfg.RepoPath(), 0o755); err != nil {
186		log.Fatalln(err)
187	}
188	switch cfg.Db.Driver {
189	case "sqlite":
190		if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0o755); err != nil {
191			log.Fatalln(err)
192		}
193		db, err := sqlite.New(cfg.DBPath())
194		if err != nil {
195			log.Fatalln(err)
196		}
197		cfg.WithDB(db)
198	}
199	if err := cfg.createDefaultConfigRepoAndUsers(); err != nil {
200		log.Fatalln(err)
201	}
202	return &cfg
203}
204
205// DB returns the database for the configuration.
206func (c *Config) DB() db.Store {
207	return c.db
208}
209
210// WithCallbacks applies the given Callbacks to the configuration.
211func (c *Config) WithCallbacks(callbacks Callbacks) *Config {
212	c.Callbacks = callbacks
213	return c
214}
215
216// WithDB sets the database for the configuration.
217func (c *Config) WithDB(db db.Store) *Config {
218	c.db = db
219	return c
220}
221
222// WithDataPath sets the data path for the configuration.
223func (c *Config) WithDataPath(path string) *Config {
224	c.DataPath = path
225	return c
226}