-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathconfig.go
505 lines (424 loc) · 14.4 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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
package config
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/go-redis/redis/v8"
"github.com/gofiber/fiber/v2"
"github.com/dechristopher/lod/env"
"github.com/dechristopher/lod/str"
"github.com/dechristopher/lod/util"
)
var (
// Version of LOD
Version = ".dev"
Namespace = "lod"
// capabilities is a store for local instance Capabilities
capabilities Capabilities
// File is a reference to the config file path to read from
File *string
// DefaultPort used if none specified in config
DefaultPort = 3100
// default number of cache workers
defaultNumWorkers = 8
)
// Capabilities of the LOD instance (the configuration)
type Capabilities struct {
Version string `json:"version"` // version string shown when viewing capabilities endpoint
Instance Instance `json:"instance" toml:"instance"` // instance configuration
Proxies []Proxy `json:"proxies" toml:"proxies"` // configured proxy instances
}
// Instance configuration for LOD
type Instance struct {
Port int `json:"port" toml:"port"` // configured LOD port
Environment string `json:"environment"` // configured LOD environment
AdminDisabled bool `json:"admin_disabled" toml:"admin_disabled"` // whether the admin endpoints are disabled
AdminToken string `json:"-" toml:"admin_token"` // admin endpoint auth bearer token
MetricsEnabled bool `json:"metrics_enabled" toml:"metrics_enabled"` // whether metrics are enabled
}
// Proxy represents a configuration for a single endpoint proxy instance
type Proxy struct {
Name string `json:"name" toml:"name"` // display name for this proxy
TileURL string `json:"tile_url" toml:"tile_url"` // templated tileserver URL that this instance will hit
HasEndpointParam bool `json:"has_endpoint_param"` // internal variable to track whether this proxy has a dynamic endpoint configured
CorsOrigins string `json:"cors_origins" toml:"cors_origins"` // allowed CORS origins, comma separated
PullHeaders []string `json:"pull_headers" toml:"pull_headers"` // additional headers to pull and cache from the tileserver
DeleteHeaders []string `json:"del_headers" toml:"del_headers"` // headers to exclude from the tileserver response
AddHeaders []Header `json:"add_headers" toml:"add_headers"` // headers to inject into upstream requests to tileserver
AccessToken string `json:"-" toml:"access_token"` // optional access token for incoming requests
NumWorkers int `json:"num_workers" toml:"num_workers"` // optionally limit number of cache workers for priming and invalidation jobs
Params []Param `json:"params" toml:"params"` // URL query parameter configurations for this instance
Cache Cache `json:"cache" toml:"cache"` // cache configuration for this proxy instance
}
// Header to inject in upstream request to tileserver
type Header struct {
Name string `json:"name" toml:"name"` // header name
Value string `json:"value" toml:"value"` // header value
}
// Param configuration for a proxy instance
type Param struct {
Name string `json:"name" toml:"name"` // parameter name - exact match in URL and used as token value for cache key
Default string `json:"default" toml:"default"` // default parameter value if none provided in URL
}
// Cache configuration for a Proxy instance
// Cache TTLs are set using Go's built-in time.ParseDuration
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
// For example: 1h, 300s, 1000ms, 2h35m, etc.
type Cache struct {
MemEnabled bool `json:"mem_enabled" toml:"mem_enabled"` // whether the in-memory cache is enabled
MemCap int `json:"mem_cap" toml:"mem_cap"` // maximum capacity in MB of the in-memory cache
MemTTL string `json:"mem_ttl" toml:"mem_ttl"` // in-memory cache TTL, ex: 1h, 30s, 1000ms, etc
MemTTLDuration time.Duration `json:"-" toml:"-"` // parsed duration from MemTTL
RedisEnabled bool `json:"redis_enabled" toml:"redis_enabled"` // whether the redis cache is enabled
// Note: our redis cache does not have a max cap on tiles. It will grow unbounded, so
// you must use a TTL to avoid capping out your cluster if you have a large tile set.
RedisTTL string `json:"redis_ttl" toml:"redis_ttl"` // redis tile cache TTL, ex: 1h, 30s, 1000ms, etc
RedisTTLDuration time.Duration `json:"-" toml:"-"` // parsed duration from RedisTTL
// Example: redis://<user>:<password>@<host>:<port>/<db_number>
RedisURL string `json:"-" toml:"redis_url"` // full redis connection URL for parsing, SENSITIVE
RedisTLS bool `json:"redis_tls" toml:"redis_tls"` // whether to use TLS when connecting to the redis server
RedisOpts *redis.Options `json:"-" toml:"-"` // internal redis options, first parsed with config
KeyTemplate string `json:"key_template" toml:"key_template"` // cache key template, supports XYZ and URL parameters
}
var defaultCache = Cache{
MemCap: 1000,
MemTTL: "24h",
KeyTemplate: "{z}/{x}/{y}",
}
var zeroCache = Cache{
MemCap: 0,
MemTTL: "",
RedisTTL: "",
RedisURL: "",
KeyTemplate: "",
}
// Get returns a pointer to the global configuration
func Get() *Capabilities {
return &capabilities
}
// Load config file into instance Capabilities
func Load() error {
var newCapabilities Capabilities
var configData []byte
var err error
if util.IsUrl(*File) {
// read config file from URL if provided as a URL
configData, err = readHttp()
} else {
// read config file from disk if provided as a local path
configData, err = os.ReadFile(*File)
}
if err != nil {
return err
}
// expand environment variables present within raw config
configData = []byte(os.ExpandEnv(string(configData)))
// decode config file as TOML
if _, err = toml.Decode(string(configData), &newCapabilities); err != nil {
return err
}
// inject instance info to config for viewing in /capabilities
newCapabilities.Instance.Environment = string(env.GetEnv())
newCapabilities.Version = Version
// validate configuration
err = validateCapabilities(&newCapabilities)
if err != nil {
return err
}
// set default cache parameters if not provided
setDefaults(&newCapabilities)
// set capabilities after validation
capabilities = newCapabilities
return nil
}
// readHttp reads the config from the
func readHttp() ([]byte, error) {
// fetch config from URL if valid
resp, err := http.Get(*File)
if err != nil {
return nil, ErrConfigGetHTTP{
URL: *File,
Err: err,
}
}
// only accept 200 status codes, reject all others
if resp.StatusCode != 200 {
return nil, ErrConfigGetHTTP{
URL: *File,
Status: resp.StatusCode,
}
}
// read all bytes from response body into configData
configData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, ErrConfigGetHTTP{
URL: *File,
Err: err,
}
}
return configData, nil
}
// set default instance and cache properties for read configuration if not provided
func setDefaults(cap *Capabilities) {
if cap.Instance.Port == 0 {
cap.Instance.Port = DefaultPort
}
for i := range cap.Proxies {
if cap.Proxies[i].Cache == zeroCache {
cap.Proxies[i].Cache = defaultCache
}
if cap.Proxies[i].Cache.KeyTemplate == "" {
cap.Proxies[i].Cache.KeyTemplate = defaultCache.KeyTemplate
}
if cap.Proxies[i].PullHeaders == nil {
cap.Proxies[i].PullHeaders = make([]string, 0)
}
if cap.Proxies[i].NumWorkers <= 0 {
cap.Proxies[i].NumWorkers = defaultNumWorkers
}
// Register default content headers
cap.Proxies[i].registerHeader(fiber.HeaderContentType)
cap.Proxies[i].registerHeader(fiber.HeaderContentEncoding)
}
}
// validateCapabilities validates instance Capabilities for sanity and errors
func validateCapabilities(c *Capabilities) error {
// allow 0 so that DefaultPort can be used if no port specified
if c.Instance.Port < 0 || c.Instance.Port > 65535 {
return ErrInvalidPort{Port: c.Instance.Port}
}
// validate each provided proxy endpoint configuration
for num := range c.Proxies {
if err := validateProxy(num, &c.Proxies[num]); err != nil {
return err
}
}
return nil
}
// registerHeader will add a header to the list of headers to pull through from
// the underlying configured tileserver
func (p *Proxy) registerHeader(header string) {
found := false
for _, key := range p.PullHeaders {
if key == header {
found = true
break
}
}
if !found {
p.PullHeaders = append(p.PullHeaders, header)
}
}
// DoPullHeaders will fill the given header map with configured headers
// extracted from proxied requests to store alongside tile data in TilePackets
func (p *Proxy) DoPullHeaders(resp *fiber.Response, headers map[string]string) {
for _, header := range p.PullHeaders {
headerValue := resp.Header.Peek(header)
if len(headerValue) > 0 {
headers[header] = string(headerValue)
}
}
}
// DoDeleteHeaders will strip headers from the response that are part of the
// DeleteHeaders list of headers to delete from the final response
func (p *Proxy) DoDeleteHeaders(c *fiber.Ctx) {
for _, delHeader := range p.DeleteHeaders {
c.Response().Header.Del(delHeader)
}
}
// validateProxy will validate an individual proxy endpoint in the configuration
func validateProxy(num int, proxy *Proxy) error {
if proxy.Name == "" {
return ErrProxyNoName{Number: num + 1}
}
matched, err := regexp.Match("^[a-zA-Z0-9_-]+$", []byte(proxy.Name))
if err != nil {
panic(err)
}
if !matched {
return ErrProxyInvalidName{
Number: num + 1,
ProxyName: proxy.Name,
}
}
if proxy.TileURL == "" {
return ErrMissingTileURL{
ProxyName: proxy.Name,
}
}
// reflect presence of dynamic endpoint template in HasEndpointParam
proxy.HasEndpointParam = strings.Contains(proxy.TileURL, str.EndpointTemplate)
if !strings.Contains(proxy.TileURL, "{z}") {
return ErrMissingTileURLTemplate{
ProxyName: proxy.Name,
TileURL: proxy.TileURL,
Parameter: "{z}",
}
}
if !strings.Contains(proxy.TileURL, "{x}") {
return ErrMissingTileURLTemplate{
ProxyName: proxy.Name,
TileURL: proxy.TileURL,
Parameter: "{x}",
}
}
if !strings.Contains(proxy.TileURL, "{y}") {
return ErrMissingTileURLTemplate{
ProxyName: proxy.Name,
TileURL: proxy.TileURL,
Parameter: "{y}",
}
}
// validate the proxy's cache configuration
if errCache := validateCache(proxy); errCache != nil {
return errCache
}
// validate the proxy's parameter configurations
if errParams := validateParams(proxy); errParams != nil {
return errParams
}
return nil
}
// validateCache will validate a proxy endpoint's cache configuration
func validateCache(proxy *Proxy) error {
// ensure at least one cache is enabled
if !proxy.Cache.MemEnabled && !proxy.Cache.RedisEnabled {
return ErrNoCacheEnabled{
ProxyName: proxy.Name,
}
}
// validate internal cache configuration
if err := validateInternalCache(proxy); err != nil {
return err
}
// validate external cache configuration
if err := validateExternalCache(proxy); err != nil {
return err
}
if !strings.Contains(proxy.Cache.KeyTemplate, "{z}") {
return ErrMissingCacheTemplate{
ProxyName: proxy.Name,
Template: proxy.Cache.KeyTemplate,
Parameter: "{z}",
}
}
if !strings.Contains(proxy.Cache.KeyTemplate, "{x}") {
return ErrMissingCacheTemplate{
ProxyName: proxy.Name,
Template: proxy.Cache.KeyTemplate,
Parameter: "{x}",
}
}
if !strings.Contains(proxy.Cache.KeyTemplate, "{y}") {
return ErrMissingCacheTemplate{
ProxyName: proxy.Name,
Template: proxy.Cache.KeyTemplate,
Parameter: "{y}",
}
}
return nil
}
// validateInternalCache validates internal cache configuration
func validateInternalCache(proxy *Proxy) error {
// parse and validate in-memory cache parameters if enabled
if proxy.Cache.MemEnabled {
if proxy.Cache.MemCap < 1 {
return ErrInvalidMemCap{ProxyName: proxy.Name}
}
memTTL, err := time.ParseDuration(proxy.Cache.MemTTL)
if err != nil {
return ErrInvalidMemTTL{
ProxyName: proxy.Name,
TTL: proxy.Cache.MemTTL,
}
}
// reject negative or zero TTLs
// since in-memory cache must expire
if memTTL < 0 {
return ErrInvalidMemTTL{
ProxyName: proxy.Name,
TTL: proxy.Cache.MemTTL,
}
}
proxy.Cache.MemTTLDuration = memTTL
}
return nil
}
// validateExternalCache validates external cache configuration
func validateExternalCache(proxy *Proxy) error {
// parse and validate redis cache parameters if enabled
if proxy.Cache.RedisEnabled {
// validate URL
var errParse error
proxy.Cache.RedisOpts, errParse = redis.ParseURL(proxy.Cache.RedisURL)
if errParse != nil {
return ErrInvalidRedisURL{
ProxyName: proxy.Name,
URL: proxy.Cache.RedisURL,
Err: errParse,
}
}
// validate that TTL is sane
if proxy.Cache.RedisTTL != "" {
redisTTL, err := time.ParseDuration(proxy.Cache.RedisTTL)
if err != nil {
return ErrInvalidRedisTTL{
ProxyName: proxy.Name,
TTL: proxy.Cache.RedisTTL,
}
}
// reject negative TTLs
if redisTTL < 0 {
return ErrInvalidRedisTTL{
ProxyName: proxy.Name,
TTL: proxy.Cache.RedisTTL,
}
}
proxy.Cache.RedisTTLDuration = redisTTL
} else {
// set TTL duration to zero if none specified, meaning permanent persistence in Redis
proxy.Cache.RedisTTLDuration = 0
}
}
return nil
}
// validateParams ensures configured params have valid and non-overlapping names
func validateParams(proxy *Proxy) error {
if len(proxy.Params) == 0 {
return nil
}
// begin with reserved parameter names
var usedNames = []string{"e", "z", "x", "y"}
for i, param := range proxy.Params {
if param.Name == "" {
return ErrParamNoName{
ProxyName: proxy.Name,
Number: i + 1,
}
}
for _, name := range usedNames {
if name == param.Name {
return ErrParamNameDuplicate{
ProxyName: proxy.Name,
Parameter: param,
}
}
}
usedNames = append(usedNames, param.Name)
}
return nil
}
// GetPort returns the configured primary HTTP port
// or DefaultPort if none configured
func GetPort() int {
return capabilities.Instance.Port
}
// GetListenPort returns the colon-formatted listen port
func GetListenPort() string {
return fmt.Sprintf(":%d", GetPort())
}