diff --git a/access-guard-ts/README.md b/access-guard-ts/README.md new file mode 100644 index 0000000..a206cf4 --- /dev/null +++ b/access-guard-ts/README.md @@ -0,0 +1,9 @@ +# access-guard.ts + +```shell +xh '0:3000/ip_addresses?__token=11' +``` + +```shell +xh POST '0:3000/ip_addresses?__token=11' ip=1.1.1.1 +``` \ No newline at end of file diff --git a/access-guard-ts/main.ts b/access-guard-ts/main.ts new file mode 100755 index 0000000..01e5ffc --- /dev/null +++ b/access-guard-ts/main.ts @@ -0,0 +1,183 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net + +import {Mutex} from "https://deno.land/x/async@v2.1.0/mutex.ts"; + +// Reference: +// * https://docs.deno.com/runtime/reference/env_variables/ +// * https://docs.deno.com/runtime/fundamentals/http_server/ + +const TOKEN = Deno.env.get("TOKEN"); +const listen = { + port: Deno.env.get("LISTEN_PORT") || 3000, + hostname: Deno.env.get("LISTEN_HOSTNAME") || "127.0.0.1", +}; +const startTime = new Date().getTime(); + +// IP: Add time +let globalIpAddressMap = {}; +const globalIpAddressMapMutex = new Mutex(); + +async function addIpAddress(ip: string): Promise { + await globalIpAddressMapMutex.acquire(); + try { + globalIpAddressMap[ip] = new Date().getTime(); + } finally { + globalIpAddressMapMutex.release(); + } +} + +async function cleanAndGetIpAddresses(): Promise> { + await globalIpAddressMapMutex.acquire(); + try { + let currentTimeMillis = new Date().getTime(); + let invalidKeys = []; + let validKeys = []; + for (let k in globalIpAddressMap) { + const t = globalIpAddressMap[k]; + if ((currentTimeMillis - t) > 60 * 60 * 1000) { + invalidKeys.push(k); + } else { + validKeys.push(k); + } + } + for (let i = 0; i < invalidKeys.length; i++) { + delete globalIpAddressMap[invalidKeys[i]]; + } + return validKeys; + } finally { + globalIpAddressMapMutex.release(); + } +} + +let globalFilters = []; +let globalHandlerMap = {}; + +type RequestHandler = (url: URL, req: any) => Promise; + + +function registerFilter(handler: RequestHandler) { + globalFilters.push(handler); +} + +function registerHandler(method: string, path: string, handler: RequestHandler) { + const requestIdent = `${method}::${path}`; + if (globalHandlerMap[requestIdent] != null) { + throw `Handler for ${method} ${path} exists.`; + } + globalHandlerMap[requestIdent] = handler; +} + +function buildJsonResponse(status: number, body: any): Response { + return new Response(JSON.stringify(body, null, " "), { + status: status, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} + +function buildOkJsonResponse(body: any): Response { + return buildJsonResponse(200, body); +} + +function notFoundHandler(url: URL, req: any): Response { + return buildJsonResponse(404, {"error": "not_found", "message": "Resource not found.",}); +} + +registerFilter(async (url: URL, req: any) => { + if (url.pathname == "/version") { + return null; + } + if (TOKEN == null) { + return buildJsonResponse(500, {"error": "bad_token", "message": "Bad token."}); + } + const token = url.searchParams.get('__token'); + if (TOKEN !== token) { + return buildJsonResponse(401, {"error": "invalid_token", "message": "Invalid token."}) + } + return null; +}); + +function formatHumanTime(time: number): string { + let t = []; + let leftMs = time % 1000; + if (leftMs > 0) { + t.push(`${leftMs}ms`); + } + let secs = Math.floor(time / 1000); + let leftSecs = secs % 60; + if (leftSecs > 0) { + t.push(`${leftSecs}s`); + } + let mins = Math.floor(secs / 60); + let leftMins = mins % 60; + if (leftMins > 0) { + t.push(`${leftMins}min`); + } + let hours = Math.floor(mins / 60); + let leftHours = hours % 24; + if (leftHours > 0) { + t.push(`${leftHours}hour`); + } + let days = Math.floor(hours / 24); + if (days > 0) { + t.push(`${days}day`); + } + return t.reverse().join(' '); +} + +registerHandler("GET", "/version", async (url, req) => { + return buildOkJsonResponse({ + "version": "0.0.1", + "uptime": formatHumanTime(new Date().getTime() - startTime), + }); +}); + +interface AddIpAddressRequest { + ip: string; +} + +registerHandler("POST", "/ip_addresses", async (url, req) => { + let addIpAddressRequest: AddIpAddressRequest; + try { + addIpAddressRequest = await req.json(); + } catch (e) { + return buildJsonResponse(400, {"error": "bad_request", "message": "Bad request."}); + } + await addIpAddress(addIpAddressRequest.ip); + return buildOkJsonResponse({}); +}); + +registerHandler("GET", "/ip_addresses", async (url, req) => { + const ipAddresses = await cleanAndGetIpAddresses(); + return buildOkJsonResponse({ + ipAddresses: ipAddresses, + }); +}); + +registerHandler("*", "/check_ip_address", async (url, req) => { + const clientIp = req.headers.get('x-real-ip'); + if (clientIp == null) { + return buildJsonResponse(400, {"error": "bad_request", "message": "Bad request: no client IP"}); + } + const ipAddresses = await cleanAndGetIpAddresses(); + if (ipAddresses.indexOf(clientIp) >= 0) { + return buildOkJsonResponse({}); + } + return buildJsonResponse(401, {"error": "access_denied", "message": "Access denied: not allowed IP"}); +}); + +Deno.serve(listen, async (req) => { + const url = new URL(req.url); + for (let i = 0; i < globalFilters.length; i++) { + const response = await globalFilters[i](url, req); + if (response != null) { + return response; + } + } + console.log("Handler request:", req.method, url.pathname); + const req1 = `${req.method}::${url.pathname}`; + const req2 = `*::${url.pathname}`; + const req_handler = globalHandlerMap[req1] || globalHandlerMap[req2] || notFoundHandler; + return await req_handler(url, req); +}); \ No newline at end of file