利用CloudflareWorker反代emby并实现访问统计
这是一个部署在 Cloudflare Workers 上的 Emby 媒体服务器反向代理。
Cloudflare Worker Emby 代理服务使用教程
📌 项目简介
这是一个部署在 Cloudflare Workers 上的 Emby 媒体服务器反向代理,主要功能包括:
- 🔒 地区限制:仅允许中国大陆 IP 访问(境外 IP 自动拦截)
- 📊 访问统计:实时记录访问日志,提供可视化统计面板
- 🔐 密码保护:统计页面需要密码才能查看
- 🌐 WebSocket 支持:完整支持 Emby 的 WebSocket 连接
- 📝 详细日志:记录 IP、国家、请求类型、响应时间等信息
🚀 部署步骤
第一步:注册并登录 Cloudflare
- 打开浏览器,访问 https://dash.cloudflare.com/sign-up
- 输入邮箱地址和密码,完成注册
- 登录 Cloudflare 控制台
第二步:将域名接入 Cloudflare(可选)
- 在 Cloudflare 控制台首页,点击 添加站点
- 输入你的域名(例如
example.com),点击 继续 - 选择 Free 套餐,点击 继续
- Cloudflare 会扫描你当前的 DNS 记录,确认无误后点击 继续
- Cloudflare 会提供两个新的 DNS 服务器地址
- 前往你的域名注册商(如阿里云、腾讯云、GoDaddy 等),将域名的 DNS 服务器修改为 Cloudflare 提供的地址
- 回到 Cloudflare 控制台,点击 完成,检查名称服务器
- 等待 DNS 生效(通常几分钟到几小时,Cloudflare 会发邮件通知)
第三步:创建 KV 命名空间
- 在 Cloudflare 控制台左侧菜单中,找到 Workers 和 Pages
- 点击展开,选择 KV
- 点击右上角的 创建命名空间
- 在弹窗中填写命名空间名称:
ACCESS_LOGS - 点击 添加 完成创建
- 你会看到新创建的命名空间出现在列表中
第四步:创建 Worker
- 在左侧菜单中,点击 Workers 和 Pages
- 点击 创建应用程序 按钮
- 选择 创建 Worker 标签页
- 给 Worker 取一个名称,例如
emby-proxy,他会生成属于你的worker地址,例如https://emby-proxy.rooxu.workers.dev - 点击右下角的 部署 按钮
- 部署成功后,点击 编辑代码 按钮
第五步:绑定 KV 命名空间
- 在 Worker 编辑页面,点击右上角的 设置 标签
- 在左侧子菜单中,点击 变量
- 向下滚动到 KV 命名空间绑定 部分
- 点击 添加绑定 按钮
在弹窗中:
- 变量名称:输入
ACCESS_LOGS(必须与代码中完全一致) - KV 命名空间:在下拉菜单中选择刚才创建的
ACCESS_LOGS
- 变量名称:输入
- 点击 保存并部署
第六步:粘贴并修改代码
- 回到 Worker 的代码编辑器(点击 资源 标签可回到代码界面)
- 删除编辑器中的默认代码
将完整的项目代码粘贴到编辑器中
// 配置 const CONFIG = { targetUrl: 'https://your-emby.com:8443', // 👈 修改为你的 Emby 服务器地址 statsPassword: 'yourpassword', // 👈 修改为你的统计页面密码 }; // 国家代码映射 const countryNames = { 'CN': '中国', 'US': '美国', 'JP': '日本', 'KR': '韩国', 'GB': '英国', 'DE': '德国', 'FR': '法国', 'CA': '加拿大', 'AU': '澳大利亚', 'SG': '新加坡', 'HK': '香港', 'TW': '台湾', 'MO': '澳门', 'RU': '俄罗斯', 'IN': '印度', 'BR': '巴西', 'IT': '意大利', 'ES': '西班牙', 'NL': '荷兰', 'SE': '瑞典', 'CH': '瑞士' }; // 请求类型映射 const typeNames = { 'success': '成功', 'blocked': '拦截', 'websocket': 'WebSocket', 'error': '错误', 'unknown': '未知' }; addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }); async function handleRequest(request) { // 获取客户端 IP 和地区信息 const clientIP = request.headers.get('CF-Connecting-IP'); const country = request.headers.get('CF-IPCountry') || 'XX'; // 1. 拦截非中国大陆 IP(对所有 /* 路径生效,包括 /stats) if (country !== 'CN') { await logAccessToKV(clientIP, country, false, 'blocked'); return createBlockedResponse(clientIP, country); } // 2. 检查是否是统计查询请求 const url = new URL(request.url); if (url.pathname === '/stats') { return handleStatsRequest(request); } // 3. 处理 OPTIONS 预检请求 if (request.method === 'OPTIONS') { return handleOptions(); } // 4. 记录访问开始时间 const startTime = Date.now(); try { // 构造目标 URL const url = new URL(request.url); const targetUrl = CONFIG.targetUrl + url.pathname + url.search; // 创建新请求 const newRequest = new Request(targetUrl, { method: request.method, headers: request.headers, body: request.body, redirect: 'follow' }); // 处理 WebSocket if (request.headers.get('Upgrade') === 'websocket') { const wsResponse = await fetch(newRequest); await logAccessToKV(clientIP, country, true, 'websocket', startTime); return wsResponse; } // 发送请求到 Emby 服务器 const response = await fetch(newRequest); // 修改响应头 const modifiedResponse = new Response(response.body, response); modifiedResponse.headers.set('Access-Control-Allow-Origin', '*'); modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); modifiedResponse.headers.set('Access-Control-Allow-Headers', '*'); modifiedResponse.headers.set('X-Proxy-By', 'Cloudflare-Worker'); // 记录成功访问 await logAccessToKV(clientIP, country, true, 'success', startTime); return modifiedResponse; } catch (error) { // 记录错误 await logAccessToKV(clientIP, country, false, 'error', startTime); return new Response('Proxy Error: ' + error.message, { status: 502, headers: { 'Content-Type': 'text/plain;charset=UTF-8', 'Access-Control-Allow-Origin': '*' } }); } } // 处理 OPTIONS 请求 function handleOptions() { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Max-Age': '86400' } }); } // 格式化时间戳为可读时间 function formatTimestamp(timestamp) { if (!timestamp) return '-'; const date = new Date(timestamp); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } // 格式化持续时间为可读格式 function formatDuration(ms) { if (!ms || ms === 0) return '-'; if (ms < 1000) return `${ms}毫秒`; if (ms < 60000) return `${(ms / 1000).toFixed(2)}秒`; const minutes = Math.floor(ms / 60000); const seconds = ((ms % 60000) / 1000).toFixed(0); return `${minutes}分${seconds}秒`; } // 格式化类型分布 function formatTypes(types) { if (!types || Object.keys(types).length === 0) return '-'; return Object.entries(types) .map(([type, count]) => `${typeNames[type] || type}:${count}`) .join(' '); } // 使用 KV 存储记录访问日志 async function logAccessToKV(ip, country, allowed, type, startTime) { try { const now = Date.now(); const duration = startTime ? now - startTime : 0; // 生成当天的日期作为 key 的一部分(按天统计) const today = new Date().toISOString().split('T')[0]; const kvKey = `access:${ip}:${today}`; // 从 KV 读取现有记录 let existingData = await ACCESS_LOGS.get(kvKey, 'json'); if (!existingData) { existingData = { ip: ip, country: country, firstAccess: now, lastAccess: now, totalRequests: 0, blockedRequests: 0, totalDuration: 0, types: {}, dailyAccess: {} }; } // 更新记录 existingData.lastAccess = now; existingData.totalRequests++; existingData.totalDuration += duration || 0; if (!allowed) { existingData.blockedRequests++; } // 记录请求类型统计 const reqType = type || 'unknown'; if (!existingData.types[reqType]) { existingData.types[reqType] = 0; } existingData.types[reqType]++; // 记录每日访问统计 if (!existingData.dailyAccess[today]) { existingData.dailyAccess[today] = { totalRequests: 0, blockedRequests: 0, totalDuration: 0 }; } existingData.dailyAccess[today].totalRequests++; existingData.dailyAccess[today].totalDuration += duration || 0; if (!allowed) { existingData.dailyAccess[today].blockedRequests++; } // 保存到 KV(设置过期时间为 30 天) await ACCESS_LOGS.put(kvKey, JSON.stringify(existingData), { expirationTtl: 30 * 24 * 60 * 60 // 30天 }); // 同时更新全局统计 await updateGlobalStats(ip, country, allowed, type, duration); } catch (error) { console.error('KV logging error:', error); } } // 更新全局统计 async function updateGlobalStats(ip, country, allowed, type, duration) { try { const statsKey = 'global:stats'; let globalStats = await ACCESS_LOGS.get(statsKey, 'json') || { totalRequests: 0, blockedRequests: 0, uniqueIPs: [], countries: {}, lastUpdated: Date.now() }; // 更新统计数据 globalStats.totalRequests++; if (!allowed) { globalStats.blockedRequests++; } // 记录唯一 IP if (!globalStats.uniqueIPs) { globalStats.uniqueIPs = []; } if (!globalStats.uniqueIPs.includes(ip)) { globalStats.uniqueIPs.push(ip); } // 记录国家统计 if (!globalStats.countries[country]) { globalStats.countries[country] = 0; } globalStats.countries[country]++; globalStats.lastUpdated = Date.now(); // 保存到 KV(设置较长的过期时间) await ACCESS_LOGS.put(statsKey, JSON.stringify(globalStats), { expirationTtl: 86400 // 24小时 }); } catch (error) { console.error('Global stats update error:', error); } } // 处理统计查询请求 async function handleStatsRequest(request) { const url = new URL(request.url); const method = request.method; // 处理退出登录 if (url.searchParams.get('logout') === '1') { return new Response(null, { status: 302, headers: { 'Location': '/stats', 'Set-Cookie': 'stats_auth=; Path=/stats; HttpOnly; SameSite=Strict; Max-Age=0', 'Access-Control-Allow-Origin': '*' } }); } // GET 请求:显示登录页面或统计页面 if (method === 'GET') { // 检查 cookie 中是否有有效的登录令牌 const cookieHeader = request.headers.get('Cookie') || ''; const cookies = parseCookies(cookieHeader); if (cookies.stats_auth === CONFIG.statsPassword) { // 已登录,显示统计页面 return await showStatsPage(); } // 未登录,显示登录页面 return showLoginPage(); } // POST 请求:处理登录 if (method === 'POST') { const contentType = request.headers.get('Content-Type') || ''; let password = ''; if (contentType.includes('application/json')) { const body = await request.json(); password = body.password || ''; } else if (contentType.includes('application/x-www-form-urlencoded')) { const formData = await request.formData(); password = formData.get('password') || ''; } if (password === CONFIG.statsPassword) { // 密码正确,设置 cookie 并重定向到统计页面 return new Response(null, { status: 302, headers: { 'Location': '/stats', 'Set-Cookie': `stats_auth=${CONFIG.statsPassword}; Path=/stats; HttpOnly; SameSite=Strict; Max-Age=86400`, 'Access-Control-Allow-Origin': '*' } }); } else { // 密码错误 return showLoginPage('密码错误,请重试'); } } // 其他请求方法 return new Response('Method Not Allowed', { status: 405 }); } // 解析 Cookies function parseCookies(cookieHeader) { const cookies = {}; cookieHeader.split(';').forEach(cookie => { const parts = cookie.trim().split('='); if (parts.length === 2) { cookies[parts[0].trim()] = parts[1].trim(); } }); return cookies; } // 显示登录页面 function showLoginPage(errorMessage = '') { const errorHtml = errorMessage ? ` <div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 8px; padding: 12px; margin-bottom: 20px; color: #721c24;"> ${errorMessage} </div> ` : ''; const html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>统计页面 - 登录</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .container { background: white; border-radius: 16px; padding: 40px; max-width: 400px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } h1 { color: #1a1a1a; font-size: 24px; text-align: center; margin-bottom: 10px; } .subtitle { color: #666; text-align: center; margin-bottom: 30px; font-size: 14px; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; color: #333; font-weight: 500; } input { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; transition: border-color 0.3s; } input:focus { outline: none; border-color: #667eea; } button { width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 500; cursor: pointer; transition: transform 0.2s; } button:hover { transform: translateY(-2px); } button:active { transform: translateY(0); } </style> </head> <body> <div class="container"> <h1>📊 统计页面</h1> <p class="subtitle">请输入密码查看访问统计</p> ${errorHtml} <form method="POST" action="/stats"> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" name="password" placeholder="请输入访问密码" required autofocus> </div> <button type="submit">登录</button> </form> </div> </body> </html>`; return new Response(html, { status: errorMessage ? 401 : 200, headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Access-Control-Allow-Origin': '*' } }); } // 显示统计页面 async function showStatsPage() { try { // 获取全局统计 const globalStats = await ACCESS_LOGS.get('global:stats', 'json'); // 获取最近的访问记录(列出所有 key) const accessKeys = await ACCESS_LOGS.list({ prefix: 'access:' }); const recentAccess = []; for (const key of accessKeys.keys) { const data = await ACCESS_LOGS.get(key.name, 'json'); if (data) { // 从 key 中正确提取 IP 和日期 // key 格式: access:1.2.3.4:2026-05-14 const keyWithoutPrefix = key.name.replace('access:', ''); const lastColonIndex = keyWithoutPrefix.lastIndexOf(':'); const ip = keyWithoutPrefix.substring(0, lastColonIndex); const date = keyWithoutPrefix.substring(lastColonIndex + 1); recentAccess.push({ ip: ip, date: date, ...data }); } } // 按最后访问时间排序 recentAccess.sort((a, b) => b.lastAccess - a.lastAccess); // 生成表格行 let tableRows = ''; const displayData = recentAccess.slice(0, 100); // 只显示最近100条 if (displayData.length === 0) { tableRows = '<tr><td colspan="9" style="text-align: center; padding: 40px; color: #888;">暂无访问记录</td></tr>'; } else { displayData.forEach((item, index) => { const countryName = countryNames[item.country] || item.country || '未知'; const isBlocked = item.blockedRequests > 0; const avgDuration = item.totalRequests > 0 ? Math.round(item.totalDuration / item.totalRequests) : 0; tableRows += ` <tr class="${isBlocked ? 'blocked-row' : ''}"> <td>${index + 1}</td> <td><code>${item.ip}</code></td> <td>${countryName}</td> <td>${formatTimestamp(item.firstAccess)}</td> <td>${formatTimestamp(item.lastAccess)}</td> <td>${item.totalRequests}</td> <td><span class="${isBlocked ? 'badge-danger' : 'badge-success'}">${item.blockedRequests}</span></td> <td>${formatDuration(avgDuration)}</td> <td><span class="type-badge">${formatTypes(item.types)}</span></td> </tr>`; }); } // 格式化全局统计 let globalStatsHtml = ''; if (globalStats && globalStats.totalRequests > 0) { const uniqueIPCount = globalStats.uniqueIPs ? globalStats.uniqueIPs.length : 0; const blockRate = globalStats.totalRequests > 0 ? ((globalStats.blockedRequests / globalStats.totalRequests) * 100).toFixed(1) : 0; // 格式化国家统计 let countriesHtml = ''; if (globalStats.countries) { countriesHtml = Object.entries(globalStats.countries) .sort((a, b) => b[1] - a[1]) .map(([code, count]) => `${countryNames[code] || code}: ${count}次`) .join('<br>'); } globalStatsHtml = ` <div class="stats-grid"> <div class="stat-card"> <div class="stat-label">总请求数</div> <div class="stat-value">${globalStats.totalRequests}</div> </div> <div class="stat-card"> <div class="stat-label">拦截请求</div> <div class="stat-value" style="color: #e94560;">${globalStats.blockedRequests}</div> </div> <div class="stat-card"> <div class="stat-label">拦截率</div> <div class="stat-value">${blockRate}%</div> </div> <div class="stat-card"> <div class="stat-label">独立IP数</div> <div class="stat-value">${uniqueIPCount}</div> </div> <div class="stat-card"> <div class="stat-label">最后更新</div> <div class="stat-value" style="font-size: 14px;">${formatTimestamp(globalStats.lastUpdated)}</div> </div> </div> <div class="section"> <h3>🌍 国家/地区分布</h3> <div class="countries-list">${countriesHtml || '暂无数据'}</div> </div>`; } else { globalStatsHtml = '<div class="no-data">📭 暂无统计数据,请等待有访问请求后查看</div>'; } const html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>访问统计</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; line-height: 1.6; } .container { max-width: 1400px; margin: 0 auto; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; flex-wrap: wrap; gap: 15px; } h1 { color: #667eea; font-size: 28px; } .logout-btn { display: inline-block; padding: 10px 20px; background: #e94560; color: white; text-decoration: none; border-radius: 6px; font-size: 14px; transition: background 0.3s; } .logout-btn:hover { background: #c73e54; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; } .stat-card { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 20px; text-align: center; } .stat-label { font-size: 13px; color: #888; margin-bottom: 8px; text-transform: uppercase; } .stat-value { font-size: 32px; font-weight: bold; color: #00ff88; } .section { background: #16213e; border: 1px solid #0f3460; border-radius: 10px; padding: 20px; margin-bottom: 20px; } .section h2 { color: #e94560; margin-bottom: 20px; font-size: 20px; display: flex; align-items: center; gap: 10px; } .section h3 { color: #667eea; margin-bottom: 15px; font-size: 18px; } .no-data { text-align: center; padding: 40px; color: #888; font-size: 16px; } .table-wrapper { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 900px; } th { background: #0f3460; color: #667eea; padding: 12px 10px; text-align: left; font-weight: 600; font-size: 13px; white-space: nowrap; } td { padding: 10px; border-bottom: 1px solid #0f3460; white-space: nowrap; } tr:hover { background: rgba(102, 126, 234, 0.05); } .blocked-row { background: rgba(233, 69, 96, 0.05); } .blocked-row:hover { background: rgba(233, 69, 96, 0.1); } code { background: #0f3460; padding: 2px 8px; border-radius: 4px; font-size: 13px; color: #00ff88; } .badge-success { display: inline-block; padding: 2px 8px; border-radius: 4px; background: rgba(0, 255, 136, 0.15); color: #00ff88; font-size: 12px; } .badge-danger { display: inline-block; padding: 2px 8px; border-radius: 4px; background: rgba(233, 69, 96, 0.15); color: #e94560; font-size: 12px; } .type-badge { font-size: 12px; color: #aaa; } .countries-list { color: #ccc; line-height: 2; } .refresh-info { text-align: center; color: #666; font-size: 12px; margin-top: 20px; padding: 15px; } </style> </head> <body> <div class="container"> <div class="header"> <h1>📊 访问统计面板</h1> <div> <a href="/stats?logout=1" class="logout-btn">退出登录</a> </div> </div> <div class="section"> <h2>📈 全局统计</h2> ${globalStatsHtml} </div> <div class="section"> <h2>📋 最近访问记录 <span style="font-size: 14px; color: #888;">(最近${Math.min(displayData.length, 100)}条)</span></h2> <div class="table-wrapper"> <table> <thead> <tr> <th>#</th> <th>IP 地址</th> <th>国家/地区</th> <th>首次访问</th> <th>最后访问</th> <th>总请求</th> <th>拦截数</th> <th>平均响应</th> <th>请求类型</th> </tr> </thead> <tbody> ${tableRows} </tbody> </table> </div> </div> <div class="refresh-info"> 📅 页面生成时间:${formatTimestamp(Date.now())} | 数据实时从 KV 存储读取 </div> </div> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Access-Control-Allow-Origin': '*' } }); } catch (error) { return new Response(JSON.stringify({ error: error.message }, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=UTF-8', 'Access-Control-Allow-Origin': '*' } }); } } // 创建拦截页面 function createBlockedResponse(clientIP, country) { const countryName = countryNames[country] || country; const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); const html = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>访问被拒绝</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .container { background: white; border-radius: 16px; padding: 40px; max-width: 500px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } .icon { text-align: center; margin-bottom: 20px; } .icon svg { width: 64px; height: 64px; } h1 { color: #1a1a1a; font-size: 24px; text-align: center; margin-bottom: 10px; } .subtitle { color: #666; text-align: center; margin-bottom: 30px; font-size: 14px; } .info-box { background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px; } .info-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e9ecef; } .info-item:last-child { border-bottom: none; } .label { color: #666; font-size: 14px; } .value { color: #333; font-weight: 500; font-size: 14px; } .tips { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 15px; margin-top: 20px; } .tips h3 { color: #856404; margin-bottom: 10px; font-size: 16px; } .tips p { color: #856404; font-size: 14px; line-height: 1.6; margin: 5px 0; } .footer { text-align: center; margin-top: 20px; color: #999; font-size: 12px; } </style> </head> <body> <div class="container"> <div style="text-align: center; margin-bottom: 30px;"> <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12.01" y2="16"></line> </svg> </div> <h1 style="text-align: center; color: #1a1a1a; margin-bottom: 10px;">访问被拒绝</h1> <p class="subtitle">此服务仅限中国大陆地区访问</p> <div class="info-box"> <div class="info-item"> <span class="label">IP 地址</span> <span class="value">${clientIP}</span> </div> <div class="info-item"> <span class="label">所在地区</span> <span class="value">${countryName}</span> </div> <div class="info-item"> <span class="label">访问时间</span> <span class="value">${now}</span> </div> <div class="info-item"> <span class="label">访问状态</span> <span style="color: #dc3545; font-weight: bold;">已拦截</span> </div> </div> <div class="tips"> <h3>💡 可能的原因</h3> <p>• 您正在使用非中国大陆的 IP 地址</p> <p>• 您可能开启了 VPN 或代理服务</p> <p>• 您的网络被识别为境外 IP</p> </div> <div class="tips" style="background: #d4edda; border-color: #28a745;"> <h3 style="color: #155724;">🔧 解决方法</h3> <p style="color: #155724;">• 关闭 VPN 或代理后重试</p> <p style="color: #155724;">• 使用中国大陆的网络访问</p> <p style="color: #155724;">• 如确认在国内,请联系管理员</p> </div> <div class="footer"> <p>Request ID: ${crypto.randomUUID()}</p> <p>© ${new Date().getFullYear()} Emby Proxy Service</p> </div> </body> </html>`; return new Response(html, { status: 403, headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' } }); }- 找到代码开头的配置部分,根据你的实际情况修改:
// 配置
const CONFIG = {
targetUrl: 'https://your-emby.com:8443', // 👈 修改为你的 Emby 服务器地址
statsPassword: 'yourpassword', // 👈 修改为你的统计页面密码
};5.修改完成后,点击右上角的 部署 按钮
6.等待部署完成,页面会提示"部署成功"
第七步:配置自定义域名(可选)
1.在 Worker 详情页面,点击 触发器 标签
2.找到 自定义域 部分
3.点击 添加自定义域
4.在弹出的对话框中输入你想使用的子域名,例如 emby.yourdomain.com
5.点击 添加自定义域
6.等待几分钟后,即可通过 https://emby.yourdomain.com 访问你的 Emby 服务
📖使用说明
正常访问 Emby
部署完成后,直接访问worker地址如:https://emby-proxy.rooxu.workers.dev即可使用 Emby,或者访问你配置的域名。
✅ 中国大陆 IP:正常访问 Emby 服务
❌ 境外 IP:看到拦截提示页面查看访问统计
访问https://emby-proxy.rooxu.workers.dev/stats
输入统计页面密码(默认:yourpassword)
登录后可以查看:
1.全局统计:总请求数、拦截数、拦截率、独立 IP 数
2.国家/地区分布:各国家访问次数统计
3.最近访问记录:最近 100 条详细访问日志
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭