From 8643a99ab3cbd19f2f04b86bd2e90e67030f45bb Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:26:45 -0400 Subject: [PATCH] Add mTLS certificate support --- .gitignore | 3 +- README.md | 7 +++++ docker-compose.yml | 1 + package.json | 3 +- src/shared/Database.ts | 2 +- src/utils/e621-utils.ts | 61 +++++++++++++++++++++++++++++++++++------ 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3cf6e48..1c9515c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist data/* -.env \ No newline at end of file +.env +certs/* \ No newline at end of file diff --git a/README.md b/README.md index f774194..3bb494f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ 8. Enter the redis url of your e621ng instance. You may need to expose the port from docker manually in development enviornments. This can be done by adding a `ports` mapping to the `redis` service in e621ng's `docker-compose.yml` file. You should map `6379:6379` 9. Enter your desired port number. It is recommended to leave this at `8000`, if you select anything different you will need to map the correct port in `docker-compose.yml` +#### Mutual TLS +This is purely for documentation purposes. E621 and this bot on the e621 discord server utilize mTLS to allow the bot to securely ensure that these requests are coming from the bot to allow it to work during DDoS attacks without issue. + +In order to enable this, first create a `certs` directory in the root directory (as in next to the `data` directory). Once this is done, go to cloudflare settings > SSL/TLS > Client Certificates. Click `Add Certificate` in the top right. Follow the directions and use `PEM` key format. Create a `cert.pem` file under `certs` and paste the contents of the `Certificate` into it. Then create a `priv.key` file under `certs` and paste the contents of `Private Key` into it. Continue the setup on cloudflare. + +When done properly the first log the bot will print on start should be `[E621 Requester] Initializing agent with certificates.` + ### Installing dependencies Run `npm i` to install all node dependencies. This is required to start the bot. diff --git a/docker-compose.yml b/docker-compose.yml index 7b50a43..8d57b5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,4 @@ services: volumes: - ./data:/app/data - ./sql:/app/sql + - ./certs:/app/certs diff --git a/package.json b/package.json index 8e68cf4..bcfafe5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "build": "npm run clean && node scripts/build.js && npm run copyfiles", "clean": "rimraf dist", "copyfiles": "copyfiles -u 1 \"./src/**/*.html\" ./dist", - "encrypt": "node ./scripts/encrypt-data.js" + "encrypt": "node ./scripts/encrypt-data.js", + "add-author-hash": "node ./scripts/add-author-id-hash.js" } } diff --git a/src/shared/Database.ts b/src/shared/Database.ts index d7f2c4b..49085c4 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -132,7 +132,7 @@ export class Database { await Database.db.run(` INSERT INTO messages (id, id_hash, author_id, author_id_hash, author_name, channel_id, attachments, stickers, content) VALUES - (:id, :id_hash, :author_id, :author_name, :channel_id, :attachments, :stickers, :content) + (:id, :id_hash, :author_id, :author_id_hash, :author_name, :channel_id, :attachments, :stickers, :content) `, ...serializedMessage); return true; diff --git a/src/utils/e621-utils.ts b/src/utils/e621-utils.ts index 3a07229..8899da3 100644 --- a/src/utils/e621-utils.ts +++ b/src/utils/e621-utils.ts @@ -1,5 +1,12 @@ +import { existsSync, readFileSync } from 'fs'; import { config } from '../config'; +import _https from 'https'; +import _http from 'http'; import { E621Pool, E621Post, E621User, PostFlag, Record } from '../types'; +import path from 'path'; + +const secure = config.E621_BASE_URL?.startsWith('https'); +const http = secure ? _https : _http; const BLACKLISTED_TAGS: string[] = []; const BLACKLISTED_NONSAFE_TAGS: string[] = ['young']; @@ -11,6 +18,16 @@ const USER_AGENT = 'E621DiscordBot'; export const SEARCH_LIMIT = 320; +let agent: _https.Agent | _http.Agent = secure ? new _https.Agent() : new _http.Agent(); +if (secure && existsSync('./certs/cert.pem') && existsSync('./certs/cert.pem')) { + console.log('[E621 Requester] Initializing agent with certificates.'); + agent = new _https.Agent({ + cert: readFileSync(path.join(__dirname, '..', '..', 'certs', 'cert.pem'), { encoding: 'utf8' }), + key: readFileSync(path.join(__dirname, '..', '..', 'certs', 'priv.key'), { encoding: 'utf8' }), + rejectUnauthorized: false + }); +} + async function request(path: string, query?: { [name: string]: string }): Promise { const url = new URL(config.E621_BASE_URL!); url.pathname = path + '.json'; @@ -21,15 +38,43 @@ async function request(path: string, query?: { [name: string]: string }): Promis } } - const res = await fetch(url, { - headers: { - 'User-Agent': USER_AGENT - } + return new Promise((resolve) => { + http.get(url, { + agent, + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'application/json' + } + }, (res) => { + if (res.statusCode! < 200 || res.statusCode! >= 300) { + console.error(`[E621 Requester] Received status code ${res.statusCode} while requesting: ${url}`); + res.resume(); + return resolve(null); + } + + res.setEncoding('utf8'); + + let data = ''; + + res.on('data', (d) => { + data += d; + }); + + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + console.error('[E621 Requester] Error parsing JSON from:'); + console.error(data); + resolve(null); + } + }); + }).on('error', (e) => { + console.error('[E621 Requester] Error fetching:'); + console.error(e); + resolve(null); + }); }); - - if (!res.ok) return null; - - return await res.json(); } export async function getE621User(idOrName: string | number): Promise {