功能修改
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
import type {
|
||||
GameClientBridgeApprovalState,
|
||||
GameClientBridgeCancelResponse,
|
||||
GameClientBridgeCommandCancellationResponse,
|
||||
GameClientBridgeCommandListResponse,
|
||||
GameClientBridgeCommandResponse,
|
||||
GameClientBridgeCommandResultResponse,
|
||||
GameClientBridgeCommandState,
|
||||
GameClientBridgeJsonObject,
|
||||
GameClientBridgeJsonValue,
|
||||
GameClientBridgeProfileDeclarationResponse,
|
||||
GameClientBridgeResultStatus,
|
||||
GameClientBridgeSnapshotListResponse,
|
||||
GameClientBridgeSnapshotResponse,
|
||||
GameClientBridgeStatusResponse
|
||||
} from "../api/types";
|
||||
|
||||
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired"]);
|
||||
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
|
||||
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled"]);
|
||||
const forbiddenKeys = new Set([
|
||||
"apikey",
|
||||
"accesskey",
|
||||
"accesskeyid",
|
||||
"claimlease",
|
||||
"componentkey",
|
||||
"componentsession",
|
||||
"componentsessionid",
|
||||
"credential",
|
||||
"credentials",
|
||||
"deploymentgeneration",
|
||||
"dsn",
|
||||
"fencingtoken",
|
||||
"hostpath",
|
||||
"installationid",
|
||||
"keygeneration",
|
||||
"leaseexpiresat",
|
||||
"password",
|
||||
"privatekey",
|
||||
"rawcredential",
|
||||
"runendpoint",
|
||||
"runendpointurl",
|
||||
"secret",
|
||||
"secretref",
|
||||
"secretvalue",
|
||||
"sessiontoken",
|
||||
"socket",
|
||||
"sourcesessionid",
|
||||
"storagecredential",
|
||||
"token"
|
||||
]);
|
||||
const forbiddenFragments = [
|
||||
"bearer ",
|
||||
"password=",
|
||||
"secret://",
|
||||
"unix://",
|
||||
"tcp://",
|
||||
"mysql://",
|
||||
"postgres://",
|
||||
"sqlite://",
|
||||
"rcon://"
|
||||
];
|
||||
const forbiddenHostPath = /(?:^|[\s"'])(?:\/[Uu]sers\/|\/home\/|\/root\/|\/var\/|\/etc\/|\/opt\/|[a-z]:[\\/]|\\\\[^\\]+\\)/;
|
||||
|
||||
export function parseSafeGameClientBridgeStatus(value: unknown): GameClientBridgeStatusResponse {
|
||||
const record = safeObject(value, "Game Client Bridge status");
|
||||
return {
|
||||
serverInstanceId: string(record.serverInstanceId, "serverInstanceId"),
|
||||
pluginId: string(record.pluginId, "pluginId"),
|
||||
available: boolean(record.available, "available"),
|
||||
reason: optionalString(record.reason, "reason"),
|
||||
profiles: array(record.profiles, "profiles").map(parseProfile)
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSafeGameClientBridgeCommand(value: unknown): GameClientBridgeCommandResponse {
|
||||
const record = safeObject(value, "Game Client Bridge command");
|
||||
const result: GameClientBridgeCommandResponse = {
|
||||
id: string(record.id, "id"),
|
||||
serverInstanceId: string(record.serverInstanceId, "serverInstanceId"),
|
||||
pluginId: string(record.pluginId, "pluginId"),
|
||||
profileKey: string(record.profileKey, "profileKey"),
|
||||
commandType: string(record.commandType, "commandType"),
|
||||
priority: number(record.priority, "priority"),
|
||||
state: commandState(record.state),
|
||||
approvalState: approvalState(record.approvalState),
|
||||
expiresAt: string(record.expiresAt, "expiresAt"),
|
||||
createdAt: string(record.createdAt, "createdAt"),
|
||||
updatedAt: string(record.updatedAt, "updatedAt")
|
||||
};
|
||||
copyOptionalString(record, result, "requesterId");
|
||||
copyOptionalString(record, result, "resultSummary");
|
||||
copyOptionalString(record, result, "completedAt");
|
||||
const auditReferences = optionalStringArray(record.auditReferences, "auditReferences");
|
||||
if (auditReferences) result.auditReferences = auditReferences;
|
||||
if (record.result !== undefined) result.result = parseResult(record.result);
|
||||
if (record.cancellation !== undefined) result.cancellation = parseCancellation(record.cancellation);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseSafeGameClientBridgeCommandList(value: unknown): GameClientBridgeCommandListResponse {
|
||||
const record = safeObject(value, "Game Client Bridge command list");
|
||||
return {
|
||||
items: array(record.items, "items").map(parseSafeGameClientBridgeCommand),
|
||||
count: nonNegativeInteger(record.count, "count")
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSafeGameClientBridgeCancellation(value: unknown): GameClientBridgeCancelResponse {
|
||||
const record = safeObject(value, "Game Client Bridge cancellation");
|
||||
const result: GameClientBridgeCancelResponse = {
|
||||
commandId: string(record.commandId, "commandId"),
|
||||
state: commandState(record.state),
|
||||
cancellation: parseCancellation(record.cancellation),
|
||||
updatedAt: string(record.updatedAt, "updatedAt")
|
||||
};
|
||||
const auditReferences = optionalStringArray(record.auditReferences, "auditReferences");
|
||||
if (auditReferences) result.auditReferences = auditReferences;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseSafeGameClientBridgeSnapshotList(value: unknown): GameClientBridgeSnapshotListResponse {
|
||||
const record = safeObject(value, "Game Client Bridge snapshot list");
|
||||
return {
|
||||
items: array(record.items, "items").map(parseSnapshot),
|
||||
count: nonNegativeInteger(record.count, "count")
|
||||
};
|
||||
}
|
||||
|
||||
function parseProfile(value: unknown): GameClientBridgeProfileDeclarationResponse {
|
||||
const record = safeObject(value, "Game Client Bridge profile");
|
||||
return {
|
||||
pluginId: string(record.pluginId, "profile.pluginId"),
|
||||
profileKey: string(record.profileKey, "profile.profileKey"),
|
||||
available: boolean(record.available, "profile.available"),
|
||||
reason: optionalString(record.reason, "profile.reason"),
|
||||
commandTypes: stringArray(record.commandTypes, "profile.commandTypes"),
|
||||
snapshotTypes: stringArray(record.snapshotTypes, "profile.snapshotTypes"),
|
||||
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys")
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): GameClientBridgeCommandResultResponse {
|
||||
const record = safeObject(value, "Game Client Bridge command result");
|
||||
const result: GameClientBridgeCommandResultResponse = {
|
||||
status: resultStatus(record.status),
|
||||
completedAt: string(record.completedAt, "result.completedAt")
|
||||
};
|
||||
const summary = optionalString(record.summary, "result.summary");
|
||||
if (summary) result.summary = summary;
|
||||
if (record.payload !== undefined) result.payload = jsonObject(record.payload, "result.payload");
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseCancellation(value: unknown): GameClientBridgeCommandCancellationResponse {
|
||||
const record = safeObject(value, "Game Client Bridge command cancellation");
|
||||
const result: GameClientBridgeCommandCancellationResponse = {
|
||||
cancelledAt: string(record.cancelledAt, "cancellation.cancelledAt")
|
||||
};
|
||||
const requestedBy = optionalString(record.requestedBy, "cancellation.requestedBy");
|
||||
const reason = optionalString(record.reason, "cancellation.reason");
|
||||
if (requestedBy) result.requestedBy = requestedBy;
|
||||
if (reason) result.reason = reason;
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseSnapshot(value: unknown): GameClientBridgeSnapshotResponse {
|
||||
const record = safeObject(value, "Game Client Bridge snapshot");
|
||||
const retention = safeObject(record.retention, "snapshot.retention");
|
||||
const result: GameClientBridgeSnapshotResponse = {
|
||||
id: string(record.id, "snapshot.id"),
|
||||
serverInstanceId: string(record.serverInstanceId, "snapshot.serverInstanceId"),
|
||||
pluginId: string(record.pluginId, "snapshot.pluginId"),
|
||||
profileKey: string(record.profileKey, "snapshot.profileKey"),
|
||||
type: string(record.type, "snapshot.type"),
|
||||
schemaVersion: string(record.schemaVersion, "snapshot.schemaVersion"),
|
||||
streamKey: string(record.streamKey, "snapshot.streamKey"),
|
||||
sequence: nonNegativeInteger(record.sequence, "snapshot.sequence"),
|
||||
observedAt: string(record.observedAt, "snapshot.observedAt"),
|
||||
payload: jsonObject(record.payload, "snapshot.payload"),
|
||||
retention: {
|
||||
keepForSeconds: nonNegativeInteger(retention.keepForSeconds, "snapshot.retention.keepForSeconds")
|
||||
},
|
||||
createdAt: string(record.createdAt, "snapshot.createdAt"),
|
||||
expiresAt: string(record.expiresAt, "snapshot.expiresAt")
|
||||
};
|
||||
if (retention.maxRecords !== undefined) result.retention.maxRecords = nonNegativeInteger(retention.maxRecords, "snapshot.retention.maxRecords");
|
||||
const auditReferences = optionalStringArray(record.auditReferences, "snapshot.auditReferences");
|
||||
if (auditReferences) result.auditReferences = auditReferences;
|
||||
return result;
|
||||
}
|
||||
|
||||
function safeObject(value: unknown, label: string): Record<string, unknown> {
|
||||
const record = object(value, label);
|
||||
rejectSensitiveProjection(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
function rejectSensitiveProjection(value: unknown): void {
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.toLowerCase();
|
||||
if (forbiddenFragments.some((fragment) => normalized.includes(fragment)) || forbiddenHostPath.test(value)) {
|
||||
throw new Error("Game Client Bridge response contains sensitive connection or host data");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(rejectSensitiveProjection);
|
||||
return;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (forbiddenKeys.has(key.toLowerCase().replace(/[^a-z0-9]/g, ""))) {
|
||||
throw new Error("Game Client Bridge response contains a forbidden field");
|
||||
}
|
||||
rejectSensitiveProjection(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function jsonObject(value: unknown, label: string): GameClientBridgeJsonObject {
|
||||
const record = object(value, label);
|
||||
return Object.fromEntries(Object.entries(record).map(([key, child]) => [key, jsonValue(child, `${label}.${key}`)]));
|
||||
}
|
||||
|
||||
function jsonValue(value: unknown, label: string): GameClientBridgeJsonValue {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (Array.isArray(value)) return value.map((child, index) => jsonValue(child, `${label}[${index}]`));
|
||||
if (value && typeof value === "object") return jsonObject(value, label);
|
||||
throw new Error(`${label} must be JSON-compatible`);
|
||||
}
|
||||
|
||||
function object(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function array(value: unknown, label: string): unknown[] {
|
||||
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || value === "") throw new Error(`${label} must be a string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown, label: string): string | undefined {
|
||||
if (value === undefined || value === "") return undefined;
|
||||
return string(value, label);
|
||||
}
|
||||
|
||||
function number(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a number`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown, label: string): number {
|
||||
const parsed = number(value, label);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative safe integer`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function boolean(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown, label: string): string[] {
|
||||
return array(value, label).map((item, index) => string(item, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
function optionalStringArray(value: unknown, label: string): string[] | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
return stringArray(value, label);
|
||||
}
|
||||
|
||||
function commandState(value: unknown): GameClientBridgeCommandState {
|
||||
const parsed = string(value, "state") as GameClientBridgeCommandState;
|
||||
if (!commandStates.has(parsed)) throw new Error("Game Client Bridge command state is invalid");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function approvalState(value: unknown): GameClientBridgeApprovalState {
|
||||
const parsed = string(value, "approvalState") as GameClientBridgeApprovalState;
|
||||
if (!approvalStates.has(parsed)) throw new Error("Game Client Bridge approval state is invalid");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resultStatus(value: unknown): GameClientBridgeResultStatus {
|
||||
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
|
||||
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function copyOptionalString<T extends object>(source: Record<string, unknown>, target: T, key: keyof T): void {
|
||||
const parsed = optionalString(source[key as string], String(key));
|
||||
if (parsed) (target as Record<keyof T, unknown>)[key] = parsed;
|
||||
}
|
||||
Reference in New Issue
Block a user