feat(backend): 实施连接与数据层速率限制
为了增强服务的稳定性和安全性,防止滥用行为,本次提交引入了两个核心的速率限制和验证机制: 1. **连接层验证 (`onConnect`)** - **IP 速率限制**:在 `connectionValidator.js` 中实现。限制单个 IP 地址每分钟最多 20 次连接,超过限制将临时封禁 5 分钟,以防御暴力连接攻击。 - **反向 DNS (PTR) 检查**:要求所有公共网络连接必须具有有效的 PTR 记录,用于区分合法邮件服务器和僵尸网络。 2. **数据层验证 (`onData`)** - **发件人域速率限制**:在 `rateLimiter.js` 中实现。在邮件数据传输阶段,限制单个发件人域名每分钟最多发送 10 封邮件,超过限制的域名将被临时封禁 5 分钟,以防止单一来源的邮件泛滥。 **主要变更:** - 新增 `backend/connectionValidator.js`:处理连接时的 IP 速率限制和 PTR 验证。 - 新增 `backend/rateLimiter.js`:在 `onData` 阶段对发件人域名进行速率限制。 - 在 `backend/app.js` 的 `SMTPServer` 配置中集成了 `onConnect` 验证钩子。 - 在 `backend/saveEmail.js` 中调用速率限制器,并在超限时拒绝邮件。 - 更新日志模块 (`logger.js`, `db.js`),输出更清晰,并记录安全相关事件。 - 新增 `backend/SECURITY_POLICIES.md` 文档,详细说明所有安全策略和可配置参数。
This commit is contained in:
+48
-8
@@ -1,6 +1,8 @@
|
||||
const { simpleParser } = require('mailparser');
|
||||
const db = require('./db');
|
||||
const emitter = require('./eventEmitter');
|
||||
const { isRateLimited } = require('./rateLimiter');
|
||||
const logger = require('./logger'); // 引入 logger
|
||||
|
||||
// Helper function to convert stream to buffer
|
||||
function streamToBuffer(stream) {
|
||||
@@ -19,20 +21,44 @@ async function saveEmail(stream) {
|
||||
|
||||
// Now, parse the buffered email content
|
||||
const parsed = await simpleParser(emailBuffer);
|
||||
const rawEmail = emailBuffer.toString();
|
||||
|
||||
const recipient = parsed.to ? parsed.to.text : 'undisclosed-recipients';
|
||||
const sender = parsed.from ? parsed.from.text : 'unknown-sender';
|
||||
//const rawEmail = emailBuffer.toString(); // 暂时去除 rawEmail,不在保存到数据库
|
||||
// 在这里进行速率限制检查
|
||||
if (isRateLimited(sender)) {
|
||||
// 记录被拒绝的事件
|
||||
logger.warn(`Email from <${sender}> rejected due to rate limiting.`, {
|
||||
sender: sender,
|
||||
recipient: recipient,
|
||||
action: 'rate-limit-reject'
|
||||
});
|
||||
// 如果被限流,则抛出错误,上游的SMTPServer会处理这个错误并拒绝邮件
|
||||
const error = new Error(`4.7.1 Domain of <${sender}> has been temporarily blocked due to rate limiting. Please try again later.`);
|
||||
error.responseCode = 421; // "Service not available, closing transmission channel"
|
||||
throw error;
|
||||
}
|
||||
|
||||
const subject = parsed.subject || 'No Subject';
|
||||
const body = parsed.text || (parsed.html || '');
|
||||
|
||||
const [result] = await db.execute(
|
||||
'INSERT INTO emails (recipient, sender, subject, body, raw) VALUES (?, ?, ?, ?, ?)',
|
||||
[recipient, sender, subject, body, rawEmail]
|
||||
'INSERT INTO emails (recipient, sender, subject, body) VALUES (?, ?, ?, ?)',
|
||||
[recipient, sender, subject, body]
|
||||
);
|
||||
// const [result] = await db.execute(
|
||||
// 'INSERT INTO emails (recipient, sender, subject, body, raw) VALUES (?, ?, ?, ?, ?)',
|
||||
// [recipient, sender, subject, body, rawEmail]
|
||||
// );
|
||||
const newEmailId = result.insertId;
|
||||
|
||||
console.log(`Email from <${sender}> to <${recipient}> saved with ID: ${newEmailId}`);
|
||||
logger.info(`Email from <${sender}> to <${recipient}> saved with ID: ${newEmailId}`, {
|
||||
sender,
|
||||
recipient,
|
||||
subject,
|
||||
emailId: newEmailId,
|
||||
action: 'email-saved'
|
||||
});
|
||||
|
||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
||||
for (const attachment of parsed.attachments) {
|
||||
@@ -40,7 +66,11 @@ async function saveEmail(stream) {
|
||||
'INSERT INTO email_attachments (email_id, filename, content_type, content) VALUES (?, ?, ?, ?)',
|
||||
[newEmailId, attachment.filename, attachment.contentType, attachment.content]
|
||||
);
|
||||
console.log(`Attachment ${attachment.filename} saved.`);
|
||||
logger.info(`Attachment ${attachment.filename} saved for email ID: ${newEmailId}`, {
|
||||
filename: attachment.filename,
|
||||
emailId: newEmailId,
|
||||
action: 'attachment-saved'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +78,22 @@ async function saveEmail(stream) {
|
||||
const [rows] = await db.execute('SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE id = ?', [newEmailId]);
|
||||
if (rows.length > 0) {
|
||||
emitter.emit('newEmail', rows[0]);
|
||||
logger.info(`Event 'newEmail' emitted for email ID: ${newEmailId}`, {
|
||||
emailId: newEmailId,
|
||||
action: 'event-emitted'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save email:', error);
|
||||
// We should not exit the process here, but maybe throw the error
|
||||
// so the caller (SMTPServer) can handle it.
|
||||
// 如果错误是带有响应码的(例如我们的速率限制错误),它已经被记录过了。
|
||||
// 我们只记录其他意想不到的错误。
|
||||
if (!error.responseCode) {
|
||||
logger.error('Failed to save email due to an unexpected error:', {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack
|
||||
});
|
||||
}
|
||||
// 重新抛出错误,以便上游的SMTPServer可以正确处理它。
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user