Skip to content

Commit 649d1cb

Browse files
committed
wip: auth
1 parent 8bc611e commit 649d1cb

File tree

8 files changed

+230
-1
lines changed

8 files changed

+230
-1
lines changed

packages/core/src/client/webcomponents/components/Dock.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ function onPointerDown(e: PointerEvent) {
6464
draggingOffset.y = e.clientY - top - height / 2
6565
}
6666
67+
const isRpcTrusted = ref(context.rpc.isTrusted)
68+
context.rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
69+
isRpcTrusted.value = isTrusted
70+
})
71+
6772
onMounted(() => {
6873
windowSize.width = window.innerWidth
6974
windowSize.height = window.innerHeight
@@ -220,7 +225,10 @@ const panelStyle = computed(() => {
220225
})
221226
222227
onMounted(() => {
223-
bringUp()
228+
if (context.panel.store.open && !isRpcTrusted.value)
229+
context.panel.store.open = false
230+
if (isRpcTrusted.value)
231+
bringUp()
224232
recalculateCounter.value++
225233
})
226234
</script>
@@ -268,6 +276,13 @@ onMounted(() => {
268276
class="w-3 h-3 absolute left-1/2 top-1/2 translate-x--1/2 translate-y--1/2 transition-opacity duration-300"
269277
:class="isMinimized ? 'op100' : 'op0'"
270278
/>
279+
<div
280+
v-if="!isRpcTrusted"
281+
class="p2"
282+
:class="isMinimized ? 'opacity-0 pointer-events-none ws-nowrap flex items-center text-sm text-orange' : 'opacity-100'"
283+
>
284+
IS NOT TRUSTED
285+
</div>
271286
<DockEntries
272287
:entries="context.docks.entries"
273288
class="transition duration-200 flex items-center w-full h-full justify-center"

packages/core/src/client/webcomponents/state/docks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,19 @@ export async function useDocksEntries(rpc: DevToolsRpcClient): Promise<Ref<DevTo
5959
}
6060
const dockEntries = _docksEntriesRef = shallowRef<DevToolsDockEntry[]>([])
6161
async function updateDocksEntries() {
62+
if (!rpc.isTrusted) {
63+
console.warn('[VITE DEVTOOLS] Untrusted client, skipping docks entries update')
64+
return
65+
}
6266
dockEntries.value = (await rpc.call('vite:internal:docks:list'))
6367
.map(entry => Object.freeze(entry))
6468
// eslint-disable-next-line no-console
6569
console.log('[VITE DEVTOOLS] Docks Entries Updated', [...dockEntries.value])
6670
}
71+
rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
72+
if (isTrusted)
73+
updateDocksEntries()
74+
})
6775
rpc.client.register({
6876
name: 'vite:internal:docks:updated' satisfies keyof DevToolsRpcClientFunctions,
6977
type: 'action',
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as p from '@clack/prompts'
2+
import { defineRpcFunction } from '@vitejs/devtools-kit'
3+
import c from 'ansis'
4+
5+
export interface DevToolsAuthInput {
6+
authId: string
7+
ua: string
8+
}
9+
10+
export interface DevToolsAuthReturn {
11+
isTrusted: boolean
12+
}
13+
14+
const TEMPORARY_STORAGE = new Map<string, {
15+
authId: string
16+
ua: string
17+
timestamp: number
18+
}>()
19+
20+
export const anonymousAuth = defineRpcFunction({
21+
name: 'vite:anonymous:auth',
22+
type: 'action',
23+
setup: () => {
24+
return {
25+
handler: async (query: DevToolsAuthInput): Promise<DevToolsAuthReturn> => {
26+
if (TEMPORARY_STORAGE.has(query.authId)) {
27+
return {
28+
isTrusted: true,
29+
}
30+
}
31+
32+
const message = [
33+
`A browser is requesting permissions to connect to the Vite DevTools.`,
34+
35+
`User Agent: ${c.yellow(c.bold(query.ua || 'Unknown'))}`,
36+
`Identifier: ${c.green(c.bold(query.authId))}`,
37+
'',
38+
'This will allow the browser to interact with the server, make file changes and run commands.',
39+
c.red(c.bold('You should only trust your local development browsers.')),
40+
]
41+
42+
p.note(
43+
c.reset(message.join('\n')),
44+
c.bold(c.yellow(' Vite DevTools Permission Request ')),
45+
)
46+
47+
const answer = await p.confirm({
48+
message: c.bold(`Do you trust this client (${c.green(c.bold(query.authId))})?`),
49+
initialValue: false,
50+
})
51+
52+
if (answer) {
53+
TEMPORARY_STORAGE.set(query.authId, {
54+
authId: query.authId,
55+
ua: query.ua,
56+
timestamp: Date.now(),
57+
})
58+
p.outro(c.green(c.bold('You have granted permissions to this client.')))
59+
60+
return {
61+
isTrusted: true,
62+
}
63+
}
64+
65+
p.outro(c.red(c.bold('You have denied permissions to this client.')))
66+
return {
67+
isTrusted: false,
68+
}
69+
},
70+
}
71+
},
72+
})

packages/core/src/node/rpc/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { DevToolsTerminalSessionStreamChunkEvent, RpcDefinitionsFilter, RpcDefinitionsToFunctions } from '@vitejs/devtools-kit'
2+
import { anonymousAuth } from './anonymous/auth'
23
import { docksList } from './internal/docks-list'
34
import { docksOnLaunch } from './internal/docks-on-launch'
45
import { rpcServerList } from './internal/rpc-server-list'
@@ -13,6 +14,10 @@ export const builtinPublicRpcDecalrations = [
1314
openInFinder,
1415
] as const
1516

17+
export const builtinAnonymousRpcDecalrations = [
18+
anonymousAuth,
19+
] as const
20+
1621
// @keep-sorted
1722
export const builtinInternalRpcDecalrations = [
1823
docksList,
@@ -24,6 +29,7 @@ export const builtinInternalRpcDecalrations = [
2429

2530
export const builtinRpcDecalrations = [
2631
...builtinPublicRpcDecalrations,
32+
...builtinAnonymousRpcDecalrations,
2733
...builtinInternalRpcDecalrations,
2834
] as const
2935

packages/core/src/node/ws.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface CreateWsServerOptions {
1515
context: DevToolsNodeContext
1616
}
1717

18+
const ANONYMOUS_SCOPE = 'vite:anonymous:'
19+
1820
export async function createWsServer(options: CreateWsServerOptions) {
1921
const rpcHost = options.context.rpc as unknown as RpcFunctionsHost
2022
const port = options.portWebSocket ?? await getPort({ port: 7812, random: true })
@@ -46,6 +48,17 @@ export async function createWsServer(options: CreateWsServerOptions) {
4648
console.error(c.red`⬢ RPC error on executing rpc`)
4749
console.error(error)
4850
},
51+
resolver(name, fn) {
52+
if (name.startsWith(ANONYMOUS_SCOPE))
53+
return fn
54+
55+
if (!this.$meta.isTrusted) {
56+
return () => {
57+
throw new Error(`Unauthorized access to method ${JSON.stringify(name)}, please trust this client first`)
58+
}
59+
}
60+
return fn
61+
},
4962
},
5063
},
5164
)

packages/kit/src/client/docks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,5 @@ export interface DockEntryStateEvents {
8585
}
8686

8787
export interface RpcClientEvents {
88+
'rpc:is-trusted:updated': (isTrusted: boolean) => void
8889
}

packages/kit/src/client/rpc.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import type { DevToolsClientContext, DevToolsClientRpcHost, RpcClientEvents } fr
55
import { createRpcClient } from '@vitejs/devtools-rpc'
66
import { createWsRpcPreset } from '@vitejs/devtools-rpc/presets/ws/client'
77
import { RpcFunctionsCollectorBase } from 'birpc-x'
8+
import { UAParser } from 'my-ua-parser'
89
import { createEventEmitter } from '../utils/events'
10+
import { nanoid } from '../utils/nanoid'
11+
import { promiseWithResolver } from '../utils/promise'
912

1013
const CONNECTION_META_KEY = '__VITE_DEVTOOLS_CONNECTION_META__'
14+
const CONNECTION_AUTH_ID_KEY = '__VITE_DEVTOOLS_CONNECTION_AUTH_ID__'
1115

1216
function isNumeric(str: string | number | undefined) {
1317
if (str == null)
@@ -27,10 +31,29 @@ export interface DevToolsRpcClient {
2731
* The events of the client
2832
*/
2933
events: EventEmitter<RpcClientEvents>
34+
35+
/**
36+
* Whether the client is trusted
37+
*/
38+
readonly isTrusted: boolean | null
3039
/**
3140
* The connection meta
3241
*/
3342
readonly connectionMeta: ConnectionMeta
43+
/**
44+
* Return a promise that resolves when the client is trusted
45+
*
46+
* Rejects with an error if the timeout is reached
47+
*
48+
* @param timeout - The timeout in milliseconds, default to 60 seconds
49+
*/
50+
ensureTrusted: (timeout?: number) => Promise<boolean>
51+
52+
/**
53+
* Request trust from the server
54+
*/
55+
requestTrust: () => Promise<boolean>
56+
3457
/**
3558
* Call a RPC function on the server
3659
*/
@@ -49,6 +72,31 @@ export interface DevToolsRpcClient {
4972
client: DevToolsClientRpcHost
5073
}
5174

75+
function getConnectionAuthIdFromWindows(): string {
76+
const getters = [
77+
() => localStorage.getItem(CONNECTION_AUTH_ID_KEY),
78+
() => (window as any)?.[CONNECTION_AUTH_ID_KEY],
79+
() => (globalThis as any)?.[CONNECTION_AUTH_ID_KEY],
80+
() => (parent.window as any)?.[CONNECTION_AUTH_ID_KEY],
81+
]
82+
83+
for (const getter of getters) {
84+
try {
85+
const value = getter()
86+
if (value) {
87+
if (!localStorage.getItem(CONNECTION_AUTH_ID_KEY))
88+
localStorage.setItem(CONNECTION_AUTH_ID_KEY, value)
89+
return value
90+
}
91+
}
92+
catch {}
93+
}
94+
95+
const uid = nanoid()
96+
localStorage.setItem(CONNECTION_AUTH_ID_KEY, uid)
97+
return uid
98+
}
99+
52100
function findConnectionMetaFromWindows(): ConnectionMeta | undefined {
53101
const getters = [
54102
() => (window as any)?.[CONNECTION_META_KEY],
@@ -104,6 +152,11 @@ export async function getDevToolsRpcClient(
104152
const context: DevToolsClientContext = {
105153
rpc: undefined!,
106154
}
155+
const authId = getConnectionAuthIdFromWindows()
156+
157+
let isTrusted = false
158+
const trustedPromise = promiseWithResolver<boolean>()
159+
107160
const clientRpc: DevToolsClientRpcHost = new RpcFunctionsCollectorBase<DevToolsRpcClientFunctions, DevToolsClientContext>(context)
108161

109162
// Create the RPC client
@@ -118,9 +171,60 @@ export async function getDevToolsRpcClient(
118171
},
119172
)
120173

174+
async function requestTrust() {
175+
if (isTrusted)
176+
return true
177+
178+
const info = new UAParser(navigator.userAgent).getResult()
179+
const ua = [
180+
info.browser.name,
181+
info.browser.version,
182+
'|',
183+
info.os.name,
184+
info.os.version,
185+
info.device.type,
186+
].filter(i => i).join(' ')
187+
188+
const result = await serverRpc.$call('vite:anonymous:auth', {
189+
authId,
190+
ua,
191+
})
192+
193+
isTrusted = result.isTrusted
194+
trustedPromise.resolve(isTrusted)
195+
events.emit('rpc:is-trusted:updated', isTrusted)
196+
return result.isTrusted
197+
}
198+
199+
async function ensureTrusted(timeout = 60_000): Promise<boolean> {
200+
if (isTrusted)
201+
trustedPromise.resolve(true)
202+
203+
if (timeout <= 0)
204+
return trustedPromise.promise
205+
206+
let clear = () => {}
207+
await Promise.race([
208+
trustedPromise.promise.then(clear),
209+
new Promise((resolve, reject) => {
210+
const id = setTimeout(() => {
211+
reject(new Error('[Vite DevTools] Timeout waiting for rpc to be trusted'))
212+
}, timeout)
213+
clear = () => clearTimeout(id)
214+
}),
215+
])
216+
217+
return isTrusted
218+
}
219+
121220
const rpc: DevToolsRpcClient = {
122221
events,
222+
get isTrusted() {
223+
return isTrusted
224+
},
123225
connectionMeta,
226+
ensureTrusted,
227+
requestTrust,
124228
call: (...args: any): any => {
125229
// @ts-expect-error casting
126230
return serverRpc.call(...args)
@@ -138,6 +242,7 @@ export async function getDevToolsRpcClient(
138242

139243
// @ts-expect-error assign to readonly property
140244
context.rpc = rpc
245+
requestTrust()
141246

142247
return rpc
143248
}

packages/rpc/src/presets/ws/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export interface WebSocketRpcServerOptions {
1111
onDisconnected?: (ws: WebSocket) => void
1212
}
1313

14+
export interface WebSocketRpcServerChannelMeta {
15+
uid: string
16+
isTrusted?: boolean
17+
}
18+
1419
function NOOP() {}
1520

1621
export const createWsRpcPreset: RpcServerPreset<
@@ -54,6 +59,10 @@ export const createWsRpcPreset: RpcServerPreset<
5459
},
5560
serialize,
5661
deserialize,
62+
meta: <WebSocketRpcServerChannelMeta>{
63+
uid: crypto.randomUUID(),
64+
isTrusted: false,
65+
},
5766
}
5867

5968
rpc.updateChannels((channels) => {

0 commit comments

Comments
 (0)