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 cwd, _ := os.Getwd()
180 log.Fatalf("invalid initial admin key %q: %v", filepath.Join(cwd, k), err)
181 }
182 // store the key in the config
183 cfg.InitialAdminKeys[i] = pk
184 }
185 // init data path and db
186 if err := os.MkdirAll(cfg.RepoPath(), 0o755); err != nil {
187 log.Fatalln(err)
188 }
189 switch cfg.Db.Driver {
190 case "sqlite":
191 if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0o755); err != nil {
192 log.Fatalln(err)
193 }
194 db, err := sqlite.New(cfg.DBPath())
195 if err != nil {
196 log.Fatalln(err)
197 }
198 cfg.WithDB(db)
199 }
200 if err := cfg.createDefaultConfigRepoAndUsers(); err != nil {
201 log.Fatalln("create default config and users", err)
202 }
203 return &cfg
204}
205
206// DB returns the database for the configuration.
207func (c *Config) DB() db.Store {
208 return c.db
209}
210
211// WithCallbacks applies the given Callbacks to the configuration.
212func (c *Config) WithCallbacks(callbacks Callbacks) *Config {
213 c.Callbacks = callbacks
214 return c
215}
216
217// WithDB sets the database for the configuration.
218func (c *Config) WithDB(db db.Store) *Config {
219 c.db = db
220 return c
221}
222
223// WithDataPath sets the data path for the configuration.
224func (c *Config) WithDataPath(path string) *Config {
225 c.DataPath = path
226 return c
227}