From f9714cbb5378cbba5721df3c258114656bb607f4 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Sun, 11 May 2025 01:10:54 +0200 Subject: [PATCH] feat(proxy): add environment config and request handling - Introduce environment-based configuration for proxy settings - Add middleware for API key authentication - Implement request forwarding to LanguageTool backend - Set up server startup and routing logic --- src/env.ts | 63 +++++++++++++++++++++++++++++++++++++++++++ src/ltProxyAuth.ts | 21 +++++++++++++++ src/ltProxyHandler.ts | 28 +++++++++++++++++++ src/main.ts | 19 +++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 src/env.ts create mode 100644 src/ltProxyAuth.ts create mode 100644 src/ltProxyHandler.ts create mode 100644 src/main.ts diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..5773d28 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,63 @@ +/** + * Environment configuration for lt-auth-proxy. + * All properties are lazily evaluated and cached on first access. + */ + +export class Env { + private static _proxyHost?: string; + private static _proxyPort?: number; + private static _apiKeys?: string[]; + private static _ltServerHost?: string; + private static _ltServerPort?: number; + + private static getEnv(key: string, required = false): string | undefined { + const value = Deno.env.get(key); + if (required && (!value || value.trim() === '')) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; + } + + /** Hostname for the proxy (default: 0.0.0.0) */ + static get proxyHost(): string { + if (this._proxyHost === undefined) { + this._proxyHost = this.getEnv('PROXY_HOST') || '0.0.0.0'; + } + return this._proxyHost; + } + + /** Port for the proxy (default: 8011) */ + static get proxyPort(): number { + if (this._proxyPort === undefined) { + this._proxyPort = Number(this.getEnv('PROXY_PORT') || 8011); + } + return this._proxyPort; + } + + /** List of allowed API keys (required) */ + static get apiKeys(): string[] { + if (this._apiKeys === undefined) { + const raw = this.getEnv('API_KEYS', true)!; + this._apiKeys = raw.split(',').map((k) => k.trim()).filter((k) => + k.length > 0 + ); + } + return this._apiKeys; + } + + /** Hostname of the LanguageTool backend (default: localhost) */ + static get ltServerHost(): string { + if (this._ltServerHost === undefined) { + this._ltServerHost = this.getEnv('LT_SERVER_HOST') || 'localhost'; + } + return this._ltServerHost; + } + + /** Port of the LanguageTool backend (default: 8010) */ + static get ltServerPort(): number { + if (this._ltServerPort === undefined) { + this._ltServerPort = Number(this.getEnv('LT_SERVER_PORT') || 8010); + } + return this._ltServerPort; + } +} diff --git a/src/ltProxyAuth.ts b/src/ltProxyAuth.ts new file mode 100644 index 0000000..b4b0340 --- /dev/null +++ b/src/ltProxyAuth.ts @@ -0,0 +1,21 @@ +import { Middleware } from 'http-kernel/Types/mod.ts'; +import { Env } from './env.ts'; + +/** + * Middleware that checks for a valid API key via ?apiKey=... query/form param. + * Rejects request with 403 if the key is missing or invalid. + */ +export const authMiddleware: Middleware = async (ctx, next) => { + const key = ctx.query.apiKey; + + // Support both ?apiKey=... and form body with apiKey=... + const extractedKey = Array.isArray(key) ? key[0] : key; + + if (!extractedKey || !Env.apiKeys.includes(extractedKey)) { + return new Response('Forbidden – Invalid API key', { status: 403 }); + } + + return await next(); +}; + +export { authMiddleware as ltProxyAuth }; diff --git a/src/ltProxyHandler.ts b/src/ltProxyHandler.ts new file mode 100644 index 0000000..18fd1f7 --- /dev/null +++ b/src/ltProxyHandler.ts @@ -0,0 +1,28 @@ +import { Handler } from 'http-kernel/Types/mod.ts'; +import { Env } from './env.ts'; + +/** + * Forwards the incoming request to the actual LanguageTool server. + * Dynamically passes through path and query string. + */ +export const handler: Handler = async (ctx) => { + const originalUrl = new URL(ctx.req.url); + const proxyUrl = new URL( + `${originalUrl.pathname}${originalUrl.search}`, + `http://${Env.ltServerHost}:${Env.ltServerPort}`, + ); + + const forwarded = await fetch(proxyUrl.toString(), { + method: ctx.req.method, + headers: ctx.req.headers, + body: ctx.req.body, + }); + + const headers = new Headers(forwarded.headers); + return new Response(forwarded.body, { + status: forwarded.status, + headers, + }); +}; + +export { handler as ltProxyHandler }; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..1d839b0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,19 @@ +import { HttpKernel } from 'http-kernel/mod.ts'; +import { Env } from './env.ts'; +import { ltProxyAuth } from './ltProxyAuth.ts'; +import { ltProxyHandler } from './ltProxyHandler.ts'; + +const httpKernel = new HttpKernel(); + +httpKernel.route({ + method: 'POST', + path: '/*', +}).middleware(ltProxyAuth).handle(ltProxyHandler); + +Deno.serve({ + port: Env.proxyPort, + hostname: Env.proxyHost, + onListen: ({ hostname, port }) => { + console.info(`lt-auth-proxy listening on ${hostname}:${port}`); + }, +}, async (req) => await httpKernel.handle(req));