183 lines
5.4 KiB
TypeScript
Executable File
183 lines
5.4 KiB
TypeScript
Executable File
#!/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<void> {
|
|
await globalIpAddressMapMutex.acquire();
|
|
try {
|
|
globalIpAddressMap[ip] = new Date().getTime();
|
|
} finally {
|
|
globalIpAddressMapMutex.release();
|
|
}
|
|
}
|
|
|
|
async function cleanAndGetIpAddresses(): Promise<Array<string>> {
|
|
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<Response>;
|
|
|
|
|
|
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);
|
|
}); |