Files
cloudflare_temp_email/worker/src/worker.ts

304 lines
8.5 KiB
TypeScript

import { Context, Hono } from 'hono'
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt'
import { Jwt } from 'hono/utils/jwt'
import { api as commonApi } from './commom_api';
import { api as mailsApi } from './mails_api'
import { api as userApi } from './user_api';
import { api as adminApi } from './admin_api';
import { api as apiSendMail } from './mails_api/send_mail_api'
import { api as telegramApi } from './telegram_api'
import i18n from './i18n';
import { email } from './email';
import { scheduled } from './scheduled';
import { getAdminPasswords, getPasswords, getBooleanValue, getStringArray } from './utils';
import { checkIpBlacklist } from './ip_blacklist';
const API_PATHS = [
"/api/",
"/open_api/",
"/user_api/",
"/admin/",
"/telegram/",
"/external/",
];
const app = new Hono<HonoCustomType>()
//cors
app.use('/*', cors());
// error handler
app.onError((err, c) => {
console.error(err)
return c.text(`${err.name} ${err.message}`, 500)
})
// global middlewares
app.use('/*', async (c, next) => {
// check if the request is for static files
if (c.env.ASSETS && !API_PATHS.some(path => c.req.path.startsWith(path))) {
const url = new URL(c.req.raw.url);
if (!url.pathname.includes('.')) {
url.pathname = ""
}
return c.env.ASSETS.fetch(url);
}
// save language in context
const lang = c.req.raw.headers.get("x-lang");
if (lang) { c.set("lang", lang); }
const msgs = i18n.getMessages(lang || c.env.DEFAULT_LANG);
if (
c.req.path.startsWith("/api/new_address")
|| c.req.path.startsWith("/api/send_mail")
|| c.req.path.startsWith("/external/api/send_mail")
|| c.req.path.startsWith("/user_api/register")
|| c.req.path.startsWith("/user_api/verify_code")
) {
// Check IP blacklist first (early rejection for blacklisted IPs)
const blacklistResponse = await checkIpBlacklist(c);
if (blacklistResponse) {
return blacklistResponse;
}
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
if (reqIp && c.env.RATE_LIMITER) {
const { success } = await c.env.RATE_LIMITER.limit(
{ key: `${c.req.path}|${reqIp}` }
)
if (!success) {
return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429)
}
}
try {
if (reqIp && c.env.KV && c.env.RATE_LIMIT_API_DAILY_REQUESTS) {
const daily_count_key = `limit|${reqIp}|${new Date().toISOString().slice(0, 10)}`
const dailyLimit = parseInt(c.env.RATE_LIMIT_API_DAILY_REQUESTS.toString(), 10);
const current_count = parseInt(await c.env.KV.get(daily_count_key) || "0", 10);
if (current_count && current_count >= dailyLimit) {
return c.text(`IP=${reqIp} Exceeded daily limit of ${dailyLimit} requests`, 429);
}
await c.env.KV.put(daily_count_key, ((current_count || 0) + 1).toString(), { expirationTtl: 24 * 60 * 60 });
}
} catch (e) {
console.error(e);
}
}
// webhook check
if (
c.req.path.startsWith("/api/webhook")
|| c.req.path.startsWith("/admin/webhook")
|| c.req.path.startsWith("/admin/mail_webhook")
) {
if (!c.env.KV) {
return c.text(msgs.KVNotAvailableMsg, 400);
}
if (!getBooleanValue(c.env.ENABLE_WEBHOOK)) {
return c.text(msgs.WebhookNotEnabledMsg, 403);
}
}
if (!c.env.DB) {
return c.text(msgs.DBNotAvailableMsg, 400);
}
if (!c.env.JWT_SECRET) {
return c.text(msgs.JWTSecretNotSetMsg, 400);
}
await next()
});
const checkUserPayload = async (
c: Context<HonoCustomType>
): Promise<void> => {
try {
const token = c.req.raw.headers.get("x-user-token");
if (!token) return;
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return;
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return;
}
c.set("userPayload", payload as UserPayload);
} catch (e) {
console.error(e);
}
}
const checkoutUserRolePayload = async (
c: Context<HonoCustomType>
): Promise<void> => {
try {
const token = c.req.raw.headers.get("x-user-access-token");
if (!token) return;
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return;
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return;
}
if (typeof payload?.user_role !== "string") return;
c.set("userRolePayload", payload.user_role);
} catch (e) {
console.error(e);
}
}
// api auth
app.use('/api/*', async (c, next) => {
// check header x-custom-auth
const passwords = getPasswords(c);
if (passwords && passwords.length > 0) {
const auth = c.req.raw.headers.get("x-custom-auth");
if (!auth || !passwords.includes(auth)) {
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const messages = i18n.getMessages(lang);
return c.text(messages.CustomAuthPasswordMsg, 401)
}
}
if (c.req.path.startsWith("/api/new_address")) {
await checkUserPayload(c);
await next();
return;
}
if (c.req.path.startsWith("/api/settings")
|| c.req.path.startsWith("/api/send_mail")
) {
await checkoutUserRolePayload(c);
}
if (c.req.path.startsWith("/api/address_login")) {
await next();
return;
}
try {
return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
} catch (e) {
console.warn(e);
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
return c.text(msgs.InvalidAddressCredentialMsg, 401)
}
});
// user_api auth
app.use('/user_api/*', async (c, next) => {
if (
c.req.path.startsWith("/user_api/open_settings")
|| c.req.path.startsWith("/user_api/register")
|| c.req.path.startsWith("/user_api/login")
|| c.req.path.startsWith("/user_api/verify_code")
|| c.req.path.startsWith("/user_api/passkey/authenticate_")
|| c.req.path.startsWith("/user_api/oauth2")
) {
await next();
return;
}
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
try {
const token = c.req.raw.headers.get("x-user-token");
if (!token) return c.text(msgs.UserTokenExpiredMsg, 401)
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return c.text(msgs.UserTokenExpiredMsg, 401);
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return c.text(msgs.UserTokenExpiredMsg, 401)
}
c.set("userPayload", payload as UserPayload);
} catch (e) {
console.error(e);
return c.text(msgs.UserTokenExpiredMsg, 401)
}
if (c.req.path.startsWith("/user_api/bind_address")) {
await checkoutUserRolePayload(c);
}
if (c.req.path.startsWith('/user_api/bind_address')
&& c.req.method === 'POST'
) {
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
}
await next();
});
// admin auth
app.use('/admin/*', async (c, next) => {
// check header x-admin-auth
const adminPasswords = getAdminPasswords(c);
if (adminPasswords && adminPasswords.length > 0) {
const adminAuth = c.req.raw.headers.get("x-admin-auth");
if (adminAuth && adminPasswords.includes(adminAuth)) {
await next();
return;
}
}
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
// check if user is admin
const access_token = c.req.raw.headers.get("x-user-access-token");
if (c.env.ADMIN_USER_ROLE && access_token) {
try {
const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return c.text(msgs.UserAcceesTokenExpiredMsg, 401);
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return c.text(msgs.UserAcceesTokenExpiredMsg, 401)
}
if (payload.user_role !== c.env.ADMIN_USER_ROLE) {
return c.text(msgs.UserRoleIsNotAdminMsg, 401)
}
await next();
return;
} catch (e) {
console.error(e);
}
}
// disable admin api check
if (getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)) {
await next();
return;
}
return c.text(msgs.NeedAdminPasswordMsg, 401)
});
app.route('/', commonApi)
app.route('/', mailsApi)
app.route('/', userApi)
app.route('/', adminApi)
app.route('/', apiSendMail)
app.route('/', telegramApi)
const health_check = async (c: Context<HonoCustomType>) => {
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!c.env.DB) {
return c.text(msgs.DBNotAvailableMsg, 400);
}
if (!c.env.JWT_SECRET) {
return c.text(msgs.JWTSecretNotSetMsg, 400);
}
if (getStringArray(c.env.DOMAINS).length === 0) {
return c.text(msgs.DomainsNotSetMsg, 400);
}
return c.text("OK");
}
app.get('/', health_check)
app.get('/health_check', health_check)
app.all('/*', async c => c.text("Not Found", 404))
export default {
fetch: app.fetch,
email: email,
scheduled: scheduled,
}