add verify before ws connection

This commit is contained in:
锦恢 2025-05-16 20:36:18 +08:00
parent ad17594360
commit a3a4f2b5cb
12 changed files with 138 additions and 114 deletions

View File

@ -45,14 +45,12 @@ onMounted(async () => {
setDefaultCss(); setDefaultCss();
pinkLog('OpenMCP Client 启动'); pinkLog('OpenMCP Client 启动');
const platform = getPlatform();
// //
if (platform !== 'web') { if (route.name !== 'debug') {
if (route.name !== 'debug') { const targetRoute = import.meta.env.BASE_URL + 'debug';
router.replace('/debug'); console.log('go to ' + targetRoute);
router.push('/debug'); router.push(targetRoute);
}
} }
// //

View File

@ -1,12 +1,10 @@
import { pinkLog, redLog } from '@/views/setting/util'; import { pinkLog, redLog } from '@/views/setting/util';
import { acquireVsCodeApi, electronApi, getPlatform } from './platform'; import { acquireVsCodeApi, electronApi, getPlatform } from './platform';
import { privilegeStatus } from '@/components/password-dialog/status';
export interface VSCodeMessage { export interface VSCodeMessage {
command: string; command: string;
data?: unknown; data?: unknown;
callbackId?: string; callbackId?: string;
password?: string;
} }
export interface RestFulResponse { export interface RestFulResponse {
@ -67,51 +65,57 @@ export class MessageBridge {
} }
// WebSocket 环境连接 // WebSocket 环境连接
private setupWebSocket() { public setupWebSocket(setupSignature?: string) {
const wsUrl = setupSignature || this.setupSignature;
const wsUrl = this.setupSignature;
if (typeof wsUrl !== 'string') { if (typeof wsUrl !== 'string') {
throw new Error('setupSignature must be a string'); throw new Error('setupSignature must be a string');
} }
this.ws = new WebSocket(wsUrl); this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data) as VSCodeMessage;
this.dispatchMessage(message);
} catch (err) {
console.error('Message parse error:', err);
console.log(event);
}
};
this.ws.onclose = () => {
redLog('WebSocket connection closed');
};
this.postMessage = (message) => {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('send', message);
message.password = privilegeStatus.password;
this.ws.send(JSON.stringify(message));
}
};
const ws = this.ws; const ws = this.ws;
this.isConnected = new Promise<boolean>((resolve, reject) => { this.isConnected = new Promise<boolean>((resolve, reject) => {
ws.onopen = () => { ws.onopen = () => {
resolve(true); resolve(true);
}; };
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data) as VSCodeMessage;
this.dispatchMessage(message);
} catch (err) {
console.error('Message parse error:', err);
console.log(event);
}
};
ws.onerror = (err) => {
redLog('WebSocket error:');
resolve(false);
};
ws.onclose = () => {
redLog('WebSocket connection closed');
resolve(false);
};
this.postMessage = (message) => {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('send', message);
this.ws.send(JSON.stringify(message));
}
};
}); });
return this.isConnected;
} }
public async awaitForWebsocket() { public async awaitForWebsocket() {
if (this.isConnected) { if (this.isConnected) {
await this.isConnected; return await this.isConnected;
} }
return false;
} }
private setupElectronListener() { private setupElectronListener() {

View File

@ -46,7 +46,7 @@
<el-tour-step <el-tour-step
target="#sidebar-connect" target="#sidebar-connect"
:prev-button-props="{ children: '上一步' }" :prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/connect') }" :next-button-props="{ children: '下一步', onClick: () => router.push(baseUrl + 'connect') }"
:show-close="false" :show-close="false"
> >
<template #header> <template #header>
@ -95,7 +95,7 @@
<el-tour-step <el-tour-step
target="#sidebar-debug" target="#sidebar-debug"
:prev-button-props="{ children: '上一步' }" :prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/debug') }" :next-button-props="{ children: '下一步', onClick: () => router.push(baseUrl + 'debug') }"
:show-close="false" :show-close="false"
> >
<template #header> <template #header>
@ -129,8 +129,8 @@
<el-tour-step <el-tour-step
target="#sidebar-setting" target="#sidebar-setting"
:prev-button-props="{ children: '上一步', onClick: () => router.push('/debug') }" :prev-button-props="{ children: '上一步', onClick: () => router.push(baseUrl + 'debug') }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/setting') }" :next-button-props="{ children: '下一步', onClick: () => router.push(baseUrl + 'setting') }"
:show-close="false" :show-close="false"
> >
<template #header> <template #header>
@ -205,7 +205,7 @@
<el-tour-step <el-tour-step
:prev-button-props="{ children: '上一步', onClick: () => router.push('/setting') }" :prev-button-props="{ children: '上一步', onClick: () => router.push(baseUrl + 'setting') }"
:next-button-props="{ children: '下一步' }" :next-button-props="{ children: '下一步' }"
:show-close="false" :show-close="false"
> >
@ -224,7 +224,7 @@
<el-tour-step <el-tour-step
:prev-button-props="{ children: '上一步', onClick: () => router.push('/setting') }" :prev-button-props="{ children: '上一步', onClick: () => router.push(baseUrl + 'setting') }"
:next-button-props="{ children: '结束', onClick: () => finishTour() }" :next-button-props="{ children: '结束', onClick: () => finishTour() }"
:show-close="false" :show-close="false"
> >
@ -260,6 +260,8 @@ const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const baseUrl = import.meta.env.BASE_URL;
function finishTour() { function finishTour() {
openTour.value = false; openTour.value = false;
userHasReadGuide.value = true; userHasReadGuide.value = true;

View File

@ -127,7 +127,7 @@ import { useI18n } from 'vue-i18n';
import MessageMeta from './message-meta.vue'; import MessageMeta from './message-meta.vue';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown'; import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import { createTest } from '@/views/setting/llm'; import { createTest } from '@/views/setting/llm';
import { IToolRenderMessage, MessageState } from '../chat-box/chat'; import { type IToolRenderMessage, MessageState } from '../chat-box/chat';
import type { ToolCallContent } from '@/hook/type'; import type { ToolCallContent } from '@/hook/type';
import ToolcallResultItem from './toolcall-result-item.vue'; import ToolcallResultItem from './toolcall-result-item.vue';

View File

@ -24,9 +24,10 @@ const dialogVisible = ref(true);
const handleSubmit = async () => { const handleSubmit = async () => {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const res = await bridge.commandRequest('ciallo', { password: privilegeStatus.password });
const ok = await bridge.setupWebSocket(import.meta.env.VITE_WEBSOCKET_URL + '?t=' + privilegeStatus.password);
if (res.code === 200) {
if (ok) {
ElMessage.success('密码验证成功,欢迎回来锦恢'); ElMessage.success('密码验证成功,欢迎回来锦恢');
dialogVisible.value = false; dialogVisible.value = false;

View File

@ -29,8 +29,10 @@ function isActive(name: string) {
return route.name === name; return route.name === name;
} }
const baseUrl = import.meta.env.BASE_URL;
function gotoOption(ident: string) { function gotoOption(ident: string) {
router.push('/' + ident); router.push(baseUrl + ident);
} }
</script> </script>

View File

@ -1,31 +1,33 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
const baseURL = import.meta.env.BASE_URL;
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
name : "default", name : "default",
path : "/", path : "/",
redirect : "/debug" redirect : baseURL + "/debug"
}, },
{ {
path: "/debug", path: baseURL + "/debug",
name: "debug", name: "debug",
component: () => import( /* webpackMode: "eager" */ "@/views/debug/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/debug/index.vue"),
meta: { title: "Debug" } meta: { title: "Debug" }
}, },
{ {
path: "/connect", path: baseURL + "/connect",
name: "connect", name: "connect",
component: () => import( /* webpackMode: "eager" */ "@/views/connect/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/connect/index.vue"),
meta: { title: "Connect" } meta: { title: "Connect" }
}, },
{ {
path: "/setting", path: baseURL + "/setting",
name: "setting", name: "setting",
component: () => import( /* webpackMode: "eager" */ "@/views/setting/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/setting/index.vue"),
meta: { title: "Setting" } meta: { title: "Setting" }
}, },
{ {
path: "/about", path: baseURL + "/about",
name: "about", name: "about",
component: () => import( /* webpackMode: "eager" */ "@/views/about/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/about/index.vue"),
meta: { title: "Tools" } meta: { title: "Tools" }

View File

@ -10,6 +10,10 @@
], ],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"lib": [
"es2022",
"dom"
],
"paths": { "paths": {
"@/*": [ "@/*": [
"./src/*" "./src/*"

View File

@ -11,7 +11,11 @@
"compilerOptions": { "compilerOptions": {
"noEmit": true, "noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext", "module": "es2022",
"lib": [
"es2022",
"dom"
],
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"types": [ "types": [
"node" "node"

View File

@ -1,4 +1,4 @@
import WebSocket from 'ws'; import { WebSocketServer } from 'ws';
import pino from 'pino'; import pino from 'pino';
import { routeMessage } from './common/router'; import { routeMessage } from './common/router';
@ -26,7 +26,6 @@ const logger = pino({
}); });
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
const wss = new (WebSocket as any).Server({ port: 8282 });
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'stdio';
@ -98,6 +97,10 @@ function updateConnectionOption(data: any) {
const devHome = path.join(__dirname, '..', '..'); const devHome = path.join(__dirname, '..', '..');
setRunningCWD(devHome); setRunningCWD(devHome);
const wss = new WebSocketServer({ port: 8282 });
console.log('listen on ws://localhost:8282');
wss.on('connection', (ws: any) => { wss.on('connection', (ws: any) => {
// 仿造 webview 进行统一接口访问 // 仿造 webview 进行统一接口访问

View File

@ -1,4 +1,4 @@
import WebSocket from 'ws'; import { WebSocketServer } from 'ws';
import pino from 'pino'; import pino from 'pino';
import { routeMessage } from './common/router'; import { routeMessage } from './common/router';
@ -27,7 +27,6 @@ const logger = pino({
}); });
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
const wss = new (WebSocket as any).Server({ port: 8282 });
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'stdio';
@ -36,7 +35,7 @@ interface IStdioLaunchSignature {
} }
interface ISSELaunchSignature { interface ISSELaunchSignature {
type:'sse'; type: 'sse';
url: string; url: string;
oauth: string; oauth: string;
} }
@ -45,13 +44,13 @@ export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
function refreshConnectionOption(envPath: string) { function refreshConnectionOption(envPath: string) {
const defaultOption = { const defaultOption = {
type:'stdio', type: 'stdio',
command: 'mcp', command: 'mcp',
args: ['run', 'main.py'], args: ['run', 'main.py'],
cwd: '../server' cwd: '../server'
}; };
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return defaultOption; return defaultOption;
} }
@ -82,7 +81,7 @@ const authPassword = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.env
function updateConnectionOption(data: any) { function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
if (data.connectionType === 'STDIO') { if (data.connectionType === 'STDIO') {
const connectionItem = { const connectionItem = {
type: 'stdio', type: 'stdio',
@ -106,6 +105,37 @@ function updateConnectionOption(data: any) {
const devHome = path.join(__dirname, '..', '..'); const devHome = path.join(__dirname, '..', '..');
setRunningCWD(devHome); setRunningCWD(devHome);
function verifyToken(url: string) {
try {
const token = url.split('=')[1];
return token === authPassword.toString();
} catch (error) {
return false;
}
}
const wss = new WebSocketServer(
{
port: 8282,
verifyClient: (info, callback) => {
console.log(info.req.url);
const ok = verifyToken(info.req.url || '');
console.log(ok);
if (!ok) {
callback(false, 401, 'Unauthorized: Invalid token');
} else {
callback(true); // 允许连接
}
}
},
);
console.log('listen on ws://localhost:8282');
wss.on('connection', (ws: any) => { wss.on('connection', (ws: any) => {
// 仿造 webview 进行统一接口访问 // 仿造 webview 进行统一接口访问
@ -125,42 +155,7 @@ wss.on('connection', (ws: any) => {
// 注册消息接受的管线 // 注册消息接受的管线
webview.onDidReceiveMessage(message => { webview.onDidReceiveMessage(message => {
logger.info(`command: [${message.command || 'No Command'}]`); logger.info(`command: [${message.command || 'No Command'}]`);
const { command, data, password } = message; const { command, data } = message;
console.log(command, data);
if (command === 'ciallo') {
if (data.password === authPassword) {
webview.postMessage({
command,
data: {
code: 200,
msg: 'ciallo'
}
});
} else {
webview.postMessage({
command,
data: {
code: 403,
msg: '没有权限'
}
});
}
return;
}
if (password !== authPassword) {
webview.postMessage({
command,
data: {
code: 403,
msg: '没有权限'
}
});
return;
}
switch (command) { switch (command) {
case 'web/launch-signature': case 'web/launch-signature':
@ -175,7 +170,7 @@ wss.on('connection', (ws: any) => {
url: option.url, url: option.url,
oauth: option.oauth || '' oauth: option.oauth || ''
}; };
const launchResult = { const launchResult = {
code: 200, code: 200,
msg: launchResultMessage msg: launchResultMessage
@ -187,11 +182,11 @@ wss.on('connection', (ws: any) => {
}); });
break; break;
case 'web/update-connection-sigature': case 'web/update-connection-sigature':
updateConnectionOption(data); updateConnectionOption(data);
break; break;
default: default:
routeMessage(command, data, webview); routeMessage(command, data, webview);
break; break;

View File

@ -1,19 +1,28 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es6", "target": "es6",
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"experimentalDecorators": true, "experimentalDecorators": true,
// 访 openmcp-sdk // 访 openmcp-sdk
"paths": { "paths": {
"@openmcp-sdk/*": ["./openmcp-sdk/*"] "@openmcp-sdk/*": [
"./openmcp-sdk/*"
]
} }
}, },
"include": ["src/**/*"], "include": [
"exclude": ["node_modules", "dist", "service", "renderer"] "src/**/*"
],
"exclude": [
"node_modules",
"dist",
"service",
"renderer"
]
} }