Commit fffcb74a authored by Chunchi Che's avatar Chunchi Che

Merge branch 'fix/match' into 'main'

fix match

See merge request !424
parents 736537f0 7f078ab1
Pipeline #42890 passed with stages
in 2 minutes and 22 seconds
......@@ -8,12 +8,12 @@
},
{
"name": "mycard-athletic",
"ip": "tiramisu.moecube.com",
"ip": "tiramisu.moenext.com",
"port": "8912"
},
{
"name": "mycard-custom",
"ip": "tiramisu.moecube.com",
"ip": "tiramisu.moenext.com",
"port": "7912"
},
{
......
......@@ -8,12 +8,12 @@
},
{
"name": "mycard-athletic",
"ip": "tiramisu.moecube.com",
"ip": "tiramisu.moenext.com",
"port": "8912"
},
{
"name": "mycard-custom",
"ip": "tiramisu.moecube.com",
"ip": "tiramisu.moenext.com",
"port": "7912"
},
{
......
......@@ -3,4 +3,5 @@ export * from "./account";
export * from "./match";
export * from "./options";
export * from "./room";
export * from "./u16Secret";
export * from "./user";
......@@ -6,13 +6,21 @@ export interface MatchInfo {
password: string;
}
/**
* 请求匹配
*
* @param username 用户名
* @param secret 认证密钥(优先使用 u16Secret,如果没有则使用 external_id)
* @param arena 匹配类型(athletic: 竞技, entertain: 娱乐)
* @returns 匹配信息(服务器地址、端口、密码)
*/
export async function match(
username: string,
extraId: number,
secret: number,
arena: "athletic" | "entertain" = "entertain",
): Promise<MatchInfo | undefined> {
const headers = {
Authorization: "Basic " + customBase64Encode(username + ":" + extraId),
Authorization: "Basic " + customBase64Encode(username + ":" + secret),
};
let response: Response | undefined = undefined;
const params = new URLSearchParams({
......
......@@ -16,19 +16,20 @@ export interface Room {
options: Options;
}
// 通过房间ID和external_id加密得出房间密码
// 通过房间ID和secret加密得出房间密码
//
// 用于加入MC服房间
// @param secret - 优先使用 u16Secret,如果没有则使用 external_id
export function getJoinRoomPasswd(
roomID: string,
external_id: number,
secret: number,
_private: boolean = false,
): string {
const optionsBuffer = new Uint8Array(6);
optionsBuffer[1] =
(_private ? RoomAction.JoinPrivate : RoomAction.JoinPublic) << 4;
encryptBuffer(optionsBuffer, external_id);
encryptBuffer(optionsBuffer, secret);
const base64String = btoa(String.fromCharCode(...optionsBuffer));
......@@ -36,10 +37,11 @@ export function getJoinRoomPasswd(
}
// 获取创建房间的密码
// @param secret - 优先使用 u16Secret,如果没有则使用 external_id
export function getCreateRoomPasswd(
options: Options,
roomID: string,
external_id: number,
secret: number,
_private: boolean = false,
) {
// ref: https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
......@@ -57,14 +59,15 @@ export function getCreateRoomPasswd(
writeUInt16LE(optionsBuffer, 3, options.start_lp);
optionsBuffer[5] = (options.start_hand << 4) | options.draw_count;
encryptBuffer(optionsBuffer, external_id);
encryptBuffer(optionsBuffer, secret);
const base64String = btoa(String.fromCharCode(...optionsBuffer));
return base64String + roomID;
}
// 填充校验码和加密
function encryptBuffer(buffer: Uint8Array, external_id: number) {
// @param secret - 优先使用 u16Secret,如果没有则使用 external_id
function encryptBuffer(buffer: Uint8Array, secret: number) {
let checksum = 0;
for (let i = 1; i < buffer.length; i++) {
......@@ -73,11 +76,11 @@ function encryptBuffer(buffer: Uint8Array, external_id: number) {
buffer[0] = checksum & 0xff;
const secret = (external_id % 65535) + 1;
const encryptSecret = (secret % 65535) + 1;
for (let i = 0; i < buffer.length; i += 2) {
const value = readUInt16LE(buffer, i);
const xorResult = value ^ secret;
const xorResult = value ^ encryptSecret;
writeUInt16LE(buffer, i, xorResult);
}
}
......
/**
* 获取用户的 u16Secret
*
* u16Secret 是用于匹配和房间认证的时间轮换密钥
* 每次使用前都需要重新获取,因为它会按时间轮换
*/
const API_URL = "https://sapi.moecube.com:444/accounts/authUser";
interface U16SecretResponse {
u16Secret: number;
}
export async function getUserU16Secret(token: string): Promise<number> {
if (!token) {
throw new Error("获取用户密钥失败:token 不存在,请重新登录");
}
try {
const response = await fetch(API_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: U16SecretResponse = await response.json();
if (data.u16Secret === null || data.u16Secret === undefined) {
throw new Error("服务器返回的数据中没有 u16Secret");
}
return data.u16Secret;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
console.error("获取 u16Secret 失败:", errorMsg);
throw new Error(`获取用户密钥失败:${errorMsg},请尝试重新登录`);
}
}
......@@ -15,6 +15,7 @@ import {
getCreateRoomPasswd,
getJoinRoomPasswd,
getPrivateRoomID,
getUserU16Secret,
match,
} from "@/api";
import { useConfig } from "@/config";
......@@ -61,11 +62,17 @@ export const Component: React.FC = () => {
const onMatch = async (arena: "athletic" | "entertain") => {
if (!user) {
message.error("请先登录萌卡账号");
} else {
return;
}
try {
arena === "athletic"
? setAthleticMatchLoading(true)
: setEntertainMatchLoading(true);
const matchInfo = await match(user.username, user.external_id, arena);
// 每次匹配前都要重新获取 u16Secret,因为它会按时间轮换
const u16Secret = await getUserU16Secret(user.token);
const matchInfo = await match(user.username, u16Secret, arena);
if (matchInfo) {
await connectSrvpro({
......@@ -75,7 +82,16 @@ export const Component: React.FC = () => {
});
} else {
message.error("匹配失败T_T");
arena === "athletic"
? setAthleticMatchLoading(false)
: setEntertainMatchLoading(false);
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
arena === "athletic"
? setAthleticMatchLoading(false)
: setEntertainMatchLoading(false);
}
};
......@@ -107,15 +123,21 @@ export const Component: React.FC = () => {
// 创建MC自定义房间
const onCreateMCRoom = async () => {
if (user) {
if (!user) {
return;
}
try {
const mcServer = serverList.find(
(server) => server.name === "mycard-custom",
);
if (mcServer) {
// 每次操作前都要重新获取 u16Secret
const u16Secret = await getUserU16Secret(user.token);
const passWd = getCreateRoomPasswd(
mcCustomRoomStore.options,
String(getPrivateRoomID(user.external_id)),
user.external_id,
u16Secret,
true,
);
await connectSrvpro({
......@@ -124,30 +146,43 @@ export const Component: React.FC = () => {
passWd,
});
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
}
};
// 加入MC自定义房间
const onJoinMCRoom = async () => {
if (user) {
if (mcCustomRoomStore.friendPrivateID !== undefined) {
const mcServer = serverList.find(
(server) => server.name === "mycard-custom",
if (!user) {
return;
}
if (mcCustomRoomStore.friendPrivateID === undefined) {
message.error("请输入朋友的私密房间密码!");
return;
}
try {
const mcServer = serverList.find(
(server) => server.name === "mycard-custom",
);
if (mcServer) {
// 每次操作前都要重新获取 u16Secret
const u16Secret = await getUserU16Secret(user.token);
const passWd = getJoinRoomPasswd(
String(mcCustomRoomStore.friendPrivateID),
u16Secret,
true,
);
if (mcServer) {
const passWd = getJoinRoomPasswd(
String(mcCustomRoomStore.friendPrivateID),
user.external_id,
true,
);
await connectSrvpro({
ip: mcServer.ip + ":" + mcServer.port,
player: user.username,
passWd,
});
}
} else {
message.error("请输入朋友的私密房间密码!");
await connectSrvpro({
ip: mcServer.ip + ":" + mcServer.port,
player: user.username,
passWd,
});
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
}
};
......@@ -161,7 +196,12 @@ export const Component: React.FC = () => {
width: "40vw",
okText: i18n("EnterSpectatorMode"),
onOk: async () => {
if (watchStore.watchID) {
if (!watchStore.watchID) {
message.error(`${i18n("PleaseSelectTheRoomToSpectate")}`);
return;
}
try {
setWatchLoading(true);
// 找到MC竞技匹配的Server
......@@ -169,10 +209,9 @@ export const Component: React.FC = () => {
(server) => server.name === "mycard-athletic",
);
if (mcServer) {
const passWd = getJoinRoomPasswd(
watchStore.watchID,
user.external_id,
);
// 每次操作前都要重新获取 u16Secret
const u16Secret = await getUserU16Secret(user.token);
const passWd = getJoinRoomPasswd(watchStore.watchID, u16Secret);
await connectSrvpro({
ip: mcServer.ip + ":" + mcServer.port,
player: user.username,
......@@ -182,9 +221,13 @@ export const Component: React.FC = () => {
message.error(
"Something unexpected happened, please contact <ccc@neos.moe> to fix",
);
setWatchLoading(false);
}
} else {
message.error(`${i18n("PleaseSelectTheRoomToSpectate")}`);
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
setWatchLoading(false);
}
},
centered: true,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment