-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
284 lines (237 loc) · 10.2 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package main
import (
"log"
"net"
"os"
"path/filepath"
"reflect"
"time"
"golang.org/x/net/idna"
"gopkg.in/yaml.v3"
)
type ServerConfig struct {
// The base directory (the web root) to serve static files from.
// Warning, the permissions for all files will be set to `a=r`, and for all directories to `a=rx`.
// This is also the directory in which to jail the process on Linux.
WebRootDirectory string `yaml:"web-root-directory"`
// Let's Encrypt certificates are stored in this directory.
CertificateCacheDirectory string `yaml:"certificate-cache-directory"`
// The HTTP address (host:port or :port) to bind the server to.
HttpAddr string `yaml:"http-addr"`
// The HTTPS address (host:port or :port) to bind the server to.
HttpsAddr string `yaml:"https-addr"`
// Let's Encrypt white list.
// These domains are allowed to fetch a Let's Encrypt certificate.
// This is not directly configurable. Instead, the domain directories in www_static will be used
// to populate this, and then SelfSignedDomains will be substracted.
letsEncryptDomains []string
// Self signed certificates white list.
// For this domains, no certificate will be fetched from Let's Encrypt.
SelfSignedDomains []string `yaml:"self-signed-domains"`
// All allowed domains. This are LetsEncryptDomains + SelfSignedDomains.
allDomains map[string]bool
// Name of the web server used as Server header.
ServerName string `yaml:"server-name"`
// Security http headers.
HttpHeaderXContentTypeOptions string `yaml:"http-header-x-content-type-options"`
HttpHeaderStrictTransportSecurity string `yaml:"http-header-strict-transport-security"`
HttpHeaderContentSecurityPolicy string `yaml:"http-header-content-security-policy"`
HttpHeaderXFrameOptions string `yaml:"http-header-x-frame-options"`
// Renew certificates, if they expire within this duration.
CertificateExpiryRefreshThreshold time.Duration `yaml:"certificate-expiry-refresh-threshold"`
// Maximum duration to wait for a request to complete.
MaxRequestTimeout time.Duration `yaml:"max-request-timeout"`
// Maximum duration to wait for a response to complete.
MaxResponseTimeout time.Duration `yaml:"max-response-timeout"`
// Maximum duration to wait for a follow up request.
MaxIdleTimeout time.Duration `yaml:"max-idle-timeout"`
// Serve files if they are not cached in memory. If this is `false`, the server will not even try to read newer files into the cache.
ServeFilesNotInCache bool `yaml:"serve-files-not-in-cache"`
// Maximum size for files that are cached in memory.
MaxCacheableFileSize int64 `yaml:"max-cacheable-file-size"`
// Log the client IP and URL path of each request.
LogRequests bool `yaml:"log-requests"`
// The name of the log file. If the name is empty, the log output will only be written to stdout.
LogFile string `yaml:"log-file"`
/*
TODO: Maybe:
The HTTPS port where to redirect HTTP connections to, because there can be a proxy in front
The maximum number of connections the server should allow at once
The maximum request body size the server should allow
The server's TLS/SSL certificate and key files
The level of access logging to enable
The location of the server's access and error logs
The type of error handling to use (e.g. detailed errors or friendly error pages)
*/
}
// Set the default values of the config variables.
var config = ServerConfig{
WebRootDirectory: "www_static",
CertificateCacheDirectory: "certcache",
HttpAddr: ":http",
HttpsAddr: ":https",
letsEncryptDomains: []string{},
SelfSignedDomains: []string{"localhost", "127.0.0.1"},
allDomains: nil,
ServerName: "dma-srv",
HttpHeaderXContentTypeOptions: "nosniff",
HttpHeaderStrictTransportSecurity: "max-age=63072000; includeSubDomains",
HttpHeaderContentSecurityPolicy: "script-src 'self'",
HttpHeaderXFrameOptions: "DENY",
CertificateExpiryRefreshThreshold: 48 * time.Hour,
MaxRequestTimeout: 15 * time.Second,
MaxResponseTimeout: 60 * time.Second,
MaxIdleTimeout: 60 * time.Second,
ServeFilesNotInCache: true,
MaxCacheableFileSize: 1024 * 1024,
LogRequests: true,
LogFile: "server.log",
}
func readConfig() {
// Read the config file.
data, err := os.ReadFile("config.yml")
if err != nil {
// If the file does not exist, create it.
log.Println("Configuration file config.yaml does not exist. Creating the file...")
data, err := yaml.Marshal(config)
if err != nil {
log.Println("Could not marshal config yaml.")
return
}
err = os.WriteFile("config.yml", data, 0644)
if err != nil {
log.Println("Could not write config yaml.")
return
}
log.Println("Done.")
}
// Unmarshal the config data into a Config struct.
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Println("config.yaml seems to have invalid syntax or entries.")
return
}
// Sanity checks.
sanityChecks()
}
func printConfig(config ServerConfig) {
log.Println("Config:")
// Get the type of the config variable.
t := reflect.TypeOf(config)
// Iterate over all the fields of the config variable.
for i := 0; i < t.NumField(); i++ {
// Get the config entries name field and its yaml tag.
nameField := t.Field(i)
yamlTag := nameField.Tag.Get("yaml")
// Get the config entries value field.
valueField := reflect.ValueOf(config).Field(i)
if valueField.CanInterface() && yamlTag != "" {
// Print the field name and its value.
log.Println(" "+yamlTag+":", valueField.Interface())
}
}
}
func sanityChecks() {
// Ensure that the HttpAddr parameter is a valid address and convert its service name into the numeric port number.
// If it is not valid, set it to ":80".
addr, err := net.ResolveTCPAddr("tcp", config.HttpAddr)
if err != nil {
config.HttpAddr = ":80"
log.Println("Warning: http-addr is invalid. Setting it to :80.")
} else {
config.HttpAddr = addr.String()
}
// Ensure that the HttpsAddr parameter is a valid address and convert its service name into the numeric port number.
// If it is not valid, set it to ":443".
addr, err = net.ResolveTCPAddr("tcp", config.HttpsAddr)
if err != nil {
config.HttpsAddr = ":443"
log.Println("Warning: https-addr is invalid. Setting it to :443.")
} else {
config.HttpsAddr = addr.String()
}
// Ensure that the CertificateExpiryRefreshThreshold parameter has a minimum value of one hour.
if config.CertificateExpiryRefreshThreshold < time.Hour {
config.CertificateExpiryRefreshThreshold = time.Hour
log.Println("Warning: certificate-expiry-refresh-threshold is too low. Setting it to one hour.")
}
// Verify that the LogFile parameter is a valid file path to an existing file.
// If it is not valid, set it to an empty string to disable file logging.
config.LogFile = filepath.Clean(config.LogFile)
if fileInfo, _ := os.Stat(config.LogFile); fileInfo != nil && fileInfo.Mode().IsDir() {
config.LogFile = ""
}
// Verify that the WebRootDirectory parameter is a valid path to an existing directory.
// Create the directory if it does not exist.
// If it is not valid, set it to "www_static".
config.WebRootDirectory = filepath.Clean(config.WebRootDirectory)
if fileInfo, _ := os.Stat(config.WebRootDirectory); fileInfo != nil && !fileInfo.Mode().IsDir() {
config.WebRootDirectory = "www_static"
}
if _, err := os.Stat(config.WebRootDirectory); os.IsNotExist(err) {
if err := os.MkdirAll(config.WebRootDirectory, 0555); err != nil {
log.Fatal(err)
}
}
// Verify that the CertificateCacheDirectory parameter is a valid path to an existing directory.
// Create the directory if it does not exist.
// If it is not valid, set it to "certcache".
config.CertificateCacheDirectory = filepath.Clean(config.CertificateCacheDirectory)
if fileInfo, _ := os.Stat(config.CertificateCacheDirectory); fileInfo != nil && !fileInfo.Mode().IsDir() {
// The server has to be able to write certificates into this directory.
// It should not be inside the jail or it will be set to read only.
config.CertificateCacheDirectory = "certcache"
}
if _, err := os.Stat(config.CertificateCacheDirectory); os.IsNotExist(err) {
if err := os.MkdirAll(config.CertificateCacheDirectory, 0700); err != nil {
log.Fatal(err)
}
}
// Fill the directory white list for which to create Let's Encrypt certificates
config.letsEncryptDomains = getAllowedDomainsFromSubdirectories(config.WebRootDirectory, config.SelfSignedDomains)
if len(config.letsEncryptDomains) == 0 && len(config.SelfSignedDomains) == 0 {
log.Fatal("Error: No domain directories specified in web root")
}
// Set all allowed domains
config.allDomains = make(map[string]bool, len(config.letsEncryptDomains)+len(config.SelfSignedDomains))
for _, h := range config.letsEncryptDomains {
if h, err := idna.Lookup.ToASCII(h); err == nil {
config.allDomains[h] = true
} else {
log.Fatalf("Error: Domain '%s' has invalid characters", h)
}
}
for _, h := range config.SelfSignedDomains {
if h, err := idna.Lookup.ToASCII(h); err == nil {
config.allDomains[h] = true
} else {
log.Fatalf("Error: Domain '%s' has invalid characters", h)
}
}
}
// getAllowedDomainsFromSubdirectories retrieves allowed domains from subdirectories in the webroot directory.
func getAllowedDomainsFromSubdirectories(webrootDir string, selfSignedDomains []string) []string {
var domains []string
files, err := os.ReadDir(webrootDir)
if err != nil {
log.Println("Error reading directory:", err)
return domains
}
for _, file := range files {
resolvedFile, err := os.Stat(filepath.FromSlash(webrootDir + "/" + file.Name()))
if err != nil {
log.Println("Error reading directory:", err)
return domains
}
if resolvedFile.IsDir() {
domain := file.Name()
for _, selfSignedDomain := range selfSignedDomains {
if domain == selfSignedDomain {
continue
}
}
domains = append(domains, domain)
}
}
return domains
}