diff --git a/remote/public/js/app.js b/remote/public/js/app.js index 5694a1f..41922b8 100644 --- a/remote/public/js/app.js +++ b/remote/public/js/app.js @@ -1,9 +1,9 @@ (function() { - const password = getCookie('auth') || ''; - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsHost = window.location.hostname; const wsPort = window.location.port; + const urlParams = new URLSearchParams(window.location.search); + const password = urlParams.get('password'); const wsUrlBase = wsPort ? `${wsProtocol}//${wsHost}:${wsPort}/ws` : `${wsProtocol}//${wsHost}/ws`; const WS_URL = password ? `${wsUrlBase}?password=${encodeURIComponent(password)}` : wsUrlBase; diff --git a/remote/src/core/App.js b/remote/src/core/App.js index a2e51f5..c11f550 100644 --- a/remote/src/core/App.js +++ b/remote/src/core/App.js @@ -240,7 +240,7 @@ class App { httpServer.renderLoginPage(res, '密码错误'); }); - httpServer.use((req, res, next) => { + httpServer.use(async (req, res, next) => { if (!authService.hasPassword()) { res.locals.authenticated = true; return next(); @@ -250,8 +250,31 @@ class App { if (token) { const decoded = tokenManager.verifyToken(token); if (decoded) { + logger.info('HTTP auth: token valid from cookie', { path: req.path, ip: req.socket?.remoteAddress }); res.locals.authenticated = true; return next(); + } else { + logger.info('HTTP auth: token invalid from cookie', { path: req.path, ip: req.socket?.remoteAddress }); + } + } + + // 检查 URL 参数中的 password + const urlPassword = req.query.password; + if (urlPassword) { + logger.info('HTTP auth: checking password from URL', { + path: req.path, + ip: req.socket?.remoteAddress, + passwordLength: urlPassword.length + }); + const isValid = await authService.authenticate(urlPassword); + if (isValid) { + const newToken = tokenManager.generateToken({ userId: 'default-user' }); + res.cookie('auth', newToken, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }); + logger.info('HTTP auth: password valid, token generated', { path: req.path, ip: req.socket?.remoteAddress }); + res.locals.authenticated = true; + return next(); + } else { + logger.warn('HTTP auth: password invalid', { path: req.path, ip: req.socket?.remoteAddress }); } } @@ -283,18 +306,63 @@ class App { const originalHandlers = this.wss.listeners('connection'); this.wss.removeAllListeners('connection'); - const securityConfig = require('../utils/config').getSecurityConfig(); - const password = securityConfig.password; - - // 未认证的连接也允许,用于剪贴板同步 - this.wss.on('connection', (ws, req) => { + this.wss.on('connection', async (ws, req) => { const url = new URL(req.url, `http://${req.headers.host}`); - const isAuthenticated = url.searchParams.get('password') === password; + const fullUrl = req.url; + + let isAuthenticated = false; + let authMethod = ''; + + // 检查 URL 参数中的 password + const urlPassword = url.searchParams.get('password'); + if (urlPassword) { + logger.info('WebSocket auth: checking password from URL', { + ip: req.socket?.remoteAddress, + urlLength: fullUrl.length, + passwordLength: urlPassword.length, + passwordPrefix: urlPassword.substring(0, 2) + '***' + }); + const isValid = await authService.authenticate(urlPassword); + if (isValid) { + isAuthenticated = true; + authMethod = 'password_url'; + logger.info('WebSocket auth: password from URL valid', { ip: req.socket?.remoteAddress }); + } else { + logger.warn('WebSocket auth: password from URL invalid', { ip: req.socket?.remoteAddress }); + } + } + + // 检查 Cookie 中的 token + if (!isAuthenticated && req.cookies && req.cookies.auth) { + logger.info('WebSocket auth: checking token from cookie', { ip: req.socket?.remoteAddress }); + const decoded = tokenManager.verifyToken(req.cookies.auth); + if (decoded) { + isAuthenticated = true; + authMethod = 'cookie_token'; + logger.info('WebSocket auth: token from cookie valid', { ip: req.socket?.remoteAddress }); + } else { + logger.warn('WebSocket auth: token from cookie invalid', { ip: req.socket?.remoteAddress }); + } + } + + // 未认证,拒绝连接 + if (!isAuthenticated) { + logger.warn('WebSocket authentication failed', { + ip: req.socket?.remoteAddress, + hasPassword: !!urlPassword, + hasCookie: !!(req.cookies && req.cookies.auth), + fullUrl: fullUrl.substring(0, 200) + }); + ws.close(1008, 'Authentication required'); + return; + } + + logger.debug('WebSocket authenticated', { ip: req.socket?.remoteAddress, authMethod }); // 保存认证状态 - ws.isAuthenticated = isAuthenticated; + ws.isAuthenticated = true; - // 处理输入消息(不检查认证) + // 处理输入消息 ws.on('message', (data) => { try { const message = JSON.parse(data); @@ -304,7 +372,7 @@ class App { } }); - // 调用原始 handlers(用于已认证的连接) + // 调用原始 handlers originalHandlers.forEach(handler => { handler(ws, req); }); diff --git a/shared/modules/remote/types.ts b/shared/modules/remote/types.ts index b871d44..5591b69 100644 --- a/shared/modules/remote/types.ts +++ b/shared/modules/remote/types.ts @@ -4,6 +4,7 @@ export interface RemoteDevice { serverHost: string desktopPort: number gitPort: number + password?: string } export interface RemoteConfig { diff --git a/src/components/tabs/TabContentCache/TabContentCache.tsx b/src/components/tabs/TabContentCache/TabContentCache.tsx index e1ac657..e104760 100644 --- a/src/components/tabs/TabContentCache/TabContentCache.tsx +++ b/src/components/tabs/TabContentCache/TabContentCache.tsx @@ -41,10 +41,12 @@ export const TabContentCache: React.FC = ({ const urlParams = new URLSearchParams(queryString) const serverHost = urlParams.get('host') || '' const port = parseInt(urlParams.get('port') || '3000', 10) + const password = urlParams.get('password') || undefined return ( closeFile(file)} /> ) diff --git a/src/modules/remote/RemotePage.tsx b/src/modules/remote/RemotePage.tsx index 705ed56..360fcf5 100644 --- a/src/modules/remote/RemotePage.tsx +++ b/src/modules/remote/RemotePage.tsx @@ -165,7 +165,10 @@ export const RemotePage: React.FC = () => { setShowConfig(true) return } - const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}` + let url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}` + if (selectedConfig.password) { + url += `?password=${encodeURIComponent(selectedConfig.password)}` + } const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `远程桌面${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) @@ -176,7 +179,7 @@ export const RemotePage: React.FC = () => { setShowConfig(true) return } - const url = `http://${selectedConfig.serverHost}:${selectedConfig.gitPort}` + let url = `http://${selectedConfig.serverHost}:${selectedConfig.gitPort}` const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteGitFileItem(url, `远程 Git 仓库${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) @@ -187,7 +190,10 @@ export const RemotePage: React.FC = () => { setShowConfig(true) return } - const url = `file-transfer-panel?host=${encodeURIComponent(selectedConfig.serverHost)}&port=${selectedConfig.desktopPort}` + let url = `file-transfer-panel?host=${encodeURIComponent(selectedConfig.serverHost)}&port=${selectedConfig.desktopPort}&device=${encodeURIComponent(selectedConfig.deviceName || '')}` + if (selectedConfig.password) { + url += `&password=${encodeURIComponent(selectedConfig.password)}` + } const fileItem: FileItem = { name: `文件传输 - ${selectedConfig.deviceName}`, path: url, @@ -203,7 +209,10 @@ export const RemotePage: React.FC = () => { setShowConfig(true) return } - const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/opencode` + let url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/opencode` + if (selectedConfig.password) { + url += `?password=${encodeURIComponent(selectedConfig.password)}` + } const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `OpenCode${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) @@ -214,7 +223,10 @@ export const RemotePage: React.FC = () => { setShowConfig(true) return } - const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/openclaw` + let url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/openclaw` + if (selectedConfig.password) { + url += `?password=${encodeURIComponent(selectedConfig.password)}` + } const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `OpenClaw${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) diff --git a/src/modules/remote/api.ts b/src/modules/remote/api.ts index dc23729..4a7d830 100644 --- a/src/modules/remote/api.ts +++ b/src/modules/remote/api.ts @@ -1,10 +1,36 @@ import { getModuleApi } from '@/lib/module-registry' import { type RemoteEndpoints } from '@shared/modules/remote' import type { RemoteConfig, RemoteFileItem } from './types' -import { fetchFiles as fetchSystemFiles } from '@/lib/api' +import type { FileItem } from '@/lib/api' export type { RemoteFileItem } from './types' -export { fetchSystemFiles } + +export const fetchSystemDrives = async (): Promise => { + const data = await fetchApi<{ items: FileItem[] }>('/api/files/drives') + return data.items +} + +export const fetchSystemFiles = async (systemPath: string): Promise => { + const data = await fetchApi<{ items: FileItem[] }>(`/api/files/system?path=${encodeURIComponent(systemPath)}`) + return data.items +} + +export const fetchSystemFileContent = async (systemPath: string): Promise => { + const response = await fetch(`/api/files/system/content?path=${encodeURIComponent(systemPath)}`) + if (!response.ok) { + throw new Error(`Failed to fetch file: ${response.statusText}`) + } + return await response.blob() +} + +const fetchApi = async (url: string, options?: RequestInit): Promise => { + const response = await fetch(url, options) + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`) + } + const result = await response.json() + return result.data +} const getApi = () => { const api = getModuleApi('remote') @@ -48,13 +74,23 @@ export const fetchRemoteFiles = async ( path: string, password?: string ): Promise => { - const url = `http://${serverHost}:${port}/api/files?path=${encodeURIComponent(path)}` + let url = `http://${serverHost}:${port}/api/files/browse?path=${encodeURIComponent(path)}` + if (password) { + url += `&password=${encodeURIComponent(password)}` + } const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to fetch remote files: ${response.statusText}`) } const data = await response.json() - return data.items || [] + const items = data.items || [] + return items.map((item: { name: string; isDirectory: boolean; size: number; modified: Date }) => ({ + name: item.name, + path: data.currentPath ? `${data.currentPath}/${item.name}` : item.name, + type: item.isDirectory ? 'dir' : 'file', + size: item.size, + modified: item.modified?.toString(), + })) } export const uploadFileToRemote = async ( @@ -65,7 +101,10 @@ export const uploadFileToRemote = async ( password?: string, onProgress?: (progress: number) => void ): Promise => { - const url = `http://${serverHost}:${port}/api/files/upload` + let url = `http://${serverHost}:${port}/api/files/upload` + if (password) { + url += `?password=${encodeURIComponent(password)}` + } const formData = new FormData() formData.append('file', file) if (remotePath) { @@ -107,7 +146,10 @@ export const downloadFileFromRemote = async ( password?: string, onProgress?: (progress: number) => void ): Promise => { - const url = `http://${serverHost}:${port}/api/files/download?path=${encodeURIComponent(remotePath)}&name=${encodeURIComponent(fileName)}` + let url = `http://${serverHost}:${port}/api/files/${encodeURIComponent(fileName)}` + if (password) { + url += `?password=${encodeURIComponent(password)}` + } const response = await fetch(url) if (!response.ok) { diff --git a/src/modules/remote/components/ConfigDialog.tsx b/src/modules/remote/components/ConfigDialog.tsx index 35b6b17..9993b41 100644 --- a/src/modules/remote/components/ConfigDialog.tsx +++ b/src/modules/remote/components/ConfigDialog.tsx @@ -154,6 +154,19 @@ export const ConfigDialog: React.FC = ({ className="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-900 dark:focus:ring-gray-100 focus:border-transparent transition-colors" /> + +
+ + handleChange('password', e.target.value)} + placeholder="远程桌面访问密码" + className="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-900 dark:focus:ring-gray-100 focus:border-transparent transition-colors" + /> +
diff --git a/src/modules/remote/components/RemoteWebView.tsx b/src/modules/remote/components/RemoteWebView.tsx index 0202d70..146296d 100644 --- a/src/modules/remote/components/RemoteWebView.tsx +++ b/src/modules/remote/components/RemoteWebView.tsx @@ -36,12 +36,26 @@ export const RemoteWebView: React.FC = ({ url, title, isDesk const clipboardWsRef = useRef(null) const [clipboardStatus, setClipboardStatus] = useState('') - // 生成 WebSocket URL(不需要密码,WebSocket 允许未认证连接) + // 检测密码变化,强制刷新页面 + const prevPasswordRef = useRef('') + useEffect(() => { + const urlObj = new URL(url) + const currentPassword = urlObj.searchParams.get('password') || '' + if (prevPasswordRef.current && prevPasswordRef.current !== currentPassword) { + console.log('[RemoteWebView] Password changed, reloading page') + webviewRef.current?.reload() + } + prevPasswordRef.current = currentPassword + }, [url]) + + // 生成 WebSocket URL(携带 password 参数进行认证) const getWsUrl = useCallback(() => { try { const urlObj = new URL(url) const wsProtocol = urlObj.protocol === 'https:' ? 'wss:' : 'ws:' - return `${wsProtocol}//${urlObj.host}/ws` + const password = urlObj.searchParams.get('password') + const wsUrl = `${wsProtocol}//${urlObj.host}/ws` + return password ? `${wsUrl}?password=${encodeURIComponent(password)}` : wsUrl } catch { return null } diff --git a/src/modules/remote/components/file-transfer/FileTransferPage.tsx b/src/modules/remote/components/file-transfer/FileTransferPage.tsx index 6463020..5a27e47 100644 --- a/src/modules/remote/components/file-transfer/FileTransferPage.tsx +++ b/src/modules/remote/components/file-transfer/FileTransferPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useRef, useEffect } from 'react' import type { FileItem } from '@/lib/api' -import { type RemoteFileItem, uploadFileToRemote, downloadFileFromRemote } from '../../api' +import { type RemoteFileItem, uploadFileToRemote, downloadFileFromRemote, fetchSystemFileContent } from '../../api' import { type TransferItem } from '../../types' import { LocalFilePanel } from './LocalFilePanel' import { RemoteFilePanel } from './RemoteFilePanel' @@ -9,10 +9,11 @@ import { TransferQueue } from './TransferQueue' interface FileTransferPageProps { serverHost: string port: number + password?: string onClose: () => void } -export const FileTransferPage: React.FC = ({ serverHost, port, onClose }) => { +export const FileTransferPage: React.FC = ({ serverHost, port, password, onClose }) => { const [localSelected, setLocalSelected] = useState(null) const [remoteSelected, setRemoteSelected] = useState(null) const [transfers, setTransfers] = useState([]) @@ -38,11 +39,10 @@ export const FileTransferPage: React.FC = ({ serverHost, setTransfers((prev) => [...prev, newTransfer]) try { - const response = await fetch(`/api/files/content?path=${encodeURIComponent(localSelected.path)}`) - const blob = await response.blob() + const blob = await fetchSystemFileContent(localSelected.path) const file = new File([blob], localSelected.name, { type: blob.type }) - await uploadFileToRemote(serverHost, port, file, '', undefined, (progress) => { + await uploadFileToRemote(serverHost, port, file, '', password, (progress) => { setTransfers((prev) => prev.map((t) => (t.id === transferId ? { ...t, progress } : t)) ) @@ -84,7 +84,7 @@ export const FileTransferPage: React.FC = ({ serverHost, setTransfers((prev) => [...prev, newTransfer]) try { - await downloadFileFromRemote(serverHost, port, remoteSelected.name, '', undefined, (progress) => { + await downloadFileFromRemote(serverHost, port, remoteSelected.name, '', password, (progress) => { setTransfers((prev) => prev.map((t) => (t.id === transferId ? { ...t, progress } : t)) ) @@ -162,6 +162,7 @@ export const FileTransferPage: React.FC = ({ serverHost, = ({ serverHost,
diff --git a/src/modules/remote/types.ts b/src/modules/remote/types.ts index e3bb1b1..73fa851 100644 --- a/src/modules/remote/types.ts +++ b/src/modules/remote/types.ts @@ -14,6 +14,7 @@ export interface RemoteDevice { serverHost: string desktopPort: number gitPort: number + password?: string } export interface RemoteConfig {