add share to guest by web client

This commit is contained in:
ljw
2024-10-09 15:52:17 +08:00
parent 3d203971b8
commit bf98a51285
15 changed files with 8729 additions and 14 deletions
+6 -3
View File
@@ -7,14 +7,16 @@
"serve": "vite preview"
},
"dependencies": {
"axios": "1.6.0",
"axios": "1.7.4",
"element-plus": "^2.8.2",
"js-cookie": "^3.0.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pinia": "2.0.3",
"vue": "3.2.37",
"vue-router": "^4.0.12"
"vue-router": "^4.0.12",
"fast-sha256": "^1.3.0",
"clipboard": "2.0.4"
},
"devDependencies": {
"@element-plus/icons": "0.0.11",
@@ -23,6 +25,7 @@
"qs": "^6.10.2",
"sass-loader": "^12.3.0",
"sass": "^1.43.4",
"vite": "^2.9.18"
"vite": "^2.9.18",
"ts-proto": "^1.141.1"
}
}
+9
View File
@@ -44,3 +44,12 @@ export function batchCreate (data) {
data,
})
}
export function shareByWebClient (data) {
return request({
url: '/address_book/shareByWebClient',
method: 'post',
data,
})
}
+36
View File
@@ -0,0 +1,36 @@
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import { T } from '@/utils/i18n'
export function handleClipboard (text, event) {
const clipboard = new Clipboard(event.target.toString(), {
text: () => text,
})
clipboard.on('success', () => {
ElMessage.success(T('CopySuccess'))
clipboard.destroy()
})
clipboard.on('error', () => {
ElMessage.error(T('CopyFailed'))
clipboard.destroy()
})
clipboard.onClick(event)
}
export function copyImage (targetNode) {
if (window.getSelection) {
// chrome等主流浏览器
var selection = window.getSelection()
selection.removeAllRanges()
var range = document.createRange()
range.selectNode(targetNode)
selection.addRange(range)
} else if (document.body.createTextRange) {
console.log('IE')
// ie
const range = document.body.createTextRange()
range.moveToElementText(targetNode)
range.select()
}
document.execCommand('copy')
}
+65
View File
@@ -282,5 +282,70 @@
},
"PleaseSelectData": {
"One": "Please select data"
},
"PasswordType": {
"One": "Password Type"
},
"OncePassword": {
"One": "One-time Password"
},
"FixedPassword": {
"One": "Fixed Password"
},
"FixedPasswordWarning": {
"One": "Fixed passwords may be leaked, so please use them with caution and use one-time passwords is recommended"
},
"ExpireTime": {
"One": "Expire Time"
},
"ShareByWebClient": {
"One": "Share By Web Client"
},
"Minutes": {
"One": "{param} Minute",
"Other": "{param} Minutes"
},
"Hours": {
"One": "{param} Hour",
"Other": "{param} Hours"
},
"Days": {
"One": "{param} Day",
"Other": "{param} Days"
},
"Weeks": {
"One": "{param} Week",
"Other": "{param} Weeks"
},
"Months": {
"One": "{param} Month",
"Other": "{param} Months"
},
"Forever": {
"One": "Forever"
},
"Error": {
"One": "Error"
},
"IDNotExist": {
"One": "ID does not exist"
},
"RemoteDesktopOffline": {
"One": "Remote desktop is offline"
},
"KeyMismatch": {
"One": "Key mismatch"
},
"KeyOveruse": {
"One": "Key overuse"
},
"Link": {
"One": "Link"
},
"CopySuccess": {
"One": "Copy Success"
},
"CopyFailed": {
"One": "Copy Failed"
}
}
+60
View File
@@ -274,5 +274,65 @@
},
"PleaseSelectData": {
"One": "请选择数据"
},
"PasswordType": {
"One": "密码类型"
},
"OncePassword": {
"One": "一次性密码"
},
"FixedPassword": {
"One": "固定密码"
},
"FixedPasswordWarning": {
"One": "固定密码可能存在泄露风险,请谨慎使用,建议使用一次性密码"
},
"ExpireTime": {
"One": "过期时间"
},
"ShareByWebClient": {
"One": "通过 Web Client 分享"
},
"Minutes": {
"One": "{param} 分钟"
},
"Hours": {
"One": "{param} 小时"
},
"Days": {
"One": "{param} 天"
},
"Weeks": {
"One": "{param} 周"
},
"Months": {
"One": "{param} 月"
},
"Forever": {
"One": "永久"
},
"Error": {
"One": "错误"
},
"IDNotExist": {
"One": "ID 不存在"
},
"RemoteDesktopOffline": {
"One": "远程电脑不在线"
},
"KeyMismatch": {
"One": "KEY不匹配"
},
"KeyOveruse": {
"One": "KEY使用过度"
},
"Link": {
"One": "链接"
},
"CopySuccess": {
"One": "复制成功"
},
"CopyFailed": {
"One": "复制失败"
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ service.interceptors.request.use(
const app = useAppStore()
const lang = app.setting.lang
if (lang) {
console.log('lang', lang)
// console.log('lang', lang)
config.headers['Accept-Language'] = lang
}
+84 -1
View File
@@ -1,5 +1,10 @@
import { ref } from 'vue'
import { config } from '@/api/rustdesk'
import Websock from '@/utils/webclient/websock'
import * as rendezvous from '@/utils/webclient/rendezvous'
import * as message from '@/utils/webclient/message'
import { ElMessageBox } from 'element-plus'
import { T } from '@/utils/i18n'
export const toWebClientLink = (row) => {
window.open(`${rustdeskConfig.value.api_server}/webclient/#/?id=${row.id}`)
@@ -23,4 +28,82 @@ export function loadRustdeskConfig () {
rustdeskConfig,
}
}
const { rustdeskConfig } = loadRustdeskConfig()
export const { rustdeskConfig } = loadRustdeskConfig()
export async function getPeerSlat (id) {
const [addr, port] = rustdeskConfig.value.id_server.split(':')
if (!addr) {
return
}
const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
const ws = new Websock(`${scheme}://${addr}:21118`, true)
await ws.open()
const conn_type = rendezvous.ConnType.DEFAULT_CONN
const nat_type = rendezvous.NatType.SYMMETRIC
const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
id,
licence_key: rustdeskConfig.value.key || undefined,
conn_type,
nat_type,
token: undefined,
})
ws.sendRendezvous({ punch_hole_request })
//rendezvous.RendezvousMessage
const msg = (await ws.next())
ws.close()
console.log(new Date() + ': Got relay response', msg)
const phr = msg.punch_hole_response
const rr = msg.relay_response
if (phr) {
if (phr?.other_failure) {
this.msgbox('error', 'Error', phr?.other_failure)
return
}
if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) {
switch (phr?.failure) {
case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST:
ElMessageBox.alert(T('IDNotExist'), T('Error'))
break
case rendezvous.PunchHoleResponse_Failure.OFFLINE:
ElMessageBox.alert(T('RemoteDesktopOffline'), T('Error'))
break
case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH:
ElMessageBox.alert(T('KeyMismatch'), T('Error'))
break
case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE:
ElMessageBox.alert(T('KeyOveruse'), T('Error'))
break
}
}
return false
} else if (rr) {
const uuid = rr.uuid
console.log(new Date() + ': Connecting to relay server')
const _ws = new Websock(`${scheme}://${addr}:21119`, false)
await _ws.open()
console.log(new Date() + ': Connected to relay server')
const request_relay = rendezvous.RequestRelay.fromPartial({
licence_key: rustdeskConfig.value.key || undefined,
uuid,
})
_ws.sendRendezvous({ request_relay })
//暂不支持pk
const public_key = message.PublicKey.fromPartial({})
_ws?.sendMessage({ public_key })
// const secure = (await this.secure(pk)) || false;
// globals.pushEvent("connection_ready", { secure, direct: false });
while (true) {
const msg = (await _ws?.next())
console.log('msg', msg)
if (msg?.hash) {
console.log('hash msg.....', msg.hash)
_ws.close()
return msg.hash
}
}
return false
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
import * as message from "./message.js";
import * as rendezvous from "./rendezvous.js";
type Keys = "message" | "open" | "close" | "error";
export default class Websock {
_websocket: WebSocket;
_eventHandlers: { [key in Keys]: Function };
_buf: (rendezvous.RendezvousMessage | message.Message)[];
_status: any;
_latency: number;
_secretKey: [Uint8Array, number, number] | undefined;
_uri: string;
_isRendezvous: boolean;
constructor(uri: string, isRendezvous: boolean = true) {
this._eventHandlers = {
message: (_: any) => {},
open: () => {},
close: () => {},
error: () => {},
};
this._uri = uri;
this._status = "";
this._buf = [];
this._websocket = new WebSocket(uri);
this._websocket.onmessage = this._recv_message.bind(this);
this._websocket.binaryType = "arraybuffer";
this._latency = new Date().getTime();
this._isRendezvous = isRendezvous;
}
latency(): number {
return this._latency;
}
setSecretKey(key: Uint8Array) {
this._secretKey = [key, 0, 0];
}
sendMessage(json: message.DeepPartial<message.Message>) {
let data = message.Message.encode(
message.Message.fromPartial(json)
).finish();
// let k = this._secretKey;
// if (k) {
// k[1] += 1;
// data = globals.encrypt(data, k[1], k[0]);
// }
this._websocket.send(data);
}
sendRendezvous(data: rendezvous.DeepPartial<rendezvous.RendezvousMessage>) {
this._websocket.send(
rendezvous.RendezvousMessage.encode(
rendezvous.RendezvousMessage.fromPartial(data)
).finish()
);
}
parseMessage(data: Uint8Array) {
return message.Message.decode(data);
}
parseRendezvous(data: Uint8Array) {
return rendezvous.RendezvousMessage.decode(data);
}
// Event Handlers
off(evt: Keys) {
this._eventHandlers[evt] = () => {};
}
on(evt: Keys, handler: Function) {
this._eventHandlers[evt] = handler;
}
async open(timeout: number = 12000): Promise<Websock> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this._status != "open") {
reject(this._status || "Timeout");
}
}, timeout);
this._websocket.onopen = () => {
this._latency = new Date().getTime() - this._latency;
this._status = "open";
console.debug(">> WebSock.onopen");
if (this._websocket?.protocol) {
console.info(
"Server choose sub-protocol: " + this._websocket.protocol
);
}
this._eventHandlers.open();
console.info("WebSock.onopen");
resolve(this);
};
this._websocket.onclose = (e) => {
if (this._status == "open") {
// e.code 1000 means that the connection was closed normally.
//
}
this._status = e;
console.error("WebSock.onclose: ");
console.error(e);
this._eventHandlers.close(e);
reject("Reset by the peer");
};
this._websocket.onerror = (e: any) => {
if (!this._status) {
reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server");
return;
}
this._status = e;
console.error("WebSock.onerror: ")
console.error(e);
this._eventHandlers.error(e);
};
});
}
async next(
timeout = 12000
): Promise<rendezvous.RendezvousMessage | message.Message> {
const func = (
resolve: (value: rendezvous.RendezvousMessage | message.Message) => void,
reject: (reason: any) => void,
tm0: number
) => {
// console.log('next')
if (this._buf.length) {
resolve(this._buf[0]);
this._buf.splice(0, 1);
} else {
if (this._status != "open") {
reject(this._status);
return;
}
if (new Date().getTime() > tm0 + timeout) {
reject("Timeout");
} else {
setTimeout(() => func(resolve, reject, tm0), 1);
}
}
};
return new Promise((resolve, reject) => {
func(resolve, reject, new Date().getTime());
});
}
close() {
this._status = "";
if (this._websocket) {
if (
this._websocket.readyState === WebSocket.OPEN ||
this._websocket.readyState === WebSocket.CONNECTING
) {
console.info("Closing WebSocket connection");
this._websocket.close();
}
this._websocket.onmessage = () => {};
}
}
_recv_message(e: any) {
if (e.data instanceof window.ArrayBuffer) {
let bytes = new Uint8Array(e.data);
// const k = this._secretKey;
// if (k) {
// k[2] += 1;
// bytes = globals.decrypt(bytes, k[2], k[0]);
// }
this._buf.push(
this._isRendezvous
? this.parseRendezvous(bytes)
: this.parseMessage(bytes)
);
}
this._eventHandlers.message(e.data);
}
}
@@ -0,0 +1,144 @@
<template>
<el-form ref="shareform" :model="formData" label-width="120px" label-suffix=" :">
<el-form-item :label="T('ID')" prop="id" required>
{{ formData.id }}
</el-form-item>
<el-form-item :label="T('PasswordType')">
<div>
<el-radio-group v-model="formData.password_type" @change="changePwdType">
<el-radio value="once">{{ T('OncePassword') }}</el-radio>
<el-radio value="fixed">{{ T('FixedPassword') }}</el-radio>
</el-radio-group>
<div v-if="formData.password_type==='fixed'" style="color: red">
{{ T('FixedPasswordWarning') }}
</div>
</div>
</el-form-item>
<el-form-item :label="T('Password')" prop="password" required>
<el-input v-model="formData.password" type="password" show-password></el-input>
</el-form-item>
<el-form-item :label="T('ExpireTime')" prop="expire" required>
<el-select v-model="formData.expire">
<el-option
v-for="item in expireTimes"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="link" :label="T('Link')">
<el-input v-model="link" readonly>
<template #append>
<el-button :icon="CopyDocument" @click="copyLink"/>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button v-if="!link" @click="cancel">{{ T('Cancel') }}</el-button>
<el-button v-if="!link" :loading="loading" @click="submitShare" type="primary">{{ T('Submit') }}</el-button>
<el-button v-else @click="cancel" type="success">{{ T('Close') }}</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { T } from '@/utils/i18n'
import { computed, reactive, ref, watch } from 'vue'
import { getPeerSlat, rustdeskConfig } from '@/utils/webclient'
import * as sha256 from 'fast-sha256'
import { shareByWebClient } from '@/api/address_book'
import { CopyDocument } from '@element-plus/icons'
import { handleClipboard } from '@/utils/clipboard'
const props = defineProps({
id: String,
hash: String,
})
const emits = defineEmits(['cancel', 'success'])
const formData = reactive({
id: props.id,
password_type: 'once',
password: '',
expire: 1800,
hash: props.hash,
})
watch(() => props.id, () => {
init()
})
const init = () => {
console.log('init')
formData.id = props.id
formData.hash = props.hash
formData.password = ''
formData.expire = 300
formData.password_type = 'once'
link.value = ''
}
const link = ref('')
const expireTimes = computed(() => [
{ label: T('Minutes', { param: 5 }, 5), value: 300 },
{ label: T('Minutes', { param: 30 }, 30), value: 1800 },
{ label: T('Hours', { param: 1 }, 1), value: 3600 },
{ label: T('Days', { param: 1 }, 1), value: 86400 },
{ label: T('Weeks', { param: 1 }, 1), value: 604800 },
{ label: T('Months', { param: 1 }, 1), value: 2592000 },
{ label: T('Forever'), value: 0 },
])
const changePwdType = (val) => {
if (val === 'fixed' && !formData.password) {
formData.password = props.hash
}
if (val === 'once') {
formData.password = ''
}
}
const cancel = () => {
loading.value = false
emits('cancel')
init()
}
const loading = ref(false)
const submitShare = async () => {
if (!formData.password) {
return
}
loading.value = true
const _formData = { ...formData }
if (formData.password !== formData.hash) {
const res = await getPeerSlat(formData.id).catch(_ => false)
if (!res) {
loading.value = false
return
}
const p = hash([formData.password, res.salt])
_formData.password = btoa(p.toString().split(',').map((v) => String.fromCharCode(v)).join(''))
}
const res = await shareByWebClient(_formData).catch(_ => false)
if (res) {
link.value = `${rustdeskConfig.value.api_server}/webclient/#/?share_token=${res.data.share_token}`
emits('success')
}
loading.value = false
}
const copyLink = (e) => {
handleClipboard(link.value, e)
}
const hash = (datas) => {
const hasher = new sha256.Hash()
datas.forEach((data) => {
if (typeof data == 'string') {
data = new TextEncoder().encode(data)
}
hasher.update(data)
})
return hasher.digest()
}
</script>
<style scoped lang="scss">
</style>
+13 -1
View File
@@ -116,7 +116,16 @@ export function useRepositories (user_id) {
getList()
}
}
const shareToWebClientVisible = ref(false)
const shareToWebClientForm = reactive({
id: '',
hash: '',
})
const toShowShare = (row) => {
shareToWebClientForm.id = row.id
shareToWebClientForm.hash = row.hash
shareToWebClientVisible.value = true
}
return {
listRes,
listQuery,
@@ -129,5 +138,8 @@ export function useRepositories (user_id) {
toEdit,
toAdd,
submit,
shareToWebClientVisible,
shareToWebClientForm,
toShowShare,
}
}
+15 -4
View File
@@ -44,9 +44,10 @@
<el-table-column prop="tags" :label="T('Tags')" align="center"/>
<!-- <el-table-column prop="created_at" label="创建时间" align="center"/>-->
<!-- <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
<el-table-column label="操作" align="center" width="400">
<el-table-column label="操作" align="center" width="500">
<template #default="{row}">
<el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
<el-button type="success" @click="toWebClientLink(row)">Web Client</el-button>
<!-- <el-button type="primary" @click="toShowShare(row)">{{ T('ShareByWebClient') }}</el-button>-->
<el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
<el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template>
@@ -139,6 +140,12 @@
</el-form-item>
</el-form>
</el-dialog>
<!-- <el-dialog v-model="shareToWebClientVisible" width="900" :close-on-click-modal="false">
<shareByWebClient :id="shareToWebClientForm.id"
:hash="shareToWebClientForm.hash"
@cancel="shareToWebClientVisible=false"
@success=""/>
</el-dialog>-->
</div>
</template>
@@ -147,9 +154,10 @@
import { list as fetchTagList } from '@/api/tag'
import { loadAllUsers } from '@/global'
import { useRepositories } from '@/views/address_book/index'
import { toWebClientLink } from '@/utils/webclient'
import { toWebClientLink, getPeerSlat } from '@/utils/webclient'
import { T } from '@/utils/i18n'
import { useRoute } from 'vue-router'
import shareByWebClient from '@/views/address_book/components/shareByWebClient.vue'
const route = useRoute()
const { allUsers, getAllUsers } = loadAllUsers()
@@ -178,7 +186,9 @@
toEdit,
toAdd,
submit,
currentColor,
shareToWebClientVisible,
shareToWebClientForm,
toShowShare,
} = useRepositories()
if (route.query?.user_id) {
@@ -192,6 +202,7 @@
watch(() => listQuery.page_size, handlerQuery)
</script>
<style scoped lang="scss">
+13 -2
View File
@@ -29,9 +29,10 @@
<el-table-column prop="tags" :label="T('Tags')" align="center"/>
<!-- <el-table-column prop="created_at" label="创建时间" align="center"/>-->
<!-- <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
<el-table-column :label="T('Actions')" align="center" width="400">
<el-table-column :label="T('Actions')" align="center" width="500">
<template #default="{row}">
<el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
<el-button type="success" @click="toWebClientLink(row)">Web Client</el-button>
<el-button type="primary" @click="toShowShare(row)">{{ T('ShareByWebClient') }}</el-button>
<el-button @click="toEdit(row)">{{ T('Edit') }}</el-button>
<el-button type="danger" @click="del(row)">{{ T('Delete') }}</el-button>
</template>
@@ -114,6 +115,12 @@
</el-form-item>
</el-form>
</el-dialog>
<el-dialog v-model="shareToWebClientVisible" width="900" :close-on-click-modal="false">
<shareByWebClient :id="shareToWebClientForm.id"
:hash="shareToWebClientForm.hash"
@cancel="shareToWebClientVisible=false"
@success=""/>
</el-dialog>
</div>
</template>
@@ -123,6 +130,7 @@
import { useRepositories } from '@/views/address_book'
import { toWebClientLink } from '@/utils/webclient'
import { T } from '@/utils/i18n'
import shareByWebClient from '@/views/address_book/components/shareByWebClient.vue'
const tagList = ref([])
const fetchTagListData = async () => {
@@ -145,6 +153,9 @@
toEdit,
toAdd,
submit,
shareToWebClientVisible,
shareToWebClientForm,
toShowShare,
} = useRepositories()
listQuery.is_my = 1
+2 -2
View File
@@ -31,8 +31,8 @@ const conf = {
},
},
build: {
target: 'es2015',
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
target: 'es2020',
minify: 'esbuild', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
manifest: false, // 是否产出maifest.json
sourcemap: false, // 是否产出soucemap.json
emptyOutDir: true,