diff options
23 files changed, 271 insertions, 166 deletions
diff --git a/deno/base/cron.ts b/deno/base/cron.ts index bf0a0be..1b74361 100644 --- a/deno/base/cron.ts +++ b/deno/base/cron.ts @@ -1,4 +1,4 @@ -export type CronCallback = (task: CronTask) => Promise<void>; +export type CronCallback = (task: CronTask) => Promise<void> | void; export interface CronTaskConfig { readonly name: string; diff --git a/deno/deno.json b/deno/deno.json index 9efebf7..888bdab 100644 --- a/deno/deno.json +++ b/deno/deno.json @@ -1,6 +1,7 @@ { - "workspace": ["./base", "./mail", "./tools"], + "workspace": ["./base", "./gateway", "./mail", "./tools"], "tasks": { + "compile:gateway": "deno task --cwd=gateway compile", "compile:mail": "deno task --cwd=mail compile", "compile:tools": "deno task --cwd=tools compile" }, diff --git a/deno/deno.lock b/deno/deno.lock index bdc8c3f..cf20d47 100644 --- a/deno/deno.lock +++ b/deno/deno.lock @@ -3,6 +3,7 @@ "specifiers": { "jsr:@db/sqlite@0.12": "0.12.0", "jsr:@denosaurs/plug@1": "1.1.0", + "jsr:@hono/hono@^4.12.8": "4.12.8", "jsr:@std/assert@0.217": "0.217.0", "jsr:@std/assert@^1.0.13": "1.0.13", "jsr:@std/async@^1.0.13": "1.0.13", @@ -14,11 +15,11 @@ "jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/expect@^1.0.16": "1.0.16", "jsr:@std/fmt@1": "1.0.8", - "jsr:@std/fs@1": "1.0.17", - "jsr:@std/fs@^1.0.17": "1.0.17", + "jsr:@std/fs@1": "1.0.18", + "jsr:@std/fs@^1.0.17": "1.0.18", "jsr:@std/fs@^1.0.18": "1.0.18", - "jsr:@std/internal@^1.0.6": "1.0.7", - "jsr:@std/internal@^1.0.7": "1.0.7", + "jsr:@std/internal@^1.0.6": "1.0.8", + "jsr:@std/internal@^1.0.7": "1.0.8", "jsr:@std/internal@^1.0.8": "1.0.8", "jsr:@std/io@~0.225.2": "0.225.2", "jsr:@std/path@0.217": "0.217.0", @@ -60,6 +61,9 @@ "jsr:@std/path@1" ] }, + "@hono/hono@4.12.8": { + "integrity": "1997fde8abf28e84d821c9c867229be020e268d4749397aec071305bf54db297" + }, "@std/assert@0.217.0": { "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" }, @@ -1306,6 +1310,11 @@ "npm:yargs@18" ], "members": { + "gateway": { + "dependencies": [ + "jsr:@hono/hono@^4.12.8" + ] + }, "mail": { "dependencies": [ "jsr:@db/sqlite@0.12", diff --git a/deno/gateway/deno.json b/deno/gateway/deno.json new file mode 100644 index 0000000..db2fab2 --- /dev/null +++ b/deno/gateway/deno.json @@ -0,0 +1,13 @@ +{ + "imports": { + "hono": "jsr:@hono/hono@^4.12.8" + }, + "tasks": { + "start": "deno run --allow-net main.ts", + "compile": "deno compile -o out/crupest-gateway -A main.ts" + }, + "compilerOptions": { + "jsx": "precompile", + "jsxImportSource": "hono/jsx" + } +} diff --git a/deno/gateway/main.ts b/deno/gateway/main.ts new file mode 100644 index 0000000..c82f1a6 --- /dev/null +++ b/deno/gateway/main.ts @@ -0,0 +1,220 @@ +import { Context, Hono } from "hono"; +import { serveStatic } from "hono/deno"; +import { logger } from "hono/logger"; + +import { ConfigDefinition, ConfigProvider } from "@crupest/base/config"; +import { CronTask } from "@crupest/base/cron"; + +const PREFIX = "crupest"; +const CONFIG_DEFINITION: ConfigDefinition = { + domain: { + description: "the root domain", + }, + github: { + description: "site owner's github url", + }, + v2rayPath: { + description: "the path for v2ray websocket", + }, + mailServerAwsInboundPath: { + description: "the path for mail server aws inbound webhook", + }, +} as const satisfies ConfigDefinition; + +const configProvider = new ConfigProvider(PREFIX, CONFIG_DEFINITION); +type Config = typeof configProvider; + +interface Bindings { + remoteAddr: string; +} + +interface Env { + Bindings: Bindings; +} + +function createReverseProxyHandler({ originServer }: { originServer: string }) { + return async (c: Context<Env>) => { + const url = new URL(c.req.url); + const proto = url.protocol === "https:" ? "https" : "http"; + + let forwardedFor = c.req.header("x-forwarded-for"); + if (forwardedFor) forwardedFor += `, ${c.env.remoteAddr}`; + else forwardedFor = c.env.remoteAddr; + + const connection = c.req.header("upgrade") ? "upgrade" : "close"; + + return await fetch(`http://${originServer}${url.pathname}${url.search}`, { + method: c.req.method, + headers: { + ...c.req.header(), + "Connection": connection, + "X-Forwarded-For": forwardedFor, + "X-Forwarded-Proto": proto, + "X-Real-IP": c.env.remoteAddr, + }, + body: c.req.method !== "GET" && c.req.method !== "HEAD" + ? await c.req.arrayBuffer() + : undefined, + }); + }; +} + +function createHttpHono() { + const app = new Hono(); + app.use(logger()); + + // Serve static files for ACME challenge + app.get( + "/.well-known/acme-challenge/*", + serveStatic({ + root: "/var/www/certbot", + }), + ); + + // Redirect all other requests to the HTTPS version of the site + app.all("*", (c) => { + return c.redirect(c.req.url.replace("http://", "https://"), 301); + }); + + return app; +} + +function createRootHono( + { basePath, config }: { basePath: string; config: Config }, +) { + const app = new Hono(); + + app.get("/github", (c) => { + const githubUrl = config.get("github"); + return c.redirect(githubUrl, 302); + }); + + app.all( + "/git/*", + createReverseProxyHandler({ originServer: "git-server:3636" }), + ); + + app.all( + `/${config.get("v2rayPath")}`, + createReverseProxyHandler({ originServer: "v2ray:10000" }), + ); + + app.get( + "*", + serveStatic({ + root: "/srv/www", + rewriteRequestPath: (path) => path.replace(basePath, ""), + }), + ); + + return app; +} + +function createMailHono( + { basePath, config }: { basePath: string; config: Config }, +) { + const app = new Hono(); + + app.get( + "/robots.txt", + serveStatic({ + root: "/srv/mail", + rewriteRequestPath: (path) => path.replace(basePath, ""), + }), + ); + + app.all( + `/${config.get("mailServerAwsInboundPath")}`, + createReverseProxyHandler({ originServer: "mail-server:2345" }), + ); + + app.all( + "*", + createReverseProxyHandler({ originServer: "roundcubemail:80" }), + ); + + return app; +} + +function createHttpsHono({ config }: { config: Config }) { + const app = new Hono({ + getPath: (req) => req.url.replace(/^https?:\/([^?]+).*$/, "$1"), + }); + app.use(logger()); + + const rootBasePath = `/${config.get("domain")}`; + app.route( + rootBasePath, + createRootHono({ basePath: rootBasePath, config }), + ); + + const mailBasePath = `/mail.${config.get("domain")}`; + app.route( + mailBasePath, + createMailHono({ basePath: mailBasePath, config }), + ); + + return app; +} + +let servers: { + httpServer: Deno.HttpServer<Deno.NetAddr>; + httpsServer: Deno.HttpServer<Deno.NetAddr>; +} | null = null; + +async function restartServer() { + if (servers) { + await Promise.all([ + servers.httpServer.shutdown(), + servers.httpsServer.shutdown(), + ]); + } + + const httpApp = createHttpHono(); + const httpsApp = createHttpsHono({ config: configProvider }); + + const httpServer = Deno.serve({ + port: 80, + }, (req, info) => { + return httpApp.fetch(req, info); + }); + + const httpsServer = Deno.serve({ + port: 443, + cert: await Deno.readTextFile( + `/etc/letsencrypt/live/${configProvider.get("domain")}/fullchain.pem`, + ), + key: await Deno.readTextFile( + `/etc/letsencrypt/live/${configProvider.get("domain")}/privkey.pem`, + ), + }, (req, info) => { + return httpsApp.fetch(req, info); + }); + + servers = { httpServer, httpsServer }; + return servers; +} + +async function certbotRenew() { + const command = new Deno.Command("certbot", { + args: ["renew", "--webroot", "-w", "/var/www/certbot"], + }); + const process = command.spawn(); + await process.status; + await restartServer(); +} + +function main() { + restartServer(); + + setTimeout(() => { + new CronTask({ + name: "certbot-renewal", + interval: 1000 * 60 * 60 * 12, + callback: certbotRenew, + startNow: true, + }); + }, 5000); +} + +main(); diff --git a/services/docker/gateway/Dockerfile b/services/docker/gateway/Dockerfile new file mode 100644 index 0000000..68ec5ca --- /dev/null +++ b/services/docker/gateway/Dockerfile @@ -0,0 +1,16 @@ +FROM ghcr.io/gohugoio/hugo AS build-www +COPY --from=www . /project/ +RUN ls && hugo + +FROM denoland/deno AS deno-build +COPY --from=deno . /workdir/ +WORKDIR /workdir +RUN deno install +RUN deno task compile:gateway + +FROM debian +RUN apt update && apt-get install -y tini certbot && rm -rf /var/lib/apt/lists/* +ADD mail-robots.txt /srv/mail/robots.txt +COPY --from=build-www /project/public /srv/www +COPY --from=deno-build /workdir/gateway/out/crupest-gateway /app/ +CMD ["/usr/bin/tini", "--", "/app/crupest-gateway"] diff --git a/services/docker/nginx/mail-robots.txt b/services/docker/gateway/mail-robots.txt index 1f53798..1f53798 100644 --- a/services/docker/nginx/mail-robots.txt +++ b/services/docker/gateway/mail-robots.txt diff --git a/services/docker/nginx/Dockerfile b/services/docker/nginx/Dockerfile deleted file mode 100644 index 3169e00..0000000 --- a/services/docker/nginx/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM ghcr.io/gohugoio/hugo AS build-www -COPY --from=www . /project/ -RUN ls && hugo - -FROM nginx:mainline -RUN apt update && apt-get install -y tini certbot && rm -rf /var/lib/apt/lists/* -ADD mail-robots.txt /srv/mail/robots.txt -ADD certbot.bash nginx-wrapper.bash /app/ -COPY --from=build-www /project/public /srv/www -CMD ["/usr/bin/tini", "--", "/app/nginx-wrapper.bash"] diff --git a/services/docker/nginx/certbot.bash b/services/docker/nginx/certbot.bash deleted file mode 100755 index cb5c636..0000000 --- a/services/docker/nginx/certbot.bash +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/bash - -set -e - -echo "Sleep 5 seconds waiting for nginx to start." -sleep 5s - -while true; do - certbot renew --webroot -w /var/www/certbot --deploy-hook "nginx -s reload" - echo "Sleep one day before next certbot renew." - sleep 1d -done diff --git a/services/docker/nginx/nginx-wrapper.bash b/services/docker/nginx/nginx-wrapper.bash deleted file mode 100755 index a4a19ec..0000000 --- a/services/docker/nginx/nginx-wrapper.bash +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/bash - -set -e -o pipefail - -die() { - echo "$@" >&2 - exit 1 -} - -/app/certbot.bash & - -/docker-entrypoint.sh nginx "-g" "daemon off;" diff --git a/services/templates/docker-compose.yaml.template b/services/templates/docker-compose.yaml.template index e96e8eb..01ab477 100644 --- a/services/templates/docker-compose.yaml.template +++ b/services/templates/docker-compose.yaml.template @@ -1,20 +1,21 @@ services: - nginx: + gateway: build: - context: "./@@CRUPEST_DOCKER_DIR@@/nginx" + context: "./@@CRUPEST_DOCKER_DIR@@/gateway" additional_contexts: - "www=./www" + - "deno=./deno" dockerfile: Dockerfile ports: - "80:80" - "443:443" - "443:443/udp" env_file: + - "./@@CRUPEST_GENERATED_DIR@@/envs/gateway.env" - "./@@CRUPEST_GENERATED_DIR@@/envs/v2ray-common.env" - "./@@CRUPEST_GENERATED_DIR@@/envs/mail-server-common.env" volumes: - - "./@@CRUPEST_GENERATED_DIR@@/nginx:/etc/nginx/conf.d" - "./@@CRUPEST_DATA_CERTBOT_DIR@@/certs:/etc/letsencrypt" - "./@@CRUPEST_DATA_CERTBOT_DIR@@/data:/var/lib/letsencrypt" - "./@@CRUPEST_DATA_CERTBOT_DIR@@/webroot:/var/www/certbot" diff --git a/services/templates/envs/gateway.env.template b/services/templates/envs/gateway.env.template new file mode 100644 index 0000000..9bde5f7 --- /dev/null +++ b/services/templates/envs/gateway.env.template @@ -0,0 +1,2 @@ +CRUPEST_DOMAIN=@@CRUPEST_DOMAIN@@ +CRUPEST_GITHUB=@@CRUPEST_GITHUB@@
\ No newline at end of file diff --git a/services/templates/nginx/common/acme-challenge b/services/templates/nginx/common/acme-challenge deleted file mode 100644 index 8280cd8..0000000 --- a/services/templates/nginx/common/acme-challenge +++ /dev/null @@ -1,3 +0,0 @@ -location /.well-known/acme-challenge { - root /var/www/certbot; -} diff --git a/services/templates/nginx/common/http-listen b/services/templates/nginx/common/http-listen deleted file mode 100644 index 76cb18d..0000000 --- a/services/templates/nginx/common/http-listen +++ /dev/null @@ -1,2 +0,0 @@ -listen 80; -listen [::]:80; diff --git a/services/templates/nginx/common/https-listen b/services/templates/nginx/common/https-listen deleted file mode 100644 index db2f68e..0000000 --- a/services/templates/nginx/common/https-listen +++ /dev/null @@ -1,3 +0,0 @@ -listen 443 ssl; -listen [::]:443 ssl; -http2 on; diff --git a/services/templates/nginx/common/https-redirect b/services/templates/nginx/common/https-redirect deleted file mode 100644 index 56d095d..0000000 --- a/services/templates/nginx/common/https-redirect +++ /dev/null @@ -1,3 +0,0 @@ -location / { - return 301 https://$host$request_uri; -} diff --git a/services/templates/nginx/common/reverse-proxy b/services/templates/nginx/common/reverse-proxy deleted file mode 100644 index 4193548..0000000 --- a/services/templates/nginx/common/reverse-proxy +++ /dev/null @@ -1,7 +0,0 @@ -proxy_http_version 1.1; -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection $connection_upgrade; -proxy_set_header Host $host; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; -proxy_set_header X-Real-IP $remote_addr; diff --git a/services/templates/nginx/default.conf b/services/templates/nginx/default.conf deleted file mode 100644 index 515942b..0000000 --- a/services/templates/nginx/default.conf +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - listen 443 ssl default_server; - listen [::]:443 ssl default_server; - http2 on; - - return 444; -} diff --git a/services/templates/nginx/mail.conf.template b/services/templates/nginx/mail.conf.template deleted file mode 100644 index 1c2a2ca..0000000 --- a/services/templates/nginx/mail.conf.template +++ /dev/null @@ -1,29 +0,0 @@ -server { - server_name mail.@@CRUPEST_DOMAIN@@; - include conf.d/common/https-listen; - - location = /robots.txt { - root /srv/mail; - } - - location = /@@CRUPEST_MAIL_SERVER_AWS_INBOUND_PATH@@ { - include conf.d/common/reverse-proxy; - proxy_pass http://mail-server:2345/@@CRUPEST_MAIL_SERVER_AWS_INBOUND_PATH@@; - } - - location / { - include conf.d/common/reverse-proxy; - proxy_pass http://roundcubemail:80/; - } - - client_max_body_size 5G; -} - - -server { - server_name mail.@@CRUPEST_DOMAIN@@; - include conf.d/common/http-listen; - - include conf.d/common/https-redirect; - include conf.d/common/acme-challenge; -} diff --git a/services/templates/nginx/root.conf.template b/services/templates/nginx/root.conf.template deleted file mode 100644 index db28f00..0000000 --- a/services/templates/nginx/root.conf.template +++ /dev/null @@ -1,40 +0,0 @@ -server { - server_name @@CRUPEST_DOMAIN@@; - include conf.d/common/https-listen; - - location / { - root /srv/www; - } - - location /git/ { - include conf.d/common/reverse-proxy; - client_max_body_size 5G; - proxy_pass http://git-server:3636; - } - - location = /github { - return 301 @@CRUPEST_GITHUB@@; - } - - location = /github/ { - return 301 @@CRUPEST_GITHUB@@; - } - - location /_@@CRUPEST_V2RAY_PATH@@ { - if ($http_upgrade != "websocket") { - return 404; - } - - proxy_redirect off; - include conf.d/common/reverse-proxy; - proxy_pass http://v2ray:10000; - } -} - -server { - server_name @@CRUPEST_DOMAIN@@; - include conf.d/common/http-listen; - - include conf.d/common/https-redirect; - include conf.d/common/acme-challenge; -} diff --git a/services/templates/nginx/ssl.conf.template b/services/templates/nginx/ssl.conf.template deleted file mode 100644 index 181a1af..0000000 --- a/services/templates/nginx/ssl.conf.template +++ /dev/null @@ -1,17 +0,0 @@ -# This file contains important security parameters. If you modify this file -# manually, Certbot will be unable to automatically provide future security -# updates. Instead, Certbot will print and log an error message with a path to -# the up-to-date file that you will need to refer to when manually updating -# this file. Contents are based on https://ssl-config.mozilla.org - -ssl_certificate /etc/letsencrypt/live/@@CRUPEST_DOMAIN@@/fullchain.pem; -ssl_certificate_key /etc/letsencrypt/live/@@CRUPEST_DOMAIN@@/privkey.pem; - -ssl_session_cache shared:le_nginx_SSL:10m; -ssl_session_timeout 1440m; -ssl_session_tickets off; - -ssl_protocols TLSv1.2 TLSv1.3; -ssl_prefer_server_ciphers off; - -ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; diff --git a/services/templates/nginx/timeline.conf.template b/services/templates/nginx/timeline.conf.template deleted file mode 100644 index 3414510..0000000 --- a/services/templates/nginx/timeline.conf.template +++ /dev/null @@ -1,6 +0,0 @@ -server { - server_name timeline.@@CRUPEST_DOMAIN@@; - include conf.d/common/http-listen; - - include conf.d/common/acme-challenge; -} diff --git a/services/templates/nginx/websocket.conf b/services/templates/nginx/websocket.conf deleted file mode 100644 index 32af4c3..0000000 --- a/services/templates/nginx/websocket.conf +++ /dev/null @@ -1,4 +0,0 @@ -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} |
