-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathorigins.ts
More file actions
210 lines (164 loc) · 6.39 KB
/
origins.ts
File metadata and controls
210 lines (164 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { v5 as uuidv5 } from 'uuid'
import { IncomingMessage } from 'http'
import queryString from 'query-string'
import accounts, { AccessRequest } from '../accounts'
import store from '../store'
import type { Permission } from '../store/state/types'
const dev = process.env.NODE_ENV === 'development'
const activeExtensionChecks: Record<string, Promise<boolean>> = {}
const activePermissionChecks: Record<string, Promise<Permission | undefined>> = {}
const extensionPrefixes = {
chrome: 'chrome-extension',
firefox: 'moz-extension',
safari: 'safari-web-extension'
}
const protocolRegex = /^(?:ws|http)s?:\/\//
interface OriginUpdateResult {
payload: RPCRequestPayload
chainId: string
}
type Browser = 'chrome' | 'firefox' | 'safari'
export interface FrameExtension {
browser: Browser
id: string
}
// allows the Frame extension to request specific methods
const trustedInternalMethods = ['wallet_getEthereumChains']
const isTrustedOrigin = (origin: string) => origin === 'frame-extension' || origin === 'frame-internal'
const isInternalMethod = (method: string) => trustedInternalMethods.includes(method)
const storeApi = {
getPermission: (address: Address, origin: string) => {
const permissions: Record<string, Permission> = store('main.permissions', address) || {}
return Object.values(permissions).find((p) => p.origin === origin)
},
getKnownExtension: (id: string) => store('main.knownExtensions', id) as boolean
}
export function parseOrigin(origin?: string) {
if (!origin) return 'Unknown'
return origin.replace(protocolRegex, '')
}
function invalidOrigin(origin: string) {
return origin !== origin.replace(/[^0-9a-z/:.[\]-]/gi, '')
}
async function getPermission(address: Address, origin: string, payload: RPCRequestPayload) {
const permission = storeApi.getPermission(address, origin)
return permission || requestPermission(address, payload)
}
async function requestExtensionPermission(extension: FrameExtension) {
if (extension.id in activeExtensionChecks) {
return activeExtensionChecks[extension.id]
}
const result = new Promise<boolean>((resolve) => {
const obs = store.observer(() => {
const isActive = extension.id in activeExtensionChecks
const isAllowed = store('main.knownExtensions', extension.id)
// wait for a response
if (isActive && typeof isAllowed !== 'undefined') {
delete activeExtensionChecks[extension.id]
obs.remove()
resolve(isAllowed)
}
}, 'origins:requestExtension')
})
activeExtensionChecks[extension.id] = result
store.notify('extensionConnect', extension)
return result
}
async function requestPermission(address: Address, fullPayload: RPCRequestPayload) {
const { _origin: originId, ...payload } = fullPayload
const permissionCheckId = `${address}:${originId}`
if (permissionCheckId in activePermissionChecks) {
return activePermissionChecks[permissionCheckId]
}
const result = new Promise<Permission | undefined>((resolve) => {
const request: AccessRequest = {
payload,
handlerId: originId,
type: 'access',
origin: originId,
account: address
}
accounts.addRequest(request, () => {
const { name: originName } = store('main.origins', originId)
const permission = storeApi.getPermission(address, originName)
delete activePermissionChecks[permissionCheckId]
resolve(permission)
})
})
activePermissionChecks[permissionCheckId] = result
return result
}
export function updateOrigin(
requestPayload: JSONRPCRequestPayload,
origin: string,
connectionMessage = false
): OriginUpdateResult {
const originId = uuidv5(origin, uuidv5.DNS)
const existingOrigin = store('main.origins', originId)
if (!connectionMessage) {
// the extension will attempt to send messages (eth_chainId and net_version) in order
// to connect. we don't want to store these origins as they'll come from every site
// the user visits in their browser
if (existingOrigin) {
store.addOriginRequest(originId)
} else {
store.initOrigin(originId, {
name: origin,
chain: {
id: 1,
type: 'ethereum'
}
})
}
}
const chainId = requestPayload.chainId || `0x${(existingOrigin?.chain.id || 1).toString(16)}`
const payload = {
...requestPayload,
_origin: originId
}
if (connectionMessage) {
payload.chainId = chainId
}
return {
payload,
chainId
}
}
export function parseFrameExtension(req: IncomingMessage): FrameExtension | undefined {
const origin = req.headers.origin || ''
const query = queryString.parse((req.url || '').replace('/', ''))
const hasExtensionIdentity = query.identity === 'frame-extension'
if (origin === 'chrome-extension://ldcoohedfbjoobcadoglnnmmfbdlmmhf') {
// Match production chrome
return { browser: 'chrome', id: 'ldcoohedfbjoobcadoglnnmmfbdlmmhf' }
} else if (origin.startsWith(`${extensionPrefixes.chrome}://`) && dev && hasExtensionIdentity) {
// Match Chrome in dev
const extensionId = origin.substring(extensionPrefixes.chrome.length + 3)
return { browser: 'chrome', id: extensionId }
} else if (origin.startsWith(`${extensionPrefixes.firefox}://`) && hasExtensionIdentity) {
// Match production Firefox
const extensionId = origin.substring(extensionPrefixes.firefox.length + 3)
return { browser: 'firefox', id: extensionId }
} else if (origin.startsWith(`${extensionPrefixes.safari}://`) && dev && hasExtensionIdentity) {
// Match Safari in dev only
return { browser: 'safari', id: 'frame-dev' }
}
}
export async function isKnownExtension(extension: FrameExtension) {
if (extension.browser === 'chrome' || extension.browser === 'safari') return true
const extensionPermission = storeApi.getKnownExtension(extension.id)
return extensionPermission ?? requestExtensionPermission(extension)
}
export async function isTrusted(payload: RPCRequestPayload) {
// Permission granted to unknown origins only persist until the Frame is closed, they are not permanent
const { name: originName } = store('main.origins', payload._origin) as { name: string }
const currentAccount = accounts.current()
if (isTrustedOrigin(originName) && isInternalMethod(payload.method)) {
return true
}
if (invalidOrigin(originName) || !currentAccount) {
return false
}
const permission = await getPermission(currentAccount.address, originName, payload)
return !!permission?.provider
}