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