docs: 补充中文代码注释
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_api.go 组装 Admin API 的共享依赖,并提供嵌入式控制台使用的顶层 HTTP 路由。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -7,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func newAdminAPIHandler() http.HandlerFunc {
|
||||
// Admin API 的路径解析放在 internal/adminhttp 中,主包只提供各业务 handler。
|
||||
// 这样测试可以复用同一套路由表,而不会依赖真实监听器。
|
||||
return adminhttp.NewAPIHandler(adminStartup.AdminAPIPrefix, adminhttp.APIHandlers{
|
||||
SetupStatus: handleAdminSetupStatus,
|
||||
Setup: handleAdminSetup,
|
||||
@@ -29,6 +33,8 @@ func newAdminAPIHandler() http.HandlerFunc {
|
||||
|
||||
AuditLogs: handleAdminAuditLogs,
|
||||
|
||||
// 插件相关接口数量较多,统一在这里接入,确保嵌入式 UI 和远程 CLI
|
||||
// 看到的是同一套 Admin API 行为。
|
||||
PluginArtifacts: handleAdminPluginArtifacts,
|
||||
PluginArtifact: handleAdminPluginArtifact,
|
||||
PluginSources: handleAdminPluginSources,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_api_test.go 包含用于约束 admin api 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_audit.go 把 HTTP 请求上下文转换为持久化审计记录,用于追踪管理端变更。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_auth_handlers.go 处理初始化、登录、登出和当前会话查询等嵌入式管理端认证接口。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/alerts.ts 集中处理告警展示,让异步界面流程可以一致地清空或显示错误。
|
||||
|
||||
import { el } from "./dom.js";
|
||||
import { localizeMessage } from "./i18n.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/api.ts 封装 fetch,统一处理 Admin API 前缀、令牌、JSON 编码和错误返回。
|
||||
|
||||
import { state } from "./state.js";
|
||||
|
||||
interface APIOptions {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/config.ts 读取嵌入式管理端 HTML 壳注入的运行时配置。
|
||||
|
||||
import type { RuntimeConfig } from "./types.js";
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/dom.ts 集中 DOM 辅助方法,包括查询、转义、徽标、去抖和表单取值。
|
||||
|
||||
export function el<T extends HTMLElement = HTMLElement>(id: string): T {
|
||||
const node = document.getElementById(id);
|
||||
if (!node) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/i18n.ts 保存嵌入式管理端翻译字典,并提供语言切换辅助方法。
|
||||
|
||||
import { el } from "./dom.js";
|
||||
import { languageStorageKey, state } from "./state.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/main.ts 启动嵌入式管理端,选择初始化/登录/应用视图,并协调按角色加载数据。
|
||||
|
||||
import { api } from "./api.js";
|
||||
import { showAlert } from "./alerts.js";
|
||||
import { runtimeConfig } from "./config.js";
|
||||
@@ -15,12 +17,14 @@ import { loadStatus } from "./views/status.js";
|
||||
import { loadUsers, openUserDialog, renderUsers, saveUser } from "./views/users.js";
|
||||
|
||||
async function boot(): Promise<void> {
|
||||
// API 前缀由后端嵌入到 HTML 中,前端启动时先读取它,避免部署在子路径时写死地址。
|
||||
state.apiBase = runtimeConfig().apiPrefix;
|
||||
initializeLanguage();
|
||||
bindEvents();
|
||||
try {
|
||||
const setup = await api<SetupStatus>("/setup");
|
||||
if (setup.required) {
|
||||
// 没有任何管理账号时只展示初始化界面,不尝试加载其他运行态数据。
|
||||
setView("setupView");
|
||||
setSubtitle("setupSubtitle");
|
||||
return;
|
||||
@@ -30,6 +34,7 @@ async function boot(): Promise<void> {
|
||||
}
|
||||
|
||||
if (!state.token) {
|
||||
// token 保存在本地状态中;没有 token 时直接进入登录视图。
|
||||
setView("loginView");
|
||||
setSubtitle("login");
|
||||
return;
|
||||
@@ -39,6 +44,7 @@ async function boot(): Promise<void> {
|
||||
state.user = await api<User>("/me");
|
||||
await showApp();
|
||||
} catch {
|
||||
// token 失效时清空本地状态,避免后续 API 调用持续带着过期凭证。
|
||||
setToken("");
|
||||
setView("loginView");
|
||||
setSubtitle("login");
|
||||
@@ -46,6 +52,7 @@ async function boot(): Promise<void> {
|
||||
}
|
||||
|
||||
function bindEvents(): void {
|
||||
// 所有顶层事件在启动时绑定一次,视图重渲染只更新内容区域。
|
||||
el<HTMLSelectElement>("languageSelect").addEventListener("change", (event) => {
|
||||
changeLanguage((event.currentTarget as HTMLSelectElement).value, rerenderCurrentView);
|
||||
});
|
||||
@@ -75,6 +82,7 @@ async function submitSetup(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget as HTMLFormElement);
|
||||
try {
|
||||
// 初始化只创建首个管理员账号,创建成功后仍要求用户走登录流程获取会话 token。
|
||||
await api("/setup", {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -94,6 +102,7 @@ async function submitLogin(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget as HTMLFormElement);
|
||||
try {
|
||||
// 登录成功后立即保存 token 和用户信息,再统一进入应用态加载流程。
|
||||
const data = await api<LoginResponse>("/auth/login", {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -114,6 +123,7 @@ async function logout(): Promise<void> {
|
||||
try {
|
||||
await api("/auth/logout", { method: "POST", body: {} });
|
||||
} catch {
|
||||
// 服务端登出失败不阻塞本地清理,避免用户卡在失效会话上。
|
||||
}
|
||||
setToken("");
|
||||
state.user = null;
|
||||
@@ -124,6 +134,7 @@ async function logout(): Promise<void> {
|
||||
}
|
||||
|
||||
async function showApp(): Promise<void> {
|
||||
// 路由列表是成员和管理员都可见的基础视图,因此先加载它。
|
||||
setView("appView");
|
||||
setSubtitle("adminSubtitle");
|
||||
renderSessionUser();
|
||||
@@ -131,12 +142,14 @@ async function showApp(): Promise<void> {
|
||||
applyRoleVisibility();
|
||||
await loadRoutes();
|
||||
if (isMember()) {
|
||||
// 成员权限可以查看运行态、服务、指标和插件,但不能管理用户与审计。
|
||||
await loadStatus();
|
||||
await loadServices();
|
||||
await loadMetrics();
|
||||
await loadPlugins();
|
||||
}
|
||||
if (isAdmin()) {
|
||||
// 管理员专属数据放在最后加载,减少普通成员的无权限请求。
|
||||
await loadUsers();
|
||||
await loadAudit();
|
||||
}
|
||||
@@ -145,6 +158,7 @@ async function showApp(): Promise<void> {
|
||||
function applyRoleVisibility(): void {
|
||||
const member = isMember();
|
||||
const admin = isAdmin();
|
||||
// 角色控制只隐藏入口;服务端仍会按 token 做权限校验。
|
||||
el("statusGrid").classList.toggle("hidden", !member);
|
||||
el("newRouteBtn").classList.toggle("hidden", !member);
|
||||
toggleTab("services", member);
|
||||
@@ -176,6 +190,7 @@ function setView(name: string): void {
|
||||
}
|
||||
|
||||
function rerenderCurrentView(): void {
|
||||
// 切换语言后复用当前内存状态重绘静态文案,再刷新会随语言展示的远端数据。
|
||||
renderSessionUser();
|
||||
renderRoutes();
|
||||
renderServices();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/session.ts 渲染会话用户状态,并提供管理端界面使用的角色判断。
|
||||
|
||||
import { el } from "./dom.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { state } from "./state.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/state.ts 保存各视图模块共享的可变客户端状态。
|
||||
|
||||
import type { PluginArtifact, PluginBuild, PluginInstrumentation, PluginServiceStatus, PluginView, RouteRecord, ServiceRecord, User } from "./types.js";
|
||||
|
||||
export const tokenStorageKey = "mcGatewayAdminToken";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/types.ts 声明 Admin API 返回并被各视图消费的 TypeScript 数据结构。
|
||||
|
||||
export type Role = "admin" | "member" | "guest";
|
||||
|
||||
export interface User {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/audit.ts 渲染管理员用于复核运行态变更的审计日志。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { badge, el, escapeHTML } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/metrics.ts 渲染管理端成员可见的网关指标计数器。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { el, escapeHTML, stat } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/plugins.ts 渲染插件清单、插件详情、配置/密钥/治理动作和运维工具。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { badge, el, escapeAttr, escapeHTML, getFormInput } from "../dom.js";
|
||||
@@ -50,6 +52,8 @@ interface OperationsResponse {
|
||||
|
||||
export async function loadPlugins(): Promise<void> {
|
||||
try {
|
||||
// 插件页首屏依赖插件记录、制品、构建、插件服务模式和观测数据;
|
||||
// 并行请求可以减少进入页面时的等待时间。
|
||||
const [data, artifacts, builds, service, instrumentation] = await Promise.all([
|
||||
api<PluginsResponse>("/plugins"),
|
||||
api<ArtifactsResponse>("/plugin-artifacts"),
|
||||
@@ -64,6 +68,7 @@ export async function loadPlugins(): Promise<void> {
|
||||
state.pluginInstrumentation = instrumentation.instrumentation || [];
|
||||
const firstPlugin = state.plugins[0];
|
||||
if (!state.selectedPluginID && firstPlugin) {
|
||||
// 初次进入时默认选中第一个已纳管插件;未纳管制品会在列表中单独展示。
|
||||
state.selectedPluginID = firstPlugin.id;
|
||||
}
|
||||
renderPlugins();
|
||||
@@ -79,6 +84,7 @@ export async function loadPlugins(): Promise<void> {
|
||||
|
||||
export function renderPlugins(): void {
|
||||
const managed = new Set(state.plugins.map((plugin) => plugin.id));
|
||||
// 未纳管制品还没有 plugins 表记录,但仍要展示,方便管理员创建期望状态。
|
||||
const unmanagedArtifacts = state.pluginArtifacts.filter((artifact) => !managed.has(artifact.plugin_id));
|
||||
renderPluginServicePanel();
|
||||
el("pluginsBody").innerHTML = state.plugins.map((plugin) => `
|
||||
@@ -136,6 +142,7 @@ export async function loadPluginDetail(pluginID: string): Promise<void> {
|
||||
try {
|
||||
const data = await api<PluginResponse>(`/plugins/${encodeURIComponent(pluginID)}`);
|
||||
if (data.plugin) {
|
||||
// 详情接口返回完整插件视图,用它回填列表中的摘要记录。
|
||||
state.plugins = state.plugins.map((plugin) => plugin.id === data.plugin?.id ? data.plugin : plugin);
|
||||
if (!state.plugins.some((plugin) => plugin.id === data.plugin?.id)) {
|
||||
state.plugins.push(data.plugin);
|
||||
@@ -152,11 +159,13 @@ export async function loadPluginDetail(pluginID: string): Promise<void> {
|
||||
export function renderPluginDetail(plugin: PluginView | null = selectedPlugin()): void {
|
||||
const detail = el("pluginDetail");
|
||||
if (!plugin) {
|
||||
// 没有选中纳管插件时展示制品库存和源码构建入口。
|
||||
detail.innerHTML = uploadInventoryDetail();
|
||||
bindInventoryEvents();
|
||||
return;
|
||||
}
|
||||
const canWrite = isAdmin();
|
||||
// 插件详情拆成多个小面板,避免配置、治理、构建和运维信息混成一个长表格。
|
||||
detail.innerHTML = `
|
||||
<div class="detail-header">
|
||||
<div>
|
||||
@@ -252,6 +261,7 @@ export function renderPluginDetail(plugin: PluginView | null = selectedPlugin())
|
||||
}
|
||||
|
||||
export function bindPluginEvents(): void {
|
||||
// 顶层插件页事件只绑定一次;详情区会在每次重绘后重新绑定动态按钮。
|
||||
el<HTMLInputElement>("pluginUploadInput").addEventListener("change", uploadPluginPackage);
|
||||
el<HTMLButtonElement>("refreshPluginsBtn").addEventListener("click", loadPlugins);
|
||||
}
|
||||
@@ -263,6 +273,7 @@ function renderPluginServicePanel(): void {
|
||||
}
|
||||
const service = state.pluginService?.service;
|
||||
const canWrite = isAdmin();
|
||||
// 插件服务模式决定插件在进程内运行还是进入未来的独立/沙箱运行模式。
|
||||
container.innerHTML = `
|
||||
<section class="panel">
|
||||
<div class="detail-header compact">
|
||||
@@ -305,6 +316,7 @@ async function updatePluginServiceMode(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget as HTMLFormElement;
|
||||
try {
|
||||
// 服务模式变更可能需要后端迁移或重启,因此保存后立即刷新插件页状态。
|
||||
await api("/plugin-service", {
|
||||
method: "PUT",
|
||||
body: { desired_mode: getFormInput(form, "desired_mode") },
|
||||
@@ -342,6 +354,7 @@ async function uploadPluginPackage(event: Event): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.set("artifact", file);
|
||||
try {
|
||||
// 浏览器只负责上传文件;manifest 校验、哈希和制品类型判断由后端完成。
|
||||
await api("/plugin-artifacts", { method: "POST", formData });
|
||||
input.value = "";
|
||||
await loadPlugins();
|
||||
@@ -352,6 +365,7 @@ async function uploadPluginPackage(event: Event): Promise<void> {
|
||||
}
|
||||
|
||||
function bindPluginDetailEvents(plugin: PluginView): void {
|
||||
// 详情区每次重绘都会替换 DOM,因此按钮事件必须在重绘后重新绑定。
|
||||
document.getElementById("pluginDryRunBtn")?.addEventListener("click", () => dryRunConfig(plugin));
|
||||
document.getElementById("pluginSaveConfigBtn")?.addEventListener("click", () => saveConfig(plugin));
|
||||
const secretForm = document.getElementById("pluginSecretForm");
|
||||
@@ -389,6 +403,7 @@ function bindPluginDetailEvents(plugin: PluginView): void {
|
||||
|
||||
async function dryRunConfig(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// dry-run 不保存配置,只返回脱敏后的校验结果、diff 和是否需要重启。
|
||||
const data = await api<DryRunResponse>(`/plugins/${encodeURIComponent(plugin.id)}/config/dry-run`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -405,6 +420,7 @@ async function dryRunConfig(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function saveConfig(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// 配置保存写入期望状态;后端会根据当前制品和运行态判断是否可热加载。
|
||||
await api(`/plugins/${encodeURIComponent(plugin.id)}/config`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
@@ -425,6 +441,7 @@ async function saveSecret(event: SubmitEvent, plugin: PluginView): Promise<void>
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget as HTMLFormElement;
|
||||
try {
|
||||
// 密钥值不回显,保存后通过重新加载详情刷新版本号和 reload 标记。
|
||||
await api(`/plugins/${encodeURIComponent(plugin.id)}/secrets`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -448,6 +465,7 @@ async function runPluginAction(pluginID: string, action: string): Promise<void>
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// enable/disable/load/delete 等动作都走统一动作接口,后端负责审计和操作日志。
|
||||
await api(`/plugins/${encodeURIComponent(pluginID)}/${action}`, { method: "POST", body: {} });
|
||||
if (action === "delete") {
|
||||
state.selectedPluginID = "";
|
||||
@@ -463,6 +481,7 @@ async function runPluginAction(pluginID: string, action: string): Promise<void>
|
||||
|
||||
async function createDesiredFromArtifact(artifact: PluginArtifact): Promise<void> {
|
||||
try {
|
||||
// 从未纳管制品创建 disabled 期望状态,管理员随后可以编辑配置再启用。
|
||||
await api(`/plugins/${encodeURIComponent(artifact.plugin_id)}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
@@ -483,6 +502,7 @@ async function createDesiredFromArtifact(artifact: PluginArtifact): Promise<void
|
||||
|
||||
async function rollbackArtifact(pluginID: string, artifactID: string): Promise<void> {
|
||||
try {
|
||||
// 制品回滚只改期望制品;后端仍会执行治理检查和配置 dry-run。
|
||||
await api(`/plugins/${encodeURIComponent(pluginID)}/rollback/artifact`, {
|
||||
method: "POST",
|
||||
body: { artifact_id: artifactID },
|
||||
@@ -499,6 +519,7 @@ async function runBuildAction(pluginID: string, buildID: number, action: string)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 构建动作可能耗时,当前界面以刷新详情的方式展示最新构建状态。
|
||||
await api(`/plugin-builds/${buildID}/${encodeURIComponent(action)}`, { method: "POST", body: {} });
|
||||
await loadPluginDetail(pluginID);
|
||||
showAlert("");
|
||||
@@ -512,6 +533,7 @@ async function rollbackSnapshot(pluginID: string, snapshotID: number, fullDesire
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 配置快照回滚可只恢复配置,也可连同 artifact/desired state/priority 一起恢复。
|
||||
await api(`/plugins/${encodeURIComponent(pluginID)}/rollback/config`, {
|
||||
method: "POST",
|
||||
body: { snapshot_id: snapshotID, full_desired: fullDesired },
|
||||
@@ -528,6 +550,7 @@ async function showSnapshotDiff(pluginID: string, snapshotID: number): Promise<v
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// diff 已由后端脱敏,前端只负责展示结果给管理员确认。
|
||||
const data = await api<SnapshotDiffResponse>(`/plugins/${encodeURIComponent(pluginID)}/config/snapshots/${snapshotID}/diff`);
|
||||
el("pluginDryRunResult").textContent = formatJSON(data.diff || {});
|
||||
showAlert("");
|
||||
@@ -538,6 +561,7 @@ async function showSnapshotDiff(pluginID: string, snapshotID: number): Promise<v
|
||||
|
||||
async function createGovernanceReview(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// 评审记录绑定当前 desired artifact 和配置哈希,用于后续启用或回滚门禁。
|
||||
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/review`, {
|
||||
method: "POST",
|
||||
body: { artifact_id: plugin.desired_artifact_id, profile: "prod", decision: "approved" },
|
||||
@@ -555,6 +579,7 @@ async function createGovernanceOverride(plugin: PluginView): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// override 是带 TTL 的临时治理豁免,必须记录人工原因。
|
||||
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/override`, {
|
||||
method: "POST",
|
||||
body: { artifact_id: plugin.desired_artifact_id, profile: "prod", action: "enable", reason, ttl_seconds: 3600 },
|
||||
@@ -568,6 +593,7 @@ async function createGovernanceOverride(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function runGovernancePreflight(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// preflight 由插件或宿主返回检查项,结果会持久化到治理面板。
|
||||
const data = await api<Record<string, unknown>>(`/plugins/${encodeURIComponent(plugin.id)}/governance/preflight`, {
|
||||
method: "POST",
|
||||
body: { artifact_id: plugin.desired_artifact_id, config_json: configEditorValue() },
|
||||
@@ -582,6 +608,7 @@ async function runGovernancePreflight(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function runGovernanceSelfTest(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// self-test 用于验证制品自身能力,不直接修改 desired state。
|
||||
const data = await api<Record<string, unknown>>(`/plugins/${encodeURIComponent(plugin.id)}/governance/self-test`, {
|
||||
method: "POST",
|
||||
body: { artifact_id: plugin.desired_artifact_id },
|
||||
@@ -600,6 +627,7 @@ async function recordGovernanceBenchmark(plugin: PluginView): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 手动录入基准差异用于治理门禁判断,避免高风险性能回退直接启用。
|
||||
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/benchmark`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -630,6 +658,7 @@ async function createArtifactRevokeAdvisory(plugin: PluginView): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 撤销公告会让命中的制品进入隔离/阻断路径,详情刷新后展示最新治理状态。
|
||||
await api("/plugin-advisories", {
|
||||
method: "POST",
|
||||
body: {
|
||||
@@ -649,6 +678,7 @@ async function createArtifactRevokeAdvisory(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function loadPluginOperations(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// 运维快照包含事件、日志、trace、任务、外部依赖和 GC 候选项,按需刷新即可。
|
||||
const data = await api<OperationsResponse>(`/plugins/${encodeURIComponent(plugin.id)}/operations`);
|
||||
el("pluginOperationsOutput").textContent = formatJSON(data.operations || {});
|
||||
showAlert("");
|
||||
@@ -659,6 +689,7 @@ async function loadPluginOperations(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function dryRunOperationsGC(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// GC dry-run 不删除文件,只展示哪些运行态数据会被保护或清理。
|
||||
const data = await api<OperationsResponse>(`/plugins/${encodeURIComponent(plugin.id)}/operations/gc`);
|
||||
el("pluginOperationsOutput").textContent = formatJSON(data);
|
||||
showAlert("");
|
||||
@@ -669,6 +700,7 @@ async function dryRunOperationsGC(plugin: PluginView): Promise<void> {
|
||||
|
||||
async function loadDiagnosticPackage(plugin: PluginView): Promise<void> {
|
||||
try {
|
||||
// 诊断包由后端生成并脱敏,前端以 JSON 文本形式展示给管理员。
|
||||
const data = await api<OperationsResponse>(`/plugins/${encodeURIComponent(plugin.id)}/operations/diagnostic`);
|
||||
el("pluginOperationsOutput").textContent = formatJSON(data);
|
||||
showAlert("");
|
||||
@@ -678,6 +710,7 @@ async function loadDiagnosticPackage(plugin: PluginView): Promise<void> {
|
||||
}
|
||||
|
||||
function uploadInventoryDetail(): string {
|
||||
// 库存视图聚合未纳管制品和构建记录,支撑上传、构建、纳管的完整流程。
|
||||
const artifact = selectedArtifact();
|
||||
if (!artifact) {
|
||||
return `
|
||||
@@ -732,6 +765,7 @@ function uploadInventoryDetail(): string {
|
||||
}
|
||||
|
||||
function bindInventoryEvents(): void {
|
||||
// 库存视图也是动态渲染,制品详情和纳管表单事件需要在渲染后绑定。
|
||||
const artifact = selectedArtifact();
|
||||
const form = document.getElementById("artifactDesiredForm");
|
||||
if (artifact && form instanceof HTMLFormElement) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/routes.ts 渲染路由列表,并通过 Admin API 保存主机到上游的变更。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { badge, el, escapeAttr, escapeHTML, getFormInput } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/services.ts 渲染监听服务设置,并持久化启停、端口和选项更新。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { el, escapeAttr, escapeHTML, getFormInput } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/status.ts 渲染管理面板上的网关健康摘要。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { el, stat } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_frontend/src/views/users.ts 渲染管理用户列表,并保存账号、角色、密码和禁用状态变更。
|
||||
|
||||
import { api } from "../api.js";
|
||||
import { showAlert } from "../alerts.js";
|
||||
import { badge, el, escapeAttr, escapeHTML, getFormInput, getFormSelect } from "../dom.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_metric_handlers.go 返回管理面板状态卡片使用的轻量运行时计数器。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_plugin_handlers.go 承载插件制品、期望状态、配置、密钥、运维操作和治理检查相关的 Admin API。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_route_handlers.go 提供修改路由记录的 HTTP 接口,并在提交后立刻刷新内存路由快照。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_routes.go 让内存中的主机到上游映射快照与 SQLite 路由记录保持同步。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,10 +10,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
routeSnapshot = adminroute.NewSnapshot()
|
||||
// routeSnapshot 是连接热路径读取的不可变快照;写路径通过 Store 整体替换它。
|
||||
routeSnapshot = adminroute.NewSnapshot()
|
||||
// routeWriteLock 串行化路由写入和快照刷新,避免并发写导致后写库、先发布的顺序错乱。
|
||||
routeWriteLock sync.Mutex
|
||||
)
|
||||
|
||||
// refreshRouteSnapshot 从 SQLite 读取启用路由并发布到热路径。数据库尚未初始化时
|
||||
// 发布空快照,方便测试和早期启动路径调用。
|
||||
func refreshRouteSnapshot(ctx context.Context) error {
|
||||
if adminDB == nil {
|
||||
publishRouteSnapshot(map[string]string{})
|
||||
@@ -26,10 +32,12 @@ func refreshRouteSnapshot(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// publishRouteSnapshot 原子替换当前路由快照;调用方应传入新 map,避免发布后继续修改。
|
||||
func publishRouteSnapshot(routes map[string]string) {
|
||||
routeSnapshot.Store(routes)
|
||||
}
|
||||
|
||||
// lookupRoute 是连接热路径使用的只读查找函数,不访问 SQLite。
|
||||
func lookupRoute(host string) (string, bool) {
|
||||
return routeSnapshot.Lookup(host)
|
||||
}
|
||||
@@ -42,6 +50,7 @@ func upsertRoute(ctx context.Context, actor, host, upstream string, enabled bool
|
||||
routeWriteLock.Lock()
|
||||
defer routeWriteLock.Unlock()
|
||||
|
||||
// 路由写入成功后必须立即刷新内存快照,否则管理端保存的配置不会影响新连接。
|
||||
if err := adminroute.NewRepository(adminDB).Upsert(ctx, actor, host, upstream, enabled, note); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,6 +61,7 @@ func deleteRoute(ctx context.Context, actor, host string) error {
|
||||
routeWriteLock.Lock()
|
||||
defer routeWriteLock.Unlock()
|
||||
|
||||
// 删除也走同一把锁,确保快照刷新顺序与数据库提交顺序一致。
|
||||
if err := adminroute.NewRepository(adminDB).Delete(ctx, host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_runtime.go 打开 SQLite 运行态数据库、写入默认数据,并为在线流量发布首个路由快照。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -36,6 +38,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// adminStartup 是启动时解析出的管理端配置;后续 HTTP handler 和静态资源注入都会读取它。
|
||||
adminStartup = adminconfig.Config{
|
||||
DBPath: defaultAdminDBPath,
|
||||
TCPAdminPort: defaultTCPPort,
|
||||
@@ -50,6 +53,8 @@ var (
|
||||
processStartAt = time.Now()
|
||||
)
|
||||
|
||||
// initializeGatewayRuntime 按固定顺序准备运行态:解析配置、打开数据库、迁移 schema、
|
||||
// 写入默认服务、应用服务配置、创建初始管理员、发布路由快照、最后启动插件管理器。
|
||||
func initializeGatewayRuntime() error {
|
||||
startup, err := parseStartupConfig(os.Getenv)
|
||||
if err != nil {
|
||||
@@ -71,6 +76,7 @@ func initializeGatewayRuntime() error {
|
||||
if err := admindb.Migrate(db); err != nil {
|
||||
return err
|
||||
}
|
||||
// 默认服务必须先存在,applyServiceConfig 才能把 SQLite 中的运行态端口写回 config。
|
||||
if err := ensureDefaultServices(context.Background(), db, startup.TCPAdminPort); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,6 +90,7 @@ func initializeGatewayRuntime() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 插件制品放在数据库同级目录下,便于容器挂载一个 data volume 即可保留全部运行态。
|
||||
pluginsManager = pluginmanager.New(pluginmanager.Options{
|
||||
DB: db,
|
||||
ArtifactRoot: filepath.Join(filepath.Dir(startup.DBPath), "plugins", "artifacts"),
|
||||
@@ -93,6 +100,7 @@ func initializeGatewayRuntime() error {
|
||||
return pluginsManager.Reconcile(context.Background())
|
||||
}
|
||||
|
||||
// closeGatewayRuntime 只关闭当前进程持有的数据库连接;SQLite 文件和插件制品都保留在数据目录中。
|
||||
func closeGatewayRuntime() {
|
||||
if adminDB != nil {
|
||||
_ = adminDB.Close()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_service_handlers.go 提供监听服务配置接口,用于维护端口、启停状态和是否需要重启。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_services.go 从 SQLite 加载监听服务配置,并暴露规范化后的运行时服务选项。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_sessions.go 提供 Admin API 认证中间件使用的内存会话管理器。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_static.go 嵌入构建后的管理前端,并通过网关 HTTP 处理器对外提供。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* cmd/gateway/admin_static/app.css 定义嵌入式管理端仪表盘、表格、表单、对话框和响应式布局样式。 */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f6f7f4;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- cmd/gateway/admin_static/index.html 提供嵌入式管理端 HTML 外壳,包含对话框、标签页和运行时 API 前缀注入。 -->
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_user_handlers.go 提供管理员维护管理账号的接口,包括创建、更新、禁用和列表查询。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/admin_users.go 初始化管理用户仓库,并在没有账号时创建首次初始化用户。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/config.go 加载静态网关配置,并与管理数据库提供的运行态状态组合使用。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/config_test.go 包含用于约束 config 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/err.go 集中放置网关请求路径使用的少量哨兵错误。
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/handle_request_test.go 包含用于约束 handle request 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/haproxy.go 为需要 HAProxy PROXY 头的上游 TCP 连接先写入代理头,再回放 Minecraft 流量。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -45,7 +47,7 @@ func haProxyUpstream(source net.Conn, host string) net.Conn {
|
||||
SourceAddr: sourceAddr,
|
||||
DestinationAddr: target,
|
||||
}
|
||||
// After the connection was created write the proxy headers first
|
||||
// 连接建立后先写入 PROXY 头,再转发 Minecraft 首包。
|
||||
_, err = header.WriteTo(conn)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to write proxy header")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/haproxy_test.go 包含用于约束 haproxy 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/kcp.go 启动可选的 KCP 监听器,并把接收到的会话转入统一网关请求处理流程。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -14,6 +16,7 @@ func runKcp(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
}
|
||||
|
||||
// KCP 监听使用运行态服务配置中的分片参数,和上游拨号保持一致。
|
||||
listener, err := kcp.ListenWithOptions(fmt.Sprintf(":%d", config.Kcp.Port), nil, config.Kcp.DataShards, config.Kcp.ParityShards)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
@@ -37,11 +40,13 @@ func runKcp(wg *sync.WaitGroup) {
|
||||
|
||||
tuneKcpConn(conn)
|
||||
|
||||
// KCP session 实现 net.Conn,可以直接进入统一网关请求流程。
|
||||
go handleRequest(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func upstreamKcp(host string) net.Conn {
|
||||
// KCP 上游使用与入口相同的 data/parity shards,确保两端编码参数匹配。
|
||||
conn, err := kcp.DialWithOptions(host, nil, config.Kcp.DataShards, config.Kcp.ParityShards)
|
||||
if err != nil {
|
||||
gatewayMetrics.UpstreamDialError()
|
||||
@@ -55,6 +60,7 @@ func upstreamKcp(host string) net.Conn {
|
||||
}
|
||||
|
||||
func tuneKcpConn(conn *kcp.UDPSession) {
|
||||
// 这里偏向低延迟交互:stream mode 模拟 TCP 字节流,禁用写延迟并打开快速 ACK。
|
||||
conn.SetStreamMode(true)
|
||||
conn.SetWriteDelay(false)
|
||||
conn.SetNoDelay(1, 10, 2, 1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/log.go 配置网关日志、日志文件、日志级别和日志轮转钩子。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/log_notunix.go 在没有 Unix 信号的平台上提供空的日志轮转信号钩子。
|
||||
|
||||
// pid_unix.go
|
||||
//go:build !unix && !plan9
|
||||
|
||||
@@ -8,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func handleLogRotate() {
|
||||
// No-op for non-unix platforms
|
||||
// Log rotation is not supported on this platform
|
||||
// This function can be left empty or removed if not needed
|
||||
// 非 Unix 平台不执行日志轮转信号处理。
|
||||
// 该平台不支持通过信号触发日志轮转。
|
||||
// 保留空实现是为了让跨平台调用点保持一致。
|
||||
log.Info().Msg("Log rotation is not supported on this platform")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/log_pid_test.go 包含用于约束 log pid 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/log_unix.go 注册 Unix 信号处理,让进程无需完整重启即可重新打开日志文件。
|
||||
|
||||
// pid_unix.go
|
||||
//go:build unix || plan9
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/main.go 负责网关进程启动、监听器选择、Minecraft 握手路由、插件钩子分发以及上游转发交接。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -17,6 +19,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 插件子命令复用网关二进制。这里先于运行态配置加载处理它们,
|
||||
// 这样本地构建和清单命令不需要一份可用的网关部署配置。
|
||||
if handled, code := runPluginCLI(os.Args[1:]); handled {
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -39,8 +43,13 @@ func main() {
|
||||
}
|
||||
|
||||
func startEnabledServices() {
|
||||
// TCP 和 Admin HTTP 始终通过共享监听器启动。共享监听器按每条连接
|
||||
// 的首包判断它是 HTTP 还是 Minecraft 协议数据,因此不需要额外维护
|
||||
// 一个手动模式开关。
|
||||
startService(runTcpWebPortReuse)
|
||||
|
||||
// 可选传输最终仍进入 handleRequest,这让插件过滤、路由解析和上游拨号
|
||||
// 在 TCP、KCP、QUIC 和 WebSocket 入口之间保持一致。
|
||||
if config.Kcp.Enable {
|
||||
startService(runKcp)
|
||||
}
|
||||
@@ -61,6 +70,8 @@ func handleRequest(conn net.Conn) {
|
||||
gatewayMetrics.ConnectionStarted()
|
||||
defer gatewayMetrics.ConnectionFinished()
|
||||
|
||||
// 插件或协议解析器的 panic 不能杀掉监听协程;当前连接会被放弃,
|
||||
// 进程继续服务其他客户端。
|
||||
defer func() {
|
||||
rec := recover()
|
||||
if rec == nil {
|
||||
@@ -91,6 +102,8 @@ func handleRequest(conn net.Conn) {
|
||||
}
|
||||
|
||||
func mapToHost(conn net.Conn) net.Conn {
|
||||
// 连接过滤器在读取 Minecraft 握手前执行,因此可以按来源地址或传输类型
|
||||
// 拒绝连接,同时不消耗客户端发送的协议字节。
|
||||
if pluginsManager != nil {
|
||||
transport, _, _ := connectionIngress(conn)
|
||||
filter, err := pluginsManager.FilterConnection(context.Background(), api.ConnectionFilterRequest{
|
||||
@@ -124,6 +137,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 第一次读取包含 Minecraft 握手数据。所有过滤器和路由决策完成后,
|
||||
// 这段数据必须原样或按插件改写后回放给选中的上游。
|
||||
initialData := append([]byte(nil), buf[:n]...)
|
||||
handshake := protocol.ParseHandshake(initialData)
|
||||
if handshake.ServerHost == "" {
|
||||
@@ -133,6 +148,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 握手过滤器可以改写目标主机名。发生改写时要立刻重建首包,
|
||||
// 确保上游看到的是改写后的 Minecraft 主机名,而不是客户端原始值。
|
||||
if pluginsManager != nil {
|
||||
filter, err := pluginsManager.FilterHandshake(context.Background(), api.HandshakeFilterRequest{
|
||||
SourceAddr: conn.RemoteAddr().String(),
|
||||
@@ -155,6 +172,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
}
|
||||
}
|
||||
|
||||
// 状态查询使用 NextState=1,并且可以由插件直接完整响应。
|
||||
// 如果这里已经处理,就不会再为该查询打开上游连接。
|
||||
if handshake.NextState == 1 {
|
||||
if handled := handleStatusPing(conn, handshake); handled {
|
||||
return nil
|
||||
@@ -226,6 +245,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
|
||||
if client == nil {
|
||||
target := upstreamtarget.Parse(host)
|
||||
// 路由值可以通过前缀选择非 TCP 传输;普通地址仍按 TCP 处理,
|
||||
// 以保持旧配置的行为不变。
|
||||
switch target.Protocol {
|
||||
case upstreamtarget.ProtocolQUIC:
|
||||
client = upstreamQuic(target.Address)
|
||||
@@ -241,6 +262,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只有在上游路径确定后才回放握手数据。这样插件在任何上游字节发出前,
|
||||
// 都还有机会阻断、代理或改写连接。
|
||||
if err := writeAll(client, initialData); err != nil {
|
||||
log.Err(err).
|
||||
Str("client", conn.RemoteAddr().String()).
|
||||
@@ -256,6 +279,8 @@ func mapToHost(conn net.Conn) net.Conn {
|
||||
|
||||
func resolveGatewayRoute(conn net.Conn, handshake protocol.Handshake) pluginmanager.RouteResolveResult {
|
||||
upstream, hit := lookupRoute(handshake.ServerHost)
|
||||
// SQLite 快照始终作为本地兜底。插件会同时拿到兜底决策和刷新回调,
|
||||
// 因此可以选择性覆盖路由,而不必在插件里复制一套路由仓库逻辑。
|
||||
req := api.RouteResolveRequest{
|
||||
Host: handshake.ServerHost,
|
||||
RawServerHost: handshake.RawServerHost,
|
||||
@@ -283,6 +308,7 @@ func resolveGatewayRoute(conn net.Conn, handshake protocol.Handshake) pluginmana
|
||||
action := api.RouteDecisionFallback
|
||||
source := "sqlite_fallback"
|
||||
if upstream == "" {
|
||||
// 没有命中兜底路由时统一表示为拒绝决策,便于热路径记录一致的失败形态。
|
||||
action = api.RouteDecisionReject
|
||||
source = "fallback_miss"
|
||||
}
|
||||
@@ -296,6 +322,8 @@ func handleStatusPing(conn net.Conn, handshake protocol.Handshake) bool {
|
||||
if pluginsManager == nil {
|
||||
return false
|
||||
}
|
||||
// Minecraft 状态响应是带长度前缀的 JSON 数据包。插件只提供高层字段,
|
||||
// Minecraft 协议封包由 protocol.StatusResponsePacket 统一完成。
|
||||
result, err := pluginsManager.StatusPing(context.Background(), api.StatusPingRequest{
|
||||
Host: handshake.ServerHost,
|
||||
RawServerHost: handshake.RawServerHost,
|
||||
@@ -339,6 +367,8 @@ func handleStatusPing(conn net.Conn, handshake protocol.Handshake) bool {
|
||||
func newUpstreamConnectRequest(conn net.Conn, upstream string, handshake protocol.Handshake, initialData []byte, routeHit bool) api.UpstreamConnectRequest {
|
||||
target := upstreamtarget.Parse(upstream)
|
||||
transport, serviceName, listenerPort := connectionIngress(conn)
|
||||
// InitialData 使用副本,避免上游插件在其他处理器或日志路径仍引用回放缓冲区时
|
||||
// 意外修改调用方持有的数据。
|
||||
req := api.UpstreamConnectRequest{
|
||||
Source: conn,
|
||||
Host: handshake.ServerHost,
|
||||
@@ -367,6 +397,8 @@ func newUpstreamConnectRequest(conn net.Conn, upstream string, handshake protoco
|
||||
func connectionIngress(conn net.Conn) (transport string, serviceName string, listenerPort int) {
|
||||
transport = "tcp"
|
||||
serviceName = serviceNameTCPAdmin
|
||||
// 具体连接包装类型记录了客户端来自哪个监听器。该元数据会传给插件,
|
||||
// 并出现在运维诊断中,同时不需要改变 net.Conn 接口。
|
||||
switch conn.(type) {
|
||||
case *webSocketConn:
|
||||
transport = "websocket"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/main_test.go 包含用于约束 gateway 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/pid.go 维护 pid 文件的写入和清理,供进程管理器按文件追踪网关进程。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/pid_unix.go 实现 Unix 平台的 pid 文件占用检查,避免覆盖仍在运行的进程记录。
|
||||
|
||||
// pid_unix.go
|
||||
//go:build unix || plan9
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//go:build unix || plan9
|
||||
|
||||
// cmd/gateway/pid_unix_test.go 包含用于约束 pid unix 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/pid_windows.go 在 Windows 上提供可移植的 pid 文件占用检查替代实现。
|
||||
|
||||
// pid_windows.go
|
||||
//go:build windows
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin.go 把网关运行时接入 pluginmanager,负责钩子分发和插件生命周期加载。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -96,12 +98,12 @@ func loadPlugins() {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConn implements api.Gateway.
|
||||
// HandleConn 实现 api.Gateway,用于让插件把连接交回网关主流程。
|
||||
func (g *Gateway) HandleConn(conn net.Conn) {
|
||||
go handleRequest(conn)
|
||||
}
|
||||
|
||||
// Hook implements api.Gateway.
|
||||
// Hook 实现 api.Gateway,用于注册旧版内存钩子处理器。
|
||||
func (g *Gateway) Hook(hook string, handler any) error {
|
||||
pluginLock.Lock()
|
||||
defer pluginLock.Unlock()
|
||||
@@ -110,7 +112,7 @@ func (g *Gateway) Hook(hook string, handler any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExitWaitGroup implements api.Gateway.
|
||||
// ExitWaitGroup 实现 api.Gateway,用于把插件后台任务纳入进程退出等待。
|
||||
func (g *Gateway) ExitWaitGroup() *sync.WaitGroup {
|
||||
return &exitWaitGroup
|
||||
}
|
||||
@@ -152,7 +154,7 @@ func (g *Gateway) RegisterBackgroundTask(task api.BackgroundTask) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestOp implements api.Gateway.
|
||||
// TestOp 实现 api.Gateway,保留给测试或调试插件能力探测。
|
||||
func (g *Gateway) TestOp() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_cli.go 分发插件相关子命令,包括本地脚手架、构建、清单和远程管理操作。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_cli_manifest.go 实现插件包清单的查看、校验、特性列表和格式化命令。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_cli_remote.go 实现通过 Admin API 驱动插件管理操作的命令行客户端。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_cli_toolchain.go 包含插件开发者使用的本地源码模板、构建打包流程和测试辅助逻辑。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -1669,7 +1671,7 @@ func manifestYAMLTemplate(opts pluginInitCLIOptions) string {
|
||||
if opts.Template == "protocol-proxy" {
|
||||
mode = pluginmanager.UpstreamModeProtocolProxy
|
||||
}
|
||||
return fmt.Sprintf(`# Human-maintained plugin manifest. Build packages normalize this into manifest.json.
|
||||
return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。
|
||||
schema_version: mc-gateway.plugin/v1
|
||||
id: %q
|
||||
name: %q
|
||||
@@ -1713,7 +1715,7 @@ func manifestTOMLTemplate(opts pluginInitCLIOptions) string {
|
||||
if opts.Template == "protocol-proxy" {
|
||||
mode = pluginmanager.UpstreamModeProtocolProxy
|
||||
}
|
||||
return fmt.Sprintf(`# Human-maintained plugin manifest. Build packages normalize this into manifest.json.
|
||||
return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。
|
||||
schema_version = "mc-gateway.plugin/v1"
|
||||
id = %q
|
||||
name = %q
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_cli_toolchain_test.go 包含用于约束 plugin cli toolchain 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/plugin_test.go 包含用于约束 plugin 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/quic.go 启动可选的 QUIC 监听器,并把 QUIC 流适配到普通网关连接流程。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -19,6 +21,8 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// quicConn 把 QUIC connection 和单条 stream 组合成 net.Conn 风格对象,
|
||||
// 使后续转发逻辑不用区分 TCP 与 QUIC。
|
||||
quicConn struct {
|
||||
quic.Connection
|
||||
quic.Stream
|
||||
@@ -30,6 +34,7 @@ func runQuic(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
}
|
||||
|
||||
// QUIC 基于 UDP 监听,端口来自运行态服务配置。
|
||||
udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: config.Quic.Port})
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to listen UDP")
|
||||
@@ -41,6 +46,7 @@ func runQuic(wg *sync.WaitGroup) {
|
||||
log.Panic().Err(err).Msg("Failed to generate TLS config")
|
||||
}
|
||||
|
||||
// quic-go 的 listener 接收 connection,真正的字节流在 stream 中。
|
||||
ln, err := quic.Listen(udpConn, tlsConf, nil)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to listen QUIC")
|
||||
@@ -63,11 +69,13 @@ func runQuic(wg *sync.WaitGroup) {
|
||||
|
||||
func upstreamQuic(host string) net.Conn {
|
||||
tlsConf := &tls.Config{
|
||||
// 网关自管的 QUIC 上游默认使用临时证书,当前先跳过证书校验。
|
||||
InsecureSkipVerify: true, // 跳过证书检查
|
||||
NextProtos: getQuicNextProtos(),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s handshake timeout
|
||||
// 上游握手使用短超时,避免连接协程在不可达上游上长期等待。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := quic.DialAddr(ctx, host, tlsConf, nil)
|
||||
@@ -86,6 +94,7 @@ func upstreamQuic(host string) net.Conn {
|
||||
}
|
||||
log.Debug().Str("host", host).Msg("QUIC stream opened")
|
||||
|
||||
// 返回的 quicConn 后续会收到 Minecraft 首包回放并进入普通双向转发。
|
||||
return quicConn{
|
||||
Connection: conn,
|
||||
Stream: stream,
|
||||
@@ -95,6 +104,7 @@ func upstreamQuic(host string) net.Conn {
|
||||
func handleQuicRequest(conn quic.Connection) {
|
||||
defer conn.CloseWithError(0, "Closing connection")
|
||||
|
||||
// 入口连接只等待第一条 stream;该 stream 承载完整 Minecraft 字节流。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
@@ -111,33 +121,33 @@ func handleQuicRequest(conn quic.Connection) {
|
||||
}
|
||||
|
||||
func generateTLSConfig() (*tls.Config, error) {
|
||||
// 生成私钥
|
||||
// 生成临时私钥;当前 QUIC 入口不依赖磁盘证书文件。
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建证书模板
|
||||
// 创建自签证书模板,满足 QUIC TLS 握手要求。
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Example Org"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 有效期 1 年
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour), // 有效期 1 年。
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// 自签名证书
|
||||
// 自签名证书用于当前进程生命周期内的 QUIC 监听。
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 编码证书和私钥
|
||||
// 编码证书和私钥,再交给 tls.X509KeyPair 解析为标准证书结构。
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
@@ -145,13 +155,13 @@ func generateTLSConfig() (*tls.Config, error) {
|
||||
}
|
||||
keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyPEM})
|
||||
|
||||
// 加载到 tls.Certificate
|
||||
// 加载到 tls.Certificate。
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEMBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回 tls.Config
|
||||
// 返回 QUIC listener 使用的 TLS 配置。
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: getQuicNextProtos(),
|
||||
@@ -161,7 +171,7 @@ func generateTLSConfig() (*tls.Config, error) {
|
||||
func getQuicNextProtos() []string {
|
||||
nextProtos := config.Quic.ApplicationProtocols
|
||||
if len(nextProtos) == 0 {
|
||||
return []string{"minecraft", "quic", "raw", "h3"} // 默认协议
|
||||
return []string{"minecraft", "quic", "raw", "h3"} // 默认协议列表。
|
||||
}
|
||||
return nextProtos
|
||||
}
|
||||
@@ -172,10 +182,12 @@ func (c quicConn) Close() error {
|
||||
}
|
||||
|
||||
func (c quicConn) CloseWrite() error {
|
||||
// QUIC stream 关闭写方向即可通知对端没有更多数据。
|
||||
return c.Stream.Close()
|
||||
}
|
||||
|
||||
func (c quicConn) CloseRead() error {
|
||||
// CancelRead 用于停止接收方向,匹配 relay.go 中的半关闭调用。
|
||||
c.Stream.CancelRead(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/quic_test.go 包含用于约束 quic 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/relay.go 实现客户端与上游之间的双向复制循环和转发缓冲池。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
|
||||
const proxyBufferSize = 64 * 1024
|
||||
|
||||
// proxyBufferPool 为普通 io.CopyBuffer 路径复用 64KiB 缓冲区,降低长连接转发时的分配压力。
|
||||
var proxyBufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, proxyBufferSize)
|
||||
@@ -30,6 +33,7 @@ type (
|
||||
func proxyConnections(a, b io.ReadWriter) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 两个方向独立复制,任意一侧读到 EOF 后通过半关闭通知对端。
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -44,6 +48,7 @@ func proxyConnections(a, b io.ReadWriter) {
|
||||
}
|
||||
|
||||
func proxyCopy(dst io.Writer, src io.Reader) {
|
||||
// 转发协程不能把 panic 带出到连接处理主协程;记录后关闭对应方向即可。
|
||||
defer recoverProxyCopy()
|
||||
defer closeRead(src)
|
||||
defer closeWrite(dst)
|
||||
@@ -55,6 +60,8 @@ func proxyCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
|
||||
func copyForward(dst io.Writer, src io.Reader) (int64, error) {
|
||||
// 优先使用标准库为具体类型提供的零拷贝/优化路径,只有普通 reader/writer
|
||||
// 才落到共享缓冲区。
|
||||
if _, ok := src.(io.WriterTo); ok {
|
||||
return io.Copy(dst, src)
|
||||
}
|
||||
@@ -81,6 +88,7 @@ func putProxyBuffer(buf []byte) {
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, buf []byte) error {
|
||||
// net.Conn.Write 允许短写;首包回放和 PROXY 头写入必须循环直到写完。
|
||||
for len(buf) > 0 {
|
||||
n, err := w.Write(buf)
|
||||
if n > 0 {
|
||||
@@ -98,6 +106,7 @@ func writeAll(w io.Writer, buf []byte) error {
|
||||
}
|
||||
|
||||
func closeWrite(conn any) {
|
||||
// TCP 支持半关闭时只关闭写方向,让反向复制还有机会读完剩余数据。
|
||||
if closer, ok := conn.(closeWriter); ok {
|
||||
if err := closer.CloseWrite(); err != nil {
|
||||
log.Debug().Err(err).Msg("failed to close write side")
|
||||
@@ -113,6 +122,7 @@ func closeWrite(conn any) {
|
||||
}
|
||||
|
||||
func closeRead(conn any) {
|
||||
// 支持 CloseRead 的连接可以显式停止读方向,帮助对端更快感知转发结束。
|
||||
if closer, ok := conn.(closeReader); ok {
|
||||
if err := closer.CloseRead(); err != nil {
|
||||
log.Debug().Err(err).Msg("failed to close read side")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/relay_benchmark_test.go 包含用于约束 relay benchmark 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/relay_test.go 包含用于约束 relay 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/tcp.go 在未与 Admin HTTP 共用端口时启动普通 TCP Minecraft 监听器。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -34,13 +36,14 @@ func runTcp(wg *sync.WaitGroup) {
|
||||
continue
|
||||
}
|
||||
setSocketOptions(conn)
|
||||
// 处理连接
|
||||
// 处理连接;后续握手解析、插件过滤和路由解析都在 handleRequest 中完成。
|
||||
gatewayMetrics.TCPConnectionStarted()
|
||||
go handleRequest(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func upstreamTcp(host string) net.Conn {
|
||||
// TCP 是默认上游传输,路由值没有协议前缀时都会走这里。
|
||||
conn, err := tcpDialer.Dial("tcp", host)
|
||||
if err != nil {
|
||||
gatewayMetrics.UpstreamDialError()
|
||||
@@ -53,13 +56,14 @@ func upstreamTcp(host string) net.Conn {
|
||||
}
|
||||
|
||||
var tcpDialer = net.Dialer{
|
||||
// 上游拨号失败应尽快返回给客户端连接处理流程,避免连接协程长期堆积。
|
||||
Timeout: 3 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
func setSocketOptions(conn net.Conn) {
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true) // 禁用 Nagle 算法
|
||||
tcpConn.SetNoDelay(true) // 禁用 Nagle 算法,降低 Minecraft 交互延迟。
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/tcp_test.go 包含用于约束 tcp 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/tcp_web_port_reuse.go 启动共享 TCP/Admin 监听器,按连接首包自动区分 HTTP 流量和 Minecraft 流量。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -12,11 +14,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTCPPort = 25565
|
||||
defaultTCPPort = 25565
|
||||
// 首包超时沿用 tcphttpmux 默认值,保持同端口分流逻辑的单一来源。
|
||||
tcpWebInitialPacketTimeout = tcphttpmux.DefaultInitialPacketTimeout
|
||||
)
|
||||
|
||||
func normalizedTCPPort() int {
|
||||
// 静态配置未指定端口时保持 Minecraft 默认端口。
|
||||
if config.Tcp.Port == 0 {
|
||||
return defaultTCPPort
|
||||
}
|
||||
@@ -31,6 +35,7 @@ func normalizedWebSocketPort() int {
|
||||
}
|
||||
|
||||
func normalizedWebSocketPath() string {
|
||||
// WebSocket 路径为空时回退到根路径,避免生成空的 HTTP 路由。
|
||||
if config.WebSocket.Path == "" {
|
||||
return "/"
|
||||
}
|
||||
@@ -38,6 +43,7 @@ func normalizedWebSocketPath() string {
|
||||
}
|
||||
|
||||
func tcpWebPortReuseEnabled() bool {
|
||||
// 是否共用端口完全由启用状态和端口相等推导,不引入额外配置开关。
|
||||
return config.Tcp.Enable &&
|
||||
config.WebSocket.Enable &&
|
||||
normalizedTCPPort() == normalizedWebSocketPort()
|
||||
@@ -49,6 +55,7 @@ func runTcpWebPortReuse(wg *sync.WaitGroup) {
|
||||
}
|
||||
|
||||
port := normalizedTCPPort()
|
||||
// 同一个 listener 同时承载 Minecraft TCP 和 Admin HTTP,由 serveTcpWebPortReuse 分流。
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
@@ -69,6 +76,7 @@ func runTcpWebPortReuse(wg *sync.WaitGroup) {
|
||||
}
|
||||
|
||||
func serveTcpWebPortReuse(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn)) error {
|
||||
// tcphttpmux 只负责协议分流;指标、socket 选项和日志通过回调接回主包。
|
||||
return tcphttpmux.Serve(listener, handler, tcpHandler, tcphttpmux.Options{
|
||||
InitialPacketTimeout: tcpWebInitialPacketTimeout,
|
||||
SetSocketOptions: setSocketOptions,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/tcp_web_port_reuse_test.go 包含用于约束 tcp web port reuse 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/test_helpers_test.go 包含用于约束 test helpers 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/websocket.go 把 WebSocket 会话适配为 net.Conn,让浏览器客户端复用网关请求路径。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// 允许所有来源的连接(生产环境中应该更严格)
|
||||
// 当前网关把 WebSocket 当作传输层入口,先允许所有来源;生产暴露时应在反向代理层收紧来源。
|
||||
return true
|
||||
},
|
||||
}
|
||||
@@ -38,12 +40,14 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
defer conn.Close()
|
||||
|
||||
gatewayMetrics.WebSocketConnectionStarted()
|
||||
// WebSocket 连接包装为 net.Conn 后进入同一个 handleRequest,复用插件、路由和转发逻辑。
|
||||
handleRequest(&webSocketConn{Conn: conn})
|
||||
}
|
||||
|
||||
func (w *webSocketConn) Read(b []byte) (n int, err error) {
|
||||
for {
|
||||
if w.reader != nil {
|
||||
// 当前消息帧没读完前持续从同一个 reader 读取,模拟流式 net.Conn。
|
||||
n, err = w.reader.Read(b)
|
||||
if errors.Is(err, io.EOF) {
|
||||
w.reader = nil
|
||||
@@ -60,6 +64,7 @@ func (w *webSocketConn) Read(b []byte) (n int, err error) {
|
||||
return 0, err
|
||||
}
|
||||
if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage {
|
||||
// 控制帧不进入 Minecraft 协议流。
|
||||
continue
|
||||
}
|
||||
w.reader = reader
|
||||
@@ -67,6 +72,7 @@ func (w *webSocketConn) Read(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (w *webSocketConn) Write(b []byte) (n int, err error) {
|
||||
// 每次 Write 输出一个二进制 WebSocket 消息,保持与 Minecraft packet 边界无关的字节流语义。
|
||||
writer, err := w.NextWriter(websocket.BinaryMessage)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -96,6 +102,7 @@ func (w *webSocketConn) SetDeadline(t time.Time) error {
|
||||
|
||||
func newWebSocketHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
// 路径来自运行态服务配置,允许管理端把 WebSocket 入口挂到子路径。
|
||||
mux.HandleFunc(normalizedWebSocketPath(), handleWebSocket)
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/gateway/websocket_test.go 包含用于约束 websocket 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/kcp/main.go 提供独立的 KCP 到 TCP 代理工具,用于测试或演示 KCP 传输行为。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/kcp/main_test.go 包含用于约束 kcp 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/quic/main.go 提供独立的 QUIC 到 TCP 代理工具,用于测试或演示 QUIC 传输行为。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -84,7 +86,7 @@ func handlerConn(conn net.Conn) {
|
||||
log.Info().
|
||||
Msg("QUIC stream opened")
|
||||
|
||||
// read and write stream data
|
||||
// 读写 QUIC 流数据。
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := conn.Read(buf)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cmd/quic/main_test.go 包含用于约束 quic 行为的测试。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user