feat: Add initial backend server with various utility APIs, Sentry, logging, rate limiting, and a multi-arch Docker build workflow.

This commit is contained in:
2026-01-02 17:26:09 +01:00
parent e5902e9747
commit 652010a92f
5 changed files with 70 additions and 37 deletions

View File

@@ -17,6 +17,8 @@ jobs:
env:
REGISTRY: docker.io
DOCKERHUB_USER_LC: ${{ secrets.DOCKERHUB_USERNAME }}
permissions:
contents: read
steps:
- name: Checkout

View File

@@ -18,6 +18,7 @@
"macaddress": "^0.5.3",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"qs": "^6.14.1",
"whois-json": "^2.0.4"
}
},
@@ -855,6 +856,21 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1224,6 +1240,21 @@
"express": "^4.11 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/express/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
@@ -1950,11 +1981,12 @@
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"

View File

@@ -19,6 +19,7 @@
"macaddress": "^0.5.3",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"qs": "^6.14.1",
"whois-json": "^2.0.4"
}
}

View File

@@ -5,7 +5,7 @@ const whois = require('whois-json');
const pino = require('pino');
// Import utilities
const { isValidIp, isValidDomain } = require('../utils');
const { isValidIp, isValidDomain, isPrivateIp } = require('../utils');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
@@ -26,6 +26,11 @@ router.get('/', async (req, res, next) => {
return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' });
}
if (isValidIp(query) && isPrivateIp(query)) {
logger.warn({ requestIp, query }, 'Attempt to WHOIS lookup private IP blocked');
return res.status(403).json({ success: false, error: 'WHOIS lookup for private or local IP addresses is not supported.' });
}
// Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range,
// and domain lookups don't involve IP ranges.

View File

@@ -5,10 +5,10 @@ const Sentry = require("@sentry/node");
// Initialize Sentry BEFORE requiring any other modules!
Sentry.init({
// DSN should now be available from process.env if set in .env
dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456",
// Enable tracing - Adjust sample rate as needed
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// DSN should now be available from process.env if set in .env
dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456",
// Enable tracing - Adjust sample rate as needed
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
});
// DEBUG: Check Sentry object after init
@@ -36,10 +36,10 @@ const macLookupRoutes = require('./routes/macLookup');
// --- Logger Initialisierung ---
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } }
: undefined,
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } }
: undefined,
});
// Create Express app instance
@@ -71,11 +71,11 @@ app.set('trust proxy', parseInt(process.env.TRUST_PROXY_COUNT || '2', 10)); // A
// --- Rate Limiter ---
// Apply a general limiter to most routes
const generalLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP, ensure integer
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || (5 * 60 * 1000).toString(), 10), // Default 5 minutes
max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: { success: false, error: 'Too many requests from this IP, please try again after 5 minutes' },
message: { success: false, error: 'Too many requests from this IP, please try again after a while' },
keyGenerator: (req, res) => req.ip, // Use client IP address from Express
handler: (req, res, next, options) => {
logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded');
@@ -87,15 +87,8 @@ const generalLimiter = rateLimit({
}
});
// Apply the limiter to specific API routes that perform external actions
// Note: /api/ipinfo and /api/version are often excluded as they are less resource-intensive
app.use('/api/ping', generalLimiter);
app.use('/api/traceroute', generalLimiter);
app.use('/api/lookup', generalLimiter);
app.use('/api/dns-lookup', generalLimiter);
app.use('/api/whois-lookup', generalLimiter);
app.use('/api/port-scan', generalLimiter);
app.use('/api/mac-lookup', generalLimiter);
// Apply the limiter to ALL API routes
app.use('/api', generalLimiter);
// --- API Routes ---
@@ -116,12 +109,12 @@ app.use('/api/mac-lookup', macLookupRoutes);
if (Sentry.Handlers && Sentry.Handlers.errorHandler) {
app.use(Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// Capture all 500 errors
if (error.status === 500) return true;
// Capture specific client errors if needed, e.g., 403
// if (error.status === 403) return true;
// By default, capture only server errors (5xx)
return error.status >= 500;
// Capture all 500 errors
if (error.status === 500) return true;
// Capture specific client errors if needed, e.g., 403
// if (error.status === 403) return true;
// By default, capture only server errors (5xx)
return error.status >= 500;
},
}));
} else {
@@ -182,9 +175,9 @@ async function gracefulShutdown(signal) {
await Sentry.close(2000); // 2 second timeout
logger.info('Sentry closed.');
} catch (e) {
logger.error({ error: e.message }, 'Error closing Sentry');
logger.error({ error: e.message }, 'Error closing Sentry');
} finally {
process.exit(128 + signals[signal]); // Standard exit code for signals
process.exit(128 + signals[signal]); // Standard exit code for signals
}
});
} else {
@@ -194,9 +187,9 @@ async function gracefulShutdown(signal) {
await Sentry.close(2000);
logger.info('Sentry closed (server never started).');
} catch (e) {
logger.error({ error: e.message }, 'Error closing Sentry (server never started)');
logger.error({ error: e.message }, 'Error closing Sentry (server never started)');
} finally {
process.exit(128 + signals[signal]);
process.exit(128 + signals[signal]);
}
}
@@ -209,5 +202,5 @@ async function gracefulShutdown(signal) {
// Register signal handlers
Object.keys(signals).forEach((signal) => {
process.on(signal, () => gracefulShutdown(signal));
process.on(signal, () => gracefulShutdown(signal));
});