Add mTLS certificate support

This commit is contained in:
Tarrgon
2026-07-07 12:26:45 -04:00
parent f32e2292ea
commit 8643a99ab3
6 changed files with 66 additions and 11 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules
dist dist
data/* data/*
.env .env
certs/*
+7
View File
@@ -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` 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` 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 ### Installing dependencies
Run `npm i` to install all node dependencies. This is required to start the bot. Run `npm i` to install all node dependencies. This is required to start the bot.
+1
View File
@@ -8,3 +8,4 @@ services:
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./sql:/app/sql - ./sql:/app/sql
- ./certs:/app/certs
+2 -1
View File
@@ -30,6 +30,7 @@
"build": "npm run clean && node scripts/build.js && npm run copyfiles", "build": "npm run clean && node scripts/build.js && npm run copyfiles",
"clean": "rimraf dist", "clean": "rimraf dist",
"copyfiles": "copyfiles -u 1 \"./src/**/*.html\" ./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"
} }
} }
+1 -1
View File
@@ -132,7 +132,7 @@ export class Database {
await Database.db.run(` await Database.db.run(`
INSERT INTO messages (id, id_hash, author_id, author_id_hash, author_name, channel_id, attachments, stickers, content) VALUES 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); `, ...serializedMessage);
return true; return true;
+53 -8
View File
@@ -1,5 +1,12 @@
import { existsSync, readFileSync } from 'fs';
import { config } from '../config'; import { config } from '../config';
import _https from 'https';
import _http from 'http';
import { E621Pool, E621Post, E621User, PostFlag, Record } from '../types'; 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_TAGS: string[] = [];
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young']; const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
@@ -11,6 +18,16 @@ const USER_AGENT = 'E621DiscordBot';
export const SEARCH_LIMIT = 320; 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<any> { async function request(path: string, query?: { [name: string]: string }): Promise<any> {
const url = new URL(config.E621_BASE_URL!); const url = new URL(config.E621_BASE_URL!);
url.pathname = path + '.json'; url.pathname = path + '.json';
@@ -21,15 +38,43 @@ async function request(path: string, query?: { [name: string]: string }): Promis
} }
} }
const res = await fetch(url, { return new Promise((resolve) => {
headers: { http.get(url, {
'User-Agent': USER_AGENT 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<E621User | null> { export async function getE621User(idOrName: string | number): Promise<E621User | null> {