我又来摸鱼了,继续从 Cloudflare接入LinuxDo有什么用? 说
有什么用呢?你可以得到一个节点分享工具
管理员(自己部署)
普通用户
可以设置几级可见
https://proxy.xlike.us.kg/dashboard
我又来摸鱼了,继续从 Cloudflare接入LinuxDo有什么用? 说
有什么用呢?你可以得到一个节点分享工具
管理员(自己部署)
可以设置几级可见
https://proxy.xlike.us.kg/dashboard
// work.js
const AUTHORIZATION_ENDPOINT = "https://connect.linux.do/oauth2/authorize";
const TOKEN_ENDPOINT = "https://connect.linux.do/oauth2/token";
const USER_ENDPOINT = "https://connect.linux.do/api/user";
let cachedKey = null;
// ========== 加解密工具 ==========
async function getKey(AUTH_PASS_WORD) {
if (cachedKey) return cachedKey;
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(AUTH_PASS_WORD),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: enc.encode("static_salt"),
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
cachedKey = key;
return key;
}
async function encrypt(text, AUTH_PASS_WORD) {
const enc = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await getKey(AUTH_PASS_WORD);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
enc.encode(text)
);
const buffer = new Uint8Array(iv.byteLength + ciphertext.byteLength);
buffer.set(iv, 0);
buffer.set(new Uint8Array(ciphertext), iv.byteLength);
let binary = "";
buffer.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary);
}
async function decrypt(data, AUTH_PASS_WORD) {
try {
const buffer = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
const iv = buffer.slice(0, 12);
const ciphertext = buffer.slice(12);
const key = await getKey(AUTH_PASS_WORD);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext
);
const dec = new TextDecoder();
return dec.decode(decrypted);
} catch (e) {
return null;
}
}
// ========== Cookie相关 ==========
function parseCookies(cookieHeader) {
if (!cookieHeader) return {};
let cookies = {};
cookieHeader.split(";").forEach((cookie) => {
const [name, ...rest] = cookie.trim().split("=");
const value = rest.join("=");
cookies[name] = value;
});
return cookies;
}
function createCookie(name, value, options = {}) {
let cookie = `${name}=${value}`;
if (options.Path) cookie += `; Path=${options.Path}`;
if (options.HttpOnly) cookie += `; HttpOnly`;
if (options.SameSite) cookie += `; SameSite=${options.SameSite}`;
// if (options.Secure) cookie += '; Secure'; // HTTPS可加
return cookie;
}
// ========== 其他工具:随机字符串、KV分页、时间格式化 ==========
function cryptoRandomString() {
return Array.from(crypto.getRandomValues(new Uint8Array(16)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// 获取所有 key
async function getAllKeys(envKV) {
let allKeys = [];
let cursor = null;
do {
const list = await envKV.list({ limit: 1000, cursor });
allKeys = allKeys.concat(list.keys);
cursor = list.cursor;
if (list.list_complete) break;
} while (true);
return allKeys;
}
function formatDateTime(dateString) {
if (!dateString) return "";
const d = new Date(dateString);
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
const hh = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
const ss = String(d.getSeconds()).padStart(2, "0");
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const pathname = url.pathname;
const searchParams = url.searchParams;
const method = request.method;
// 解密 Cookie
async function getUserInfo(request) {
const cookieHeader = request.headers.get("Cookie");
const cookies = parseCookies(cookieHeader);
if (!cookies.auth) return null;
const decrypted = await decrypt(cookies.auth, env.AUTH_PASS_WORD);
if (!decrypted) return null;
try {
const userInfo = JSON.parse(decrypted);
return userInfo;
} catch (e) {
return null;
}
}
// ========== 路由 ==========
// 1) 首页 '/'
if (pathname === "/") {
return new Response(
`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>LinuxDo-ProxyShare</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100 flex items-center justify-center h-screen">
<div class="text-center">
<h1 class="text-2xl mb-5 font-bold">LinuxDo-ProxyShare</h1>
<button id="loginButton" class="px-6 py-3 bg-green-500 text-white rounded hover:bg-green-600">一键登录</button>
</div>
<script>
document.getElementById('loginButton').addEventListener('click', () => {
fetch('/oauth2/initiate', { method: 'GET' })
.then(res => res.json())
.then(data => {
window.location.href = data.url;
})
.catch(() => alert('Error initiating OAuth2'));
});
</script>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } }
);
}
// 2) OAuth2 初始化 '/oauth2/initiate'
else if (pathname === "/oauth2/initiate") {
const state = cryptoRandomString();
await env.user.put(`state:${state}`, "valid", { expirationTtl: 300 });
const authorizationUrl = `${AUTHORIZATION_ENDPOINT}?client_id=${encodeURIComponent(
env.CLIENT_ID
)}&response_type=code&redirect_uri=${encodeURIComponent(
env.REDIRECT_URI + "/oauth2/callback"
)}&scope=read,write&state=${state}`;
return new Response(JSON.stringify({ url: authorizationUrl }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
// 3) OAuth2 回调 '/oauth2/callback'
else if (pathname === "/oauth2/callback") {
const code = searchParams.get("code");
const state = searchParams.get("state");
const stateValue = await env.user.get(`state:${state}`);
if (!code || !state || !stateValue) {
return new Response("Invalid or missing code/state", {
status: 400,
headers: { "Content-Type": "text/plain" },
});
}
await env.user.delete(`state:${state}`);
try {
// 交换 code -> token
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization:
"Basic " + btoa(`${env.CLIENT_ID}:${env.CLIENT_SECRET}`),
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: `${env.REDIRECT_URI}/oauth2/callback`,
}),
});
if (!tokenResponse.ok) throw new Error("Failed to exchange code");
const tokenData = await tokenResponse.json();
if (!tokenData.access_token) throw new Error("No access_token");
// 获取用户信息
const userResponse = await fetch(USER_ENDPOINT, {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
});
if (!userResponse.ok) throw new Error("Failed to fetch user info");
const userData = await userResponse.json();
// 存储完整
await env.user.put(String(userData.id), JSON.stringify(userData));
// 只在 Cookie 中存部分
const userInfo = {
id: userData.id,
username: userData.username,
trust_level: userData.trust_level || 0,
avatar_url: userData.avatar_url,
};
const encrypted = await encrypt(JSON.stringify(userInfo), env.AUTH_PASS_WORD);
const cookie = createCookie("auth", encrypted, {
Path: "/",
HttpOnly: true,
SameSite: "Strict",
});
// const baseURL = "https://proxy.xlike.us.kg/dashboard";
url.pathname = "/dashboard";
const redirectUrl = url.href;
return new Response("", {
status: 302,
headers: {
"Location": redirectUrl,
"Set-Cookie": cookie,
},
});
} catch (err) {
return new Response(err.message, {
status: 500,
headers: { "Content-Type": "text/plain" },
});
}
}
// 4) 仪表板 '/dashboard'
else if (pathname === "/dashboard") {
const userInfo = await getUserInfo(request);
if (!userInfo) {
// 未登录 -> 重定向 '/'
url.pathname = "/";
return new Response("", {
status: 302,
headers: { "Location": url.href },
});
}
const isAdmin = userInfo.username === env.AUTH_NAME;
// 获取全部
const allKeys = await getAllKeys(env.proxy);
let data = [];
for (const kv of allKeys) {
const item = await env.proxy.get(kv.name, { type: "json" });
if (item) {
// 判断可见性
// 如果 item.visible_level 不存在,默认为0
const requiredLevel = item.visible_level ?? 0;
if (userInfo.trust_level >= requiredLevel) {
data.push(item);
}
}
}
// 时间降序
data.sort((a, b) => {
let ad = new Date(a.created_at).getTime();
let bd = new Date(b.created_at).getTime();
return bd - ad; // b在前
});
// 分页:每页5条
const page = parseInt(searchParams.get("page") || "1", 10) || 1;
const pageSize = 5;
const total = data.length;
const totalPages = Math.ceil(total / pageSize);
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageData = data.slice(startIndex, endIndex);
// 格式化时间
pageData.forEach(item => {
item.created_at_formatted = formatDateTime(item.created_at);
});
// 构造分页链接
function pageLink(p) {
const newUrl = new URL(request.url);
newUrl.searchParams.set("page", p);
return newUrl.pathname + newUrl.search;
}
// 构造分页HTML
let paginationHtml = `
<div class="flex justify-center items-center space-x-4 mt-4">
${page > 1 ? `<a href="${pageLink(page - 1)}" class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded">上一页</a>` : ""}
<span>第 ${page} 页 / 共 ${totalPages} 页</span>
${page < totalPages ? `<a href="${pageLink(page + 1)}" class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded">下一页</a>` : ""}
</div>
`;
// 返回HTML
return new Response(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>节点订阅管理</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100 min-h-screen">
<!-- 顶部导航 -->
<nav class="bg-blue-600 p-4 text-white">
<div class="max-w-7xl mx-auto flex justify-between">
<div class="text-xl font-bold">
欢迎, ${userInfo.username} (${isAdmin ? "管理员" : "普通用户"}) 信任等级: ${userInfo.trust_level}
</div>
<a href="/" class="hover:underline">退出</a>
</div>
</nav>
<div class="max-w-7xl mx-auto p-4">
<div class="flex justify-between items-center mt-6 mb-4">
<h2 class="text-2xl font-bold">节点订阅管理</h2>
<!-- 无论是否管理员,都可以新增 -->
<button onclick="showAddModal()" class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">新增</button>
</div>
<!-- 为了防止页面太长,加一个固定高度+滚动条 -->
<div class="max-h-[500px] overflow-y-auto bg-white rounded shadow">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">序号</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">节点说明</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">可见级别</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">昵称/头像</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">创建时间</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">订阅链接</th>
${
isAdmin
? `<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">操作</th>`
: ``
}
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
${pageData
.map((item, index) => {
return `
<tr>
<td class="px-6 py-4 text-center">${startIndex + index + 1}</td>
<td class="px-6 py-4 text-center">${item.title || "-"}</td>
<td class="px-6 py-4 text-center">${item.visible_level ?? 0}</td>
<td class="px-6 py-4 text-center">
<div class="flex flex-col items-center">
<img src="${item.avatar_url ||
"https://via.placeholder.com/50"}" class="w-10 h-10 rounded-full mb-1" />
<span>${item.username || "未知"}</span>
</div>
</td>
<td class="px-6 py-4 text-center">${item.created_at_formatted || "-"}</td>
<td class="px-6 py-4 text-center">
<a href="#" class="text-blue-600 hover:underline" onclick="copyToClipboard('${item.subscription || ""}'); return false;">复制订阅</a>
</td>
${
isAdmin
? `
<td class="px-6 py-4 text-center">
<button onclick="showEditModal('${item.id}')" class="bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600">编辑</button>
<button onclick="deleteItem('${item.id}')" class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600 ml-2">删除</button>
</td>`
: ``
}
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
${paginationHtml}
</div>
<!-- 新增 Modal -->
<div id="addModalOverlay" class="fixed inset-0 bg-black bg-opacity-50 hidden justify-center items-center z-10" onclick="hideAddModal()"></div>
<div id="addModal" class="fixed bg-white p-6 rounded shadow hidden z-20"
style="width: 320px; transform: translate(-50%, -50%); left: 50%; top: 50%;"
onclick="event.stopPropagation()">
<h3 class="text-lg font-bold mb-2">新增节点订阅</h3>
<div class="mb-2">
<label class="block text-sm mb-1">节点说明</label>
<input type="text" id="addTitle" class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="mb-2">
<label class="block text-sm mb-1">节点订阅链接</label>
<input type="text" id="addLink" class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="mb-2">
<label class="block text-sm mb-1">可见级别(0~3)</label>
<select id="addVisible" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="0">0 (所有人可见)</option>
<option value="1">1 (trust_level >=1 可见)</option>
<option value="2">2 (trust_level >=2 可见)</option>
<option value="3">3 (trust_level >=3 可见)</option>
</select>
</div>
<button onclick="addNewItem()" class="bg-green-600 text-white px-4 py-1 rounded hover:bg-green-700">提交</button>
<button onclick="hideAddModal()" class="ml-2 text-gray-500 hover:text-gray-700">取消</button>
</div>
<!-- 编辑 Modal(只有管理员可发PUT) -->
<div id="editModalOverlay" class="fixed inset-0 bg-black bg-opacity-50 hidden justify-center items-center z-10" onclick="hideEditModal()"></div>
<div id="editModal" class="fixed bg-white p-6 rounded shadow hidden z-20"
style="width: 320px; transform: translate(-50%, -50%); left: 50%; top: 50%;"
onclick="event.stopPropagation()">
<h3 class="text-lg font-bold mb-2">编辑节点订阅</h3>
<input type="hidden" id="editItemId" />
<div class="mb-2">
<label class="block text-sm mb-1">标题</label>
<input type="text" id="editTitle" class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="mb-2">
<label class="block text-sm mb-1">节点订阅链接</label>
<input type="text" id="editLink" class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="mb-2">
<label class="block text-sm mb-1">可见级别(0~3)</label>
<select id="editVisible" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
<button onclick="editItem()" class="bg-blue-600 text-white px-4 py-1 rounded hover:bg-blue-700">保存</button>
<button onclick="hideEditModal()" class="ml-2 text-gray-500 hover:text-gray-700">取消</button>
</div>
<script>
function showAddModal() {
document.getElementById('addModalOverlay').classList.remove('hidden');
document.getElementById('addModalOverlay').classList.add('flex');
document.getElementById('addModal').classList.remove('hidden');
}
function hideAddModal() {
document.getElementById('addModalOverlay').classList.add('hidden');
document.getElementById('addModalOverlay').classList.remove('flex');
document.getElementById('addModal').classList.add('hidden');
}
function showEditModal(id) {
document.getElementById('editModalOverlay').classList.remove('hidden');
document.getElementById('editModalOverlay').classList.add('flex');
document.getElementById('editModal').classList.remove('hidden');
fetch('/proxy?id=' + id)
.then(res => res.json())
.then(item => {
document.getElementById('editItemId').value = item.id;
document.getElementById('editTitle').value = item.title || '';
document.getElementById('editLink').value = item.subscription || '';
const vis = item.visible_level ?? 0;
document.getElementById('editVisible').value = vis;
})
.catch(() => alert('获取信息失败'));
}
function hideEditModal() {
document.getElementById('editModalOverlay').classList.add('hidden');
document.getElementById('editModalOverlay').classList.remove('flex');
document.getElementById('editModal').classList.add('hidden');
}
// 新增
async function addNewItem() {
const title = document.getElementById('addTitle').value.trim();
const link = document.getElementById('addLink').value.trim();
const vis = parseInt(document.getElementById('addVisible').value, 10);
if(!title || !link) {
alert('标题和链接不能为空');
return;
}
const time = new Date().toISOString();
const body = { title, subscription: link, visible_level: vis, created_at: time };
const res = await fetch('/proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
alert('新增成功');
location.reload();
} else {
alert('新增失败');
}
}
// 编辑
async function editItem() {
const id = document.getElementById('editItemId').value;
const title = document.getElementById('editTitle').value.trim();
const link = document.getElementById('editLink').value.trim();
const vis = parseInt(document.getElementById('editVisible').value, 10);
if (!id) return alert('无效的ID');
const res = await fetch('/proxy', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, title, subscription: link, visible_level: vis })
});
if (res.ok) {
alert('修改成功');
location.reload();
} else {
alert('修改失败');
}
}
// 删除
async function deleteItem(id) {
if (!confirm('确定删除该订阅吗?')) return;
const res = await fetch('/proxy', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
if (res.ok) {
alert('删除成功');
location.reload();
} else {
alert('删除失败');
}
}
// 复制
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('已复制到剪贴板');
}, () => {
alert('复制失败');
});
}
</script>
</body>
</html>`,
{
headers: {
"Content-Type": "text/html",
// 禁止缓存
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
'Expires': '0',
},
}
);
}
// 5) 代理 API '/proxy'
else if (pathname.startsWith("/proxy")) {
const userInfo = await getUserInfo(request);
if (!userInfo) {
return new Response("Unauthorized", { status: 401 });
}
const isAdmin = userInfo.username === env.AUTH_NAME;
if (method === "GET") {
// GET /proxy?id=xxx 单条
const id = searchParams.get("id");
if (id) {
const item = await env.proxy.get(id, { type: "json" });
if (!item) {
return new Response("Not Found", { status: 404 });
}
return new Response(JSON.stringify(item), {
headers: { "Content-Type": "application/json" },
});
} else {
// 否则全部
const all = await getAllKeys(env.proxy);
let arr = [];
for (const kv of all) {
const item = await env.proxy.get(kv.name, { type: "json" });
if (item) arr.push(item);
}
return new Response(JSON.stringify(arr), {
headers: { "Content-Type": "application/json" },
});
}
}
else if (method === "POST") {
// 普通用户也能新增
const body = await request.json();
const kvId = cryptoRandomString();
// 拿完整信息
const fullUserDataStr = await env.user.get(String(userInfo.id));
let fullUserData = {};
try {
fullUserData = JSON.parse(fullUserDataStr || "{}");
} catch(e){}
// 组合
const newSub = {
id: kvId,
username: fullUserData.username || "未知",
avatar_url: fullUserData.avatar_url || "",
...body,
created_at: body.created_at || new Date().toISOString(),
};
await env.proxy.put(kvId, JSON.stringify(newSub));
return new Response(JSON.stringify(newSub), {
headers: { "Content-Type": "application/json" },
});
}
else if (method === "PUT") {
// 只有管理员能编辑
if (!isAdmin) {
return new Response("Forbidden", { status: 403 });
}
const body = await request.json();
const existing = await env.proxy.get(body.id, { type: "json" });
if (!existing) {
return new Response("Not Found", { status: 404 });
}
// 合并
const updated = { ...existing, ...body, id: body.id };
await env.proxy.put(body.id, JSON.stringify(updated));
return new Response(JSON.stringify(updated), {
headers: { "Content-Type": "application/json" },
});
}
else if (method === "DELETE") {
// 只有管理员能删除
if (!isAdmin) {
return new Response("Forbidden", { status: 403 });
}
const body = await request.json();
if (!body.id) {
return new Response("Missing id", { status: 400 });
}
await env.proxy.delete(body.id);
return new Response("Deleted");
}
else {
return new Response("Method Not Allowed", { status: 405 });
}
}
// 6) 其他路径
else {
return new Response("Not Found", { status: 404 });
}
},
};
没人点个赞吗?
不懂,感觉很厉害
大佬666
原来可以这样实现登录,之前还纠结要怎么写
晚点,出一个详细的部署教程
马可一下![]()
这个感觉不错 ![]()
详细教程 → https://linux.do/t/topic/323067/3
感谢分享 ![]()
此话题已在最后回复的 30 天后被自动关闭。不再允许新回复。