mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-28 19:48:01 +08:00
refactor: keep existing rate limit behavior
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
- fix: |Admin| 修复切换一级标签页时二级标签页偶发无选中项、内容不显示及指示条偏移的问题
|
- fix: |Admin| 修复切换一级标签页时二级标签页偶发无选中项、内容不显示及指示条偏移的问题
|
||||||
- fix: |发信页面| 统一邮箱与名称字段顺序,并修复空正文输入框的光标与占位文字错位
|
- fix: |发信页面| 统一邮箱与名称字段顺序,并修复空正文输入框的光标与占位文字错位
|
||||||
- fix: |用户发信| 用户地址发信接口支持角色无限额度,并让同一 IP 的所有绑定地址共用发信频率限制
|
- fix: |用户发信| 用户地址发信接口支持角色无限额度
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
- fix: |Admin| Fix secondary tabs occasionally losing their active item, hiding content, and leaving the indicator offset after switching primary tabs
|
- fix: |Admin| Fix secondary tabs occasionally losing their active item, hiding content, and leaving the indicator offset after switching primary tabs
|
||||||
- fix: |Send Mail| Use a consistent address/name field order and align the empty content editor caret with its placeholder
|
- fix: |Send Mail| Use a consistent address/name field order and align the empty content editor caret with its placeholder
|
||||||
- fix: |User Send Mail| Apply role-based unlimited sending to user-address APIs and share one send-rate limit across all bound addresses for the same IP
|
- fix: |User Send Mail| Apply role-based unlimited sending to user-address APIs
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
|||||||
@@ -90,24 +90,6 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
worker-rate-limit:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: e2e/Dockerfile.worker
|
|
||||||
args:
|
|
||||||
WRANGLER_TOML: e2e/fixtures/wrangler.toml.e2e.rate-limit
|
|
||||||
ports:
|
|
||||||
- "8792:8792"
|
|
||||||
command: ["pnpm", "exec", "wrangler", "dev", "--port", "8792", "--ip", "0.0.0.0"]
|
|
||||||
depends_on:
|
|
||||||
- mailpit
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:8792/health_check"]
|
|
||||||
interval: 3s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 10s
|
|
||||||
retries: 20
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
@@ -165,7 +147,6 @@ services:
|
|||||||
WORKER_URL_ENV_OFF: http://worker-env-off:8790
|
WORKER_URL_ENV_OFF: http://worker-env-off:8790
|
||||||
WORKER_GZIP_URL: http://worker-gzip:8788
|
WORKER_GZIP_URL: http://worker-gzip:8788
|
||||||
WORKER_URL_SEND_MAIL_DOMAIN: http://worker-send-mail-domain:8791
|
WORKER_URL_SEND_MAIL_DOMAIN: http://worker-send-mail-domain:8791
|
||||||
WORKER_URL_RATE_LIMIT: http://worker-rate-limit:8792
|
|
||||||
FRONTEND_URL: https://frontend:5173
|
FRONTEND_URL: https://frontend:5173
|
||||||
MAILPIT_API: http://mailpit:8025/api
|
MAILPIT_API: http://mailpit:8025/api
|
||||||
SMTP_PROXY_HOST: smtp-proxy
|
SMTP_PROXY_HOST: smtp-proxy
|
||||||
@@ -186,8 +167,6 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker-send-mail-domain:
|
worker-send-mail-domain:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker-rate-limit:
|
|
||||||
condition: service_healthy
|
|
||||||
frontend:
|
frontend:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
smtp-proxy:
|
smtp-proxy:
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name = "cloudflare_temp_email_rate_limit"
|
|
||||||
main = "src/worker.ts"
|
|
||||||
compatibility_date = "2025-04-01"
|
|
||||||
compatibility_flags = [ "nodejs_compat" ]
|
|
||||||
keep_vars = true
|
|
||||||
|
|
||||||
[vars]
|
|
||||||
PREFIX = "tmp"
|
|
||||||
DEFAULT_DOMAINS = ["test.example.com"]
|
|
||||||
DOMAINS = ["test.example.com"]
|
|
||||||
JWT_SECRET = "e2e-test-secret-key-rate-limit"
|
|
||||||
BLACK_LIST = ""
|
|
||||||
ENABLE_USER_CREATE_EMAIL = true
|
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
|
||||||
DEFAULT_SEND_BALANCE = 10
|
|
||||||
DISABLE_ADMIN_PASSWORD_CHECK = true
|
|
||||||
E2E_TEST_MODE = true
|
|
||||||
SMTP_CONFIG = """
|
|
||||||
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "KV"
|
|
||||||
id = "e2e-test-kv-rate-limit-00000000-0000-0000-0000-000000000000"
|
|
||||||
|
|
||||||
[[d1_databases]]
|
|
||||||
binding = "DB"
|
|
||||||
database_name = "e2e-temp-email-rate-limit"
|
|
||||||
database_id = "e2e-test-db-rate-limit-00000000-0000-0000-0000-000000000000"
|
|
||||||
|
|
||||||
[[ratelimits]]
|
|
||||||
name = "RATE_LIMITER"
|
|
||||||
namespace_id = "1001"
|
|
||||||
simple = { limit = 2, period = 60 }
|
|
||||||
@@ -110,12 +110,5 @@ if [ -n "${WORKER_GZIP_URL:-}" ]; then
|
|||||||
echo " Gzip worker database initialized"
|
echo " Gzip worker database initialized"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${WORKER_URL_RATE_LIMIT:-}" ]; then
|
|
||||||
echo "==> Initializing rate-limit worker database"
|
|
||||||
curl -sf -X POST "$WORKER_URL_RATE_LIMIT/admin/db_initialize" > /dev/null
|
|
||||||
curl -sf -X POST "$WORKER_URL_RATE_LIMIT/admin/db_migration" > /dev/null
|
|
||||||
echo " Rate-limit worker database initialized"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> Running Playwright tests"
|
echo "==> Running Playwright tests"
|
||||||
exec npx playwright test "$@"
|
exec npx playwright test "$@"
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ import {
|
|||||||
updateAddressSender,
|
updateAddressSender,
|
||||||
} from '../../fixtures/test-helpers';
|
} from '../../fixtures/test-helpers';
|
||||||
|
|
||||||
async function createUser(request: APIRequestContext, workerUrl = WORKER_URL) {
|
async function createUser(request: APIRequestContext) {
|
||||||
const email = `user-send-${Date.now()}@test.example.com`;
|
const email = `user-send-${Date.now()}@test.example.com`;
|
||||||
const password = hashPassword('test-password-123');
|
const password = hashPassword('test-password-123');
|
||||||
const registerRes = await request.post(`${workerUrl}/user_api/register`, {
|
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||||
data: { email, password },
|
data: { email, password },
|
||||||
});
|
});
|
||||||
expect(registerRes.ok()).toBe(true);
|
expect(registerRes.ok()).toBe(true);
|
||||||
|
|
||||||
const loginRes = await request.post(`${workerUrl}/user_api/login`, {
|
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||||
data: { email, password },
|
data: { email, password },
|
||||||
});
|
});
|
||||||
expect(loginRes.ok()).toBe(true);
|
expect(loginRes.ok()).toBe(true);
|
||||||
@@ -32,9 +32,8 @@ async function bindAddress(
|
|||||||
request: APIRequestContext,
|
request: APIRequestContext,
|
||||||
userJwt: string,
|
userJwt: string,
|
||||||
addressJwt: string,
|
addressJwt: string,
|
||||||
workerUrl = WORKER_URL,
|
|
||||||
) {
|
) {
|
||||||
const response = await request.post(`${workerUrl}/user_api/bind_address`, {
|
const response = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${addressJwt}`,
|
Authorization: `Bearer ${addressJwt}`,
|
||||||
'x-user-token': userJwt,
|
'x-user-token': userJwt,
|
||||||
@@ -369,74 +368,4 @@ test.describe('User send mail API', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shares one rate-limit bucket across bound addresses', async ({ request }) => {
|
|
||||||
const workerUrl = process.env.WORKER_URL_RATE_LIMIT || '';
|
|
||||||
test.skip(!workerUrl, 'WORKER_URL_RATE_LIMIT is not configured');
|
|
||||||
const addressIds: number[] = [];
|
|
||||||
let userId: number | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const enableUserRes = await request.post(`${workerUrl}/admin/user_settings`, {
|
|
||||||
data: {
|
|
||||||
enable: true,
|
|
||||||
enableMailVerify: false,
|
|
||||||
maxAddressCount: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(enableUserRes.ok()).toBe(true);
|
|
||||||
const user = await createUser(request, workerUrl);
|
|
||||||
userId = user.userId;
|
|
||||||
const createAddress = async (name: string) => {
|
|
||||||
const response = await request.post(`${workerUrl}/api/new_address`, {
|
|
||||||
data: { name, domain: 'test.example.com' },
|
|
||||||
});
|
|
||||||
expect(response.ok()).toBe(true);
|
|
||||||
return await response.json() as {
|
|
||||||
jwt: string;
|
|
||||||
address_id: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const first = await createAddress(`rate-first-${Date.now()}`);
|
|
||||||
const second = await createAddress(`rate-second-${Date.now()}`);
|
|
||||||
addressIds.push(first.address_id, second.address_id);
|
|
||||||
await bindAddress(request, user.jwt, first.jwt, workerUrl);
|
|
||||||
await bindAddress(request, user.jwt, second.jwt, workerUrl);
|
|
||||||
|
|
||||||
const requestAccess = async (address: typeof first) => {
|
|
||||||
const accessRes = await request.post(
|
|
||||||
`${workerUrl}/user_api/address/${address.address_id}/request_send_mail_access`,
|
|
||||||
{ headers: { 'x-user-token': user.jwt } },
|
|
||||||
);
|
|
||||||
expect(accessRes.ok()).toBe(true);
|
|
||||||
};
|
|
||||||
await requestAccess(first);
|
|
||||||
await requestAccess(second);
|
|
||||||
|
|
||||||
const send = (address: typeof first, sequence: number) => request.post(
|
|
||||||
`${workerUrl}/user_api/address/${address.address_id}/send_mail`,
|
|
||||||
{
|
|
||||||
headers: { 'x-user-token': user.jwt },
|
|
||||||
data: {
|
|
||||||
to_mail: 'recipient@test.example.com',
|
|
||||||
subject: `Shared rate limit ${sequence} ${Date.now()}`,
|
|
||||||
content: 'Rate limit test',
|
|
||||||
is_html: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect((await send(first, 1)).ok()).toBe(true);
|
|
||||||
expect((await send(second, 2)).ok()).toBe(true);
|
|
||||||
const limitedRes = await send(first, 3);
|
|
||||||
expect(limitedRes.status()).toBe(429);
|
|
||||||
expect(await limitedRes.text()).toContain('Rate limit exceeded');
|
|
||||||
} finally {
|
|
||||||
await Promise.allSettled(addressIds.map((addressId) => (
|
|
||||||
request.delete(`${workerUrl}/admin/delete_address/${addressId}`)
|
|
||||||
)));
|
|
||||||
if (userId !== undefined) {
|
|
||||||
await request.delete(`${workerUrl}/admin/users/${userId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,11 +74,9 @@ app.use('/*', async (c, next) => {
|
|||||||
) {
|
) {
|
||||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||||
if (reqIp && c.env.RATE_LIMITER) {
|
if (reqIp && c.env.RATE_LIMITER) {
|
||||||
const { success } = await c.env.RATE_LIMITER.limit({
|
const { success } = await c.env.RATE_LIMITER.limit(
|
||||||
key: `${c.req.path.startsWith("/user_api/address/")
|
{ key: `${c.req.path}|${reqIp}` }
|
||||||
? "/user_api/address/send_mail"
|
)
|
||||||
: c.req.path}|${reqIp}`
|
|
||||||
})
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429)
|
return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user