Resolve hostnames from whitelist

0bc4396427612fd5e590a78c2792d080cea482c8

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +64 -0Ignore whitespace
default/config.yaml+1 -0
@@ -42,6 +42,7 @@ enableForwardedWhitelist: true
42whitelist:42whitelist:
43 - ::143 - ::1
44 - 127.0.0.144 - 127.0.0.1
45 - host.docker.internal
45# Toggle basic authentication for endpoints46# Toggle basic authentication for endpoints
46basicAuthMode: false47basicAuthMode: false
47# Basic authentication credentials48# Basic authentication credentials
src/middleware/whitelist.js+63 -0
@@ -1,7 +1,9 @@
1import path from 'node:path';1import path from 'node:path';
2import fs from 'node:fs';2import fs from 'node:fs';
3import process from 'node:process';3import process from 'node:process';
4import dns from 'node:dns';
4import Handlebars from 'handlebars';5import Handlebars from 'handlebars';
6import ipRegex from 'ip-regex';
5import ipMatching from 'ip-matching';7import ipMatching from 'ip-matching';
68
7import { getIpFromRequest } from '../express-common.js';9import { getIpFromRequest } from '../express-common.js';
@@ -20,6 +22,8 @@ if (fs.existsSync(whitelistPath)) {
20 }22 }
21}23}
2224
25await resolveHostnames();
26
23/**27/**
24 * Get the client IP address from the request headers.28 * Get the client IP address from the request headers.
25 * @param {import('express').Request} req Express request object29 * @param {import('express').Request} req Express request object
@@ -46,6 +50,65 @@ function getForwardedIp(req) {
46}50}
4751
48/**52/**
53 * Checks if a string is a valid hostname according to RFC 1123
54 * @param {string} hostname The string to test
55 * @returns {boolean} True if the string is a valid hostname
56 */
57function isValidHostname(hostname) {
58 const hostnameRegex = /^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/i;
59 return hostnameRegex.test(hostname);
60}
61
62/**
63 * Checks if a string is an IP address, CIDR notation, or IP wildcard
64 * @param {string} entry The string to test
65 * @returns {boolean} True if the string matches any IP format
66 */
67function isIpFormat(entry) {
68 // Match CIDR notation (e.g. 192.168.0.0/24)
69 if (entry.includes('/')) {
70 return true;
71 }
72
73 // Match exact IP address
74 if (ipRegex({ exact: true }).test(entry)) {
75 return true;
76 }
77
78 // Match IPv4 with wildcards (e.g. 192.168.*.* or 192.168.0.*)
79 const ipWildcardRegex = /^(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)$/;
80 return ipWildcardRegex.test(entry);
81}
82
83/**
84 * Validate the IP addresses in the whitelist.
85 * Hostnames are resolved to IP addresses.
86 */
87async function resolveHostnames() {
88 for (let i = 0; i < whitelist.length; i++) {
89 try {
90 const entry = whitelist[i];
91
92 // Skip if entry appears to be an IP address, CIDR notation, or IP wildcard
93 if (isIpFormat(entry)) {
94 continue;
95 }
96
97 if (isValidHostname(entry)) {
98 const result = await dns.promises.lookup(entry);
99
100 if (result.address) {
101 console.info('Resolved whitelist hostname', entry, 'to IP address', result.address);
102 whitelist[i] = result.address;
103 }
104 }
105 } catch {
106 // Ignore errors when resolving hostnames
107 }
108 }
109}
110
111/**
49 * Returns a middleware function that checks if the client IP is in the whitelist.112 * Returns a middleware function that checks if the client IP is in the whitelist.
50 * @returns {import('express').RequestHandler} The middleware function113 * @returns {import('express').RequestHandler} The middleware function
51 */114 */