diff --git a/README.md b/README.md index 79c4bd13b..9b4bf5b3a 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ Under the `maps` section, map layers are associated with data provider layers an [webserver] port = ":9090" # port to bind the web server to. defaults ":8080" + [webserver.headers] + Access-Control-Allow-Origin = "*" + Cache-Control = "no-cache, no-store, must-revalidate" + [cache] # configure a tile cache type = "file" # a file cache will cache to the local file system basepath = "/tmp/tegola" # where to write the file cache diff --git a/cmd/tegola/cmd/server.go b/cmd/tegola/cmd/server.go index 7b7ed5195..115a56855 100644 --- a/cmd/tegola/cmd/server.go +++ b/cmd/tegola/cmd/server.go @@ -37,10 +37,8 @@ var serverCmd = &cobra.Command{ server.Version = Version server.HostName = string(conf.Webserver.HostName) - // set the CORSAllowedOrigin if a value is provided - if conf.Webserver.CORSAllowedOrigin != "" { - server.CORSAllowedOrigin = string(conf.Webserver.CORSAllowedOrigin) - } + // set the http reply headers + server.Headers = conf.Webserver.Headers // set tile buffer if conf.TileBuffer != nil { diff --git a/cmd/tegola_lambda/main.go b/cmd/tegola_lambda/main.go index 2adcdded0..da7a35f3f 100644 --- a/cmd/tegola_lambda/main.go +++ b/cmd/tegola_lambda/main.go @@ -7,7 +7,6 @@ import ( "os" "github.com/arolek/algnhsa" - "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/cmd/internal/register" "github.com/go-spatial/tegola/config" @@ -81,10 +80,8 @@ func main() { server.HostName = string(conf.Webserver.HostName) } - // set the CORSAllowedOrigin if a value is provided - if conf.Webserver.CORSAllowedOrigin != "" { - server.CORSAllowedOrigin = string(conf.Webserver.CORSAllowedOrigin) - } + // set the http reply headers + server.Headers = conf.Webserver.Headers // set tile buffer if conf.TileBuffer != nil { diff --git a/config/config.go b/config/config.go index 93c4f98b8..ea489f8f4 100644 --- a/config/config.go +++ b/config/config.go @@ -12,12 +12,13 @@ import ( "time" "github.com/BurntSushi/toml" - "github.com/go-spatial/tegola" "github.com/go-spatial/tegola/internal/env" "github.com/go-spatial/tegola/internal/log" ) +var blacklistHeaders = []string{"content-encoding", "content-length", "content-type"} + // Config represents a tegola config file. type Config struct { // the tile buffer to use @@ -34,9 +35,9 @@ type Config struct { } type Webserver struct { - HostName env.String `toml:"hostname"` - Port env.String `toml:"port"` - CORSAllowedOrigin env.String `toml:"cors_allowed_origin"` + HostName env.String `toml:"hostname"` + Port env.String `toml:"port"` + Headers env.Dict `toml:"headers"` } // A Map represents a map in the Tegola Config file. @@ -126,6 +127,15 @@ func (c *Config) Validate() error { } } + // check for blacklisted headers + for k := range c.Webserver.Headers { + for _, v := range blacklistHeaders { + if v == strings.ToLower(k) { + return ErrInvalidHeader{Header: k} + } + } + } + return nil } diff --git a/config/config_test.go b/config/config_test.go index b89393220..7c701e9ba 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -99,6 +99,10 @@ func TestParse(t *testing.T) { port = ":8080" cors_allowed_origin = "tegola.io" + [webserver.headers] + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + [cache] type = "file" basepath = "/tmp/tegola-cache" @@ -133,9 +137,12 @@ func TestParse(t *testing.T) { TileBuffer: env.IntPtr(env.Int(12)), LocationName: "", Webserver: config.Webserver{ - HostName: "cdn.tegola.io", - Port: ":8080", - CORSAllowedOrigin: "tegola.io", + HostName: "cdn.tegola.io", + Port: ":8080", + Headers: env.Dict{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + }, }, Cache: env.Dict{ "type": "file", @@ -690,6 +697,20 @@ func TestValidate(t *testing.T) { ProviderLayer2: "provider2.water_default_z", }, }, + "6 blocked headers": { + config: config.Config{ + LocationName: "", + Webserver: config.Webserver{ + Port: ":8080", + Headers: env.Dict{ + "Content-Encoding": "plain/text", + }, + }, + }, + expectedErr: config.ErrInvalidHeader{ + Header: "Content-Encoding", + }, + }, } for name, tc := range tests { diff --git a/config/errors.go b/config/errors.go index dde1016ed..1dd613df5 100644 --- a/config/errors.go +++ b/config/errors.go @@ -52,3 +52,11 @@ type ErrMissingEnvVar struct { func (e ErrMissingEnvVar) Error() string { return fmt.Sprintf("config: config file is referencing an environment variable that is not set (%v)", e.EnvVar) } + +type ErrInvalidHeader struct { + Header string +} + +func (e ErrInvalidHeader) Error() string { + return fmt.Sprintf("config: header (%v) blacklisted", e.Header) +} diff --git a/server/middleware_cors.go b/server/middleware_cors.go deleted file mode 100644 index 735e50e0c..000000000 --- a/server/middleware_cors.go +++ /dev/null @@ -1,20 +0,0 @@ -package server - -import "net/http" - -func CORSHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - w.Header().Set("Access-Control-Allow-Origin", CORSAllowedOrigin) - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") - - // stop here if the request is an OPTIONS preflight - if r.Method == "OPTIONS" { - return - } - - next.ServeHTTP(w, r) - - return - }) -} diff --git a/server/middleware_headers.go b/server/middleware_headers.go new file mode 100644 index 000000000..28b0d599c --- /dev/null +++ b/server/middleware_headers.go @@ -0,0 +1,23 @@ +package server + +import "net/http" + +func HeadersHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // default CORS headers. may be overwritten by the user + w.Header().Set("Access-Control-Allow-Origin", CORSAllowedOrigin) + w.Header().Set("Access-Control-Allow-Methods", CORSAllowedMethods) + + for name, value := range Headers { + v, ok := value.(string) + if ok { + w.Header().Set(name, v) + } + } + + next.ServeHTTP(w, r) + + return + }) +} diff --git a/server/server.go b/server/server.go index 2e2bfaf2e..1895ebfc3 100644 --- a/server/server.go +++ b/server/server.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/dimfeld/httptreemux" - "github.com/go-spatial/tegola" "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/internal/log" @@ -36,6 +35,14 @@ var ( // configurable via the tegola config.toml file (set in main.go) CORSAllowedOrigin string = "*" + // CORSAllowedMethods is the "Access-Control-Allow-Methods" CORS header. + // configurable via the tegola config.toml file (set in main.go) + CORSAllowedMethods string = "GET, OPTIONS" + + // Headers is the map of http reply headers. + // configurable via the tegola config.toml file (set in main.go) + Headers map[string]interface{} + // TileBuffer is the tile buffer to use. // configurable via tegola config.tomal file (set in main.go) TileBuffer float64 = tegola.DefaultTileBuffer @@ -50,16 +57,16 @@ func NewRouter(a *atlas.Atlas) *httptreemux.TreeMux { r.OptionsHandler = corsHandler // capabilities endpoints - group.UsingContext().Handler("GET", "/capabilities", CORSHandler(HandleCapabilities{})) - group.UsingContext().Handler("GET", "/capabilities/:map_name", CORSHandler(HandleMapCapabilities{})) + group.UsingContext().Handler("GET", "/capabilities", HeadersHandler(HandleCapabilities{})) + group.UsingContext().Handler("GET", "/capabilities/:map_name", HeadersHandler(HandleMapCapabilities{})) // map tiles hMapLayerZXY := HandleMapLayerZXY{Atlas: a} - group.UsingContext().Handler("GET", "/maps/:map_name/:z/:x/:y", CORSHandler(GZipHandler(TileCacheHandler(a, hMapLayerZXY)))) - group.UsingContext().Handler("GET", "/maps/:map_name/:layer_name/:z/:x/:y", CORSHandler(GZipHandler(TileCacheHandler(a, hMapLayerZXY)))) + group.UsingContext().Handler("GET", "/maps/:map_name/:z/:x/:y", HeadersHandler(GZipHandler(TileCacheHandler(a, hMapLayerZXY)))) + group.UsingContext().Handler("GET", "/maps/:map_name/:layer_name/:z/:x/:y", HeadersHandler(GZipHandler(TileCacheHandler(a, hMapLayerZXY)))) // map style - group.UsingContext().Handler("GET", "/maps/:map_name/style.json", CORSHandler(HandleMapStyle{})) + group.UsingContext().Handler("GET", "/maps/:map_name/style.json", HeadersHandler(HandleMapStyle{})) // setup viewer routes, which can be excluded via build flags setupViewer(group) @@ -148,6 +155,6 @@ var URLRoot = func(r *http.Request) string { // corsHanlder is used to respond to all OPTIONS requests for registered routes func corsHandler(w http.ResponseWriter, r *http.Request, params map[string]string) { w.Header().Set("Access-Control-Allow-Origin", CORSAllowedOrigin) - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", CORSAllowedMethods) return }