Skip to content

Commit

Permalink
Merge pull request #174 from Michad/fix-misc
Browse files Browse the repository at this point in the history
Fix CORS handling when using auth,  support {ctx.query-string} and EPSG:3857 in proxy provider
  • Loading branch information
Michad authored Aug 17, 2024
2 parents 176ea86 + ec8be3d commit 8aba962
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 38 deletions.
18 changes: 11 additions & 7 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Configuration options:
| --- | --- | --- | --- | --- |
| url | string | Yes | None | A URL pointing to the tile server. Should contain placeholders surrounded by "{}" that are replaced on-the-fly |
| inverty | bool | No | false | Changes Y tile numbering to be South-to-North instead of North-to-South. Only impacts Y/y placeholder |
| srid | uint | No | 4326 | What projection bounds should be in. Can only be 4326 or 3857 |


The following placeholders are available in the URL:
Expand All @@ -86,10 +87,10 @@ The following placeholders are available in the URL:
| x or X | The X tile coordinate from the incoming request |
| y or Y | The Y tile coordinate either from the incoming request or the "flipped" equivalent if the `invertY` parameter is specified. |
| z or Z | The Z tile coordinate from the incoming request (aka "zoom") |
| xmin | The "west" coordinate of the bounding box defined by the incoming tile coordinates. |
| xmax | The "east" coordinate of the bounding box defined by the incoming tile coordinates. |
| ymin | The "north" coordinate of the bounding box defined by the incoming tile coordinates. Not impacted by the `invertY` parameter. |
| ymax | The "south" coordinate of the bounding box defined by the incoming tile coordinates. Not impacted by the `invertY` parameter. |
| xmin | The "west" coordinate of the bounding box defined by the incoming tile coordinates. In the projection specified by `srid`. |
| xmax | The "east" coordinate of the bounding box defined by the incoming tile coordinates. In the projection specified by `srid`. |
| ymin | The "north" coordinate of the bounding box defined by the incoming tile coordinates. In the projection specified by `srid`. Not impacted by the `invertY` parameter. |
| ymax | The "south" coordinate of the bounding box defined by the incoming tile coordinates. In the projection specified by `srid`. Not impacted by the `invertY` parameter. |
| env.XXX | An environment variable whose name is XXX |
| ctx.XXX | A context variable (typically an HTTP header) whose name is XXX |
| layer.XXX | If the layer includes a pattern with a placeholder of XXX, this is the replacement value from the used layer name |
Expand All @@ -106,15 +107,18 @@ provider:

The URL Template provider overlaps with the Proxy provider but is meant specifically for WMS endpoints. Instead of merely supplying tile coordinates, the URL Template provider will supply the bounding box. This provider is available mostly for compatibility, you generally should use Proxy instead.

Currently only supports EPSG:4326
Currently only supports EPSG:4326 and EPSG:3857

Name should be "url template"

Configuration options:

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| template | string | Yes | None | A URL pointing to the tile server. Should contain placeholders `$xmin` `$xmax` `$ymin` and `$ymax` for tile coordinates |
| template | string | Yes | None | A URL pointing to the tile server. Should contain placeholders `$xmin` `$xmax` `$ymin` and `$ymax` for tile bounds and can also contains `$srs` `$width` and `$height` |
| width | uint | No | 256 | What to use for $width placeholder |
| height | uint | No | 256 | What to use for $height placeholder |
| srid | uint | No | 4326 | What projection the bounds should be in and what to use for $srs placeholder. Can only be 4326 or 3857 |

### Effect

Expand Down Expand Up @@ -579,7 +583,7 @@ Configuration options:
| --- | --- | --- | --- | --- |
| Key | string | Yes | None | The key for verifying the signature. The public key if using asymmetric signing. If the value starts with "env." the remainder is interpreted as the name of the Environment Variable to use to retrieve the verification key. |
| Algorithm | string | Yes | None | Algorithm to allow for JWT signature. One of: "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "EdDSA" |
| HeaderName | string | No | Authorization | The header to extract the JWT from. If this is "Authorization" it removes "Bearer " from the start |
| HeaderName | string | No | Authorization | The header to extract the JWT from. If this is "Authorization" it removes "Bearer " from the start. Make sure this is in "canonical case" e.g. X-Header - auth will always fail otherwise |
| MaxExpiration | uint32 | No | 1 day | How many seconds from now can the expiration be. JWTs more than X seconds from now will result in a 401 |
| ExpectedAudience | string | No | None | Require the "aud" grant to be this string |
| ExpectedSubject | string | No | None | Require the "sub" grant to be this string |
Expand Down
2 changes: 1 addition & 1 deletion examples/providers/custom_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,5 @@ func generateTile(
//The third parameter is a map containing custom HTTP headers to include, which should be used for Authentication
//You can also perform HTTP calls via standard go HTTP library for cases where a GET doesn't suffice. It's recommended
//to use GetTile where possible for consistency and ensure the configured rules are followed
return tilegroxy.GetTile(ctx, &clientConfig, url, make(map[string]string))
return tilegroxy.GetTile(ctx, clientConfig, url, make(map[string]string))
}
2 changes: 1 addition & 1 deletion internal/providers/cgi.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (t CGI) GenerateTile(ctx context.Context, _ layer.ProviderContext, tileRequ
uri = "/" + uri
}

uri, err = replaceURLPlaceholders(ctx, tileRequest, uri, false)
uri, err = replaceURLPlaceholders(ctx, tileRequest, uri, false, pkg.SRIDWGS84)
if err != nil {
return nil, err
}
Expand Down
10 changes: 9 additions & 1 deletion internal/providers/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
type ProxyConfig struct {
URL string
InvertY bool // Used for TMS
Srid uint
}

type Proxy struct {
Expand Down Expand Up @@ -54,6 +55,13 @@ func (s ProxyRegistration) Initialize(cfgAny any, clientConfig config.ClientConf
return nil, fmt.Errorf(errorMessages.InvalidParam, "provider.proxy.url", "")
}

if cfg.Srid == 0 {
cfg.Srid = pkg.SRIDWGS84
}
if cfg.Srid != pkg.SRIDWGS84 && cfg.Srid != pkg.SRIDPsuedoMercator {
return nil, fmt.Errorf(errorMessages.EnumError, "provider.url template.srid", cfg.Srid, []int{pkg.SRIDPsuedoMercator, pkg.SRIDWGS84})
}

return &Proxy{cfg, clientConfig}, nil
}

Expand All @@ -62,7 +70,7 @@ func (t Proxy) PreAuth(_ context.Context, _ layer.ProviderContext) (layer.Provid
}

func (t Proxy) GenerateTile(ctx context.Context, _ layer.ProviderContext, tileRequest pkg.TileRequest) (*pkg.Image, error) {
url, err := replaceURLPlaceholders(ctx, tileRequest, t.URL, t.InvertY)
url, err := replaceURLPlaceholders(ctx, tileRequest, t.URL, t.InvertY, t.Srid)
if err != nil {
return nil, err
}
Expand Down
26 changes: 22 additions & 4 deletions internal/providers/url_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import (

type URLTemplateConfig struct {
Template string
Width uint16
Height uint16
Srid uint
}

type URLTemplate struct {
Expand Down Expand Up @@ -59,11 +62,26 @@ func (s URLTemplateRegistration) Initialize(cfgAny any, clientConfig config.Clie
return nil, fmt.Errorf(errorMessages.InvalidParam, "provider.url template.url", "")
}

if cfg.Height == 0 {
cfg.Height = 256
}

if cfg.Width == 0 {
cfg.Width = 256
}

if cfg.Srid == 0 {
cfg.Srid = pkg.SRIDWGS84
}
if cfg.Srid != pkg.SRIDWGS84 && cfg.Srid != pkg.SRIDPsuedoMercator {
return nil, fmt.Errorf(errorMessages.EnumError, "provider.url template.srid", cfg.Srid, []int{pkg.SRIDPsuedoMercator, pkg.SRIDWGS84})
}

return &URLTemplate{cfg, clientConfig}, nil
}

func (t URLTemplate) GenerateTile(ctx context.Context, _ layer.ProviderContext, tileRequest pkg.TileRequest) (*pkg.Image, error) {
b, err := tileRequest.GetBounds()
b, err := tileRequest.GetBoundsProjection(t.Srid)

if err != nil {
return nil, err
Expand All @@ -75,9 +93,9 @@ func (t URLTemplate) GenerateTile(ctx context.Context, _ layer.ProviderContext,
url = strings.ReplaceAll(url, "$ymin", fmt.Sprintf("%f", b.South))
url = strings.ReplaceAll(url, "$ymax", fmt.Sprintf("%f", b.North))
url = strings.ReplaceAll(url, "$zoom", strconv.Itoa(tileRequest.Z))
url = strings.ReplaceAll(url, "$width", "256") //TODO: allow these being dynamic
url = strings.ReplaceAll(url, "$height", "256")
url = strings.ReplaceAll(url, "$srs", "4326") //TODO: decide if I want this to be dynamic
url = strings.ReplaceAll(url, "$width", strconv.Itoa(int(t.Width)))
url = strings.ReplaceAll(url, "$height", strconv.Itoa(int(t.Height)))
url = strings.ReplaceAll(url, "$srs", strconv.Itoa(int(t.Srid)))

return getTile(ctx, t.clientConfig, url, make(map[string]string))
}
4 changes: 2 additions & 2 deletions internal/providers/utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ var envRegex = regexp.MustCompile(`{env\.[^{}}]*}`)
var ctxRegex = regexp.MustCompile(`{ctx\.[^{}}]*}`)
var lyrRegex = regexp.MustCompile(`{layer\.[^{}}]*}`)

func replaceURLPlaceholders(ctx context.Context, tileRequest pkg.TileRequest, url string, invertY bool) (string, error) {
b, err := tileRequest.GetBounds()
func replaceURLPlaceholders(ctx context.Context, tileRequest pkg.TileRequest, url string, invertY bool, srid uint) (string, error) {
b, err := tileRequest.GetBoundsProjection(srid)

if err != nil {
return "", err
Expand Down
25 changes: 17 additions & 8 deletions internal/server/tile_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ func (h *tileHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
slog.DebugContext(ctx, "server: tile handler started")
defer slog.DebugContext(ctx, "server: tile handler ended")

h.writeHeaders(w)

if req.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}

if !h.auth.CheckAuthentication(ctx, req) {
writeError(ctx, w, &h.config.Error, pkg.UnauthorizedError{Message: "CheckAuthentication returned false"})
return
Expand Down Expand Up @@ -134,14 +141,6 @@ func (h *tileHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}

for h, v := range h.config.Server.Headers {
w.Header().Add(h, v)
}

if !h.config.Server.Production {
w.Header().Add("X-Powered-By", "tilegroxy "+version)
}

w.WriteHeader(http.StatusOK)

_, err = w.Write(*img)
Expand All @@ -158,6 +157,16 @@ func (h *tileHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.tileSuccessCounter.Add(ctx, 1)
}

func (h *tileHandler) writeHeaders(w http.ResponseWriter) {
for h, v := range h.config.Server.Headers {
w.Header().Add(h, v)
}

if !h.config.Server.Production {
w.Header().Add("X-Powered-By", "tilegroxy "+version)
}
}

func (h *tileHandler) extractAndValidateRequest(ctx context.Context, req *http.Request, span trace.Span, w http.ResponseWriter) (pkg.TileRequest, bool) {
layerName := req.PathValue("layer")
zStr := req.PathValue("z")
Expand Down
24 changes: 17 additions & 7 deletions pkg/entities/layer/layergroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/Michad/tilegroxy/pkg/entities/secret"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)

type LayerGroup struct {
Expand Down Expand Up @@ -118,18 +119,27 @@ func (lg LayerGroup) RenderTile(ctx context.Context, tileRequest pkg.TileRequest
ctxSkipCacheSave, _ := pkg.SkipCacheSaveFromContext(ctx)

if !*ctxSkipCacheSave {
go func() {
err = l.Cache.Save(ctx, tileRequest, img)

if err != nil {
slog.WarnContext(ctx, fmt.Sprintf("Cache save error %v\n", err))
}
}()
go writeCache(ctx, l.Cache, tileRequest, img)
}

return img, nil
}

func writeCache(ctx context.Context, cache cache.Cache, tileRequest pkg.TileRequest, img *pkg.Image) {
// We need to make a new context to avoid the request finishing cancelling the ctx sent into the cache
newCtx := pkg.BackgroundContext()

// Copy span over from original context
span := trace.SpanFromContext(ctx)
newCtx = trace.ContextWithSpan(newCtx, span)

err := cache.Save(newCtx, tileRequest, img)

if err != nil {
slog.WarnContext(newCtx, fmt.Sprintf("Cache save error %v\n", err))
}
}

func (LayerGroup) checkPermission(ctx context.Context, l *Layer, tileRequest pkg.TileRequest) error {
ctxLimitLayers, _ := pkg.LimitLayersFromContext(ctx)
ctxAllowedLayers, _ := pkg.AllowedLayersFromContext(ctx)
Expand Down
19 changes: 19 additions & 0 deletions pkg/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ func (e RemoteServerError) External(messages config.ErrorMessages) string {
return messages.ProviderError
}

type InvalidSridError struct {
srid uint
}

func (e InvalidSridError) Error() string {
// notest
return fmt.Sprintf("Supported projections only includes 4326 and 3857, not %v", e.srid)
}

func (e InvalidSridError) Type() TypeOfError {
// notest
return TypeOfErrorOther
}

func (e InvalidSridError) External(messages config.ErrorMessages) string {
// notest
return fmt.Sprintf(messages.EnumError, "provider.url template.srid", e.srid, []int{SRIDPsuedoMercator, SRIDWGS84})
}

// Indicates an input from the user is outside the valid range allowed for a numeric parameter - primarily tile coordinates
type RangeError struct {
ParamName string
Expand Down
1 change: 1 addition & 0 deletions pkg/request_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func NewRequestContext(req *http.Request) context.Context {
ctx = context.WithValue(ctx, "uri", req.RequestURI)
ctx = context.WithValue(ctx, "path", req.URL.Path)
ctx = context.WithValue(ctx, "query", req.URL.Query())
ctx = context.WithValue(ctx, "query-string", req.URL.RawQuery)
ctx = context.WithValue(ctx, "proto", req.Proto)
ctx = context.WithValue(ctx, "ip", strings.Split(req.RemoteAddr, ":")[0])
ctx = context.WithValue(ctx, "method", req.Method)
Expand Down
44 changes: 37 additions & 7 deletions pkg/tile_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
"github.com/mmcloughlin/geohash"
)

const SRIDWGS84 = 4326
const SRIDPsuedoMercator = 3857

type TileRequest struct {
LayerName string
Z int
Expand All @@ -35,16 +38,31 @@ type Bounds struct {
}

const (
MaxZoom = 21
delta = .00000001
maxLat = 85.0511
minLat = -85.0511
MaxZoom = 21
delta = .00000001
maxLat = 85.0511
minLat = -85.0511
max3857CoordInMeters = 20037508.34
)

func convertLat4326To3857(lat float64) float64 {
return math.Log(math.Tan((90+lat)*math.Pi/360)) / (math.Pi / 180) * (max3857CoordInMeters / 180)
}
func convertLon4326To3857(lon float64) float64 {
return lon * max3857CoordInMeters / 180
}

func (t TileRequest) GetBounds() (*Bounds, error) {
return t.GetBoundsProjection(SRIDWGS84)
}

func (t TileRequest) GetBoundsProjection(srid uint) (*Bounds, error) {
if t.Z < 0 || t.Z > MaxZoom {
return nil, RangeError{ParamName: "Z", MinValue: 0, MaxValue: MaxZoom}
}
if srid != SRIDWGS84 && srid != SRIDPsuedoMercator {
return nil, InvalidSridError{srid}
}

z := float64(t.Z)
x := float64(t.X)
Expand All @@ -70,16 +88,28 @@ func (t TileRequest) GetBounds() (*Bounds, error) {
west := math.Min(x2, x1)
east := math.Max(x2, x1)

return &Bounds{south, north, west, east}, nil
if srid == SRIDWGS84 {
return &Bounds{south, north, west, east}, nil
}

return &Bounds{
convertLat4326To3857(south),
convertLat4326To3857(north),
convertLon4326To3857(west),
convertLon4326To3857(east)}, nil
}

func (t TileRequest) IntersectsBounds(b Bounds) (bool, error) {
return t.IntersectsBoundsProjection(b, SRIDWGS84)
}

func (t TileRequest) IntersectsBoundsProjection(b Bounds, srid uint) (bool, error) {
// Treat null-island only bounds as everything
if b.North == 0 && b.East == 0 && b.South == 0 && b.West == 0 {
if b.IsNullIsland() {
return true, nil
}

b2, err := t.GetBounds()
b2, err := t.GetBoundsProjection(srid)
if err != nil {
return false, err
}
Expand Down

0 comments on commit 8aba962

Please sign in to comment.