mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-04-05 16:22:00 +02:00
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:
2
.github/workflows/docker-build-push.yml
vendored
2
.github/workflows/docker-build-push.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
DOCKERHUB_USER_LC: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
40
backend/package-lock.json
generated
40
backend/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
Reference in New Issue
Block a user