From 543b8ef50f32979c1e16189119b781bd8115c521 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 19 Oct 2022 22:36:28 +0800 Subject: [PATCH] tile_path and error handling for lambda --- serverless/awsjs/src/index.ts | 138 ++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 21 deletions(-) diff --git a/serverless/awsjs/src/index.ts b/serverless/awsjs/src/index.ts index 814b58f..9581f75 100644 --- a/serverless/awsjs/src/index.ts +++ b/serverless/awsjs/src/index.ts @@ -1,14 +1,20 @@ import { Readable } from "stream"; -import { Context, APIGatewayProxyResult, APIGatewayEvent } from "aws-lambda"; +import { + Context, + APIGatewayProxyResult, + APIGatewayProxyEventV2, +} from "aws-lambda"; import { PMTiles, ResolvedValueCache, RangeResponse, Source, + Compression, } from "../../../js"; // @ts-ignore import https from "https"; + // @ts-ignore import s3client from "/var/runtime/node_modules/aws-sdk/clients/s3.js"; @@ -21,6 +27,47 @@ const s3 = new s3client({ // TODO: figure out how much memory to allocate const CACHE = new ResolvedValueCache(); +export const pmtiles_path = (name: string, setting?: string): string => { + if (setting) { + return setting.replace("{name}", name); + } + return name + ".pmtiles"; +}; + +const TILE = + /^\/(?[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?\d+)\/(?\d+)\/(?\d+).(?[a-z]+)$/; + +export const tile_path = ( + path: string, + setting?: string +): { + ok: boolean; + name: string; + tile: [number, number, number]; + ext: string; +} => { + let pattern = TILE; + if (setting) { + // escape regex + setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); + setting = setting.replace("{name}", "(?[0-9a-zA-Z/!-_.*'()]+)"); + setting = setting.replace("{z}", "(?\\d+)"); + setting = setting.replace("{x}", "(?\\d+)"); + setting = setting.replace("{y}", "(?\\d+)"); + setting = setting.replace("{ext}", "(?[a-z]+)"); + pattern = new RegExp(setting); + } + + let match = path.match(pattern); + + if (match) { + const g = match.groups!; + return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT }; + } + return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; +}; + + class S3Source implements Source { archive_name: string; @@ -36,7 +83,7 @@ class S3Source implements Source { const resp = await s3 .getObject({ Bucket: process.env.BUCKET!, - Key: this.archive_name + ".pmtiles", + Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH), Range: "bytes=" + offset + "-" + (offset + length - 1), }) .promise(); @@ -45,37 +92,86 @@ class S3Source implements Source { } } +interface Headers { + [key: string]: string; +} + +const apiResp = ( + statusCode: number, + body: string, + isBase64Encoded = false, + headers: Headers = {} +): APIGatewayProxyResult => { + return { + statusCode: statusCode, + body: body, + headers: headers, + isBase64Encoded: isBase64Encoded, + }; +}; + +// Assumes event is a API Gateway V2 or Lambda Function URL formatted dict +// and returns API Gateway V2 / Lambda Function dict responses +// Does not work with CloudFront events/Lambda@Edge; see README export const handler = async ( - event: APIGatewayEvent, + event: APIGatewayProxyEventV2, context: Context ): Promise => { + var path; + var is_api_gateway; + if (event.pathParameters) { + is_api_gateway = true; + if (event.pathParameters.proxy) { + path = "/" + event.pathParameters.proxy; + } else { + return apiResp(500, "Proxy integration missing tile_path parameter"); + } + } else { + path = event.rawPath; + } + + if (!path) { + return apiResp(500, "Invalid event configuration"); + } + + const { ok, name, tile } = tile_path(path, process.env.TILE_PATH); + + if (!ok) { + return apiResp(400, "Invalid tile URL"); + } + + var headers: Headers = {}; + if (process.env.CORS) { + headers["Access-Control-Allow-Origin"] = process.env.CORS; + } + // TODO: extension enforcement and MIME types + + const source = new S3Source("stamen_toner_z3"); + const p = new PMTiles(source, CACHE); try { - const source = new S3Source("stamen_toner_z3"); - const p = new PMTiles(source, CACHE); + const header = await p.getHeader(); + // TODO optimize by checking min/max zoom, return 404 + + headers["Content-Type"] = "application/vnd.vector-tile"; const tile = await p.getZxy(0, 0, 0); if (tile) { - return { - statusCode: 200, - body: Buffer.from(tile.data).toString("base64"), - }; + // return suncompressed response + // TODO: may need to special case API gateway to return compressed response with gzip content-encoding header + return apiResp( + 200, + Buffer.from(tile.data).toString("base64"), + true, + headers + ); } else { - return { - statusCode: 204, - body: "", - }; + return apiResp(204, "", false, headers); } } catch (e) { if ((e as Error).name === "AccessDenied") { - return { - statusCode: 403, - body: "Bucket access failed: Unauthorized", - }; + return apiResp(403, "Bucket access unauthorized"); } throw e; } - return { - statusCode: 404, - body: "Invalid URL", - }; + return apiResp(404, "Invalid URL"); };