@mefistofelix/cdp.js 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +855 -28
- package/cdp.js +335 -36
- package/cdp_ext/cdp_ext.js +263 -0
- package/cdp_ext/manifest.json +76 -0
- package/package.json +4 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const config = chrome.runtime.getManifest().cdp ?? {}
|
|
2
|
+
const sessions = {}
|
|
3
|
+
const attached = {}
|
|
4
|
+
const attaching = {}
|
|
5
|
+
let socket
|
|
6
|
+
let disconnecting
|
|
7
|
+
let discovering = false
|
|
8
|
+
let auto_attach = {}
|
|
9
|
+
|
|
10
|
+
function send(message) {
|
|
11
|
+
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function target_info(tab) {
|
|
15
|
+
return {
|
|
16
|
+
targetId: String(tab.id),
|
|
17
|
+
type: 'page',
|
|
18
|
+
title: tab.title ?? '',
|
|
19
|
+
url: tab.url ?? tab.pendingUrl ?? '',
|
|
20
|
+
attached: !!attached[tab.id],
|
|
21
|
+
incognito: tab.incognito,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function detached(source) {
|
|
26
|
+
if (!attached[source.tabId]) return
|
|
27
|
+
delete attached[source.tabId]
|
|
28
|
+
for (const [id, session] of Object.entries(sessions)) {
|
|
29
|
+
if (session.tabId === source.tabId) delete sessions[id]
|
|
30
|
+
}
|
|
31
|
+
send({
|
|
32
|
+
method: 'Target.detachedFromTarget',
|
|
33
|
+
params: {
|
|
34
|
+
sessionId: String(source.tabId),
|
|
35
|
+
targetId: String(source.tabId),
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function attach_tab(tabId) {
|
|
41
|
+
const source = { tabId }
|
|
42
|
+
await chrome.debugger.attach(source, '1.3')
|
|
43
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
44
|
+
await chrome.debugger.detach(source)
|
|
45
|
+
throw new Error('Reverse connection closed during attachment')
|
|
46
|
+
}
|
|
47
|
+
const sessionId = String(tabId)
|
|
48
|
+
sessions[sessionId] = source
|
|
49
|
+
attached[tabId] = true
|
|
50
|
+
await chrome.debugger.sendCommand(source, 'Target.setAutoAttach', auto_attach)
|
|
51
|
+
send({
|
|
52
|
+
method: 'Target.attachedToTarget',
|
|
53
|
+
params: {
|
|
54
|
+
sessionId,
|
|
55
|
+
targetInfo: target_info(await chrome.tabs.get(tabId)),
|
|
56
|
+
waitingForDebugger: false,
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
return { sessionId }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const methods = {
|
|
63
|
+
'Browser.getVersion'() {
|
|
64
|
+
return {
|
|
65
|
+
protocolVersion: '1.3',
|
|
66
|
+
product: navigator.userAgent.match(/Chrome\/[^ ]+/)?.[0] ?? 'Chrome',
|
|
67
|
+
userAgent: navigator.userAgent,
|
|
68
|
+
revision: '',
|
|
69
|
+
jsVersion: '',
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async 'Target.getTargets'() {
|
|
73
|
+
return { targetInfos: (await chrome.tabs.query({})).map(target_info) }
|
|
74
|
+
},
|
|
75
|
+
async 'Target.getTargetInfo'(params) {
|
|
76
|
+
return { targetInfo: target_info(await chrome.tabs.get(Number(params.targetId))) }
|
|
77
|
+
},
|
|
78
|
+
async 'Target.setDiscoverTargets'(params) {
|
|
79
|
+
discovering = params.discover
|
|
80
|
+
if (discovering) {
|
|
81
|
+
for (const tab of await chrome.tabs.query({})) {
|
|
82
|
+
send({
|
|
83
|
+
method: 'Target.targetCreated',
|
|
84
|
+
params: { targetInfo: target_info(tab) },
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
'Target.setAutoAttach'(params) {
|
|
90
|
+
if (params.waitForDebuggerOnStart) throw new Error('Pausing new browser tabs is unavailable through chrome.debugger')
|
|
91
|
+
auto_attach = params
|
|
92
|
+
},
|
|
93
|
+
async 'Target.createTarget'(params) {
|
|
94
|
+
if (Object.keys(params).some(key => !['url', 'background', 'incognito'].includes(key))) {
|
|
95
|
+
throw new Error('Reverse Target.createTarget supports only url, background and incognito')
|
|
96
|
+
}
|
|
97
|
+
const options = {
|
|
98
|
+
url: params.url ?? 'about:blank',
|
|
99
|
+
active: params.background !== true,
|
|
100
|
+
}
|
|
101
|
+
let tab
|
|
102
|
+
if (params.incognito) {
|
|
103
|
+
if (!await chrome.extension.isAllowedIncognitoAccess()) {
|
|
104
|
+
throw new Error('Enable Allow in incognito for cdp_ext in chrome://extensions')
|
|
105
|
+
}
|
|
106
|
+
const window = (await chrome.windows.getAll()).find(window => window.incognito)
|
|
107
|
+
if (window) tab = await chrome.tabs.create({
|
|
108
|
+
...options,
|
|
109
|
+
windowId: window.id,
|
|
110
|
+
})
|
|
111
|
+
else tab = (await chrome.windows.create({
|
|
112
|
+
incognito: true,
|
|
113
|
+
url: options.url,
|
|
114
|
+
focused: options.active,
|
|
115
|
+
})).tabs[0]
|
|
116
|
+
} else tab = await chrome.tabs.create(options)
|
|
117
|
+
return { targetId: String(tab.id) }
|
|
118
|
+
},
|
|
119
|
+
async 'Target.attachToTarget'(params) {
|
|
120
|
+
const tabId = Number(params.targetId)
|
|
121
|
+
if (attaching[tabId]) return attaching[tabId]
|
|
122
|
+
if (attached[tabId]) return { sessionId: String(tabId) }
|
|
123
|
+
return attaching[tabId] = attach_tab(tabId).finally(() => delete attaching[tabId])
|
|
124
|
+
},
|
|
125
|
+
async 'Target.detachFromTarget'(params) {
|
|
126
|
+
const source = sessions[params.sessionId]
|
|
127
|
+
if (!source) throw new Error('Unknown debugger session')
|
|
128
|
+
if (source.sessionId) {
|
|
129
|
+
await chrome.debugger.sendCommand({ tabId: source.tabId }, 'Target.detachFromTarget', { sessionId: source.sessionId })
|
|
130
|
+
} else {
|
|
131
|
+
await chrome.debugger.detach(source)
|
|
132
|
+
detached(source)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async 'Target.closeTarget'(params) {
|
|
136
|
+
await chrome.tabs.remove(Number(params.targetId))
|
|
137
|
+
return { success: true }
|
|
138
|
+
},
|
|
139
|
+
async 'Target.activateTarget'(params) {
|
|
140
|
+
const tab = await chrome.tabs.update(Number(params.targetId), { active: true })
|
|
141
|
+
await chrome.windows.update(tab.windowId, { focused: true })
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
146
|
+
if (!attached[source.tabId]) return
|
|
147
|
+
if (method === 'Target.attachedToTarget' || method === 'Target.detachedFromTarget') {
|
|
148
|
+
if (method === 'Target.attachedToTarget') sessions[params.sessionId] = {
|
|
149
|
+
tabId: source.tabId,
|
|
150
|
+
sessionId: params.sessionId,
|
|
151
|
+
}
|
|
152
|
+
else delete sessions[params.sessionId]
|
|
153
|
+
}
|
|
154
|
+
send({
|
|
155
|
+
method,
|
|
156
|
+
params,
|
|
157
|
+
sessionId: source.sessionId ?? String(source.tabId),
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
chrome.debugger.onDetach.addListener(detached)
|
|
161
|
+
chrome.tabs.onCreated.addListener(tab => {
|
|
162
|
+
if (discovering) send({
|
|
163
|
+
method: 'Target.targetCreated',
|
|
164
|
+
params: { targetInfo: target_info(tab) },
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
chrome.tabs.onUpdated.addListener((id, changes, tab) => {
|
|
168
|
+
if (discovering) send({
|
|
169
|
+
method: 'Target.targetInfoChanged',
|
|
170
|
+
params: { targetInfo: target_info(tab) },
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
chrome.tabs.onRemoved.addListener(id => {
|
|
174
|
+
if (discovering) send({
|
|
175
|
+
method: 'Target.targetDestroyed',
|
|
176
|
+
params: { targetId: String(id) },
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
for (const [name, options] of Object.entries({
|
|
181
|
+
onBeforeRequest: [],
|
|
182
|
+
onBeforeSendHeaders: ['requestHeaders'],
|
|
183
|
+
onSendHeaders: ['requestHeaders'],
|
|
184
|
+
onHeadersReceived: ['responseHeaders'],
|
|
185
|
+
onAuthRequired: ['responseHeaders'],
|
|
186
|
+
onResponseStarted: ['responseHeaders'],
|
|
187
|
+
onBeforeRedirect: ['responseHeaders'],
|
|
188
|
+
onCompleted: ['responseHeaders'],
|
|
189
|
+
onErrorOccurred: [],
|
|
190
|
+
})) {
|
|
191
|
+
chrome.webRequest[name].addListener(params => send({
|
|
192
|
+
method: 'chrome.webRequest.' + name,
|
|
193
|
+
params,
|
|
194
|
+
sessionId: params.tabId >= 0 ? String(params.tabId) : undefined,
|
|
195
|
+
}), { urls: ['<all_urls>'] }, options)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function connect() {
|
|
199
|
+
if (disconnecting) return
|
|
200
|
+
if (socket?.readyState === WebSocket.CONNECTING) return
|
|
201
|
+
if (socket?.readyState === WebSocket.OPEN) return socket.send('')
|
|
202
|
+
const connection = socket = new WebSocket(config.websocket_url ?? 'ws://127.0.0.1:9223')
|
|
203
|
+
connection.onopen = () => connection.send(JSON.stringify({ browser: chrome.runtime.id }))
|
|
204
|
+
connection.onmessage = async event => {
|
|
205
|
+
if (!event.data) return
|
|
206
|
+
const request = JSON.parse(event.data)
|
|
207
|
+
const { method, params = {}, sessionId, id } = request
|
|
208
|
+
let response
|
|
209
|
+
try {
|
|
210
|
+
let result
|
|
211
|
+
if (method.startsWith('chrome.')) {
|
|
212
|
+
const names = method.split('.').slice(1)
|
|
213
|
+
const name = names.pop()
|
|
214
|
+
const object = names.reduce((object, name) => object[name], chrome)
|
|
215
|
+
const args = params.args ?? []
|
|
216
|
+
if (method === 'chrome.scripting.executeScript' && typeof args[0]?.func === 'string') {
|
|
217
|
+
const injection = args[0]
|
|
218
|
+
injection.args = [injection.func, ...(injection.args ?? [])]
|
|
219
|
+
injection.func = (source, ...args) => (0, eval)('(' + source + ')')(...args)
|
|
220
|
+
}
|
|
221
|
+
result = params.callback
|
|
222
|
+
? await new Promise((resolve, reject) => object[name](...args, value => {
|
|
223
|
+
const error = chrome.runtime.lastError
|
|
224
|
+
if (error) reject(new Error(error.message))
|
|
225
|
+
else resolve(value)
|
|
226
|
+
}))
|
|
227
|
+
: await object[name](...args)
|
|
228
|
+
} else {
|
|
229
|
+
if (sessionId == null && !methods[method]) throw new Error('Unsupported reverse browser command: ' + method)
|
|
230
|
+
result = sessionId != null
|
|
231
|
+
? await chrome.debugger.sendCommand(sessions[sessionId], method, params)
|
|
232
|
+
: await methods[method](params)
|
|
233
|
+
}
|
|
234
|
+
response = {
|
|
235
|
+
id,
|
|
236
|
+
result: result === undefined ? {} : result,
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
response = {
|
|
240
|
+
id,
|
|
241
|
+
error: {
|
|
242
|
+
code: -32000,
|
|
243
|
+
message: error.message,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (id != null && connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify(response))
|
|
248
|
+
}
|
|
249
|
+
connection.onclose = () => {
|
|
250
|
+
discovering = false
|
|
251
|
+
disconnecting = Promise.all(Object.keys(attached).map(id => {
|
|
252
|
+
const source = { tabId: Number(id) }
|
|
253
|
+
detached(source)
|
|
254
|
+
return chrome.debugger.detach(source).catch(() => {})
|
|
255
|
+
})).finally(() => disconnecting = null)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
chrome.runtime.onStartup.addListener(connect)
|
|
260
|
+
chrome.alarms.onAlarm.addListener(connect)
|
|
261
|
+
chrome.alarms.create('cdp-reconnect', { periodInMinutes: 0.5 })
|
|
262
|
+
setInterval(connect, config.reconnect_ms ?? 5000)
|
|
263
|
+
connect()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "CDP reverse connection",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"minimum_chrome_version": "125",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"accessibilityFeatures.read",
|
|
8
|
+
"accessibilityFeatures.modify",
|
|
9
|
+
"alarms",
|
|
10
|
+
"bookmarks",
|
|
11
|
+
"browsingData",
|
|
12
|
+
"clipboardRead",
|
|
13
|
+
"clipboardWrite",
|
|
14
|
+
"contentSettings",
|
|
15
|
+
"contextMenus",
|
|
16
|
+
"cookies",
|
|
17
|
+
"debugger",
|
|
18
|
+
"declarativeContent",
|
|
19
|
+
"declarativeNetRequest",
|
|
20
|
+
"declarativeNetRequestFeedback",
|
|
21
|
+
"desktopCapture",
|
|
22
|
+
"downloads",
|
|
23
|
+
"downloads.open",
|
|
24
|
+
"downloads.ui",
|
|
25
|
+
"favicon",
|
|
26
|
+
"fontSettings",
|
|
27
|
+
"gcm",
|
|
28
|
+
"geolocation",
|
|
29
|
+
"history",
|
|
30
|
+
"identity",
|
|
31
|
+
"identity.email",
|
|
32
|
+
"idle",
|
|
33
|
+
"management",
|
|
34
|
+
"nativeMessaging",
|
|
35
|
+
"notifications",
|
|
36
|
+
"offscreen",
|
|
37
|
+
"pageCapture",
|
|
38
|
+
"power",
|
|
39
|
+
"privacy",
|
|
40
|
+
"proxy",
|
|
41
|
+
"readingList",
|
|
42
|
+
"scripting",
|
|
43
|
+
"search",
|
|
44
|
+
"sessions",
|
|
45
|
+
"sidePanel",
|
|
46
|
+
"storage",
|
|
47
|
+
"system.cpu",
|
|
48
|
+
"system.display",
|
|
49
|
+
"system.memory",
|
|
50
|
+
"system.storage",
|
|
51
|
+
"tabCapture",
|
|
52
|
+
"tabGroups",
|
|
53
|
+
"tabs",
|
|
54
|
+
"topSites",
|
|
55
|
+
"tts",
|
|
56
|
+
"ttsEngine",
|
|
57
|
+
"unlimitedStorage",
|
|
58
|
+
"userScripts",
|
|
59
|
+
"webAuthenticationProxy",
|
|
60
|
+
"webNavigation",
|
|
61
|
+
"webRequest",
|
|
62
|
+
"webRequestAuthProvider"
|
|
63
|
+
],
|
|
64
|
+
"host_permissions": [
|
|
65
|
+
"<all_urls>"
|
|
66
|
+
],
|
|
67
|
+
"action": {},
|
|
68
|
+
"incognito": "spanning",
|
|
69
|
+
"background": {
|
|
70
|
+
"service_worker": "cdp_ext.js"
|
|
71
|
+
},
|
|
72
|
+
"cdp": {
|
|
73
|
+
"websocket_url": "ws://127.0.0.1:9223",
|
|
74
|
+
"reconnect_ms": 5000
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mefistofelix/cdp.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Low-level Chromium DevTools Protocol client for Node.js, with no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./cdp.js",
|
|
7
7
|
"files": [
|
|
8
|
-
"cdp.js"
|
|
8
|
+
"cdp.js",
|
|
9
|
+
"cdp_ext/manifest.json",
|
|
10
|
+
"cdp_ext/cdp_ext.js"
|
|
9
11
|
],
|
|
10
12
|
"engines": {
|
|
11
13
|
"node": ">=22.19.0"
|