feat: Implement the core InterNetX DDNS updater application with API, services, middleware, Docker support, and a detailed README.

This commit is contained in:
2026-01-26 19:53:31 +01:00
parent bba71a7272
commit 11d2c1fce2
19 changed files with 1739 additions and 2 deletions

View File

@@ -0,0 +1,47 @@
const config = require('../config/config');
const { logger, redactToken } = require('../utils/logger');
/**
* Authentication middleware - validates token
*/
const authenticate = (req, res, next) => {
const token = req.query.token || req.body.token;
if (!token) {
logger.warn('Authentication failed: No token provided', {
ip: req.ip,
path: req.path
});
return res.status(401).json({
error: 'Authentication required',
message: 'Missing token parameter'
});
}
// Check if token is valid
const isValid = config.security.authTokens.includes(token);
if (!isValid) {
logger.warn('Authentication failed: Invalid token', {
ip: req.ip,
token: redactToken(token),
path: req.path
});
return res.status(401).json({
error: 'Authentication failed',
message: 'Invalid token'
});
}
// Token is valid, attach redacted version for logging
req.tokenPrefix = redactToken(token);
logger.debug('Authentication successful', {
ip: req.ip,
token: req.tokenPrefix
});
next();
};
module.exports = authenticate;

View File

@@ -0,0 +1,50 @@
const rateLimit = require('express-rate-limit');
const config = require('../config/config');
const { logger } = require('../utils/logger');
/**
* Rate limiter for update endpoint
*/
const updateLimiter = rateLimit({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.maxRequests,
message: {
error: 'Too many requests',
message: 'Rate limit exceeded. Please try again later.',
},
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false, // Disable `X-RateLimit-*` headers
// Custom handler for rate limit exceeded
handler: (req, res) => {
logger.warn('Rate limit exceeded', {
ip: req.ip,
path: req.path,
limit: config.rateLimit.maxRequests,
window: `${config.rateLimit.windowMs / 1000}s`,
});
res.status(429).json({
error: 'Too many requests',
message: 'You have exceeded the rate limit. Please try again later.',
retryAfter: Math.ceil(config.rateLimit.windowMs / 1000),
});
},
// Skip rate limiting for successful requests (optional)
skip: (req) => {
// Could skip based on certain conditions, e.g., whitelisted IPs
return false;
},
// Key generator - rate limit per IP
keyGenerator: (req) => {
return req.ip;
},
});
module.exports = {
updateLimiter,
};

View File

@@ -0,0 +1,87 @@
const Joi = require('joi');
const config = require('../config/config');
const { logger } = require('../utils/logger');
/**
* Validation schema for update endpoint
*/
const updateSchema = Joi.object({
hostname: Joi.string()
.hostname()
.pattern(new RegExp(`^[a-z0-9\\-]+\\.${config.ddns.defaultZone.replace('.', '\\.')}$`, 'i'))
.required()
.messages({
'string.pattern.base': `Hostname must be a subdomain of ${config.ddns.defaultZone}`,
'any.required': 'Hostname is required',
}),
myip: Joi.string()
.ip({ version: ['ipv4'], cidr: 'forbidden' })
.optional()
.messages({
'string.ip': 'Invalid IPv4 address format',
}),
myipv6: Joi.string()
.ip({ version: ['ipv6'], cidr: 'forbidden' })
.optional()
.messages({
'string.ip': 'Invalid IPv6 address format',
}),
token: Joi.string()
.min(8)
.max(256)
.required()
.messages({
'string.min': 'Token is too short',
'any.required': 'Token is required',
}),
}).or('myip', 'myipv6')
.messages({
'object.missing': 'At least one IP address (myip or myipv6) is required',
});
/**
* Validation middleware factory
* @param {Object} schema - Joi validation schema
* @returns {Function} - Express middleware
*/
const validate = (schema) => {
return (req, res, next) => {
// Merge query and body for validation
const data = { ...req.query, ...req.body };
const { error, value } = schema.validate(data, {
abortEarly: false, // Return all errors, not just the first
stripUnknown: true, // Remove unknown fields
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
}));
logger.warn('Validation failed', {
ip: req.ip,
errors,
data: { ...data, token: '***' } // Redact token in logs
});
return res.status(400).json({
error: 'Validation failed',
details: errors,
});
}
// Attach validated data to request
req.validatedData = value;
next();
};
};
module.exports = {
validate,
updateSchema,
};