From ca4a94e0c5a4487c64d271e6d1428a6aefcd094c Mon Sep 17 00:00:00 2001 From: Brendan Ward Date: Mon, 11 May 2020 11:18:31 -0700 Subject: [PATCH] Refactor of handlers, middleware, command line options (#100) --- CHANGELOG.md | 114 +++++ README.md | 88 ++-- handlers/arcgis.go | 519 ++++++++++++---------- handlers/assets_vfsdata.go | 12 +- handlers/handlers.go | 502 --------------------- handlers/handlers_test.go | 9 - handlers/id.go | 24 + handlers/middleware.go | 85 ++++ handlers/request.go | 45 ++ handlers/serviceset.go | 258 +++++++++++ handlers/static.go | 14 +- handlers/templates.go | 38 ++ handlers/templates/map.html | 142 +++--- handlers/templates/map_gl.html | 139 +++--- handlers/tile.go | 71 +++ handlers/tileset.go | 336 ++++++++++++++ main.go | 211 ++++++--- mbtiles/export_test.go | 4 + mbtiles/mbtiles.go | 36 +- mbtiles/mbtiles_test.go | 239 +++++++++- mbtiles/testdata/incomple.mbtiles-journal | Bin 0 -> 491520 bytes 21 files changed, 1872 insertions(+), 1014 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 handlers/handlers.go delete mode 100644 handlers/handlers_test.go create mode 100644 handlers/id.go create mode 100644 handlers/middleware.go create mode 100644 handlers/request.go create mode 100644 handlers/serviceset.go create mode 100644 handlers/templates.go create mode 100644 handlers/tile.go create mode 100644 handlers/tileset.go create mode 100644 mbtiles/export_test.go create mode 100644 mbtiles/testdata/incomple.mbtiles-journal diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2c5ccd5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,114 @@ +# Changelog + +## O.7 (in progress) + +This version involved a significant refactor of internal functionality and HTTP +handlers to provide better ability to modify services at runtime, provide +granular control over the endpoints that are exposed, and cleanup handling +of middleware. + +Most internal HTTP handlers for `ServiceSet` and `Tileset` in the +`github.com/consbio/mbtileserver/handlers` package are now `http.HandlerFunc`s +instead of custom handlers that returned status codes or errors as in the previous +versions. + +The internal routing within these handlers has been modified to enable +tilesets to change at runtime. Previously, we were using an `http.ServeMux` +for all routes, which breaks when the `Tileset` instances pointed to by those +routes have changed at runtime. Now, the top-level `ServiceSet.Handler()` +allows dynamic routing to any `Tileset` instances currently published. Each +`Tileset` is now responsible for routing to its subpaths (e.g., tile endpoint). + +The singular public handler endpoint is still an `http.Handler` instance but +no longer takes any parameters. Those parameters are now handled using +configuration options instead. + +`ServiceSet` now enables configuration to set the root URL, toggle which endpoints +are exposed and set the internal error logger. These are passed in using a +`ServiceSetConfig` struct when the service is constructed; these configuration +options are not modifiable at runtime. + +`Tileset` instances are now created individually from a set of source `mbtiles` +files, instead of generated within `ServiceSet` from a directory. This provides +more granular control over assigning IDs to tilesets as well as creating, +updating, or deleting `Tileset` instances. You must generate unique IDs for +tilesets before adding to the `ServiceSet`; you can use +`handlers.SHA1ID(filename)` to generate a unique SHA1 ID of the service based on +its full filename path, or `handlers.RelativePathID(filename, tilePath)` to +generate the ID from its path and filename within the tile directory `tilePath`. + +HMAC authorization has been refactored into middleware external to the Go API. +It now is instantiated as middleware in `main.go`; this provides better +separation of concerns between the server (`main.go`) and the Go API. The API +for interacting with HMAC authorization from the CLI or endpoints remains the +same. + +Most of the updates are demonstrated in `main.go`. + +### General changes + +### Command-line interface + +- added support for automatically generating unique tileset IDs using `--generate-ids` option +- added ability to toggle off non-tile endpoints: + + - `--disable-preview`: disables the map preview, enabled by default. + - `--disable-svc-list`: disables the list of map services, enabled by default + - `--disable-tilejson`: disables the TileJSON endpoint for each tile service + - `--tiles-only`: shortcut that disables preview, service list, and TileJSON endpoints + +- added ability to have multiple tile paths using a comma-delimited list of paths passed to `--dir` option + +- moved static assets for map preview that were originally served on `/static` + endpoint to `/services//map/static` so that this endpoint is + disabled when preview is disabled via `--disable-preview`. + +### Go API + +- added `ServiceSetConfig` for configuration options for `ServiceSet` instances +- added `ServiceSet.AddTileset()`, `ServiceSet.UpdateTileset()`, + `ServiceSet.RemoveTileset()`, and `ServiceSet.HasTileset()` functions. + WARNING: these functions are not yet thread-safe. + +### Breaking changes + +#### Command-line interface: + +- ArcGIS endpoints are now opt-in via `--enable-arcgis` option (disabled by default) +- `--path` option has been renamed to `--root-url` for clarity (env var is now `ROOT_URL`) +- `--enable-reload` has been renamed to `--enable-reload-signal` + +#### Handlers API + +- `ServiceSet.Handler` parameters have been replaced with `ServiceSetConfig` + passed to `handlers.New()` instead. +- removed `handlers.NewFromBaseDir()`, replaced with `handlers.New()` and calling + `ServiceSet.AddTileset()` for each `Tileset` to register. +- removed `ServiceSet.AddDBOnPath()`; this is replaced by calling + `ServiceSet.AddTileset()` for each `Tileset` to register. + +## 0.6.1 + +- upgraded Docker containers to Go 1.14 (solves out of memory issues during builds on small containers) + +## 0.6 + +- fixed bug in map preview when bounds are not defined for a tileset (#84) +- updated Leaflet to 1.6.0 and Mapbox GL to 0.32.0 (larger upgrades contingent on #65) +- fixed issues with `--tls` option (#89) +- added example proxy configuration for Caddy and NGINX (#91) +- fixed issues with map preview page using HTTP basemaps (#90) +- resolved template loading issues (#85) + +### Breaking changes - handlers API: + +- Removed `TemplatesFromAssets` as it was not used internally, and unlikely used externally +- Removed `secretKey` from `NewFromBaseDir` parameters; this is replaced by calling `SetRequestAuthKey` on a `ServiceSet`. + +## 0.5.0 + +- Added Docker support (#74, #75) +- Fix case-sensitive mbtiles URLs (#77) +- Add support for graceful reloading (#69, #72, #73) +- Add support for environment args (#70) +- All changes prior to 6/1/2019 diff --git a/README.md b/README.md index 0a2aeb8..1b2619f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ In addition to tile-level access, it provides: - TileJSON 2.1.0 endpoint for each tileset, with full metadata from the mbtiles file. - a preview map for exploring each tileset. -- a minimal ArcGIS tile map service API (work in progress) +- a minimal ArcGIS tile map service API We have been able to host a bunch of tilesets on an [AWS t2.nano](https://aws.amazon.com/about-aws/whats-new/2015/12/introducing-t2-nano-the-smallest-lowest-cost-amazon-ec2-instance/) @@ -66,19 +66,25 @@ Usage: mbtileserver [flags] Flags: - -c, --cert string X.509 TLS certificate filename. If present, will be used to enable SSL on the server. - -d, --dir string Directory containing mbtiles files. (default "./tilesets") - --domain string Domain name of this server. NOTE: only used for AutoTLS. - --dsn string Sentry DSN - -h, --help help for mbtileserver - -k, --key string TLS private key - --path string URL root path of this server (if behind a proxy) - -p, --port int Server port. Default is 443 if --cert or --tls options are used, otherwise 8000. (default -1) - -s, --secret-key string Shared secret key used for HMAC request authentication - -t, --tls Auto TLS using Let's Encrypt - -r, --redirect Redirect HTTP to HTTPS - --enable-reload Enable graceful reload - -v, --verbose Verbose logging + -c, --cert string X.509 TLS certificate filename. If present, will be used to enable SSL on the server. + -d, --dir string Directory containing mbtiles files. Directory containing mbtiles files. Can be a comma-delimited list of directories. (default "./tilesets") + --disable-preview Disable map preview for each tileset (enabled by default) + --disable-svc-list Disable services list endpoint (enabled by default) + --disable-tilejson Disable TileJSON endpoint for each tileset (enabled by default) + --domain string Domain name of this server. NOTE: only used for AutoTLS. + --dsn string Sentry DSN + --enable-arcgis Enable ArcGIS Mapserver endpoints + --enable-reload-signal Enable graceful reload using HUP signal to the server process + --generate-ids Automatically generate tileset IDs instead of using relative path + -h, --help help for mbtileserver + -k, --key string TLS private key + -p, --port int Server port. Default is 443 if --cert or --tls options are used, otherwise 8000. (default -1) + -r, --redirect Redirect HTTP to HTTPS + --root-url string Root URL of services endpoint (default "/services") + -s, --secret-key string Shared secret key used for HMAC request authentication + --tiles-only Only enable tile endpoints (shortcut for --disable-svc-list --disable-tilejson --disable-preview) + -t, --tls Auto TLS via Let's Encrypt + -v, --verbose Verbose logging ``` So hosting tiles is as easy as putting your mbtiles files in the `tilesets` @@ -88,6 +94,9 @@ You can have multiple directories in your `tilesets` directory; these will be co `/foo/bar/baz.mbtiles` will be available at `/services/foo/bar/baz`. +If `--generate-ids` is provided, tileset IDs are automatically generated using a SHA1 hash of the path to each tileset. +By default, tileset IDs are based on the relative path of each tileset to the base directory provided using `--dir`. + When you want to remove, modify, or add new tilesets, simply restart the server process or use the reloading process below. If a valid Sentry DSN is provided, warnings, errors, fatal errors, and panics will be reported to Sentry. @@ -98,19 +107,20 @@ If the `--tls` option is provided, the Let's Encrypt Terms of Service are accept If either `--cert` or `--tls` are provided, the default port is 443. -You can also set up server config using environment variables instead of flags, which may be more helpful when deploying in a docker image. Use the associated flag to determine usage. The following variables are available: +You can also use environment variables instead of flags, which may be more helpful when deploying in a docker image. Use the associated flag to determine usage. The following variables are available: - `PORT` (`--port`) - `TILE_DIR` (`--dir`) -- `PATH_PREFIX` (`--path`) +- `GENERATE_IDS` (`--generate-ids`) +- `ROOT_URL_PATH` (`--root-url-path`) - `DOMAIN` (`--domain`) - `TLS_CERT` (`--cert`) - `TLS_PRIVATE_KEY` (`--key`) +- `HMAC_SECRET_KEY` (`--secret-key`) - `AUTO_TLS` (`--tls`) - `REDIRECT` (`--redirect`) - `DSN` (`--dsn`) - `VERBOSE` (`--verbose`) -- `HMAC_SECRET_KEY` (`--secret-key`) Example: @@ -136,7 +146,7 @@ mbtileserver: ### Reload mbtileserver optionally supports graceful reload (without interrupting any in-progress requests). This functionality -must be enabled with the `--enable-reload` flag. When enabled, the server can be reloaded by sending it a `HUP` signal: +must be enabled with the `--enable-reload-signal` flag. When enabled, the server can be reloaded by sending it a `HUP` signal: ``` $ kill -HUP @@ -275,7 +285,7 @@ file. `mbtileserver` automatically creates a TileJSON endpoint for each service at `/services/`. The TileJSON uses the same scheme and domain name as is used for the incoming request; the `--domain` setting does not -have an affect on auto-generated URLs. +affect auto-generated URLs. This API provides most elements of the `metadata` table in the mbtiles file as well as others that are automatically inferred from tile data. @@ -327,10 +337,19 @@ This currently uses `Leaflet` for image tiles and `Mapbox GL JS` for vector tile ## ArcGIS API This project currently provides a minimal ArcGIS tiled map service API for tiles stored in an mbtiles file. + +This is enabled with the `--enable-arcgis` flag. + This should be sufficient for use with online platforms such as [Data Basin](https://databasin.org). Because the ArcGIS API relies on a number of properties that are not commonly available within an mbtiles file, so certain aspects are stubbed out with minimal information. This API is not intended for use with more full-featured ArcGIS applications such as ArcGIS Desktop. +Available endpoints: + +- Service info: `http://localhost:8000/arcgis/rest/services//MapServer` +- Layer info: `http://localhost:8000/arcgis/rest/services//MapServer/layers` +- Tiles: `http://localhost:8000/arcgis/rest/services//MapServer/tile/0/0/0` + ## Request authorization Providing a secret key with `-s/--secret-key` or by setting the `HMAC_SECRET_KEY` environment variable will @@ -347,7 +366,7 @@ A signature is a URL-safe, base64 encoded HMAC hash using the `SHA1` algorithm. from a randomly generated salt, and the **secret key** string. The hash payload is a combination of the ISO-formatted date when the hash was created, and the authorized service id. -The following is an example signature, created in Go for the serivce id `test`, the date +The following is an example signature, created in Go for the service id `test`, the date `2019-03-08T19:31:12.213831+00:00`, the salt `0EvkK316T-sBLA`, and the secret key `YMIVXikJWAiiR3q-JMz1v2Mfmx3gTXJVNqme5kyaqrY` @@ -429,12 +448,12 @@ $gulp build Modifying the `.go` files always requires re-running `go build .`. -In case you have modified the templates and static assets, you need to run `go generate ./handlers` to ensure that your modifications +In case you have modified the templates and static assets, you need to run `go generate ./handlers/templates.go` to ensure that your modifications are embedded into the executable. For this to work, you must have [github.com/shurcooL/vfsgen)[https://github.com/shurcooL/vfsgen) installed. ```bash -go generate ./handlers/handlers.go +go generate ./handlers/templates.go ``` This will rewrite the `assets_vfsdata.go` which you must commit along with your @@ -447,30 +466,7 @@ But do not forget to perform it in the end. ## Changes -### 0.6.1 - -- upgraded Docker containers to Go 1.14 - -### 0.6 - -- fixed bug in map preview when bounds are not defined for a tileset (#84) -- updated Leaflet to 1.6.0 and Mapbox GL to 0.32.0 (larger upgrades contingent on #65) -- fixed issues with `--tls` option (#89) -- added example proxy configuration for Caddy and NGINX (#91) -- fixed issues with map preview page using HTTP basemaps (#90) -- resolved template loading issues (#85) -- breaking changes: - - `handlers.go`: - - Removed `TemplatesFromAssets` as it was not used internally, and unlikely used externally - - Removed `secretKey` from `NewFromBaseDir` parameters; this is replaced by calling `SetRequestAuthKey` on a `ServiceSet`. - -### 0.5.0 - -- Added Docker support (#74, #75) -- Fix case-sensitive mbtiles URLs (#77) -- Add support for graceful reloading (#69, #72, #73) -- Add support for environment args (#70) -- All changes prior to 6/1/2019 +See [CHANGELOG](CHANGELOG.md). ## Contributors ✨ diff --git a/handlers/arcgis.go b/handlers/arcgis.go index aa95d6a..8983a51 100644 --- a/handlers/arcgis.go +++ b/handlers/arcgis.go @@ -2,12 +2,9 @@ package handlers import ( "encoding/json" - "fmt" "math" "net/http" "strings" - - "github.com/consbio/mbtileserver/mbtiles" ) type arcGISLOD struct { @@ -63,265 +60,307 @@ type arcGISLayer struct { CurrentVersion float32 `json:"currentVersion"` } -const ( - earthRadius = 6378137.0 - earthCircumference = math.Pi * earthRadius - initialResolution = 2 * earthCircumference / 256 - dpi uint8 = 96 -) +const ArcGISRoot = "/arcgis/rest/services/" var webMercatorSR = arcGISSpatialReference{Wkid: 3857} var geographicSR = arcGISSpatialReference{Wkid: 4326} -// wrapJSONP writes b (JSON marshalled to bytes) as a JSONP response to -// w if the callback query parameter is present, and writes b as a JSON -// response otherwise. Any error that occurs during writing is returned. -func wrapJSONP(w http.ResponseWriter, r *http.Request, b []byte) (err error) { - callback := r.URL.Query().Get("callback") +// arcGISServiceJSON returns ArcGIS standard JSON describing the ArcGIS +// tile service. +func (ts *Tileset) arcgisServiceJSON() ([]byte, error) { + db := ts.db + imgFormat := db.TileFormatString() + metadata, err := db.ReadMetadata() + if err != nil { + return nil, err + } + name, _ := metadata["name"].(string) + description, _ := metadata["description"].(string) + attribution, _ := metadata["attribution"].(string) + tags, _ := metadata["tags"].(string) + credits, _ := metadata["credits"].(string) - if callback != "" { - w.Header().Set("Content-Type", "application/javascript") - _, err = w.Write([]byte(fmt.Sprintf("%s(%s);", callback, b))) + // TODO: make sure that min and max zoom always populated + minZoom, _ := metadata["minzoom"].(uint8) + maxZoom, _ := metadata["maxzoom"].(uint8) + // TODO: extract dpi from the image instead + var lods []arcGISLOD + for i := minZoom; i <= maxZoom; i++ { + scale, resolution := calcScaleResolution(i, dpi) + lods = append(lods, arcGISLOD{ + Level: i, + Resolution: resolution, + Scale: scale, + }) + } + + minScale := lods[0].Scale + maxScale := lods[len(lods)-1].Scale + + bounds, ok := metadata["bounds"].([]float32) + if !ok { + bounds = []float32{-180, -85, 180, 85} // default to world bounds + } + extent := geoBoundsToWMExtent(bounds) + + tileInfo := map[string]interface{}{ + "rows": 256, + "cols": 256, + "dpi": dpi, + "origin": map[string]float32{ + "x": -20037508.342787, + "y": 20037508.342787, + }, + "spatialReference": webMercatorSR, + "lods": lods, + } + + documentInfo := map[string]string{ + "Title": name, + "Author": attribution, + "Comments": "", + "Subject": "", + "Category": "", + "Keywords": tags, + "Credits": credits, + } + + out := map[string]interface{}{ + "currentVersion": "10.4", + "id": ts.id, + "name": name, + "mapName": name, + "capabilities": "Map,TilesOnly", + "description": description, + "serviceDescription": description, + "copyrightText": attribution, + "singleFusedMapCache": true, + "supportedImageFormatTypes": strings.ToUpper(imgFormat), + "units": "esriMeters", + "layers": []arcGISLayerStub{ + { + ID: 0, + Name: name, + ParentLayerID: -1, + DefaultVisibility: true, + SubLayerIDs: nil, + MinScale: minScale, + MaxScale: maxScale, + }, + }, + "tables": []string{}, + "spatialReference": webMercatorSR, + "minScale": minScale, + "maxScale": maxScale, + "tileInfo": tileInfo, + "documentInfo": documentInfo, + "initialExtent": extent, + "fullExtent": extent, + "exportTilesAllowed": false, + "maxExportTilesCount": 0, + "resampling": false, + } + + bytes, err := json.Marshal(out) + if err != nil { + return nil, err + } + return bytes, nil +} + +// arcgisServiceHandler is an http.HandlerFunc that returns standard ArcGIS +// JSON for a given ArcGIS tile service +func (ts *Tileset) arcgisServiceHandler(w http.ResponseWriter, r *http.Request) { + svcJSON, err := ts.arcgisServiceJSON() + + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS Service JSON for %v: %v", r.URL.Path, err) return } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(b) - return -} + err = wrapJSONP(w, r, svcJSON) -func (s *ServiceSet) arcgisService(id string, db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - imgFormat := db.TileFormatString() - metadata, err := db.ReadMetadata() - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("Could not read metadata for tileset %v", id) - } - name, _ := metadata["name"].(string) - description, _ := metadata["description"].(string) - attribution, _ := metadata["attribution"].(string) - tags, _ := metadata["tags"].(string) - credits, _ := metadata["credits"].(string) - - // TODO: make sure that min and max zoom always populated - minZoom, _ := metadata["minzoom"].(uint8) - maxZoom, _ := metadata["maxzoom"].(uint8) - // TODO: extract dpi from the image instead - var lods []arcGISLOD - for i := minZoom; i <= maxZoom; i++ { - scale, resolution := calcScaleResolution(i, dpi) - lods = append(lods, arcGISLOD{ - Level: i, - Resolution: resolution, - Scale: scale, - }) - } - - minScale := lods[0].Scale - maxScale := lods[len(lods)-1].Scale - - bounds, ok := metadata["bounds"].([]float32) - if !ok { - bounds = []float32{-180, -85, 180, 85} // default to world bounds - } - extent := geoBoundsToWMExtent(bounds) - - tileInfo := map[string]interface{}{ - "rows": 256, - "cols": 256, - "dpi": dpi, - "origin": map[string]float32{ - "x": -20037508.342787, - "y": 20037508.342787, - }, - "spatialReference": webMercatorSR, - "lods": lods, - } - - documentInfo := map[string]string{ - "Title": name, - "Author": attribution, - "Comments": "", - "Subject": "", - "Category": "", - "Keywords": tags, - "Credits": credits, - } - - out := map[string]interface{}{ - "currentVersion": "10.4", - "id": id, - "name": name, - "mapName": name, - "capabilities": "Map,TilesOnly", - "description": description, - "serviceDescription": description, - "copyrightText": attribution, - "singleFusedMapCache": true, - "supportedImageFormatTypes": strings.ToUpper(imgFormat), - "units": "esriMeters", - "layers": []arcGISLayerStub{ - { - ID: 0, - Name: name, - ParentLayerID: -1, - DefaultVisibility: true, - SubLayerIDs: nil, - MinScale: minScale, - MaxScale: maxScale, - }, - }, - "tables": []string{}, - "spatialReference": webMercatorSR, - "minScale": minScale, - "maxScale": maxScale, - "tileInfo": tileInfo, - "documentInfo": documentInfo, - "initialExtent": extent, - "fullExtent": extent, - "exportTilesAllowed": false, - "maxExportTilesCount": 0, - "resampling": false, - } - - bytes, err := json.Marshal(out) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("cannot marshal ArcGIS service info JSON: %v", err) - } - - return http.StatusOK, wrapJSONP(w, r, bytes) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS Service JSON to JSONP for %v: %v", r.URL.Path, err) } } -func (s *ServiceSet) arcgisLayers(id string, db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - metadata, err := db.ReadMetadata() - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("Could not read metadata for tileset %v", id) - } +// arcGISLayersJSON returns JSON for the layers in a given ArcGIS tile service +func (ts *Tileset) arcgisLayersJSON() ([]byte, error) { + metadata, err := ts.db.ReadMetadata() + if err != nil { + return nil, err + } - name, _ := metadata["name"].(string) - description, _ := metadata["description"].(string) - attribution, _ := metadata["attribution"].(string) + name, _ := metadata["name"].(string) + description, _ := metadata["description"].(string) + attribution, _ := metadata["attribution"].(string) - bounds, ok := metadata["bounds"].([]float32) - if !ok { - bounds = []float32{-180, -85, 180, 85} // default to world bounds - } - extent := geoBoundsToWMExtent(bounds) + bounds, ok := metadata["bounds"].([]float32) + if !ok { + bounds = []float32{-180, -85, 180, 85} // default to world bounds + } + extent := geoBoundsToWMExtent(bounds) - minZoom, _ := metadata["minzoom"].(uint8) - maxZoom, _ := metadata["maxzoom"].(uint8) - minScale, _ := calcScaleResolution(minZoom, dpi) - maxScale, _ := calcScaleResolution(maxZoom, dpi) + minZoom, _ := metadata["minzoom"].(uint8) + maxZoom, _ := metadata["maxzoom"].(uint8) + minScale, _ := calcScaleResolution(minZoom, dpi) + maxScale, _ := calcScaleResolution(maxZoom, dpi) - // for now, just create a placeholder root layer - emptyArray := []interface{}{} - emptyLayerArray := []arcGISLayerStub{} + // for now, just create a placeholder root layer + emptyArray := []interface{}{} + emptyLayerArray := []arcGISLayerStub{} - var layers [1]arcGISLayer - layers[0] = arcGISLayer{ - ID: 0, - DefaultVisibility: true, - ParentLayer: nil, - Name: name, - Description: description, - Extent: extent, - MinScale: minScale, - MaxScale: maxScale, - CopyrightText: attribution, - HTMLPopupType: "esriServerHTMLPopupTypeAsHTMLText", - Fields: emptyArray, - Relationships: emptyArray, - SubLayers: emptyLayerArray, - CurrentVersion: 10.4, - Capabilities: "Map", - } + var layers [1]arcGISLayer + layers[0] = arcGISLayer{ + ID: 0, + DefaultVisibility: true, + ParentLayer: nil, + Name: name, + Description: description, + Extent: extent, + MinScale: minScale, + MaxScale: maxScale, + CopyrightText: attribution, + HTMLPopupType: "esriServerHTMLPopupTypeAsHTMLText", + Fields: emptyArray, + Relationships: emptyArray, + SubLayers: emptyLayerArray, + CurrentVersion: 10.4, + Capabilities: "Map", + } - out := map[string]interface{}{ - "layers": layers, - } + out := map[string]interface{}{ + "layers": layers, + } - bytes, err := json.Marshal(out) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("cannot marshal ArcGIS layer info JSON: %v", err) - } + bytes, err := json.Marshal(out) + if err != nil { + return nil, err + } + return bytes, nil +} - return http.StatusOK, wrapJSONP(w, r, bytes) +// arcgisLayersHandler is an http.HandlerFunc that returns standard ArcGIS +// Layers JSON for a given ArcGIS tile service +func (ts *Tileset) arcgisLayersHandler(w http.ResponseWriter, r *http.Request) { + layersJSON, err := ts.arcgisLayersJSON() + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS layer JSON for %v: %v", r.URL.Path, err) + return + } + + err = wrapJSONP(w, r, layersJSON) + + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS layers JSON to JSONP for %v: %v", r.URL.Path, err) } } -func (s *ServiceSet) arcgisLegend(id string, db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { +// arcgisLegendJSON returns minimal ArcGIS legend JSON for a given ArcGIS +// tile service. Legend elements are not yet supported. +func (ts *Tileset) arcgisLegendJSON() ([]byte, error) { + metadata, err := ts.db.ReadMetadata() + if err != nil { + return nil, err + } - metadata, err := db.ReadMetadata() - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("Could not read metadata for tileset %v", id) - } + name, _ := metadata["name"].(string) - name, _ := metadata["name"].(string) + // TODO: pull the legend from ArcGIS specific metadata tables + var elements [0]interface{} + var layers [1]map[string]interface{} - // TODO: pull the legend from ArcGIS specific metadata tables - var elements [0]interface{} - var layers [1]map[string]interface{} + layers[0] = map[string]interface{}{ + "layerId": 0, + "layerName": name, + "layerType": "", + "minScale": 0, + "maxScale": 0, + "legend": elements, + } - layers[0] = map[string]interface{}{ - "layerId": 0, - "layerName": name, - "layerType": "", - "minScale": 0, - "maxScale": 0, - "legend": elements, - } + out := map[string]interface{}{ + "layers": layers, + } - out := map[string]interface{}{ - "layers": layers, - } + bytes, err := json.Marshal(out) + if err != nil { + return nil, err + } + return bytes, nil +} - bytes, err := json.Marshal(out) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("cannot marshal ArcGIS legend info JSON: %v", err) - } - return http.StatusOK, wrapJSONP(w, r, bytes) +// arcgisLegendHandler is an http.HandlerFunc that returns minimal ArcGIS +// legend JSON for a given ArcGIS tile service +func (ts *Tileset) arcgisLegendHandler(w http.ResponseWriter, r *http.Request) { + legendJSON, err := ts.arcgisLegendJSON() + + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS legend JSON for %v: %v", r.URL.Path, err) + return + } + + err = wrapJSONP(w, r, legendJSON) + + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("Could not render ArcGIS legend JSON to JSONP for %v: %v", r.URL.Path, err) } } -func (s *ServiceSet) arcgisTiles(db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - // split path components to extract tile coordinates x, y and z - pcs := strings.Split(r.URL.Path[1:], "/") - // strip off /arcgis/rest/services/ and then - // we should have at least , "MapServer", "tiles", , , - l := len(pcs) - if l < 6 || pcs[5] == "" { - return http.StatusBadRequest, fmt.Errorf("requested path is too short") - } - z, y, x := pcs[l-3], pcs[l-2], pcs[l-1] - tc, _, err := tileCoordFromString(z, x, y) - if err != nil { - return http.StatusBadRequest, err - } +// arcgisTileHandler returns an image tile or blank image for a given +// tile request within a given ArcGIS tile service +func (ts *Tileset) arcgisTileHandler(w http.ResponseWriter, r *http.Request) { + db := ts.db - var data []byte - err = db.ReadTile(tc.z, tc.x, tc.y, &data) + // split path components to extract tile coordinates x, y and z + pcs := strings.Split(r.URL.Path[1:], "/") + // strip off /arcgis/rest/services/ and then + // we should have at least , "MapServer", "tiles", , , + l := len(pcs) + if l < 6 || pcs[5] == "" { + http.Error(w, "requested path is too short", http.StatusBadRequest) + return + } + z, y, x := pcs[l-3], pcs[l-2], pcs[l-1] + tc, _, err := tileCoordFromString(z, x, y) + if err != nil { + http.Error(w, "invalid tile coordinates", http.StatusBadRequest) + return + } + + var data []byte + err = db.ReadTile(tc.z, tc.x, tc.y, &data) + + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("cannot fetch tile from DB for z=%d, x=%d, y=%d for %v: %v", tc.z, tc.x, tc.y, r.URL.Path, err) + return + } + + if data == nil || len(data) <= 1 { + // Return blank PNG for all image types + w.Header().Set("Content-Type", "image/png") + _, err = w.Write(BlankPNG()) if err != nil { - // augment error info - t := "tile" - err = fmt.Errorf("cannot fetch %s from DB for z=%d, x=%d, y=%d: %v", t, tc.z, tc.x, tc.y, err) - return http.StatusInternalServerError, err + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("could not return blank image for %v: %v", r.URL.Path, err) } + } else { + w.Header().Set("Content-Type", db.ContentType()) + _, err = w.Write(data) - if data == nil || len(data) <= 1 { - // Return blank PNG for all image types - w.Header().Set("Content-Type", "image/png") - _, err = w.Write(BlankPNG()) - } else { - w.Header().Set("Content-Type", db.ContentType()) - _, err = w.Write(data) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + ts.svc.logError("could not write tile data to response for %v: %v", r.URL.Path, err) } - - return http.StatusOK, err } } @@ -337,13 +376,6 @@ func geoBoundsToWMExtent(bounds []float32) arcGISExtent { } } -func calcScaleResolution(zoomLevel uint8, dpi uint8) (float64, float64) { - var denom = 1 << zoomLevel - resolution := initialResolution / float64(denom) - scale := float64(dpi) * 39.37 * resolution // 39.37 in/m - return scale, resolution -} - // Cast interface to a string if not nil, otherwise empty string func toString(s interface{}) string { if s != nil { @@ -370,17 +402,18 @@ func geoToMercator(longitude, latitude float64) (float64, float64) { // ArcGISHandler returns a http.Handler that serves the ArcGIS endpoints of the ServiceSet. // The function ef is called with any occurring error if it is non-nil, so it // can be used for e.g. logging with logging facilities of the caller. -func (s *ServiceSet) ArcGISHandler(ef func(error)) http.Handler { - m := http.NewServeMux() - rootPath := "/arcgis/rest/services/" +// func (s *ServiceSet) ArcGISHandler(ef func(error)) http.Handler { +// m := http.NewServeMux() - for id, db := range s.tilesets { - p := rootPath + id + "/MapServer" - m.Handle(p, wrapGetWithErrors(ef, hmacAuth(s.arcgisService(id, db), s.secretKey, id))) - m.Handle(p+"/layers", wrapGetWithErrors(ef, hmacAuth(s.arcgisLayers(id, db), s.secretKey, id))) - m.Handle(p+"/legend", wrapGetWithErrors(ef, hmacAuth(s.arcgisLegend(id, db), s.secretKey, id))) +// root := "/arcgis/rest/services/" - m.Handle(p+"/tile/", wrapGetWithErrors(ef, hmacAuth(s.arcgisTiles(db), s.secretKey, id))) - } - return m -} +// for id, ts := range s.tilesets { +// prefix := root + id + "/MapServer" +// m.Handle(prefix, wrapGetWithErrors(ef, hmacAuth(ts.arcgisServiceHandler(), s.secretKey, id))) +// m.Handle(prefix+"/layers", wrapGetWithErrors(ef, hmacAuth(ts.arcgisLayersHandler(), s.secretKey, id))) +// m.Handle(prefix+"/legend", wrapGetWithErrors(ef, hmacAuth(ts.arcgisLegendHandler(), s.secretKey, id))) + +// m.Handle(prefix+"/tile/", wrapGetWithErrors(ef, hmacAuth(ts.arcgisTileHandler(), s.secretKey, id))) +// } +// return m +// } diff --git a/handlers/assets_vfsdata.go b/handlers/assets_vfsdata.go index 0781071..2d093c0 100644 --- a/handlers/assets_vfsdata.go +++ b/handlers/assets_vfsdata.go @@ -25,17 +25,17 @@ var Assets = func() http.FileSystem { }, "/map.html": &vfsgen۰CompressedFileInfo{ name: "map.html", - modTime: time.Date(2020, 2, 12, 21, 21, 7, 463497861, time.UTC), - uncompressedSize: 7159, + modTime: time.Date(2020, 2, 23, 17, 3, 51, 543922674, time.UTC), + uncompressedSize: 6972, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x59\x6d\x4f\x1b\xb9\xf6\x7f\x5f\xa9\xdf\xe1\xfc\x67\xf5\x47\x41\x1a\x66\x02\xb4\xa8\x1a\x48\xa5\x34\xa4\x94\x5d\x08\x5c\x12\xee\xdd\x5d\x2e\xaa\x9c\x99\x93\x8c\x5b\x8f\x3d\xb2\x9d\x90\x34\xcd\x77\xbf\xb2\x27\x03\x79\xf0\x04\xba\x2f\xba\x6b\x21\x98\xd8\xe7\xc1\xc7\xbf\xdf\x39\x3e\x4c\x66\x33\x48\x70\x40\x39\x82\x97\x91\xdc\x83\xf9\xfc\xf5\xab\x93\xff\x3b\xbd\x6a\xf5\xfe\xb8\x6e\x43\xaa\x33\xf6\xfe\xf5\xab\x93\xc5\x5f\x00\x80\x93\x14\x49\x02\x8c\xf0\x61\xc3\x43\xee\x2d\x66\xed\x4a\x86\x9a\x40\x9c\x12\xa9\x50\x37\xbc\xdb\xde\xc7\xbd\x77\x1e\x84\xcb\x12\x9a\x6a\x86\xef\x67\xb3\xe0\xfc\x74\x3e\x87\x6b\x89\x63\x8a\x0f\x27\x61\x31\xbd\x24\xa7\x62\x49\x73\x0d\x4a\xc6\x0d\x2f\x54\x9a\x68\x1a\x87\xb1\x90\x18\x64\x94\x07\x5f\x94\xf7\xfe\x24\x2c\x44\x96\x95\x18\xe5\x5f\x21\x95\x38\x70\xe8\xc4\x4a\x79\x20\x91\x35\x3c\xa5\xa7\x0c\x55\x8a\xa8\xd7\xf6\x66\x17\x96\x26\xcc\x30\x71\xc3\x6c\x75\xce\xce\x23\x1d\xa6\x3a\x82\xfd\x7a\xfd\xff\x8f\x57\x97\xe7\xab\x1f\xfb\x22\x99\xba\x2c\x64\x44\x0e\x29\x8f\xa0\x7e\xbc\xb9\x96\x93\x24\xa1\x7c\xe8\x5e\x7c\xb9\xeb\xf4\xcd\x8f\x38\x5e\x53\xfe\xe5\x92\xe4\x2e\xf5\x5c\x28\xaa\xa9\xe0\x11\x90\xbe\x12\x6c\xa4\xd1\xb1\x47\x2d\x72\xf7\xe6\x19\x0e\xb4\x7b\xa5\x2f\xb4\x16\x99\x7b\x4d\x16\x11\xbb\xd5\x64\x82\x72\xcf\x3a\x3c\xc8\x27\xa0\x04\xa3\x09\xfc\x42\x08\xd9\x1e\x5e\xd0\x27\x0a\x33\x92\x03\xcd\x86\xae\x30\x1f\x68\xa2\xd3\x08\x0e\x8f\xf2\xc9\x33\x86\x28\x1f\x08\xe7\x41\x95\x20\x1e\xe5\x13\x78\xb7\x61\xc6\x8c\x81\xe0\x06\xc8\x37\xf9\x24\xdc\x37\x52\x4d\x49\x09\xf3\xe1\x13\xb2\x31\x6a\x1a\x13\x1f\x14\xe1\x6a\x4f\xa1\xa4\x03\x57\xec\x24\xfe\x3a\x94\x62\xc4\x93\x08\x1e\x52\xea\x44\x62\x59\x46\x0e\xfb\xa4\x76\xf0\xf6\xad\x0f\x4f\xbf\xea\xc1\xbb\x5d\xe7\xb1\x4e\xf6\x54\x4a\x12\xf1\x10\x41\x1d\xea\xb0\xff\x36\x9f\x14\xfa\x75\x1f\x16\x3f\xc1\x81\x5b\xd3\x02\x22\x49\x42\x47\x2a\x82\xb7\xce\xc0\x5f\x44\xa2\x6f\x7b\x94\x27\x38\x89\xe0\xa0\x5e\x77\x41\x6f\x31\xdf\xaf\x3b\x1d\xc8\x32\x47\x5e\x06\x5f\x7a\xf8\x17\x33\xe5\x24\x5c\xae\x1a\x27\xa1\xa9\x8d\xe5\x07\x93\xf9\xcb\xf5\x25\xa1\x63\xa0\x49\xc3\xbb\x24\xb9\xa9\x5f\x09\x1d\x6f\x56\xbc\xb5\xf2\x33\x26\x12\x16\x44\x55\xd0\x80\xbb\xcd\x3d\x5e\x04\x9a\x32\xbc\x20\x53\x94\xb5\xcd\x55\x33\xbc\x30\x9c\xa9\x79\x40\x64\x3c\xa4\x4a\x70\x46\x39\x06\xb1\xc8\xc2\xa6\x8c\xcf\xce\xbb\xa1\x44\xa5\x43\x85\x72\x4c\x63\x54\xe1\x7f\x84\x64\xc9\xe7\x9e\xc8\xc5\xe7\x4b\x92\x87\x97\x24\xef\xa2\x1c\xa3\x0c\x8d\x97\x70\xf6\x6d\x1e\xce\xa6\xf3\x70\x36\x99\x7b\xbe\xdb\x9b\xe3\x18\xcb\x41\xb4\x96\xb4\x3f\xb2\xb8\x57\x4b\xd9\x2d\xf7\x28\x43\x05\x3b\xb1\xc8\xa7\xc7\xd0\x56\x92\xc2\x4e\x96\x10\x95\x16\x1f\x7c\x38\xc5\x0b\x21\x33\xf4\xa1\xd3\xfc\x77\xaf\xfd\x2f\x1f\x7a\x22\xeb\x89\xcc\x87\x73\xae\x51\x66\x24\xf7\x81\x5e\xb7\x7c\xb8\xed\x9e\x75\x7d\xf8\xd8\xbc\xf2\xa1\x73\xdd\xf5\xa1\x73\xd3\x6a\x76\x7c\x38\x43\xf1\x81\x28\xf4\xe1\x37\x92\x10\xa5\x51\x42\xe7\xc2\x87\x2b\x99\x70\xc2\x63\x84\xee\x48\x8e\x71\xea\x17\x8e\x7f\x25\x39\xe1\x3e\x5c\xb6\x7b\xe7\x8b\x99\x56\x4a\x39\x81\xda\x27\xc1\x87\xf0\x9b\xe0\xc3\x5d\x1f\x08\x4f\x40\xa7\x08\x67\xe7\x5d\xb8\x55\x28\xa1\x25\xb2\x6c\xc4\xa9\x9e\x56\x9d\x93\x19\x6a\xd4\x4f\x44\x46\x28\x57\x11\xdc\x79\xca\x1e\xb4\xe7\x83\x57\xa2\xe1\xdd\x6f\x51\x66\xa4\x8f\x2c\x02\xaf\xdd\xbd\x39\x07\x03\x98\xe7\x96\x9d\x6f\x4e\xef\x3a\xcc\xbe\x88\x47\xc5\x16\x7f\x88\x4a\xe7\x19\x19\xa2\x9c\xfe\x03\x99\xd4\x15\x23\x19\x63\xb4\x60\x14\xdd\x8b\x47\x7d\x4c\x0c\x65\x4e\x9b\x25\x71\x9a\xed\xdf\x2d\x59\xda\x53\x34\x7f\x75\x46\xf2\x9c\xf2\xa1\x0f\x4d\x94\x62\x28\x69\xe2\xc3\xf9\x59\xc7\xfc\xba\xf6\xe1\xf6\xfa\x66\xaf\x6d\x1e\xfe\x1a\x19\x56\xf0\x5c\x9c\xda\x4f\x80\xf4\xe5\xa5\xa1\x45\xf8\x98\x94\xb0\x5e\x98\xfa\xfa\xf9\x4c\x92\xe9\x67\x93\x49\xff\x40\x78\x9d\x85\x62\x1b\x00\x19\x99\xfc\x29\x4c\x07\xb2\x7f\xf4\x53\x72\xd6\x1c\xde\xdf\x05\x70\x31\xb1\x06\x70\x9b\xe1\x98\x98\x43\x5f\x60\xfc\x89\x32\x66\x7a\x80\xbf\x0f\xdd\x02\xc3\x22\x17\x3b\x67\x4d\x83\x62\xb7\xe9\x43\xeb\xec\xbc\x79\xe3\x43\x07\x6e\x44\x9f\x72\x25\xb8\x0f\x9d\x56\xbb\x69\x84\x2e\xba\x3e\x5c\x99\x87\xcb\xa6\xcd\xdc\x84\x68\xa2\xf4\x54\x22\x53\xc8\x7d\xb8\xa1\x5f\xbe\xaa\x07\xa2\x51\x2a\x4d\x88\xf6\xe1\xac\x5b\xc8\x31\xc2\x13\x1f\x3e\xb6\x8d\x5a\x79\x87\xac\x24\xf2\xc8\x24\x72\xfc\x92\x44\x7e\xe4\xd1\xc1\xe1\x4f\xe1\xd1\x23\x6c\x3f\x40\xa6\xd5\xa9\xfb\xe3\xd7\xaf\x36\xfb\x0e\x73\x02\x0d\xb8\x08\x32\x92\xd7\x6c\xcf\xe2\xc3\x6c\xbe\xeb\x14\x65\x86\x82\xd0\x00\x3e\x62\x6c\xad\x57\x4a\x0e\x83\x2f\x4a\xf0\x9a\x17\x84\xde\x6e\xa0\x53\xe4\xb5\xc1\x88\xc7\x66\xbf\x35\x43\xa6\x5f\xbb\x57\x9d\x5d\x17\x5d\xe8\x00\x1e\x05\x82\xbe\x69\x60\x95\x53\xae\xdc\x43\x1f\x1a\xb0\x26\xbf\xb1\xd7\x72\x84\x21\xf4\xae\x4e\xaf\x22\x10\xb9\xa6\x19\xfd\x86\x16\xea\x5c\xe2\x18\xb9\x86\x2f\x54\x6b\x94\x6e\xcd\x8c\xe4\xc1\x80\xea\x0f\xd6\x7e\xcd\xd1\x8f\x95\xe3\xae\x7f\xb7\x7f\xef\x43\xff\xae\x7e\xbf\x0d\xc9\xbb\xfe\xdd\xa1\x15\x3b\xb8\xbf\x77\x4b\xdd\xbb\x9a\xec\x39\x18\x42\x57\x1d\xc7\x62\x93\x36\x8b\x6b\x4e\x75\xd7\xc1\xac\x1c\x78\x46\x26\xdf\x84\xc8\x60\x67\x07\x36\xe6\x4e\xac\x83\x21\x6a\x43\xf3\xda\x6e\x25\x2a\x46\x4a\x2d\xa4\xd6\x8d\xbc\x78\x57\x25\xb5\x96\x6b\xdd\xa3\x31\xf3\xa0\xee\xea\xf7\x7e\xe5\x16\x28\x2f\x72\xf1\xc9\x3f\xe5\x36\x88\xef\xdf\xa1\x5e\x81\xcb\x63\x02\x6f\x44\xfe\xfd\x3b\x1c\x1c\x3a\x36\xbe\x99\x15\x65\xfc\x24\x49\x8a\x3d\xdb\x38\xdc\x72\x36\x83\x70\x88\x3c\x31\xce\x96\x69\x5c\xcc\x3a\x8e\xca\x60\xb5\xa4\xb2\xb3\xb3\x64\x20\x50\x48\x64\x9c\xd6\xc2\xff\xce\xc2\x5d\x68\x34\x1a\x50\xaf\x84\x68\xc5\xad\x55\xce\x89\x54\xb8\x64\xdb\x05\x14\x14\x29\x74\x49\xbe\x22\xa8\x91\x44\xd0\x29\x55\x40\x95\x2d\x96\x85\xaa\x35\x06\x4a\xcb\x51\xac\x8d\xc4\x03\x02\x4e\x72\x8c\xb5\xdb\xda\x6a\x3c\x01\x43\x3e\xd4\xe9\x6a\x58\x77\xf5\xfb\x00\x19\x66\xc8\x75\x75\x21\x58\x3a\xf6\x96\xe0\x5a\x0a\x56\x71\x31\x96\xe3\x22\x88\x0b\x39\xfb\x3e\xe0\xe8\x4d\xe1\xaf\xb6\xc5\x7c\x39\x9e\xfe\x85\xf5\xb4\xc8\xed\xff\x9b\xdb\x6e\x85\x72\x14\x0e\x54\xb4\x14\xd9\x0b\xb4\x62\xc1\x18\xc9\x15\x76\x69\x96\x33\x8c\x40\xcb\x11\xbe\x40\x2d\x41\x8d\xb1\xee\x6a\x89\x3a\x4e\x31\x29\xf4\xb6\xab\xcd\x77\xab\xd7\xab\xb8\xe0\xb8\x61\x9e\xaf\x30\xa6\x7d\x56\x2b\xf5\xc5\xce\x94\xd8\xbf\xdf\xc2\x5a\x93\x30\x46\xf8\xf6\xe6\x62\x39\x5b\xac\xfe\x5d\xfd\xbe\x62\x97\x46\x6b\xa4\x07\x46\xca\xd6\x93\x91\x1e\x9c\x49\x9a\xd4\x16\x96\x2a\x6b\x88\x19\x12\xed\x5b\x0a\x0b\xf7\x9b\x2d\x07\x9f\x0b\x6a\x9a\x87\xd6\x48\x2a\x21\x9f\x85\x29\x13\x23\x85\xb6\xdb\x18\x13\x16\xc1\xd1\x91\xc9\xaa\x53\x64\x64\x0a\x03\x21\x8b\xe5\x4c\x8c\x11\xec\xbd\xa4\x2a\x0e\xbf\x0a\x95\x45\xac\x26\x17\x7a\xa2\x96\x91\xdc\x5d\x7d\xca\xa3\xa1\x7c\x20\x4c\xce\x10\xca\x17\x05\xf7\x54\x64\xb7\x9a\xb2\x20\x96\x48\x34\x6e\x49\x24\x2f\xa1\xe3\x6d\xdc\xf7\x8c\xed\x6d\x02\x4f\xbe\x86\xa8\x8b\x66\xa3\x82\x86\x55\xc1\x9a\x08\x34\x4e\x74\x47\x24\xe8\xda\xbc\x97\x1e\x9a\xee\xca\xf3\x57\xe3\xac\x32\x57\x9a\x0a\x28\xe7\x28\x3f\xf5\x2e\x2f\xa0\xb1\x25\xbe\x9e\xa9\x7f\x8b\xce\x0d\x52\xa2\xc0\xbe\xfa\x06\x43\x2f\x75\x0c\x0a\x11\x62\xc1\x95\x60\x68\x71\xb5\x0c\x34\x8d\x69\xe0\x55\x22\x52\x82\x67\x3a\x27\xcb\x03\x51\xf4\x87\x8f\xbd\x13\x6e\xad\x81\x0b\x77\x01\x13\xc3\x9a\x77\xdb\xfb\xf8\xe4\x33\xf2\x7c\xc0\xc0\x3c\x55\x66\x73\xc5\xdd\xbc\x21\xb3\x3a\x65\xca\xae\xb9\x24\x17\x75\xd7\xdc\xfc\xd7\x8b\x12\x59\x33\x15\x92\xe1\x40\x7b\xeb\xa6\x9f\xad\xd5\x4f\xf5\xd9\xd8\xfe\x20\x26\xb5\x19\x64\x22\x31\xe9\x62\x93\x6b\xad\x0c\x5b\x27\x1b\x35\xcc\xdd\xb4\x2a\x46\x93\x05\xd3\x4b\x1f\x92\xf0\x21\xba\x8a\xbf\xc3\x8b\x83\xce\x99\x7d\x59\xe8\x5a\x20\x93\x08\xf6\x1d\x0b\x63\xc2\x46\xe8\x5e\x52\x1a\xf3\x08\xea\x41\xdd\xb5\x28\x24\x45\xae\x23\xf0\xc6\x28\x35\x8d\x09\x73\x6d\x87\xc6\x82\xb7\x18\x51\x2a\x02\x8f\x21\x19\x30\xd4\x7b\x36\xc2\x3d\xb3\xe2\x3d\x8b\x68\x71\x40\x96\x82\x94\xe7\x23\x0d\x71\x6a\xb4\x9f\x67\xa1\xed\x75\x0c\x01\xae\x72\x12\x53\x3d\xad\x61\x60\x03\x5d\x47\xbf\x82\x45\x4b\x84\x28\xb6\xf0\xac\xd8\x36\xde\x94\xaf\x4f\xab\xee\xf4\x25\x68\x8b\xaf\x20\xb6\x5e\xe5\xa5\xb5\xe8\xf1\xa9\x42\xd0\xdc\x4a\xbf\xbb\xd9\x50\x2e\xff\xb1\x7d\xf9\xcf\x08\xf6\x5d\xbd\xe6\x26\xb7\xcb\xe7\xb5\xef\xc4\x4e\xc2\xc5\x1b\xe8\x93\x70\xf1\xf5\xdd\x6c\x06\xa6\x37\x9b\xcf\x5f\xbf\xfa\x5f\x00\x00\x00\xff\xff\x49\xce\x3c\xa8\xf7\x1b\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x59\xff\x4f\xdb\xc8\x12\xff\xbd\x52\xff\x87\x79\x3e\x3d\x14\x24\x63\x07\x68\xd1\xc9\x90\x4a\x69\x48\x29\x77\x21\xf0\x48\x78\xef\xee\x78\xa8\xda\xd8\x93\x78\xdb\xf5\xae\xb5\xbb\x09\x49\xd3\xfc\xef\x4f\xbb\xb6\x21\x5f\x36\x40\xef\x49\xbd\x5b\x21\x70\x76\xbe\xed\xcc\x7c\x66\x76\x62\xe6\x73\x48\x70\x48\x39\x82\x97\x91\xdc\x83\xc5\xe2\xf5\xab\x93\x7f\x9c\x5e\xb6\xfa\xbf\x5f\xb5\x21\xd5\x19\x7b\xf7\xfa\xd5\x49\xf9\x17\x00\xe0\x24\x45\x92\x00\x23\x7c\xd4\xf0\x90\x7b\xe5\xae\xa5\x64\xa8\x09\xc4\x29\x91\x0a\x75\xc3\xbb\xe9\x7f\xd8\xfb\xd9\x83\x70\x99\x43\x53\xcd\xf0\xdd\x7c\x1e\x9c\x9f\x2e\x16\x70\x25\x71\x42\xf1\xfe\x24\x2c\xb6\x97\xf8\x54\x2c\x69\xae\x41\xc9\xb8\xe1\xcd\xe7\xc1\xcd\x75\x67\xb1\x08\x33\x92\x87\x4a\x13\x4d\xe3\x30\x16\x12\x83\x8c\xf2\xe0\xb3\xf2\xde\x9d\x84\x05\xfb\xb2\x02\x46\xf9\x17\x48\x25\x0e\x9f\x91\x8f\x95\xf2\x40\x22\x6b\x78\x4a\xcf\x18\xaa\x14\x51\xaf\x9d\xd9\x12\x96\x36\xcc\x32\xf1\x80\xf9\xea\x9e\xdd\x47\x3a\x4a\x75\x04\xfb\xf5\xfa\x3f\x8f\x57\xc9\x8b\xd5\x8f\x03\x91\xcc\x5c\x1a\x32\x22\x47\x94\x47\x50\x3f\xde\xa4\xe5\x24\x49\x28\x1f\xb9\x89\x2f\x37\x9d\xbe\xf9\x1e\xc3\x6b\xc2\x3f\x5d\x90\xdc\x25\x9e\x0b\x45\x35\x15\x3c\x02\x32\x50\x82\x8d\x35\x3a\xce\xa8\x45\xee\x3e\x3c\xc3\xa1\x76\x53\x06\x42\x6b\x91\xb9\x69\xb2\xf0\xd8\x2d\x26\x13\x94\x7b\xd6\xe0\x41\x3e\x05\x25\x18\x4d\xe0\x27\x42\xc8\xd3\xee\x05\x03\xa2\x30\x23\x39\xd0\x6c\xe4\x72\xf3\x9e\x26\x3a\x8d\xe0\xf0\x28\x9f\x3e\xa3\x88\xf2\xa1\x70\x06\xaa\x4a\xe2\x51\x3e\x85\x9f\x37\xd4\x98\x35\x14\xdc\x24\xf2\x4d\x3e\x0d\xf7\x0d\x57\x53\x52\xc2\x7c\xf8\x88\x6c\x82\x9a\xc6\xc4\x07\x45\xb8\xda\x53\x28\xe9\xd0\xe5\x3b\x89\xbf\x8c\xa4\x18\xf3\x24\x82\xfb\x94\x3a\x33\xb1\xcc\x23\x47\x03\x52\x3b\x78\xfb\xd6\x87\xc7\x5f\xf5\xe0\xe7\x5d\x67\x58\xa7\x7b\x2a\x25\x89\xb8\x8f\xa0\x0e\x75\xd8\x7f\x9b\x4f\x0b\xf9\xba\x0f\xe5\x4f\x70\xe0\x96\xb4\x09\x91\x24\xa1\x63\x15\xc1\x5b\xa7\xe3\x2f\x02\xd1\xd7\x3d\xca\x13\x9c\x46\x70\x50\xaf\xbb\x52\x6f\x73\xbe\x5f\x77\x1a\x90\x55\x8d\xbc\x2c\x7d\xe9\xe1\x9f\xac\x94\x93\x70\xb9\x6b\x9c\x84\xa6\x67\x56\x1f\x4c\xe5\x2f\xf7\x97\x84\x4e\x80\x26\x0d\xef\x82\xe4\xa6\x97\x25\x74\xb2\xd9\x09\xd7\xda\x4f\x18\x42\x47\x90\x04\x24\xb9\x87\x5f\x7a\x97\x5d\x48\xa8\xc4\x58\xb3\x19\x0c\xa5\xc8\x40\x63\x96\x33\xa2\x71\x55\x68\x42\x24\x68\xca\xd0\x0a\x34\x60\x3e\x0f\xfa\xe5\xa7\xc5\xe2\xf8\xf5\xab\x4d\xe6\xb2\x14\x14\x34\xe0\x76\x33\x0a\x9d\xc0\x28\xeb\x90\x19\xca\xda\x26\xd5\x2c\x2f\x0c\xe7\x6a\x11\x10\x19\x8f\xa8\x12\x9c\x51\x8e\x41\x2c\xb2\xb0\x29\xe3\xb3\xf3\x5e\x28\x51\xe9\x50\xa1\x9c\xd0\x18\x55\xf8\x1f\x21\x59\xf2\xa9\x2f\x72\xf1\xe9\x82\xe4\xe1\x05\xc9\x7b\x28\x27\x28\x43\x63\x25\x9c\x7f\x5d\x84\xf3\xd9\x22\x9c\x4f\x17\x9e\xef\xb6\xe6\x48\x54\xb5\x88\xd6\x92\x0e\xc6\x16\x59\xdb\xb9\xec\x91\x4d\x48\x14\xec\xc4\x22\x9f\x1d\x43\x5b\x49\x0a\x3b\x59\x42\x54\x5a\x7c\xf0\xe1\x14\x3b\x42\x66\xe8\x43\xb7\xf9\xef\x7e\xfb\x5f\x3e\xf4\x45\xd6\x17\x99\x0f\xe7\x5c\xa3\xcc\x48\xee\x03\xbd\x6a\xf9\x70\xd3\x3b\xeb\xf9\xf0\xa1\x79\xe9\x43\xf7\xaa\xe7\x43\xf7\xba\xd5\xec\xfa\x70\x86\xe2\x3d\x51\xe8\xc3\xaf\x24\x21\x4a\xa3\x84\x6e\xc7\x87\x4b\x99\x70\xc2\x63\x84\xde\x58\x4e\x70\xe6\x17\x86\x7f\x21\x39\xe1\x3e\x5c\xb4\xfb\xe7\xe5\x4e\x2b\xa5\x9c\x40\xed\xa3\xe0\x23\xf8\x55\xf0\xd1\xae\x0f\x84\x27\xa0\x53\x84\xb3\xf3\x1e\xdc\x28\x94\xd0\x12\x59\x36\xe6\x54\xcf\xb6\xc5\xc9\x2c\x35\x1e\x24\x22\x23\x94\xab\x08\x6e\x3d\x65\x03\xed\xf9\xe0\x55\xd9\xf0\xee\x9e\x10\x66\x64\x80\x2c\x02\xaf\xdd\xbb\x3e\x07\x93\x30\xcf\xcd\xbb\xd8\xdc\xde\x75\xa8\x7d\x11\x8e\x8a\x23\x7e\x17\x94\xce\x33\x32\x42\x39\xfb\x1b\x22\xa9\x27\xc6\x32\xc6\xa8\x44\x14\xdd\x8b\xc7\x03\x4c\x0c\x64\x4e\x9b\x15\x70\x9a\xed\xdf\x2c\x58\xda\x33\x34\x7f\x75\x46\xf2\x9c\xf2\x91\x0f\x4d\x94\x62\x24\x69\xe2\xc3\xf9\x59\xd7\xfc\xba\xf2\xe1\xe6\xea\x7a\xaf\x6d\x1e\xfe\x1c\x18\x56\xf2\x59\x46\xed\x07\xa4\xf4\xe5\xad\xa1\x45\xf8\x84\x54\x69\xed\x98\x0e\xfe\xe9\x4c\x92\xd9\x27\x53\x49\x7f\xc3\xf4\x3a\x1b\xc5\x53\x09\xc8\xc8\xf4\x0f\x61\x66\x9c\xfd\xa3\x1f\x52\xb3\x26\x78\x7f\x55\x82\x8b\x8d\xb5\x04\xb7\x19\x4e\x88\x09\x7a\x99\xe3\x8f\x94\x31\x33\x65\xfc\x75\xd9\x2d\x72\x58\xd4\x62\xf7\xac\x69\xb2\xd8\x6b\xfa\xd0\x3a\x3b\x6f\x5e\xfb\xd0\x85\x6b\x31\xa0\x5c\x09\xee\x43\xb7\xd5\x6e\x1a\xa6\x4e\xcf\x87\x4b\xf3\x70\xd1\xb4\x95\x9b\x10\x4d\x94\x9e\x49\x64\x0a\xb9\x0f\xd7\xf4\xf3\x17\x75\x4f\x34\x4a\xa5\x09\xd1\x3e\x9c\xf5\x0a\x3e\x46\x78\xe2\xc3\x87\xb6\x11\xab\xee\x90\x95\x42\x1e\x9b\x42\x8e\x5f\x52\xc8\x0f\x38\x3a\x38\xfc\x21\x38\x7a\x48\xdb\x77\x80\x69\x75\xeb\xce\x39\x77\x98\x08\x34\xa0\x13\x64\x24\xaf\xd9\xa9\xc8\x87\xf9\x62\x7d\xa6\xcc\x48\x1e\x90\x24\x69\x09\xae\xa5\x60\x0e\x14\x76\x82\xb8\xa0\x55\x23\xbd\xaa\x6d\xc1\xc6\xe3\xd4\xe9\x15\xdf\x36\xec\x94\xb8\x2d\xd2\x95\xb6\xe8\xe1\x69\x0b\xa3\x41\xec\x6f\x11\xd4\x9f\x20\xff\xfe\x34\xf9\x8f\x08\xf6\x37\xa9\x8b\xb5\x20\xee\xda\x20\xae\xee\xd1\x21\xd4\xaa\x59\x2f\x18\x98\x31\x5f\xed\xba\x4a\xc3\x8e\x79\xd0\x80\x35\xde\x8d\xb4\x40\x31\x74\xf6\x2f\x4f\x2f\x23\x10\xb9\xa6\x19\xfd\x8a\x16\xa6\xb9\xc4\x09\x72\x0d\x9f\xa9\xd6\x28\x5d\xc3\x72\x1e\x0c\xa9\x7e\x6f\xf5\xd6\x1c\x73\xa4\x59\xb7\x83\xdb\xfd\x3b\x1f\x06\xb7\xf5\xbb\x6d\xe8\xbb\x1d\xdc\x1e\x5a\x96\x83\xbb\xbb\x4d\x8e\xbb\x75\x78\x2c\xc0\x14\x9e\x7b\x7a\xb7\x07\xb2\x9d\xa6\xb6\x21\xf6\x64\x20\x33\x32\xfd\x2a\x44\x06\x3b\x3b\xb0\xb1\x77\x62\x15\x8f\x50\x9b\x12\xac\xed\x3a\xa3\x6d\x38\x54\xc9\xb1\xae\xe0\xd9\x93\x98\x54\x31\xd3\x71\x6d\x6d\x3c\xf6\xdf\x07\x45\xe6\x41\xdd\xd6\xef\x7c\xa7\x69\xca\x8b\xde\xf0\x68\x97\x72\x7b\xf0\x6f\xdf\x9c\x18\x7c\x68\x26\x1b\x9e\x7e\xfb\x06\x07\x87\x6b\x87\xdd\xdd\x40\x4c\x59\x9f\xc5\x19\xed\xb9\x37\x79\xac\x4b\x38\x42\x9e\x94\xdf\x49\x1e\x6c\x15\xbb\xc7\x9b\xb9\x58\x62\xdf\xd9\x59\x12\x0e\x14\x12\x19\xa7\xb5\xf0\xbf\xf3\x70\x17\x1a\x8d\x06\xd4\x9d\x29\x58\x31\x67\x05\x73\x22\x15\x2e\xe9\x75\x7d\x79\x0d\x43\xb8\x20\x5f\x10\xd4\x58\x22\xe8\x94\x2a\xa0\xca\x36\xe8\x42\xac\xf8\x0e\xa6\xb4\x1c\xc7\xda\x70\xdc\x23\xe0\x34\xc7\x58\x6f\x6a\x5a\xf5\x21\x60\xc8\x47\x3a\x5d\x75\xe5\xb6\x7e\x17\x20\xc3\x0c\xb9\x76\x17\x2d\xbc\xa4\xfd\x55\x6b\xb5\x0d\x1e\xbd\x29\xec\x6c\x6b\x85\xd5\x5a\x6a\x89\x5a\xe4\x4f\xf6\xc3\xd5\xc8\xaa\x68\xc9\x93\x67\x24\x62\xc1\x18\xc9\x15\xf6\x68\x96\x33\x8c\x40\xcb\x31\x3e\x23\x92\xa0\xc6\x58\xf7\xb4\x44\x1d\xa7\x98\x14\x32\xdb\x45\xd6\x5b\x65\xb5\x5c\x39\x5e\x7c\x4f\x27\x30\x23\xb8\x5a\xe9\x03\x76\xa7\xca\xe7\xbb\x2d\xe8\x33\x80\x37\x8c\x37\xd7\x9d\x65\xb4\x5b\xd9\xdb\xfa\x9d\xe3\x54\x46\x62\xac\x87\x86\xc3\xd6\xfd\x58\x0f\xcf\x24\x4d\x6a\xa5\x16\x67\xad\x9b\x25\xd1\xbe\x3f\xb1\x29\x7c\xb3\x25\xa8\xb9\xa0\x66\xe0\x68\x8d\xa5\x12\xf2\xc9\xf0\x67\x62\xac\xd0\x4e\x27\x13\xc2\x22\x38\x3a\x32\x15\x71\x8a\x8c\xcc\x60\x28\x64\x41\xce\xc4\x04\xc1\xde\x05\xca\x75\x67\x39\x7c\x2b\xfd\x32\x38\xee\x8b\x5a\x46\xf2\xcd\x0e\x51\x85\x80\xf2\xa1\x30\x58\x27\x94\x97\x0d\xf0\x54\x64\x37\x9a\xb2\x20\x96\x48\x34\x6e\x9b\x42\x13\x3a\xd9\x86\x5b\xcf\xe8\xdc\x46\x7c\xd4\x3f\x42\x5d\x0c\x21\x0e\x28\xb9\x9c\xb2\x6f\x59\x70\xaa\xbb\x22\x41\xd7\x41\xbd\xf4\xd0\x4c\x5b\x9e\xbf\xea\x93\x4b\x55\xa5\x26\xa0\x9c\xa3\xfc\xd8\xbf\xe8\x40\x63\x8b\x2f\x7d\xd3\x93\xca\x09\x0e\x52\xa2\xc0\xbe\x7c\x07\x03\x15\x75\x0c\x0a\x11\x62\xc1\x95\x60\x68\xf3\x65\xd1\x64\x06\xd4\xc0\x73\x46\xbc\x4a\x8c\xe0\x35\xcf\xe6\x56\x14\x33\xe2\x70\xcc\x63\x03\xa9\x1a\x6e\xed\x4b\xa5\x99\x80\x89\x51\xcd\xbb\xe9\x7f\x78\xb4\x15\x79\x3e\x60\x60\x9e\x9c\xd5\xf7\xec\xfd\x67\x3a\x9e\xb9\x7f\xca\x96\x67\x2e\xd2\xab\xb2\x4b\xd5\x4c\x93\x62\x38\xd4\xde\xff\x31\x25\x1a\xdd\xef\xc5\xb4\x36\x87\x4c\x24\x06\xe5\xb6\x1e\xd6\x3a\xa1\x35\xb2\x65\x02\x5b\xde\x31\x28\x50\x8c\x26\x25\x58\x2b\x1b\x92\xf0\x11\xba\x7a\xaf\xc3\x8a\xeb\x4e\xb6\x6f\x1d\xdd\x97\x75\x04\xfb\x0e\xc2\x84\xb0\x31\xba\x49\x4a\x63\x1e\x41\x3d\xa8\xbb\x88\x42\x52\xe4\x3a\x02\x6f\x82\x52\xd3\x98\x30\xd7\x71\x68\x2c\x78\x8b\x11\xa5\x22\xf0\x18\x92\x21\x43\xbd\x67\x3d\xdc\x33\x14\xef\xd9\x11\xa1\x08\x90\x45\x19\xe5\xf9\x58\x43\x9c\x1a\xe9\xe7\x81\x66\xc7\x09\x03\x80\xcb\x9c\xc4\x54\xcf\x6a\x18\x58\x47\x37\x30\xb4\x75\x2c\xa9\x00\x51\x1c\x61\x59\x6e\xed\x9f\x49\x27\x61\xf9\xba\xf6\x24\x2c\xff\x07\x36\x9f\x83\xb9\xf2\x17\x8b\xd7\xaf\xfe\x17\x00\x00\xff\xff\x09\x76\xa4\xc4\x3c\x1b\x00\x00"), }, "/map_gl.html": &vfsgen۰CompressedFileInfo{ name: "map_gl.html", - modTime: time.Date(2020, 2, 11, 4, 32, 27, 1359921, time.UTC), - uncompressedSize: 4393, + modTime: time.Date(2020, 2, 23, 17, 3, 51, 543869305, time.UTC), + uncompressedSize: 4143, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x58\x5f\x6f\xdb\x36\x10\x7f\xcf\xa7\xb8\xb1\x5b\xe1\x60\xb6\xe4\x7a\xe8\x56\x28\x56\x86\xae\xed\x8a\x0e\x69\x1a\xcc\x29\x86\x2d\x28\x02\x5a\x3a\xdb\xec\x28\x92\x20\x69\x37\xae\xa1\xef\x3e\x50\xf4\x1f\x29\xa2\xed\xa0\x7d\x58\xf9\x12\x93\xc7\x3b\xde\xdd\xef\xc7\xe3\x29\xab\x15\xe4\x38\x61\x02\x81\x14\x54\xdd\x4e\x39\x81\xb2\x3c\x19\x7e\xf7\xf2\xdd\x8b\xeb\xbf\xaf\x5e\xc1\xcc\x16\xfc\xfc\x64\xe8\xff\x00\x00\x0c\x67\x48\x73\xff\xb3\x9a\x16\x68\x29\x64\x33\xaa\x0d\xda\x94\xcc\xed\xa4\xf7\x8c\x40\x5c\xdb\x60\x99\xe5\x78\xbe\x5a\x45\x6f\x5e\x96\x25\x5c\x69\x5c\x30\xfc\x34\x8c\xfd\x72\xd3\xce\x76\xe6\x86\xa0\x05\xa6\xc4\x6d\x56\x52\x5b\xd2\x90\x65\x52\x58\x14\x36\x25\x4c\x30\xcb\x28\xef\x99\x8c\x72\x4c\x9f\x74\x0b\x7a\xc7\x8a\x79\xb1\x9d\xcf\x0d\xea\x6a\x42\xc7\x1c\x53\x21\x77\x66\xea\x2e\x9a\x4c\x33\x65\xc1\xe8\x2c\x25\x33\x6b\x95\x49\xe2\x98\x2a\x16\x59\xc6\xd1\x44\x05\x55\x63\x79\x17\x65\xb2\x88\xfd\xcf\xde\x94\xf7\x3e\x9a\x78\xd1\x8f\x7e\x1a\x44\xfd\xdd\x62\xf4\xd1\x90\xf3\x61\xec\xad\xd5\xcc\x73\x26\xfe\x6d\xb8\x3f\xd3\x38\xf9\xba\x93\x32\x63\x9a\x19\xd1\xc8\x53\x62\xec\x92\xa3\x99\x21\xda\x3d\x71\x3a\xf9\x79\x43\x6f\x2c\xf3\x25\xac\x1a\x4b\x6e\x14\x54\x4f\x99\x48\xa0\x7f\xd6\x12\x29\x9a\xe7\x4c\x4c\x5b\xb2\xf2\xa4\x31\x7d\x54\x50\x15\x30\xac\xa4\x61\x96\x49\x91\x00\x1d\x1b\xc9\xe7\x16\xdb\x27\x58\xa9\x82\x27\x8f\xa5\xb5\xb2\x08\x8a\x3e\xb1\xdc\xce\x12\x78\xd2\xef\xff\x70\xdf\xab\x6d\xf4\x71\x2d\xfc\x61\xec\x69\xec\x27\x2e\x09\xb5\x34\xe5\x6c\x01\x2c\x4f\xdd\x85\x70\x78\xe6\x6c\xd1\xe2\x4a\x33\x89\x0b\xaa\x61\x4c\x0d\x16\x54\x8d\xe4\x5c\x67\x08\x69\x20\x74\xbb\x54\x98\x00\xd1\xd4\x58\xd4\xa4\xdb\x96\x3b\x0e\x24\x70\xd3\x12\xb8\x41\xe2\xd8\xa0\x5e\xa0\x8e\xa8\xce\xa6\xcc\x48\xc1\x99\xc0\x8a\x2a\xcf\x75\xf6\xfa\xcd\x28\xd6\x68\x6c\xb5\x87\x65\x68\xe2\xbf\xa4\xe6\xf9\xed\xb5\x54\xf2\xf6\x2d\x55\xf1\x5b\xaa\x46\x95\x7a\xec\x4e\x89\x57\x9f\xcb\x78\xb5\x2c\xe3\xd5\x5d\x49\x5a\xc7\x7d\x08\xbb\x36\x62\x9f\x31\x81\xc1\xd3\x9f\xdb\x62\x6a\xad\x66\xe3\x79\x85\x6b\xd8\xfb\x6b\x17\x1b\x3c\xce\xa4\x5a\x9e\xc1\x2b\xa3\x19\x3c\x2e\x72\x6a\x66\x67\xe0\x13\x96\xc0\xfb\xd1\xeb\x51\xb7\x12\x75\xe1\xfa\xf9\xe5\xf3\x2e\xbc\xc4\x0b\xa9\x0b\xec\x02\x15\x39\x5c\x5e\x8d\x9a\x9e\x96\x67\x7b\x21\x70\x30\x07\x11\x60\x79\x02\x64\xbd\x2b\x04\xc0\x11\x80\xcc\xda\xd5\x03\x26\x0a\x26\x3e\xcb\x8a\xa3\x01\x19\xbd\xf3\xb2\xc1\xe0\x7e\x24\xad\x50\x0a\xaa\x9a\xf1\xf9\xab\x3f\xe5\xd1\xdc\x32\x1e\x4d\xd1\xfe\x31\x7a\x77\xd9\x21\xab\x55\xf4\xfe\xcf\x8b\xb2\x24\x5d\x98\xcc\x45\xe6\x10\xe8\x60\xb7\xc2\xeb\xa3\x91\xe2\x34\x90\x04\x67\x9e\xd3\x25\x6a\x03\x29\xdc\x7c\x08\x5c\xbf\xb5\x72\xb4\xc0\xcc\x4a\x7d\xeb\x37\x47\x13\xa9\x5f\xd1\x6c\xd6\xd9\x9e\x63\x74\x76\xb1\xd4\x5d\x60\xa1\x53\xdc\x58\x2b\xaa\xb9\x99\x75\xc2\x3b\x60\x03\x8a\x5c\xa0\xe6\x74\xd9\x53\x92\x2f\x7b\x04\x7e\x04\xd6\xce\xdf\x66\x6c\x61\x58\x2b\x05\x60\xd8\x0c\xe2\xf7\xf6\x2a\x4f\x48\x02\xde\xe5\x88\xe5\xfb\x55\x26\x8c\x5b\xd4\x09\xdc\x90\x34\x25\x5d\x20\xdf\x3b\x56\xb8\x1f\x57\x92\x2f\xa7\x52\x90\xc0\xf5\xd8\x66\xce\x13\x68\xc2\x38\x3f\xe0\x94\xa2\x4c\xd8\x64\x4f\xce\xb6\x9e\x3b\x23\xbd\x4c\x72\xe9\xfc\x26\x52\x53\x31\xc5\x03\x46\x77\x3a\x52\xd1\x8c\xd9\x25\x49\xa0\x1f\x3d\x7d\x90\xc2\xdc\xba\x5a\xb2\x3b\x4c\x63\xde\xae\x0a\x9b\x51\x06\x25\xe5\xe9\x3d\x12\x6f\xc6\x17\x70\xa0\x72\xe6\xdb\xe4\xc0\x05\x13\x38\xb2\x9a\x89\xe9\x03\x68\xe0\xe2\xf8\x6a\x1a\xb4\x91\x39\x02\x69\xa5\x50\xe7\xc0\x2f\xc7\x48\x50\x69\x54\x0f\x28\x49\x60\xf0\x3f\x02\xaf\x24\x13\xf6\x1b\x45\xfe\xca\xf9\xf6\x00\xd0\x33\xa6\x33\xfe\xf5\xb0\x7b\x33\x3d\x4d\x73\x36\x37\x24\x81\xc0\xa3\x1b\xda\xbf\x25\xca\xa3\xdf\xfb\xfd\x63\x4c\x59\xeb\xec\xb8\xf2\xe4\x0b\xb0\x0f\xad\x05\x5e\x3e\x05\x29\x08\xfc\xb4\x7b\xc6\xde\x52\xb5\x87\x16\xae\xb7\xa7\x4c\x38\x14\x48\xf8\x91\x75\xa3\xea\xe5\x0e\xa5\x71\x81\xda\x54\x7d\xe6\xb3\x63\x5c\x32\xc7\xd0\x58\x3f\xf7\x49\xb3\xc7\x3b\x9c\xdd\x35\x3b\x8f\x99\x86\x1d\x73\xfc\x6b\x7b\x04\x34\xd8\xf5\x89\xdb\x67\xba\x9a\x1f\x57\xdb\xb6\x26\x5b\xc5\xf5\xca\x03\x54\x37\x9d\xcb\x4e\xd5\xaf\x1c\xd4\x0c\x93\xa6\x92\xec\x3f\xd2\x97\x8d\x04\x6e\xea\xcd\xdc\x87\x28\x93\x22\xa3\xb6\xe3\xa5\xa7\x61\x36\x86\x8d\x16\xf4\xee\x9f\xca\xf7\xba\xc1\x8d\xff\x61\x95\x7b\xb1\x8e\xe5\x5c\xe4\x06\x7e\x85\x3e\xd4\x56\x33\x14\x16\xf5\xcd\x60\x4f\x49\xf0\xe2\xd6\xfe\xc8\x70\x96\x61\xa7\xdf\x85\x41\x3b\x8a\xe0\xdd\x61\x13\xe8\xdc\x73\x65\x5f\xbf\x55\xf5\xc0\xde\xd9\x74\xcf\x67\x04\xd4\xfb\x3b\xbf\xb7\xee\xd2\x81\xf2\x16\x54\x1a\x74\x5b\x02\x8e\x62\x6a\x67\x61\x88\x02\xcd\x26\xf8\xf2\x10\x4d\x98\xfd\xad\x32\xd0\x59\xc7\x18\x28\x2d\xc1\xc2\x12\xd1\x3c\x7f\x21\x85\xd5\x92\x77\x1a\x15\xe6\x92\x2e\xd8\x94\x56\x9d\xea\xe9\x3d\x6b\xf5\xc2\xd5\xfc\x5e\x1f\xc6\xfe\x5b\x70\x18\xfb\xff\x78\xac\x56\x80\x22\x87\xb2\x3c\xf9\x2f\x00\x00\xff\xff\x88\x40\x3b\x63\x29\x11\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x57\x6d\x6f\xdb\x36\x10\xfe\x9e\x5f\x71\x63\xb7\xc2\xc1\x6c\xc9\x31\xd0\x2d\x50\xac\x0c\x59\x93\x15\x1d\x92\x34\x80\x33\x0c\x5b\x10\x04\x34\x75\xb6\xd9\x51\x24\x41\xd2\x4e\x14\x41\xff\x7d\xa0\xe4\xd8\x96\x25\x3b\x68\x07\x0c\xd5\x17\xfb\xde\x5f\x9e\xa3\x78\xca\x73\x48\x70\xc2\x25\x02\x49\xa9\x7e\x98\x0a\x02\x45\x71\x30\xfc\xee\xfc\xd3\xfb\xdb\xbf\x6e\x2e\x60\xe6\x52\x71\x7a\x30\xac\x7e\x00\x00\x86\x33\xa4\x49\xf5\xb7\x24\x53\x74\x14\xd8\x8c\x1a\x8b\x2e\x26\x73\x37\xe9\x1d\x13\x08\x37\x14\x1c\x77\x02\x4f\xf3\x3c\xf8\x78\x5e\x14\x70\x63\x70\xc1\xf1\x71\x18\x56\xec\xba\x9f\x15\xe5\x1f\x49\x53\x8c\x89\x57\xd6\xca\x38\x52\x93\x31\x25\x1d\x4a\x17\x13\x2e\xb9\xe3\x54\xf4\x2c\xa3\x02\xe3\xa3\x6e\x4a\x9f\x78\x3a\x4f\x57\xf4\xdc\xa2\x29\x09\x3a\x16\x18\x4b\xb5\x76\xb3\x99\xa2\x65\x86\x6b\x07\xd6\xb0\x98\xcc\x9c\xd3\x36\x0a\x43\xaa\x79\xe0\xb8\x40\x1b\xa4\x54\x8f\xd5\x53\xc0\x54\x1a\x56\x7f\x7b\x53\xd1\xfb\x6c\xc3\xc5\x51\x70\x1c\xf4\xd7\xbc\xe0\xb3\x25\xa7\xc3\xb0\x72\xb6\xe1\x5d\x70\xf9\x4f\x2d\xfb\x99\xc1\xc9\x7f\x0a\xc4\xac\xad\xf7\xc3\xa0\x88\x89\x75\x99\x40\x3b\x43\x74\x3b\xaa\xf4\xf2\xd3\x9a\xdd\x58\x25\x19\xe4\x35\x96\x7f\x52\x6a\xa6\x5c\x46\xd0\x3f\x69\x88\x34\x4d\x12\x2e\xa7\x0d\x59\x71\x50\x23\xdf\xa4\x54\xb7\x38\xd6\xca\x72\xc7\x95\x8c\x80\x8e\xad\x12\x73\x87\xcd\x08\x4e\xe9\xd6\xc8\x63\xe5\x9c\x4a\x5b\x45\x8f\x3c\x71\xb3\x08\x8e\xfa\xfd\x1f\xb6\xb3\x5a\x55\x1f\x6e\x94\x3f\x0c\xab\x21\xae\x08\xdf\x84\x8d\x36\x25\x7c\x01\x3c\x89\xfd\x71\xf0\x70\x26\x7c\xd1\x98\x94\x7a\x13\x17\xd4\xc0\x98\x5a\x4c\xa9\x1e\xa9\xb9\x61\x08\x71\x4b\xe9\x2e\xd3\x18\x01\x31\xd4\x3a\x34\xa4\xdb\x94\xfb\x11\x88\xe0\xae\x21\xf0\x0f\x09\x43\x8b\x66\x81\x26\xa0\x86\x4d\xb9\x55\x52\x70\x89\xe5\xa4\x9c\x19\xf6\xe1\xe3\x28\x34\x68\x5d\xa9\xc3\x19\xda\xf0\x4f\x65\x44\xf2\x70\xab\xb4\x7a\xb8\xa2\x3a\xbc\xa2\x7a\x54\x9a\x87\x3e\x4a\x98\x3f\x17\x61\x9e\x15\x61\xfe\x54\x90\x46\xb8\xfb\xf6\xd4\x46\xfc\x19\x23\x18\xbc\xfb\xa9\x29\xa6\xce\x19\x3e\x9e\x97\xb8\xb6\x67\x7f\xeb\x6b\x83\xb7\x4c\xe9\xec\x04\x2e\xac\xe1\xf0\x36\x4d\xa8\x9d\x9d\x40\xd5\xb0\x08\xfe\x18\x7d\x18\x75\x4b\x51\x17\x6e\xcf\xae\xcf\xba\x70\x8e\x97\xca\xa4\xd8\x05\x2a\x13\xb8\xbe\x19\xd5\x33\x2d\x4e\x76\x42\xe0\x61\x6e\x45\x80\x27\x11\x90\xa5\x56\x1b\x00\xaf\x00\x64\x97\xa9\xee\x71\x91\x72\xf9\xac\xca\x19\x6d\x91\xd1\xa7\x4a\x36\x18\x6c\x57\xd2\x28\x25\xa5\xba\x5e\x5f\x18\xc2\xa5\xa2\x09\x18\xfa\x08\xbf\x8f\x3e\x5d\x43\xc2\x0d\x32\x27\x32\x98\x18\x95\x82\xc3\x54\x0b\xea\xb0\xe1\xc8\x23\x57\xea\xc7\x90\xe7\xc1\xed\x92\x2a\xda\x62\x0a\x9a\xa1\xb1\x10\xc3\xdd\x7d\x3d\xf6\x8b\x8f\x60\x81\xcc\x29\xf3\x50\x29\x06\x13\x65\x2e\x28\x9b\x75\x26\x73\xc9\x3c\xf4\x1d\x6b\xd8\x65\x66\xba\xc0\x0f\x5b\x7a\xbf\x34\xd2\x73\x3b\xeb\x34\xa5\xf0\x82\x8e\x5a\xa0\x11\x34\xeb\x69\x25\xb2\x1e\x81\x1f\x81\x37\x1b\x59\xc3\x62\x69\xd0\x82\x85\x7f\x48\xa5\xd7\x2b\xa3\x93\x08\xaa\x14\x03\x9e\xb4\xab\x4f\xb8\x70\x68\x22\xb8\x23\x71\x4c\xba\x40\xbe\xf7\x23\xe1\xff\xdc\x28\x91\x4d\x95\x24\x2d\x67\x03\xd6\x93\x33\xe1\x42\xec\x48\x44\x53\x2e\x5d\xd4\xd2\x97\x55\xa6\xde\xb8\xc7\x94\x50\x3e\x4f\xa2\x0c\x95\x53\xdc\xe1\x6c\xad\xaf\x34\x65\xdc\x65\x24\x82\x7e\xf0\xee\x55\xe5\xb9\xf3\x2f\x8d\x75\x10\x83\x49\xf3\xf8\x43\xed\xad\xb9\xe2\x1c\x6e\x4d\x0c\x7c\x39\xa6\x65\xf0\x6f\x07\xd3\x4b\x2e\x71\xe4\x0c\x97\xd3\x57\x60\xf5\x79\x7f\x35\xac\xcd\x8e\xef\x81\xa9\x54\xde\xc4\xf4\xe7\x7d\xa0\x96\xda\xe5\xcd\x47\x22\x18\xfc\x8f\x40\x6a\xc5\xa5\xfb\x86\x90\xbc\xf1\xf9\xbc\x02\x22\xe3\x86\x89\xaf\x87\xb1\x32\xef\x19\x9a\xf0\xb9\x25\x11\xb4\xdc\x82\xdb\xba\x2b\xd0\xdf\xfc\xd6\xef\xef\x43\x7d\xa9\xbf\xc6\xfd\xe8\x0b\xb0\xdc\xa6\x6b\x0c\xbf\x83\xc5\x20\xf1\x11\xaa\xd5\x71\x2a\x82\x2b\xaa\x5b\x20\xf6\xeb\x34\xe5\xd2\x77\x97\xb4\xdf\x6c\xe5\xf2\xb4\xab\x45\x0b\x34\xb6\x5c\xea\x8e\xf7\xcd\x83\xdd\xd7\xe1\xe5\x9d\x1a\xd5\x17\xa9\xdd\x5d\x5b\x4e\xd6\x3e\x97\xb0\x46\xbf\xba\xb9\xf6\x80\x00\xeb\x05\x6c\x75\xdd\x95\xf4\x7e\x93\xd5\x7d\xbf\x32\x5a\x72\x5e\x31\x7b\x59\x05\xd6\x66\x15\x67\xa7\x55\x13\xfc\x92\xdb\x1e\xa6\x3a\xce\x11\xdc\x6d\x6e\x44\xf7\x01\x53\x92\x51\xd7\xa9\xa4\x87\xcd\x69\x6a\x5d\x59\xfe\x2e\xf3\xdc\x74\xf4\x92\x6b\x53\x7d\xab\xa6\xb1\x9a\xcb\xc4\xc2\x2f\xd0\x87\x0d\x2e\x43\xe9\xd0\xdc\x0d\x5a\x8e\x6b\x25\x6a\xe8\x06\x56\x70\x86\x9d\x7e\x17\x06\x87\xfb\xe7\x9d\x4f\xa0\xb3\x15\xbe\x6d\x17\x29\x97\xc5\x2a\xb9\x78\xc7\xbe\xbd\xe5\x65\x33\x85\x1d\xaf\x99\x56\x83\x41\xb7\x21\x10\x28\xa7\x6e\xd6\x6c\xff\x7d\xf3\xab\x26\xa5\x3a\x98\x70\xf7\x6b\x69\xd8\x59\xd6\xb3\xf7\xab\xcb\x5b\xd0\x24\x79\xaf\xa4\x33\x4a\x74\x6a\x27\xff\x9a\x2e\xf8\x94\xfa\x4d\xed\x45\x7c\xb8\xe1\xac\xfe\xd9\x3a\x0c\xab\x6f\xa2\x61\x58\x7d\xf7\xe7\x39\xa0\x4c\xa0\x28\x0e\xfe\x0d\x00\x00\xff\xff\x99\x18\x26\x4e\x2f\x10\x00\x00"), }, "/static": &vfsgen۰DirInfo{ name: "static", diff --git a/handlers/handlers.go b/handlers/handlers.go deleted file mode 100644 index 8671848..0000000 --- a/handlers/handlers.go +++ /dev/null @@ -1,502 +0,0 @@ -package handlers - -//go:generate go run -tags=dev assets_generate.go - -import ( - "bytes" - "crypto/hmac" - "crypto/sha1" - "crypto/subtle" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "html/template" - "io" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/shurcooL/httpfs/html/vfstemplate" - log "github.com/sirupsen/logrus" - - "github.com/consbio/mbtileserver/mbtiles" -) - -const maxSignatureAge = time.Duration(15) * time.Minute - -// scheme returns the underlying URL scheme of the original request. -func scheme(r *http.Request) string { - if r.TLS != nil { - return "https" - } - if scheme := r.Header.Get("X-Forwarded-Proto"); scheme != "" { - return scheme - } - if scheme := r.Header.Get("X-Forwarded-Protocol"); scheme != "" { - return scheme - } - if ssl := r.Header.Get("X-Forwarded-Ssl"); ssl == "on" { - return "https" - } - if scheme := r.Header.Get("X-Url-Scheme"); scheme != "" { - return scheme - } - return "http" -} - -type handlerFunc func(http.ResponseWriter, *http.Request) (int, error) - -func wrapGetWithErrors(ef func(error), hf handlerFunc) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - status := http.StatusMethodNotAllowed - http.Error(w, http.StatusText(status), status) - return - } - status, err := hf(w, r) // run the handlerFunc and obtain the return codes - if err != nil && ef != nil { - ef(err) // handle the error with the supplied function - } - // in case it's an error, write the status code for the requester - if status >= 400 { - http.Error(w, http.StatusText(status), status) - } - }) -} - -// hmacAuth wraps handler functions to provide request authentication. If -// -s/--secret-key is provided at startup, this function will enforce proper -// request signing. Otherwise, it will simply pass requests through to the -// handler. -func hmacAuth(hf handlerFunc, secretKey string, serviceId string) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - // If secret key isn't set, allow all requests - if secretKey == "" { - return hf(w, r) - } - - query := r.URL.Query() - - rawSignature := query.Get("signature") - if rawSignature == "" { - rawSignature = r.Header.Get("X-Signature") - } - if rawSignature == "" { - return 400, errors.New("No signature provided") - } - - rawSignDate := query.Get("date") - if rawSignDate == "" { - rawSignDate = r.Header.Get("X-Signature-Date") - } - if rawSignDate == "" { - return 400, errors.New("No signature date provided") - } - - signDate, err := time.Parse(time.RFC3339Nano, rawSignDate) - if err != nil { - return 400, errors.New("Signature date is not valid RFC3339") - } - if time.Now().Sub(signDate) > maxSignatureAge { - return 400, errors.New("Signature is expired") - } - - signatureParts := strings.SplitN(rawSignature, ":", 2) - if len(signatureParts) != 2 { - return 400, errors.New("Signature does not contain salt") - } - salt, signature := signatureParts[0], signatureParts[1] - - key := sha1.New() - key.Write([]byte(salt + secretKey)) - hash := hmac.New(sha1.New, key.Sum(nil)) - message := fmt.Sprintf("%s:%s", rawSignDate, serviceId) - hash.Write([]byte(message)) - checkSignature := base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) - - if subtle.ConstantTimeCompare([]byte(signature), []byte(checkSignature)) == 1 { - return hf(w, r) - } - - return 400, errors.New("Invalid signature") - } -} - -// ServiceInfo consists of two strings that contain the image type and a URL. -type ServiceInfo struct { - ImageType string `json:"imageType"` - URL string `json:"url"` -} - -// ServiceSet is the base type for the HTTP handlers which combines multiple -// mbtiles.DB tilesets. -type ServiceSet struct { - tilesets map[string]*mbtiles.DB - templates *template.Template - Domain string - Path string - secretKey string -} - -// New returns a new ServiceSet. Use AddDBOnPath to add a mbtiles file. -func New() *ServiceSet { - s := &ServiceSet{ - tilesets: make(map[string]*mbtiles.DB), - templates: template.New("_base_"), - } - - // load templates - vfstemplate.ParseFiles(Assets, s.templates, "map.html", "map_gl.html") - - return s -} - -// AddDBOnPath interprets filename as mbtiles file which is opened and which will be -// served under "/services/" by Handler(). The parameter urlPath may not be -// nil, otherwise an error is returned. In case the DB cannot be opened the returned -// error is non-nil. -func (s *ServiceSet) AddDBOnPath(filename string, urlPath string) error { - var err error - if urlPath == "" { - return fmt.Errorf("path parameter may not be empty") - } - ts, err := mbtiles.NewDB(filename) - if err != nil { - return fmt.Errorf("Invalid mbtiles file %q: %v", filename, err) - } - s.tilesets[urlPath] = ts - return nil -} - -// NewFromBaseDir returns a ServiceSet that combines all .mbtiles files under -// the directory at baseDir. The DBs will all be served under their relative paths -// to baseDir. If baseDir does not exist, is not a valid path, or does not contain -// any valid .mbtiles files, an empty ServiceSet will be returned along with the error. -func NewFromBaseDir(baseDir string) (*ServiceSet, error) { - s := New() - - var filenames []string - err := filepath.Walk(baseDir, func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if _, err := os.Stat(p + "-journal"); err == nil { - // Don't try to load .mbtiles files that are being written - return nil - } - if ext := filepath.Ext(p); ext == ".mbtiles" { - filenames = append(filenames, p) - } - return nil - }) - if err != nil { - return s, err - } - - if len(filenames) == 0 { - return s, fmt.Errorf("no tilesets found in %s", baseDir) - } - - for _, filename := range filenames { - subpath, err := filepath.Rel(baseDir, filename) - if err != nil { - return nil, fmt.Errorf("unable to extract URL path for %q: %v", filename, err) - } - e := filepath.Ext(filename) - p := filepath.ToSlash(subpath) - id := p[:len(p)-len(e)] - err = s.AddDBOnPath(filename, id) - if err != nil { - log.Warnf("%s\nThis tileset will not be available from the API", err.Error()) - } - } - return s, nil -} - -// SetRequestAuthKey sets the secret key used to verify that incoming requests -// are authorized. If blank, no authorization is performed. -func (s *ServiceSet) SetRequestAuthKey(key string) { - s.secretKey = key -} - -// Size returns the number of tilesets in this ServiceSet -func (s *ServiceSet) Size() int { - return len(s.tilesets) -} - -// rootURL returns the root URL of the service. If s.Domain is non-empty, it -// will be used as the hostname. If s.Path is non-empty, it will be used as a -// prefix. -func (s *ServiceSet) rootURL(r *http.Request) string { - host := r.Host - if len(s.Domain) > 0 { - host = s.Domain - } - - root := fmt.Sprintf("%s://%s", scheme(r), host) - if len(s.Path) > 0 { - root = fmt.Sprintf("%s/%s", root, s.Path) - } - - return root -} - -func (s *ServiceSet) listServices(w http.ResponseWriter, r *http.Request) (int, error) { - rootURL := fmt.Sprintf("%s%s", s.rootURL(r), r.URL) - services := []ServiceInfo{} - for id, tileset := range s.tilesets { - services = append(services, ServiceInfo{ - ImageType: tileset.TileFormatString(), - URL: fmt.Sprintf("%s/%s", rootURL, id), - }) - } - bytes, err := json.Marshal(services) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("cannot marshal services JSON: %v", err) - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(bytes) - return http.StatusOK, err -} - -func (s *ServiceSet) tileJSON(id string, db *mbtiles.DB, mapURL bool) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - query := "" - if r.URL.RawQuery != "" { - query = "?" + r.URL.RawQuery - } - - svcURL := fmt.Sprintf("%s%s", s.rootURL(r), r.URL.Path) - imgFormat := db.TileFormatString() - out := map[string]interface{}{ - "tilejson": "2.1.0", - "id": id, - "scheme": "xyz", - "format": imgFormat, - "tiles": []string{fmt.Sprintf("%s/tiles/{z}/{x}/{y}.%s%s", svcURL, imgFormat, query)}, - } - if mapURL { - out["map"] = fmt.Sprintf("%s/map", svcURL) - } - metadata, err := db.ReadMetadata() - if err != nil { - return http.StatusInternalServerError, err - } - for k, v := range metadata { - switch k { - // strip out values above - case "tilejson", "id", "scheme", "format", "tiles", "map": - continue - - // strip out values that are not supported or are overridden below - case "grids", "interactivity", "modTime": - continue - - // strip out values that come from TileMill but aren't useful here - case "metatile", "scale", "autoscale", "_updated", "Layer", "Stylesheet": - continue - - default: - out[k] = v - } - } - - if db.HasUTFGrid() { - out["grids"] = []string{fmt.Sprintf("%s/tiles/{z}/{x}/{y}.json%s", svcURL, query)} - } - bytes, err := json.Marshal(out) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("cannot marshal service info JSON: %v", err) - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(bytes) - return http.StatusOK, err - } -} - -// executeTemplates first tries to find the template with the given name for -// the ServiceSet. If that fails because it is not available, an HTTP status -// Internal Server Error is returned. -func (s *ServiceSet) executeTemplate(w http.ResponseWriter, name string, data interface{}) (int, error) { - t := s.templates.Lookup(name) - if t == nil { - return http.StatusInternalServerError, fmt.Errorf("template not found %q", name) - } - buf := &bytes.Buffer{} - err := t.Execute(buf, data) - if err != nil { - return http.StatusInternalServerError, err - } - _, err = io.Copy(w, buf) - return http.StatusOK, err -} - -func (s *ServiceSet) serviceHTML(id string, db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - p := struct { - URL string - ID string - }{ - fmt.Sprintf("%s%s", s.rootURL(r), strings.TrimSuffix(r.URL.Path, "/map")), - id, - } - - switch db.TileFormat() { - default: - return s.executeTemplate(w, "map", p) - case mbtiles.PBF: - return s.executeTemplate(w, "map_gl", p) - - } - } -} - -type tileCoord struct { - z uint8 - x, y uint64 -} - -// tileCoordFromString parses and returns tileCoord coordinates and an optional -// extension from the three parameters. The parameter z is interpreted as the -// web mercator zoom level, it is supposed to be an unsigned integer that will -// fit into 8 bit. The parameters x and y are interpreted as longitude and -// latitude tile indices for that zoom level, both are supposed be integers in -// the integer interval [0,2^z). Additionally, y may also have an optional -// filename extension (e.g. "42.png") which is removed before parsing the -// number, and returned, too. In case an error occurred during parsing or if the -// values are not in the expected interval, the returned error is non-nil. -func tileCoordFromString(z, x, y string) (tc tileCoord, ext string, err error) { - var z64 uint64 - if z64, err = strconv.ParseUint(z, 10, 8); err != nil { - err = fmt.Errorf("cannot parse zoom level: %v", err) - return - } - tc.z = uint8(z64) - const ( - errMsgParse = "cannot parse %s coordinate axis: %v" - errMsgOOB = "%s coordinate (%d) is out of bounds for zoom level %d" - ) - if tc.x, err = strconv.ParseUint(x, 10, 64); err != nil { - err = fmt.Errorf(errMsgParse, "first", err) - return - } - if tc.x >= (1 << z64) { - err = fmt.Errorf(errMsgOOB, "x", tc.x, tc.z) - return - } - s := y - if l := strings.LastIndex(s, "."); l >= 0 { - s, ext = s[:l], s[l:] - } - if tc.y, err = strconv.ParseUint(s, 10, 64); err != nil { - err = fmt.Errorf(errMsgParse, "y", err) - return - } - if tc.y >= (1 << z64) { - err = fmt.Errorf(errMsgOOB, "y", tc.y, tc.z) - return - } - return -} - -// tileNotFoundHandler writes the default response for a non-existing tile of type f to w -func tileNotFoundHandler(w http.ResponseWriter, f mbtiles.TileFormat) (int, error) { - var err error - switch f { - case mbtiles.PNG, mbtiles.JPG, mbtiles.WEBP: - // Return blank PNG for all image types - w.Header().Set("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - _, err = w.Write(BlankPNG()) - case mbtiles.PBF: - // Return 204 - w.WriteHeader(http.StatusNoContent) - default: - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - fmt.Fprint(w, `{"message": "Tile does not exist"}`) - } - return http.StatusOK, err // http.StatusOK doesn't matter, code was written by w.WriteHeader already -} - -func (s *ServiceSet) tiles(db *mbtiles.DB) handlerFunc { - return func(w http.ResponseWriter, r *http.Request) (int, error) { - // split path components to extract tile coordinates x, y and z - pcs := strings.Split(r.URL.Path[1:], "/") - // we are expecting at least "services", , "tiles", , , - l := len(pcs) - if l < 6 || pcs[5] == "" { - return http.StatusBadRequest, fmt.Errorf("requested path is too short") - } - z, x, y := pcs[l-3], pcs[l-2], pcs[l-1] - tc, ext, err := tileCoordFromString(z, x, y) - if err != nil { - return http.StatusBadRequest, err - } - var data []byte - // flip y to match the spec - tc.y = (1 << uint64(tc.z)) - 1 - tc.y - isGrid := ext == ".json" - switch { - case !isGrid: - err = db.ReadTile(tc.z, tc.x, tc.y, &data) - case isGrid && db.HasUTFGrid(): - err = db.ReadGrid(tc.z, tc.x, tc.y, &data) - default: - err = fmt.Errorf("no grid supplied by tile database") - } - if err != nil { - // augment error info - t := "tile" - if isGrid { - t = "grid" - } - err = fmt.Errorf("cannot fetch %s from DB for z=%d, x=%d, y=%d: %v", t, tc.z, tc.x, tc.y, err) - return http.StatusInternalServerError, err - } - if data == nil || len(data) <= 1 { - return tileNotFoundHandler(w, db.TileFormat()) - } - - if isGrid { - w.Header().Set("Content-Type", "application/json") - if db.UTFGridCompression() == mbtiles.ZLIB { - w.Header().Set("Content-Encoding", "deflate") - } else { - w.Header().Set("Content-Encoding", "gzip") - } - } else { - w.Header().Set("Content-Type", db.ContentType()) - if db.TileFormat() == mbtiles.PBF { - w.Header().Set("Content-Encoding", "gzip") - } - } - _, err = w.Write(data) - return http.StatusOK, err - } -} - -// Handler returns a http.Handler that serves the endpoints of the ServiceSet. -// The function ef is called with any occurring error if it is non-nil, so it -// can be used for e.g. logging with logging facilities of the caller. -// When the publish parameter is true, a listing of all available services and -// an endpoint with a HTML slippy map for each service are served by the Handler. -func (s *ServiceSet) Handler(ef func(error), publish bool) http.Handler { - m := http.NewServeMux() - if publish { - m.Handle("/services", wrapGetWithErrors(ef, hmacAuth(s.listServices, s.secretKey, ""))) - } - for id, db := range s.tilesets { - p := "/services/" + id - m.Handle(p, wrapGetWithErrors(ef, hmacAuth(s.tileJSON(id, db, publish), s.secretKey, id))) - m.Handle(p+"/tiles/", wrapGetWithErrors(ef, hmacAuth(s.tiles(db), s.secretKey, id))) - if publish { - m.Handle(p+"/map", wrapGetWithErrors(ef, hmacAuth(s.serviceHTML(id, db), s.secretKey, id))) - } - } - return m -} diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go deleted file mode 100644 index 95c3fb5..0000000 --- a/handlers/handlers_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package handlers_test - -import ( - "testing" -) - -func testFoo(t *testing.T) { - -} diff --git a/handlers/id.go b/handlers/id.go new file mode 100644 index 0000000..4dd6f09 --- /dev/null +++ b/handlers/id.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "crypto/sha1" + "encoding/base64" + "path/filepath" +) + +// SHA1ID generates a URL safe base64 encoded SHA1 hash of the filename. +func SHA1ID(filename string) string { + // generate IDs from hash of full file path + filenameHash := sha1.Sum([]byte(filename)) + return base64.RawURLEncoding.EncodeToString(filenameHash[:]) +} + +// RelativePathID returns a relative path from the basedir to the filename. +func RelativePathID(filename, baseDir string) (string, error) { + subpath, err := filepath.Rel(baseDir, filename) + if err != nil { + return "", err + } + subpath = filepath.ToSlash(subpath) + return subpath[:len(subpath)-len(filepath.Ext(filename))], nil +} diff --git a/handlers/middleware.go b/handlers/middleware.go new file mode 100644 index 0000000..2b2f8d9 --- /dev/null +++ b/handlers/middleware.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/subtle" + "encoding/base64" + "fmt" + "net/http" + "strings" + "time" +) + +// HandlerWrapper is a type definition for a function that takes an http.Handler +// and returns an http.Handler +type HandlerWrapper func(http.Handler) http.Handler + +// maxSignatureAge defines the maximum amount of time, in seconds +// that an HMAC signature can remain valid +const maxSignatureAge = time.Duration(15) * time.Minute + +// HMACAuthMiddleware wraps incoming requests to enforce HMAC signature authorization. +// All requests are expected to have either "signature" and "date" query parameters +// or "X-Signature" and "X-Signature-Date" headers. +func HMACAuthMiddleware(secretKey string, serviceSet *ServiceSet) HandlerWrapper { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + query := r.URL.Query() + + rawSignature := query.Get("signature") + if rawSignature == "" { + rawSignature = r.Header.Get("X-Signature") + } + if rawSignature == "" { + http.Error(w, "No signature provided", http.StatusUnauthorized) + return + } + + rawSignDate := query.Get("date") + if rawSignDate == "" { + rawSignDate = r.Header.Get("X-Signature-Date") + } + if rawSignDate == "" { + http.Error(w, "No signature date provided", http.StatusUnauthorized) + return + } + + signDate, err := time.Parse(time.RFC3339Nano, rawSignDate) + if err != nil { + http.Error(w, "Signature date is not valid RFC3339", http.StatusBadRequest) + return + } + if time.Now().Sub(signDate) > maxSignatureAge { + http.Error(w, "Signature is expired", http.StatusUnauthorized) + return + } + + signatureParts := strings.SplitN(rawSignature, ":", 2) + if len(signatureParts) != 2 { + http.Error(w, "Signature does not contain salt", http.StatusBadRequest) + return + } + salt, signature := signatureParts[0], signatureParts[1] + + tilesetID := serviceSet.IDFromURLPath(r.URL.Path) + + key := sha1.New() + key.Write([]byte(salt + secretKey)) + hash := hmac.New(sha1.New, key.Sum(nil)) + message := fmt.Sprintf("%s:%s", rawSignDate, tilesetID) + hash.Write([]byte(message)) + checkSignature := base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) + + if subtle.ConstantTimeCompare([]byte(signature), []byte(checkSignature)) != 1 { + // Signature is not valid for the requested resource + // either tilesetID does not match in the signature, or date + http.Error(w, "Signature not authorized for resource", http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/handlers/request.go b/handlers/request.go new file mode 100644 index 0000000..a2ddaf2 --- /dev/null +++ b/handlers/request.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "fmt" + "net/http" +) + +// scheme returns the underlying URL scheme of the original request. +func scheme(r *http.Request) string { + if r.TLS != nil { + return "https" + } + if scheme := r.Header.Get("X-Forwarded-Proto"); scheme != "" { + return scheme + } + if scheme := r.Header.Get("X-Forwarded-Protocol"); scheme != "" { + return scheme + } + if ssl := r.Header.Get("X-Forwarded-Ssl"); ssl == "on" { + return "https" + } + if scheme := r.Header.Get("X-Url-Scheme"); scheme != "" { + return scheme + } + return "http" +} + +type handlerFunc func(http.ResponseWriter, *http.Request) (int, error) + +// wrapJSONP writes b (JSON marshalled to bytes) as a JSONP response to +// w if the callback query parameter is present, and writes b as a JSON +// response otherwise. Any error that occurs during writing is returned. +func wrapJSONP(w http.ResponseWriter, r *http.Request, b []byte) (err error) { + callback := r.URL.Query().Get("callback") + + if callback != "" { + w.Header().Set("Content-Type", "application/javascript") + _, err = w.Write([]byte(fmt.Sprintf("%s(%s);", callback, b))) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(b) + return +} diff --git a/handlers/serviceset.go b/handlers/serviceset.go new file mode 100644 index 0000000..6e4b8df --- /dev/null +++ b/handlers/serviceset.go @@ -0,0 +1,258 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "sort" + "strings" +) + +// ServiceSetConfig provides configuration options for a ServiceSet +type ServiceSetConfig struct { + EnableServiceList bool + EnableTileJSON bool + EnablePreview bool + EnableArcGIS bool + RootURL *url.URL + ErrorWriter io.Writer +} + +// ServiceSet is a group of tilesets plus configuration options. +// It provides access to all tilesets from a root URL. +type ServiceSet struct { + tilesets map[string]*Tileset + + enableServiceList bool + enableTileJSON bool + enablePreview bool + enableArcGIS bool + + domain string + rootURL *url.URL + errorWriter io.Writer +} + +// New returns a new ServiceSet. +// If no ServiceSetConfig is provided, the service is initialized with default +// values of ServiceSetConfig. +func New(cfg *ServiceSetConfig) (*ServiceSet, error) { + if cfg == nil { + cfg = &ServiceSetConfig{} + } + + s := &ServiceSet{ + tilesets: make(map[string]*Tileset), + enableServiceList: cfg.EnableServiceList, + enableTileJSON: cfg.EnableTileJSON, + enablePreview: cfg.EnablePreview, + enableArcGIS: cfg.EnableArcGIS, + rootURL: cfg.RootURL, + errorWriter: cfg.ErrorWriter, + } + + return s, nil +} + +// AddTileset adds a single tileset identified by idGenerator using the filename. +// If a service already exists with that ID, an error is returned. +func (s *ServiceSet) AddTileset(filename, id string) error { + + if _, ok := s.tilesets[id]; ok { + return fmt.Errorf("Tileset already exists for ID: %q", id) + } + + path := s.rootURL.Path + "/" + id + ts, err := newTileset(s, filename, id, path) + if err != nil { + return err + } + + s.tilesets[id] = ts + + return nil +} + +// UpdateTileset reloads the Tileset identified by id, if it already exists. +// Otherwise, this returns an error. +// Any errors encountered updating the Tileset are returned. +func (s *ServiceSet) UpdateTileset(id string) error { + ts, ok := s.tilesets[id] + if !ok { + return fmt.Errorf("Tileset does not exist with ID: %q", id) + } + + err := ts.reload() + if err != nil { + return err + } + + return nil +} + +// RemoveTileset removes the Tileset and closes the associated mbtiles file +// identified by id, if it already exists. +// If it does not exist, this returns without error. +// Any errors encountered removing the Tileset are returned. +func (s *ServiceSet) RemoveTileset(id string) error { + ts, ok := s.tilesets[id] + if !ok { + return nil + } + + err := ts.delete() + if err != nil { + return err + } + + // remove from tilesets and router + delete(s.tilesets, id) + + return nil +} + +// HasTileset returns true if the tileset identified by id exists within this +// ServiceSet. +func (s *ServiceSet) HasTileset(id string) bool { + if _, ok := s.tilesets[id]; ok { + return true + } + return false +} + +// Size returns the number of tilesets in this ServiceSet +func (s *ServiceSet) Size() int { + return len(s.tilesets) +} + +// ServiceInfo provides basic information about the service. +type ServiceInfo struct { + ImageType string `json:"imageType"` + URL string `json:"url"` + Name string `json:"name"` +} + +// logError writes to the configured ServiceSet.errorWriter if available +// or the standard logger otherwise. +func (s *ServiceSet) logError(format string, args ...interface{}) { + if s.errorWriter != nil { + s.errorWriter.Write([]byte(fmt.Sprintf(format, args...))) + } else { + log.Printf(format, args...) + } +} + +// serviceListHandler is an http.HandlerFunc that provides a listing of all +// published services in this ServiceSet +func (s *ServiceSet) serviceListHandler(w http.ResponseWriter, r *http.Request) { + rootURL := fmt.Sprintf("%s://%s%s", scheme(r), r.Host, r.URL) + services := []ServiceInfo{} + + // sort ids alpabetically + var ids []string + for id := range s.tilesets { + ids = append(ids, id) + } + sort.Strings(ids) + + for _, id := range ids { + ts := s.tilesets[id] + services = append(services, ServiceInfo{ + ImageType: ts.tileFormatString(), + URL: fmt.Sprintf("%s/%s", rootURL, id), + Name: ts.name, + }) + } + bytes, err := json.Marshal(services) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + s.logError("Error marshalling service list JSON for %v: %v", r.URL.Path, err) + return + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(bytes) + + if err != nil { + s.logError("Error writing service list content: %v", err) + } +} + +// tilesetHandler is an http.HandlerFunc that handles a given tileset +// and associated subpaths +func (s *ServiceSet) tilesetHandler(w http.ResponseWriter, r *http.Request) { + id := s.IDFromURLPath((r.URL.Path)) + + if id == "" { + http.Error(w, "404 page not found", http.StatusNotFound) + return + } + + s.tilesets[id].router.ServeHTTP(w, r) +} + +// IDFromURLPath extracts a tileset ID from a URL Path. +// If no valid ID is found, a blank string is returned. +func (s *ServiceSet) IDFromURLPath(id string) string { + root := s.rootURL.Path + "/" + + if strings.HasPrefix(id, root) { + id = strings.TrimPrefix(id, root) + + // test exact match first + if _, ok := s.tilesets[id]; ok { + return id + } + + // Split on /tiles/ and /map/ and trim /map + i := strings.LastIndex(id, "/tiles/") + if i != -1 { + id = id[:i] + } else if s.enablePreview { + id = strings.TrimSuffix(id, "/map") + + i = strings.LastIndex(id, "/map/") + if i != -1 { + id = id[:i] + } + } + } else if s.enableArcGIS && strings.HasPrefix(id, ArcGISRoot) { + id = strings.TrimPrefix(id, ArcGISRoot) + // MapServer should be a reserved word, so should be OK to split on it + id = strings.Split(id, "/MapServer")[0] + } else { + // not on a subpath of service roots, so no id + return "" + } + + // make sure tileset exists + if _, ok := s.tilesets[id]; ok { + return id + } + + return "" +} + +// Handler returns a http.Handler that serves the endpoints of the ServiceSet. +// The function ef is called with any occurring error if it is non-nil, so it +// can be used for e.g. logging with logging facilities of the caller. +func (s *ServiceSet) Handler() http.Handler { + m := http.NewServeMux() + + root := s.rootURL.Path + "/" + + // Route requests at the tileset or subpath to the corresponding tileset + m.HandleFunc(root, s.tilesetHandler) + + if s.enableServiceList { + m.HandleFunc(s.rootURL.Path, s.serviceListHandler) + } + + if s.enableArcGIS { + m.HandleFunc(ArcGISRoot, s.tilesetHandler) + } + + return m +} diff --git a/handlers/static.go b/handlers/static.go index 011290f..51e1586 100644 --- a/handlers/static.go +++ b/handlers/static.go @@ -3,16 +3,20 @@ package handlers import ( "net/http" "path" + "strings" "time" ) -// Static returns an http.Handler that will serve the contents of -// the subdirectory "/static/dist" of the Assets. -func Static() http.Handler { +// staticHandler returns a handler that retrieves static files from the virtual +// assets filesystem based on a path. The URL prefix of the resource where +// these are accessed is first trimmed before requesting from the filesystem. +func staticHandler(prefix string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - f, err := Assets.Open(path.Join("/static/dist", r.URL.Path)) + filePath := strings.TrimPrefix(r.URL.Path, prefix) + f, err := Assets.Open(path.Join("/static/dist", filePath)) if err != nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + // not an error, file was not found + http.NotFound(w, r) return } defer f.Close() diff --git a/handlers/templates.go b/handlers/templates.go new file mode 100644 index 0000000..2651446 --- /dev/null +++ b/handlers/templates.go @@ -0,0 +1,38 @@ +package handlers + +//go:generate go run -tags=dev assets_generate.go + +import ( + "bytes" + "fmt" + "html/template" + "io" + "net/http" + + "github.com/shurcooL/httpfs/html/vfstemplate" +) + +var templates *template.Template + +func init() { + // load templates + templates = template.New("_base_") + vfstemplate.ParseFiles(Assets, templates, "map.html", "map_gl.html") +} + +// executeTemplates first tries to find the template with the given name for +// the ServiceSet. If that fails because it is not available, an HTTP status +// Internal Server Error is returned. +func executeTemplate(w http.ResponseWriter, name string, data interface{}) (int, error) { + t := templates.Lookup(name) + if t == nil { + return http.StatusInternalServerError, fmt.Errorf("template not found %q", name) + } + buf := &bytes.Buffer{} + err := t.Execute(buf, data) + if err != nil { + return http.StatusInternalServerError, err + } + _, err = io.Copy(w, buf) + return http.StatusOK, err +} diff --git a/handlers/templates/map.html b/handlers/templates/map.html index 6185b6b..f99a904 100644 --- a/handlers/templates/map.html +++ b/handlers/templates/map.html @@ -4,8 +4,8 @@ {{.ID}} Preview - - + +