├── __tests__ ├── wwwA │ ├── index.js │ └── index.html ├── wwwB │ ├── index.js │ └── index.html ├── matcher.spec.js └── ProxyServer.spec.js ├── .husky └── pre-commit ├── docs ├── imgs │ ├── ca.png │ ├── boxx.png │ ├── foxy.png │ ├── devtools-flow.png │ ├── devtools-test.png │ └── san-devtools.png └── rootCA.md ├── src ├── assets │ └── ssl.png ├── icons │ ├── bottom.svg │ ├── right.svg │ └── unknown.svg ├── utils │ ├── createFrontendUrl.js │ ├── getSessionId.js │ ├── getCurrentScript.js │ ├── getUaInfo.js │ ├── getFavicon.js │ └── createBridge.js ├── backend.js ├── lib │ ├── EventEmitter.js │ ├── Bridge.js │ └── WebSocket.js ├── runtime.js └── components │ └── home.less ├── frontend ├── $_bridge │ ├── $_bridge.js │ ├── $_bridge-legacy.js │ ├── module.json │ └── Bridge.js ├── network_app.js ├── network-main │ ├── network-main.js │ ├── module.json │ ├── network-main-legacy.js │ └── NetworkMain.js ├── network-standalone │ ├── eventSourceMessagesView.css │ ├── requestHTMLView.css │ ├── signedExchangeInfoView.css │ ├── binaryResourceView.css │ ├── networkWaterfallColumn.css │ ├── requestInitiatorView.css │ ├── requestHeadersView.css │ ├── requestCookiesView.css │ ├── networkManageCustomHeadersView.css │ ├── webSocketFrameView.css │ ├── blockedURLsPane.css │ ├── signedExchangeInfoTree.css │ ├── networkConfigView.css │ ├── requestHeadersTree.css │ ├── NetworkFrameGrouper.js │ ├── RequestHTMLView.js │ ├── network-standalone.js │ ├── networkTimingTable.css │ ├── networkPanel.css │ ├── RequestPreviewView.js │ ├── EventSourceMessagesView.js │ ├── RequestResponseView.js │ ├── HARWriter.js │ ├── network-standalone-legacy.js │ ├── RequestInitiatorView.js │ └── NetworkManageCustomHeadersView.js ├── main-for-network-standalone │ ├── main-for-network-standalone.js │ ├── SimpleApp.js │ └── main-for-network-standalone-legacy.js ├── network.html ├── inspector.html ├── devtools_app.json ├── shell.json └── network_app.json ├── .browserslistrc ├── server ├── utils │ ├── createDebug.js │ ├── getTime.js │ ├── sendFile.js │ ├── normalizeWebSocketPayload.js │ ├── copyHeaders.js │ ├── index.js │ ├── internalIPSync.js │ ├── decompress.js │ ├── matcher.js │ ├── findCacheDir.js │ ├── getResourceType.js │ ├── test.js │ ├── filterRule.js │ ├── htmlUtils.js │ ├── htmlTags.js │ └── logger.js ├── constants.js ├── middlewares │ ├── json_protocol.js │ ├── alive.js │ ├── dist.js │ ├── backend.js │ └── frontend.js ├── proxy │ ├── MITMProxy.js │ ├── InterceptorFactory.js │ ├── plugins │ │ ├── certfile.js │ │ └── injectBackend.js │ ├── CDPClient.js │ └── Recorder.js ├── websocket │ ├── Manager.js │ ├── Channel.js │ └── ChannelMultiplex.js └── WebSocketServer.js ├── .eslintignore ├── .npmignore ├── postcss.config.js ├── .eslintrc.js ├── .prettierrc ├── init.sh ├── interceptors ├── userAgent.js ├── forward.js ├── localMock.js └── pathRewrite.js ├── .editorconfig ├── index.js ├── pages ├── home.html └── demo.html ├── .babelrc ├── .gitignore ├── readme.md └── bin └── normalizePlugins.js /__tests__/wwwA/index.js: -------------------------------------------------------------------------------- 1 | console.log('wwwa'); 2 | -------------------------------------------------------------------------------- /__tests__/wwwB/index.js: -------------------------------------------------------------------------------- 1 | console.log('wwwb'); 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | lint-staged 5 | -------------------------------------------------------------------------------- /docs/imgs/ca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/ca.png -------------------------------------------------------------------------------- /docs/imgs/boxx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/boxx.png -------------------------------------------------------------------------------- /docs/imgs/foxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/foxy.png -------------------------------------------------------------------------------- /src/assets/ssl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/src/assets/ssl.png -------------------------------------------------------------------------------- /frontend/$_bridge/$_bridge.js: -------------------------------------------------------------------------------- 1 | import * as Bridge from './Bridge.js'; 2 | 3 | export {Bridge}; 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not ie < 11 3 | last 2 versions 4 | > 1% 5 | iOS 8 6 | last 3 iOS versions 7 | -------------------------------------------------------------------------------- /docs/imgs/devtools-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/devtools-flow.png -------------------------------------------------------------------------------- /docs/imgs/devtools-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/devtools-test.png -------------------------------------------------------------------------------- /docs/imgs/san-devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanwu/devtools-pro/HEAD/docs/imgs/san-devtools.png -------------------------------------------------------------------------------- /server/utils/createDebug.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug'); 2 | 3 | module.exports = scope => debug(`devtools-pro:${scope}`); 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | __mocks__ 4 | test 5 | __test__ 6 | example 7 | examples 8 | output 9 | dist 10 | -------------------------------------------------------------------------------- /src/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | __tests__ 3 | devtools.config.js 4 | src 5 | yarn-error.log 6 | webpack.config.js 7 | 8 | .http-mitm-proxy 9 | test 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file postcss config 3 | */ 4 | 5 | const autoprefixer = require('autoprefixer'); 6 | 7 | module.exports = { 8 | plugins: [autoprefixer()] 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '@ecomfe/eslint-config' 5 | ], 6 | rules: { 7 | 'comma-dangle': 'off' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /server/utils/getTime.js: -------------------------------------------------------------------------------- 1 | const initTime = process.hrtime(); 2 | module.exports = function getTime() { 3 | let diff = process.hrtime(initTime); 4 | 5 | return diff[0] + diff[1] / 1e9; 6 | }; 7 | -------------------------------------------------------------------------------- /server/constants.js: -------------------------------------------------------------------------------- 1 | exports.BACKEND_JS_FILE = '/backend.js'; 2 | 3 | exports.BACKENDJS_PATH = '/backend.js'; 4 | 5 | exports.FRONTEND_PATH = 'devtools/inspector.html'; 6 | exports.BLOCKING_IGNORE_STRING = '$blocking_ignore$'; 7 | -------------------------------------------------------------------------------- /server/middlewares/json_protocol.js: -------------------------------------------------------------------------------- 1 | module.exports = router => { 2 | const protocolJson = require('./data/protocol.json'); 3 | router.get('/json/protocol', ctx => { 4 | ctx.body = protocolJson; 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/network_app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | Root.Runtime.startApplication('network_app'); 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 120, 5 | "tabWidth": 4, 6 | "useTabs": false, 7 | "bracketSpacing": false, 8 | "trailingComma": "none", 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /frontend/$_bridge/$_bridge-legacy.js: -------------------------------------------------------------------------------- 1 | import * as BridgeModule from './$_bridge.js'; 2 | 3 | self.Bridge = self.Bridge || {}; 4 | // console.log(BridgeModule); 5 | Bridge.Main = BridgeModule.Bridge.Main; 6 | Bridge.Model = BridgeModule.Bridge.BridgeModel; 7 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | local_front_end=chrome-devtools-frontend 2 | if test -f "$local_front_end"; then 3 | echo "$local_front_end exists." 4 | rm -rf $local_front_end 5 | fi 6 | 7 | mkdir $local_front_end 8 | cp -R node_modules/chrome-devtools-frontend/front_end/* $local_front_end 9 | -------------------------------------------------------------------------------- /interceptors/userAgent.js: -------------------------------------------------------------------------------- 1 | module.exports = (userAgent, callback) => { 2 | return interceptor => { 3 | return interceptor.request.add(callback, { 4 | headers: { 5 | 'User-Agent': userAgent 6 | } 7 | }); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/network-main/network-main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import * as NetworkMain from './NetworkMain.js'; 6 | 7 | export {NetworkMain}; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [{node.js.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /frontend/network-standalone/eventSourceMessagesView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .event-source-messages-view .data-grid { 8 | flex: auto; 9 | border: none; 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/wwwA/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WWWA 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /__tests__/wwwB/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WWWB 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/$_bridge/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | { 4 | "type": "early-initialization", 5 | "className": "Bridge.Main" 6 | } 7 | ], 8 | "dependencies": ["sdk", "protocol"], 9 | "scripts": [], 10 | "modules": ["Bridge.js", "$_bridge.js", "$_bridge-legacy.js"], 11 | "resources": [] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/network-main/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | { 4 | "type": "early-initialization", 5 | "className": "NetworkMain.NetworkMain" 6 | } 7 | ], 8 | "dependencies": ["components"], 9 | "scripts": [], 10 | "modules": ["network-main.js", "network-main-legacy.js", "NetworkMain.js"] 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/createFrontendUrl.js: -------------------------------------------------------------------------------- 1 | export const FRONTEND_PATH = 'devtools/inspector.html'; 2 | 3 | export default (protocol, hostname, port, id, frontendPath = FRONTEND_PATH) => { 4 | // 注意,这里是&,不是?链接!! 5 | return `${protocol}//${hostname}:${port}/${frontendPath}?${ 6 | protocol === 'https:' ? 'wss' : 'ws' 7 | }=${hostname}:${port}/frontend/${id}`; 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/network-standalone/requestHTMLView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | -theme-preserve, .html-preview-frame { 7 | box-shadow: var(--drop-shadow); 8 | background: white; 9 | flex-grow: 1; 10 | margin: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /server/utils/sendFile.js: -------------------------------------------------------------------------------- 1 | const send = require('koa-send'); 2 | 3 | module.exports = async function sendFile(ctx, pathname, root) { 4 | try { 5 | return await send(ctx, pathname || ctx.path, { 6 | maxage: 60 * 60 * 2 * 1e3, 7 | root 8 | }); 9 | } catch (err) { 10 | if (err.status !== 404) { 11 | throw err; 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/network-standalone/signedExchangeInfoView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .signed-exchange-info-view { 8 | user-select: text; 9 | overflow: auto; 10 | } 11 | 12 | .signed-exchange-info-tree { 13 | flex-grow: 1; 14 | overflow-y: auto; 15 | margin: 0; 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Server = require('./server/Server'); 2 | const WebSocketServer = require('./server/WebSocketServer'); 3 | const ProxyServer = require('./server/ProxyServer'); 4 | // const localMock = require('./server/interceptors/localMock'); 5 | // const pathWrite = require('./server/interceptors/pathWrite'); 6 | // const userAgent = require('./server/interceptors/userAgent'); 7 | module.exports = { 8 | Server, 9 | WebSocketServer, 10 | ProxyServer 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/main-for-network-standalone/main-for-network-standalone.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import * as ExecutionContextSelector from './ExecutionContextSelector.js'; 6 | import * as MainImpl from './MainImpl.js'; 7 | import * as SimpleApp from './SimpleApp.js'; 8 | 9 | export {ExecutionContextSelector, MainImpl, SimpleApp}; 10 | -------------------------------------------------------------------------------- /frontend/network-main/network-main-legacy.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import * as NetworkMainModule from './network-main.js'; 6 | 7 | self.NetworkMain = self.NetworkMain || {}; 8 | NetworkMain = NetworkMain || {}; 9 | 10 | /** 11 | * @constructor 12 | */ 13 | NetworkMain.NetworkMain = NetworkMainModule.NetworkMain.NetworkMainImpl; 14 | -------------------------------------------------------------------------------- /frontend/network-standalone/binaryResourceView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .panel.network .toolbar.binary-view-toolbar { 8 | border-top: 1px solid #ccc; 9 | border-bottom: 0px; 10 | padding-left: 5px; 11 | } 12 | 13 | .binary-view-copied-text { 14 | opacity: 1; 15 | } 16 | 17 | .binary-view-copied-text.fadeout { 18 | opacity: 0; 19 | transition: opacity 1s; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/network.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/inspector.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/network-standalone/networkWaterfallColumn.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2016 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. 4 | */ 5 | .network-waterfall-v-scroll { 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | overflow-x: hidden; 11 | margin-top: 31px; 12 | z-index: 200; 13 | } 14 | 15 | .network-waterfall-v-scroll.small { 16 | margin-top: 27px; 17 | } 18 | 19 | .network-waterfall-v-scroll-content { 20 | width: 15px; 21 | pointer-events: none; 22 | } 23 | -------------------------------------------------------------------------------- /server/utils/normalizeWebSocketPayload.js: -------------------------------------------------------------------------------- 1 | function normalizeWebSocketPayload(data) { 2 | // 过滤私有的数据 3 | if (Array.isArray(data)) { 4 | return data.map(d => normalizeWebSocketPayload(d)); 5 | } 6 | const r = {}; 7 | Object.keys(data).map(key => { 8 | if (/^_/.test(key)) { 9 | return; 10 | } 11 | if (typeof data[key] === 'object' || Array.isArray(data[key])) { 12 | return (r[key] = normalizeWebSocketPayload(data[key])); 13 | } 14 | r[key] = data[key]; 15 | }); 16 | return r; 17 | } 18 | 19 | module.exports = normalizeWebSocketPayload; 20 | -------------------------------------------------------------------------------- /pages/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inspect - Remote Developer Tools 6 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/utils/getSessionId.js: -------------------------------------------------------------------------------- 1 | import {nanoid} from 'nanoid'; 2 | const key = '$devtools_sid_'; 3 | const sessionStorage = window.sessionStorage; 4 | export default (url, useCache = true) => { 5 | if (!url) { 6 | url = location.pathname + location.search; 7 | } 8 | let sKey = `${key}${url}`; 9 | let sessionId; 10 | if (useCache) { 11 | sessionId = sessionStorage.getItem(sKey); 12 | if (sessionId) { 13 | return sessionId; 14 | } 15 | } 16 | 17 | sessionId = nanoid(); 18 | if (useCache) { 19 | sessionStorage.setItem(sKey, sessionId); 20 | } 21 | return sessionId; 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/network-standalone/requestInitiatorView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .request-initiator-view { 8 | display: block; 9 | margin: 6px; 10 | } 11 | 12 | .request-initiator-view-section-title { 13 | font-weight: bold; 14 | padding: 4px; 15 | } 16 | 17 | .request-initiator-view-section-title[data-keyboard-focus="true"]:focus { 18 | background-color: rgba(0, 0, 0, 0.08); 19 | } 20 | 21 | .request-initiator-view-section-content { 22 | margin-left: 6px; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/getCurrentScript.js: -------------------------------------------------------------------------------- 1 | export default function getCurrentScriptSource() { 2 | // `document.currentScript` is the most accurate way to find the current script, 3 | // but is not supported in all browsers. 4 | if (document.currentScript) { 5 | return document.currentScript.src; 6 | } 7 | // Fall back to getting all scripts in the document. 8 | const scriptElements = document.scripts || []; 9 | const currentScript = scriptElements[scriptElements.length - 1]; 10 | 11 | if (currentScript) { 12 | return currentScript.src; 13 | } 14 | // Fail as there was no script to use. 15 | throw new Error('Failed to get current script source.'); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/network-standalone/requestHeadersView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .request-headers-view { 8 | user-select: text; 9 | overflow: auto; 10 | } 11 | 12 | .resource-status-image { 13 | margin-top: -2px; 14 | margin-right: 3px; 15 | } 16 | 17 | .request-headers-tree { 18 | flex-grow: 1; 19 | overflow-y: auto; 20 | margin: 0; 21 | } 22 | 23 | .header-decode-error { 24 | color: red; 25 | } 26 | 27 | .-theme-with-dark-background .header-decode-error { 28 | color: hsl(0, 100%, 65%); 29 | } 30 | -------------------------------------------------------------------------------- /server/middlewares/alive.js: -------------------------------------------------------------------------------- 1 | module.exports = (router, logger) => { 2 | router.get('/_alive_/(.+)', async (ctx, next) => { 3 | const targetId = ctx.params[0]; 4 | if (targetId) { 5 | const backendChannel = ctx 6 | .getWebSocketServer() 7 | .getChannelManager() 8 | .getBackendById(targetId); 9 | const isAlive = backendChannel && backendChannel.alive; 10 | logger.debug(targetId, !!isAlive); 11 | 12 | if (isAlive) { 13 | ctx.body = '1'; 14 | } else { 15 | ctx.body = '0'; 16 | } 17 | } else { 18 | ctx.body = '-1'; 19 | } 20 | return; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": false, 7 | "corejs": 3, 8 | "useBuiltIns": "usage" 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "san-hot-loader/lib/babel-plugin", 14 | [ 15 | "import", 16 | { 17 | "libraryName": "santd", 18 | "libraryDirectory": "es", 19 | "style": true 20 | } 21 | ], 22 | "@babel/plugin-transform-object-assign", 23 | "@babel/plugin-syntax-import-meta", 24 | "@babel/plugin-proposal-class-properties", 25 | "@babel/plugin-transform-new-target" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/network-standalone/requestCookiesView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .request-cookies-view { 8 | overflow: auto; 9 | padding: 12px; 10 | height: 100%; 11 | } 12 | 13 | .request-cookies-view .request-cookies-title { 14 | font-size: 12px; 15 | font-weight: bold; 16 | margin-right: 30px; 17 | color: rgb(97, 97, 97); 18 | } 19 | 20 | .request-cookies-view .cookie-line { 21 | margin-top: 6px; 22 | display: inline-block; 23 | } 24 | 25 | .request-cookies-view .cookies-panel-item { 26 | margin-top: 6px; 27 | margin-bottom: 16px; 28 | flex: none; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/network-standalone/networkManageCustomHeadersView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | .custom-headers-list { 7 | height: 272px; 8 | width: 250px; 9 | } 10 | 11 | .custom-headers-wrapper{ 12 | margin: 10px; 13 | } 14 | 15 | .header { 16 | padding: 0 0 6px; 17 | font-size: 18px; 18 | font-weight: normal; 19 | flex: none; 20 | } 21 | 22 | .custom-headers-header { 23 | padding:2px; 24 | } 25 | 26 | .custom-headers-list-item { 27 | padding-left: 5px; 28 | } 29 | 30 | .editor-container { 31 | padding: 5px 0px 0px 5px; 32 | } 33 | 34 | .add-button { 35 | width: 150px; 36 | margin: auto; 37 | margin-top: 5px; 38 | } 39 | -------------------------------------------------------------------------------- /server/middlewares/dist.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const sendFile = require('../utils/sendFile'); 3 | 4 | const distPath = path.join(__dirname, '../../dist'); 5 | module.exports = (router, logger, serverInstance) => { 6 | async function staticSend(ctx) { 7 | // 前面中间件有返回则不发送 8 | if (ctx.body != null || ctx.status !== 404) { 9 | logger.info(ctx.status); 10 | return; 11 | } 12 | logger.debug(ctx.path); 13 | return await sendFile(ctx, ctx.path, serverInstance.getDistPath()); 14 | } 15 | 16 | router.get('/', async (ctx, next) => { 17 | ctx.path = '/index.html'; 18 | await staticSend(ctx); 19 | }); 20 | router.get('/(.+)', async (ctx, next) => { 21 | await next(); 22 | await staticSend(ctx); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/network-main/NetworkMain.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /** 6 | * @implements {Common.Runnable} 7 | */ 8 | export class NetworkMainImpl extends Common.Object { 9 | /** 10 | * @override 11 | */ 12 | async run() { 13 | // Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConnectToNodeJSDirectly); 14 | await SDK.initMainConnection(() => { 15 | const target = SDK.targetManager.createTarget('main', ls`Main`, SDK.Target.Type.Network, null); 16 | target.runtimeAgent().runIfWaitingForDebugger(); 17 | window.t = target; 18 | }, Components.TargetDetachedDialog.webSocketConnectionLost); 19 | UI.viewManager.showView('network'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/devtools_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | {"name": "$_bridge", "type": "autostart"}, 4 | {"name": "emulation", "type": "autostart"}, 5 | {"name": "inspector_main", "type": "autostart"}, 6 | {"name": "mobile_throttling", "type": "autostart"}, 7 | {"name": "accessibility"}, 8 | {"name": "animation"}, 9 | {"name": "css_overview"}, 10 | {"name": "cookie_table"}, 11 | {"name": "dagre_layout", "type": "remote"}, 12 | {"name": "devices"}, 13 | {"name": "elements"}, 14 | {"name": "emulated_devices", "type": "remote"}, 15 | {"name": "har_importer"}, 16 | {"name": "layers"}, 17 | {"name": "layer_viewer"}, 18 | {"name": "network"}, 19 | {"name": "performance_monitor"}, 20 | {"name": "resources"} 21 | ], 22 | "extends": "shell", 23 | "has_html": true 24 | } 25 | -------------------------------------------------------------------------------- /frontend/main-for-network-standalone/SimpleApp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /** 6 | * @implements {Common.App} 7 | * @unrestricted 8 | */ 9 | export default class SimpleApp { 10 | /** 11 | * @override 12 | * @param {!Document} document 13 | */ 14 | presentUI(document) { 15 | const rootView = new UI.RootView(); 16 | UI.inspectorView.show(rootView.element); 17 | rootView.attachToDocument(document); 18 | rootView.focus(); 19 | } 20 | } 21 | 22 | /** 23 | * @implements {Common.AppProvider} 24 | * @unrestricted 25 | */ 26 | export class SimpleAppProvider { 27 | /** 28 | * @override 29 | * @return {!Common.App} 30 | */ 31 | createApp() { 32 | return new SimpleApp(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/icons/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/utils/copyHeaders.js: -------------------------------------------------------------------------------- 1 | module.exports = function copyHeaders(originalHeaders) { 2 | const headers = {}; 3 | 4 | let keys = Object.keys(originalHeaders); 5 | // ignore chunked, gzip... 6 | keys = keys.filter(key => !['content-encoding', 'transfer-encoding'].includes(key.toLowerCase())); 7 | 8 | keys.forEach(key => { 9 | let value = originalHeaders[key]; 10 | 11 | if (key === 'set-cookie') { 12 | // remove cookie domain 13 | value = Array.isArray(value) ? value : [value]; 14 | value = value.map(x => x.replace(/Domain=[^;]+?/i, '')); 15 | } else { 16 | let canonizedKey = key.trim(); 17 | if (/^public\-key\-pins/i.test(canonizedKey)) { 18 | // HPKP header => filter 19 | return; 20 | } 21 | } 22 | 23 | headers[key] = value; 24 | }); 25 | headers['x-intercepted-by'] = 'devtools-pro-foxy'; 26 | return headers; 27 | }; 28 | -------------------------------------------------------------------------------- /server/proxy/MITMProxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 兜住错误,重写部分方法 3 | */ 4 | const HTTPMITMProxy = require('@wanwu/mitm-proxy'); 5 | 6 | module.exports = class MITMProxy extends HTTPMITMProxy.Proxy { 7 | _onError(kind, ctx, err) { 8 | // kind == 'CERTIFICATE_MISSING_ERROR' 9 | this.onErrorHandlers.forEach(function(handler) { 10 | return handler(ctx, err, kind); 11 | }); 12 | if (ctx) { 13 | ctx.onErrorHandlers.forEach(function(handler) { 14 | return handler(ctx, err, kind); 15 | }); 16 | // 最后兜一下 17 | const res = ctx.proxyToClientResponse; 18 | if (res && !res.headersSent) { 19 | res.writeHead(504, 'Proxy Error'); 20 | } 21 | if (res && !res.finished) { 22 | res.end('' + kind + ': ' + err, 'utf8'); 23 | } 24 | } 25 | } 26 | }; 27 | module.exports.wildcard = HTTPMITMProxy.wildcard; 28 | module.exports.gunzip = HTTPMITMProxy.gunzip; 29 | -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | const color = require('colorette'); 2 | const {BACKENDJS_PATH, FRONTEND_PATH} = require('../constants'); 3 | 4 | exports.truncate = function truncate(txt, width = 10) { 5 | if (!txt) { 6 | return ''; 7 | } 8 | const ellipsis = '...'; 9 | const len = txt.length; 10 | if (width > len) { 11 | return txt; 12 | } 13 | let end = width - ellipsis.length; 14 | if (end < 1) { 15 | return ellipsis; 16 | } 17 | return txt.slice(0, end) + ellipsis; 18 | }; 19 | function getColorfulName(role) { 20 | role = role.toUpperCase(); 21 | switch (role) { 22 | case 'FRONTEND': 23 | return color.blue(role); 24 | case 'BACKEND': 25 | // 为了对齐 26 | return color.yellow('BACK_END'); 27 | case 'HOME': 28 | return color.magenta(role); 29 | case 'GET': 30 | return color.green(role); 31 | } 32 | return color.cyan(role); 33 | } 34 | exports.getColorfulName = getColorfulName; 35 | -------------------------------------------------------------------------------- /docs/rootCA.md: -------------------------------------------------------------------------------- 1 | # SSL 证书安装 2 | 3 | 由于浏览器安全限制,在 https 协议页面添加 backend.js 需要使用 https,这时候需要启动 DevTools-pro 的 https server,如果启动 devtools-pro 的 https server 或者使用[Foxy 代理服务](./foxy.md),那么你需要使用`--https`或者 config 文件中的`https`配置,这时候 DevTools-pro 会默认生成一个自认证的 CA 证书,可以通过访问`devtools.pro/ssl`下载或者 home 页面扫描对应二维码进行下载(前提是启动了 foxy 功能、并且设置对应机器的代理服务到 foxy 端口号) 4 | 5 | CA 证书下载完毕后需要添加到信任列表中,在列表中找到**DevtoolsProFoxy**的证书,然后对应的操作可以参考 Fiddler/Charles 这类证书的安装方式。 6 | 7 | ![](./imgs/ca.png) 8 | 9 | ### 证书过期 10 | 11 | 证书默认生成了两年的有效期,如果过期则执行:`devtools-pro clean-ca`命令删除对应的证书,然后重新启动 devtools-pro,按照之前操作重新下载信任即可 12 | 13 | > 注意: 14 | > 15 | > 1. 在现在新版本的浏览器中,HTTPS 页面如果访问 HTTP 的资源会报[Mixed Content 错误](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content),所以 HTTPS 页面要进行调试需要建立 WSS 的 Websocket 连接,一般内核/Webview 可以在创建 Webview 的时候默认关闭该安全配置,用于线下包的开发调试。 16 | > 2. iOS15+ Safari 在使用 https 的 URL,如果要链接 WSS 协议的 Websocket,需要关闭「NSURLSession WebSocket」(iOS15-默认是关闭的),路径 「iOS 设置 -> Safari -> 高级 -> Experimental Features -> NSURLSession WebSocket」 设置为关闭。详细:https://developer.apple.com/forums/thread/685403 17 | -------------------------------------------------------------------------------- /src/utils/getUaInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | */ 4 | export function getPlatform(userAgent) { 5 | const ua = userAgent.toLowerCase(); 6 | const testUa = regexp => regexp.test(ua); 7 | // 系统 8 | let system = 'unknow'; 9 | if (testUa(/windows|win32|win64|wow32|wow64/g)) { 10 | system = 'windows'; // windows系统 11 | } else if (testUa(/macintosh|macintel/g)) { 12 | system = 'macos'; // macos系统 13 | } else if (testUa(/x11/g)) { 14 | system = 'linux'; // linux系统 15 | } else if (testUa(/android|adr/g)) { 16 | system = 'android'; // android系统 17 | } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) { 18 | system = 'ios'; // ios系统 19 | } 20 | let platform = 'unknow'; 21 | if (system === 'windows' || system === 'macos' || system === 'linux') { 22 | platform = 'desktop'; // 桌面端 23 | } else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) { 24 | platform = 'mobile'; // 移动端 25 | } 26 | return { 27 | platform, 28 | system 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /server/utils/internalIPSync.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const ipaddr = require('ipaddr.js'); 3 | const defaultGateway = require('default-gateway'); 4 | const cached = {}; 5 | function internalIPSync(family = 'v4') { 6 | if (cached[family]) { 7 | return cached[family]; 8 | } 9 | try { 10 | const {gateway} = defaultGateway[family].sync(); 11 | const ip = findIp(gateway); 12 | cached[family] = ip; 13 | return ip; 14 | } catch { 15 | // ignore 16 | } 17 | } 18 | 19 | function findIp(gateway) { 20 | const gatewayIp = ipaddr.parse(gateway); 21 | 22 | // Look for the matching interface in all local interfaces. 23 | for (const addresses of Object.values(os.networkInterfaces())) { 24 | for (const {cidr} of addresses) { 25 | const net = ipaddr.parseCIDR(cidr); 26 | 27 | if (net[0] && net[0].kind() === gatewayIp.kind() && gatewayIp.match(net)) { 28 | return net[0].toString(); 29 | } 30 | } 31 | } 32 | } 33 | // console.log(internalIPSync()); 34 | module.exports = internalIPSync; 35 | -------------------------------------------------------------------------------- /server/proxy/InterceptorFactory.js: -------------------------------------------------------------------------------- 1 | const matcher = require('../utils/matcher'); 2 | 3 | class InterceptorFactory { 4 | constructor() { 5 | this._hanlders = []; 6 | } 7 | add(handler, filterDetail) { 8 | return ( 9 | this._hanlders.push({ 10 | handler, 11 | filterDetail 12 | }) - 1 13 | ); 14 | } 15 | remove(id) { 16 | if (this._handlers && this._handlers[id]) { 17 | this._handlers[id] = null; 18 | } 19 | } 20 | async run(params, test) { 21 | for (let {handler, filterDetail} of this._hanlders) { 22 | let runIt = true; 23 | if (filterDetail && typeof test === 'function') { 24 | runIt = test(filterDetail); 25 | } 26 | if (runIt) { 27 | await handler(params); 28 | } 29 | } 30 | } 31 | } 32 | 33 | module.exports = InterceptorFactory; 34 | module.exports.createFilter = context => { 35 | return function interceptorFilterFunc(filterDetail) { 36 | return matcher(filterDetail, context); 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /server/proxy/plugins/certfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | module.exports = ({request, response}, proxyInstance) => { 4 | const id = request.add( 5 | ({request: req, response: res}) => { 6 | res.setHeader('Access-Control-Allow-Origin', '*'); 7 | const caFilePath = proxyInstance.ca.caFilepath; 8 | // path.join(proxyInstance.sslCaDir, 'certs/ca.pem'); 9 | // console.log(caFilePath); 10 | if (fs.existsSync(caFilePath)) { 11 | const extname = path.extname(caFilePath); 12 | res.setHeader('Content-Type', 'application/x-x509-ca-cert'); 13 | res.setHeader('Content-Disposition', `attachment; filename="rootCA${extname}"`); 14 | res.end(fs.readFileSync(caFilePath, {encoding: null})); 15 | } else { 16 | res.setHeader('Content-Type', 'text/html'); 17 | res.end('Can not found rootCA'); 18 | } 19 | }, 20 | {host: 'devtools.pro', path: '/ssl'} 21 | ); 22 | return () => { 23 | request.remove(id); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /interceptors/forward.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {Sting|Function} forwardConfig 转发配置 4 | * @param {Sting|function|object} filterOptions filter配置 5 | * @returns 6 | */ 7 | module.exports = (forwardConfig, filterOptions) => { 8 | return interceptor => { 9 | // TODO 实现它 10 | // 将百度域名转到到google域名:forwardConfig = {host:'google.com'}, filterOptions = {host:'baidu.com'} 11 | // 将luoyi.baidu.com 转发到 172.102.112.233:8080 12 | // -> forwardConfig = {host:'172.102.112.233',port:8080}, filterOptions = {host:'luoyi.baidu.com'} 13 | // 自定义转发配置:forwardConfig = fn(request, response) 14 | return interceptor.request.add(async ({request, response}) => { 15 | if (typeof forwardConfig === 'function') { 16 | return await forwardConfig(request, response); 17 | } 18 | if (typeof forwardConfig === 'object') { 19 | Object.keys(forwardConfig).forEach(key => { 20 | request[key] = forwardConfig[key]; 21 | }); 22 | } 23 | // 调用end,结束请求 24 | // response.end(); 25 | }, filterOptions); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/getFavicon.js: -------------------------------------------------------------------------------- 1 | export default function getFavicon() { 2 | const selectors = [ 3 | 'link[rel=apple-touch-icon-precomposed][href]', 4 | 'link[rel=apple-touch-icon][href]', 5 | 'link[rel="shortcut icon"][href]', 6 | 'link[rel=icon][href]', 7 | 'meta[name=msapplication-TileImage][content]', 8 | 'meta[name=twitter\\:image][content]', 9 | 'meta[property=og\\:image][content]' 10 | ]; 11 | let url = ''; 12 | 13 | selectors.find(selector => { 14 | const $node = document.querySelector(selector); 15 | if ($node) { 16 | switch ($node.tagName) { 17 | case 'LINK': 18 | url = $node.href; 19 | 20 | break; 21 | case 'META': 22 | url = $node.content; 23 | if (url) { 24 | return url; 25 | } 26 | break; 27 | } 28 | } 29 | if (url) { 30 | return true; 31 | } 32 | return false; 33 | }); 34 | if (url) { 35 | return url; 36 | } 37 | return ''; 38 | } 39 | -------------------------------------------------------------------------------- /server/utils/decompress.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib'); 2 | 3 | module.exports = (responseBody, res) => { 4 | const contentEncoding = (res.headers['content-encoding'] || res.headers['Content-Encoding'] || '').toLowerCase(); 5 | const contentLength = Buffer.byteLength(responseBody); 6 | if (!['gzip', 'deflate', 'br'].includes(contentEncoding) || !contentLength) { 7 | return Promise.resolve(responseBody); 8 | } 9 | // 删除encoding header 10 | delete res.headers['content-encoding']; 11 | delete res.headers['Content-Encoding']; 12 | 13 | return new Promise((resolve, reject) => { 14 | const callback = (err, data) => { 15 | if (err) { 16 | reject(err); 17 | } else { 18 | resolve(data); 19 | } 20 | }; 21 | switch (contentEncoding) { 22 | case 'gzip': 23 | zlib.gunzip(responseBody, callback); 24 | break; 25 | case 'deflate': 26 | zlib.inflate(responseBody, callback); 27 | break; 28 | case 'br': 29 | zlib.brotliDecompress(responseBody, callback); 30 | break; 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /server/middlewares/backend.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | // const mergeStream = require('merge-stream'); 4 | 5 | const distPath = path.join(__dirname, '../../dist'); 6 | module.exports = (router, logger, serverInstance) => { 7 | const beFilepath = path.join(distPath, 'backend.js'); 8 | router.get('/backend.js', async (ctx, next) => { 9 | let s = fs.readFileSync(beFilepath).toString(); 10 | const backendFiles = serverInstance._backends || []; 11 | backendFiles.forEach(filepath => { 12 | s += fs.readFileSync(filepath).toString(); 13 | }); 14 | 15 | // const mergedStream = mergeStream(fs.createReadStream(beFilepath)); 16 | // const backendFiles = serverInstance._backends || []; 17 | // backendFiles.forEach(filepath => { 18 | // log.debug(`add ${filepath}`); 19 | // mergedStream.add(fs.createReadStream(filepath)); 20 | // }); 21 | // console.log(mergedStream.toString()); 22 | // ctx.body = mergedStream; 23 | ctx.res.setHeader('Access-Control-Allow-Origin', '*'); 24 | ctx.res.setHeader('Content-Type', 'application/javascript'); 25 | ctx.body = s; 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/main-for-network-standalone/main-for-network-standalone-legacy.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import * as MainModule from './main-for-network-standalone.js'; 6 | 7 | self.Main = self.Main || {}; 8 | Main = Main || {}; 9 | 10 | /** 11 | * @constructor 12 | */ 13 | Main.ExecutionContextSelector = MainModule.ExecutionContextSelector.ExecutionContextSelector; 14 | 15 | /** 16 | * @constructor 17 | */ 18 | Main.Main = MainModule.MainImpl.MainImpl; 19 | 20 | /** 21 | * @constructor 22 | */ 23 | Main.Main.ZoomActionDelegate = MainModule.MainImpl.ZoomActionDelegate; 24 | 25 | /** 26 | * @constructor 27 | */ 28 | Main.Main.SearchActionDelegate = MainModule.MainImpl.SearchActionDelegate; 29 | 30 | /** 31 | * @constructor 32 | */ 33 | Main.Main.MainMenuItem = MainModule.MainImpl.MainMenuItem; 34 | 35 | /** 36 | * @constructor 37 | */ 38 | Main.ReloadActionDelegate = MainModule.MainImpl.ReloadActionDelegate; 39 | 40 | /** 41 | * @constructor 42 | */ 43 | Main.SimpleApp = MainModule.SimpleApp.SimpleApp; 44 | 45 | /** 46 | * @constructor 47 | */ 48 | Main.SimpleAppProvider = MainModule.SimpleApp.SimpleAppProvider; 49 | -------------------------------------------------------------------------------- /server/utils/matcher.js: -------------------------------------------------------------------------------- 1 | const test = require('./test'); 2 | const isObject = obj => obj && obj.constructor && obj.constructor === Object; 3 | 4 | module.exports = function match(options, context) { 5 | if (typeof options === 'function') { 6 | return options(context); 7 | } 8 | if (isObject(options)) { 9 | // const {path, url, headers, method, host, sourceType, userAgent, statusCode} = context; 10 | 11 | const keys = Object.keys(options).filter(key => key !== 'headers'); 12 | for (let i = 0; i < keys.length; i++) { 13 | const key = keys[i]; 14 | const value = context[key]; 15 | if (!test(options[key], value)) { 16 | return false; 17 | } 18 | } 19 | 20 | if (options.headers && context.headers) { 21 | const headersKey = Object.keys(options.headers); 22 | for (let i = 0; i < headersKey.length; i++) { 23 | const key = headersKey[i]; 24 | const value = context.headers[key]; 25 | if (!test(options.headers[key], value)) { 26 | return false; 27 | } 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | // 默认是string host匹配 34 | return test(options, context.host); 35 | }; 36 | -------------------------------------------------------------------------------- /server/utils/findCacheDir.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const findCacheDir = require('find-cache-dir'); 5 | const {name: pkgName} = require('../../package.json'); 6 | module.exports = (name, thunk) => { 7 | if (!name) { 8 | name = './'; 9 | } 10 | /** 11 | * 12 | * const thunk = findCacheDir({name: 'foo', thunk: true}); 13 | thunk(); 14 | //=> '/some/path/node_modules/.cache/foo' 15 | 16 | thunk('bar.js') 17 | //=> '/some/path/node_modules/.cache/foo/bar.js' 18 | 19 | thunk('baz', 'quz.js') 20 | //=> '/some/path/node_modules/.cache/foo/baz/quz.js' 21 | */ 22 | const cacheDir = findCacheDir({name: path.join(pkgName, name), create: true, thunk}); 23 | if (!cacheDir) { 24 | // 不存在则自己尝试创建,注意这里的位置跟find-cache-dir的不一样 25 | const cachePath = path.join(os.tmpdir(), name); 26 | let cacheDirExists = fs.existsSync(cachePath); 27 | if (!cacheDirExists) { 28 | fs.mkdirSync(cachePath, {recursive: true}); 29 | } 30 | if (thunk) { 31 | return (...args) => { 32 | return path.join(cachePath, ...args); 33 | }; 34 | } 35 | return cacheDir; 36 | } 37 | return cacheDir; 38 | }; 39 | -------------------------------------------------------------------------------- /src/backend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 放到被调试页面的laucher 3 | */ 4 | import chobitsu from '@ksky521/chobitsu'; 5 | import getFavicon from './utils/getFavicon'; 6 | import getSessionId from './utils/getSessionId'; 7 | import createRuntime from './runtime'; 8 | 9 | // 初始化runtime 10 | const runtime = createRuntime(chobitsu); 11 | const sid = getSessionId(); 12 | 13 | // 得到ws地址 14 | const backendWSURL = runtime.createWebsocketUrl(`/backend/${sid}`, {}); 15 | 16 | // 建立连接 17 | const wss = runtime.createWebsocketConnection(backendWSURL); 18 | 19 | wss.on('message', event => { 20 | chobitsu.sendRawMessage(event.data); 21 | }); 22 | 23 | chobitsu.setOnMessage(message => { 24 | wss.send(message); 25 | }); 26 | 27 | // 第一次发送 28 | sendRegisterMessage(); 29 | // 第二次更新 30 | window.addEventListener('onload', sendRegisterMessage); 31 | // ws链接建立成功之后主动发送页面信息 32 | wss.on('open', sendRegisterMessage); 33 | 34 | function sendRegisterMessage() { 35 | const favicon = getFavicon(); 36 | const title = document.title || 'Untitled'; 37 | const {userAgent, platform} = navigator; 38 | wss.send({ 39 | event: 'updateBackendInfo', 40 | payload: { 41 | id: sid, 42 | favicon, 43 | title, 44 | metaData: {userAgent, platform}, 45 | url: location.href 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /server/utils/getResourceType.js: -------------------------------------------------------------------------------- 1 | const mime = require('mime-types'); 2 | 3 | module.exports = function getResourceType(contentType, path) { 4 | if (contentType && contentType.match) { 5 | contentType = contentType.toLowerCase(); 6 | if (contentType.match(/application/)) { 7 | const newContentType = mime.lookup(path); 8 | if (newContentType) { 9 | contentType = newContentType; 10 | } 11 | } 12 | if (contentType.match('text/css')) { 13 | return 'Stylesheet'; 14 | } 15 | if (contentType.match('text/html')) { 16 | return 'Document'; 17 | } 18 | if (contentType.match('/(x-)?javascript')) { 19 | return 'Script'; 20 | } 21 | if (contentType.match('image/')) { 22 | // TODO svg图片处理 image/svg+xml 23 | return 'Image'; 24 | } 25 | if (contentType.match('video/')) { 26 | return 'Media'; 27 | } 28 | if (contentType.match('font/') || contentType.match('/(x-font-)?woff')) { 29 | return 'Font'; 30 | } 31 | if (contentType.match('/(json|xml)')) { 32 | return 'XHR'; 33 | } 34 | } 35 | 36 | return 'Other'; 37 | // 'XHR', 'Fetch', 'EventSource', 'Script', 'Stylesheet', 'Image', 'Media', 'Font', 'Document', 'TextTrack', 'WebSocket', 'Other', 'SourceMapScript', 'SourceMapStyleSheet', 'Manifest', 'SignedExchange' 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/createBridge.js: -------------------------------------------------------------------------------- 1 | import Bridge from '@lib/Bridge'; 2 | 3 | export default function(ws) { 4 | const messageListeners = []; 5 | 6 | ws.on('message', event => { 7 | let data; 8 | try { 9 | if (typeof event.data === 'string') { 10 | data = JSON.parse(event.data); 11 | } else { 12 | throw Error(); 13 | } 14 | } catch (e) { 15 | console.error(`[Remote Devtools] Failed to parse JSON: ${event.data}`); 16 | return; 17 | } 18 | messageListeners.forEach(fn => { 19 | try { 20 | fn(data); 21 | } catch (error) { 22 | // jsc doesn't play so well with tracebacks that go into eval'd code, 23 | // so the stack trace here will stop at the `eval()` call. Getting the 24 | // message that caused the error is the best we can do for now. 25 | console.log('[Remote Devtools] Error calling listener', data); 26 | console.log('error:', error); 27 | throw error; 28 | } 29 | }); 30 | }); 31 | 32 | return new Bridge({ 33 | listen(fn) { 34 | messageListeners.push(fn); 35 | return () => { 36 | const index = messageListeners.indexOf(fn); 37 | if (index >= 0) { 38 | messageListeners.splice(index, 1); 39 | } 40 | }; 41 | }, 42 | send(data) { 43 | ws.sendRawMessage(JSON.stringify(data)); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/network-standalone/webSocketFrameView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .websocket-frame-view { 8 | user-select: text; 9 | } 10 | 11 | .websocket-frame-view .data-grid { 12 | flex: auto; 13 | border: none; 14 | } 15 | 16 | .websocket-frame-view .data-grid .data { 17 | background-image: none; 18 | } 19 | 20 | .websocket-frame-view-td { 21 | border-bottom: 1px solid #ccc; 22 | } 23 | 24 | .websocket-frame-view .data-grid tr.selected { 25 | background-color: #def; 26 | } 27 | 28 | .websocket-frame-view .data-grid td, 29 | .websocket-frame-view .data-grid th { 30 | border-left-color: #ccc; 31 | } 32 | 33 | .websocket-frame-view-row-send td:first-child::before { 34 | content: "\2B06"; 35 | color: #080; 36 | padding-right: 4px; 37 | } 38 | 39 | .websocket-frame-view-row-receive td:first-child::before { 40 | content: "\2B07"; 41 | color: #E65100; 42 | padding-right: 4px; 43 | } 44 | 45 | .data-grid:focus .websocket-frame-view-row-send.selected td:first-child::before, 46 | .data-grid:focus .websocket-frame-view-row-receive.selected td:first-child::before { 47 | color: white; 48 | } 49 | 50 | .websocket-frame-view-row-send { 51 | background-color: rgb(226, 247, 218); 52 | } 53 | 54 | .websocket-frame-view-row-error { 55 | background-color: rgb(255, 237, 237); 56 | color: rgb(182, 0, 0); 57 | } 58 | 59 | .websocket-frame-view .toolbar { 60 | border-bottom: var(--divider-border); 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Referenced from https://github.com/github/gitignore/blob/master/Node.gitignore 2 | temp 3 | .http-mitm-proxy 4 | output 5 | dist 6 | devtools.config.js 7 | chrome-devtools-frontend 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | lerna-debug.log 16 | .vscode 17 | package-lock.json 18 | dist 19 | docs/_book 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Typescript v1 declaration files 53 | typings/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | 73 | # next.js build output 74 | .next 75 | 76 | # other stuff 77 | .DS_Store 78 | Thumbs.db 79 | 80 | # IDE configurations 81 | .idea 82 | .vscode 83 | 84 | # build assets 85 | /output 86 | /dist 87 | /dll 88 | -------------------------------------------------------------------------------- /frontend/network-standalone/blockedURLsPane.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .list { 8 | border: none !important; 9 | border-top: var(--divider-border) !important; 10 | } 11 | 12 | .blocking-disabled { 13 | pointer-events: none; 14 | opacity: 0.8; 15 | } 16 | 17 | .editor-container { 18 | padding: 0 4px; 19 | } 20 | 21 | .no-blocked-urls, .blocked-urls { 22 | overflow-x: hidden; 23 | overflow-y: auto; 24 | } 25 | 26 | .no-blocked-urls { 27 | display: flex; 28 | justify-content: center; 29 | padding: 10px; 30 | } 31 | 32 | .no-blocked-urls > span { 33 | white-space: pre; 34 | } 35 | 36 | .blocked-url { 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | flex: auto; 41 | } 42 | 43 | .blocked-url-count { 44 | flex: none; 45 | padding-right: 9px; 46 | } 47 | 48 | .blocked-url-checkbox { 49 | margin-left: 8px; 50 | flex: none; 51 | } 52 | 53 | .blocked-url-label { 54 | white-space: nowrap; 55 | text-overflow: ellipsis; 56 | overflow: hidden; 57 | flex: auto; 58 | padding: 0 3px; 59 | } 60 | 61 | .blocked-url-edit-row { 62 | flex: none; 63 | display: flex; 64 | flex-direction: row; 65 | margin: 7px 5px 0 5px; 66 | align-items: center; 67 | } 68 | 69 | .blocked-url-edit-value { 70 | user-select: none; 71 | flex: 1 1 0px; 72 | } 73 | 74 | .blocked-url-edit-row input { 75 | width: 100%; 76 | text-align: inherit; 77 | height: 22px; 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/EventEmitter.js: -------------------------------------------------------------------------------- 1 | export default class EventEmitter { 2 | constructor() { 3 | this.listeners = new Map(); 4 | } 5 | on(eventName, listener) { 6 | let listeners = this.listeners.get(eventName); 7 | if (typeof listener !== 'function') { 8 | throw new Error(`[EventEmitter.on] ${listener} is not Function`); 9 | } 10 | const added = listeners && listeners.push(listener); 11 | if (!added) { 12 | this.listeners.set(eventName, [listener]); 13 | } 14 | } 15 | once(eventName, listener) { 16 | let onceListener = (...args) => { 17 | this.off(eventName, onceListener); 18 | listener.apply(this, args); 19 | }; 20 | this.on(eventName, onceListener); 21 | } 22 | off(eventName, listener) { 23 | const listeners = this.listeners.get(eventName); 24 | if (listeners) { 25 | listeners.splice(listeners.indexOf(listener) >>> 0, 1); 26 | } 27 | } 28 | emit(eventName, data) { 29 | const listeners = this.listeners.get(eventName); 30 | if (listeners && listeners.length > 0) { 31 | listeners.map(listener => { 32 | listener(data); 33 | }); 34 | } 35 | } 36 | removeAllListeners(eventName) { 37 | if (eventName) { 38 | const listeners = this.listeners.get(eventName); 39 | if (listeners && listeners.length > 0) { 40 | listeners.length = 0; 41 | this.listeners.delete(eventName); 42 | } 43 | } else { 44 | this.listeners.clear(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /interceptors/localMock.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const mimeType = require('mime-types'); 4 | const logger = require('../server/utils/logger'); 5 | module.exports = (mockConfig, filterOptions) => { 6 | // [{host: '', path: ''}, 'path string'/ function] 7 | return interceptor => { 8 | return interceptor.request.add(async ({request, response}) => { 9 | if (typeof mockConfig === 'function') { 10 | const p = await mockConfig(request, response); 11 | if (response.finished) { 12 | // 已经执行了response.end(),说明自己反悔了 13 | return; 14 | } 15 | if (typeof p === 'string') { 16 | // 当做返回路径处理 17 | mockConfig = p; 18 | } 19 | } 20 | if (typeof mockConfig === 'string') { 21 | // 作为文件路径 22 | path.isAbsolute(mockConfig) || (mockConfig = path.join(process.cwd(), mockConfig)); 23 | // 从url得到path 24 | const url = request.url.split('?')[0]; 25 | const filepath = path.join(mockConfig, url); 26 | try { 27 | const contentType = mimeType.lookup(filepath); 28 | response.type = contentType; 29 | response.end(fs.readFileSync(filepath)); 30 | } catch (e) { 31 | logger.info('localmock inerceptor 读取文件失败'); 32 | logger.error(e); 33 | } 34 | } 35 | // 调用end,结束请求 36 | // response.end(); 37 | }, filterOptions); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/shell.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | {"name": "bindings", "type": "autostart"}, 4 | {"name": "common", "type": "autostart"}, 5 | {"name": "components", "type": "autostart"}, 6 | {"name": "console_counters", "type": "autostart"}, 7 | {"name": "dom_extension", "type": "autostart"}, 8 | {"name": "extensions", "type": "autostart"}, 9 | {"name": "host", "type": "autostart"}, 10 | {"name": "main", "type": "autostart"}, 11 | {"name": "persistence", "type": "autostart"}, 12 | {"name": "platform", "type": "autostart"}, 13 | {"name": "protocol", "type": "autostart"}, 14 | {"name": "sdk", "type": "autostart"}, 15 | {"name": "browser_sdk", "type": "autostart"}, 16 | {"name": "services", "type": "autostart"}, 17 | {"name": "text_utils", "type": "autostart"}, 18 | {"name": "ui", "type": "autostart"}, 19 | {"name": "workspace", "type": "autostart"}, 20 | 21 | {"name": "changes"}, 22 | {"name": "cm"}, 23 | {"name": "cm_modes"}, 24 | {"name": "cm_web_modes"}, 25 | {"name": "color_picker"}, 26 | {"name": "console"}, 27 | {"name": "coverage"}, 28 | {"name": "data_grid"}, 29 | {"name": "diff"}, 30 | {"name": "event_listeners"}, 31 | {"name": "formatter"}, 32 | {"name": "heap_snapshot_model"}, 33 | {"name": "inline_editor"}, 34 | {"name": "javascript_metadata"}, 35 | {"name": "object_ui"}, 36 | {"name": "perf_ui"}, 37 | {"name": "quick_open"}, 38 | {"name": "search"}, 39 | {"name": "snippets"}, 40 | {"name": "source_frame"}, 41 | {"name": "sources"}, 42 | {"name": "text_editor"}, 43 | {"name": "workspace_diff"}, 44 | {"name": "input"} 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /server/utils/test.js: -------------------------------------------------------------------------------- 1 | const isGlob = require('is-glob'); 2 | const micromatch = require('micromatch'); 3 | module.exports = function test(tester, testee) { 4 | if (tester === undefined || testee === undefined) { 5 | return true; 6 | } 7 | if (tester instanceof RegExp) { 8 | return tester.test(testee); 9 | } 10 | // single glob path 11 | if (isGlobPath(tester)) { 12 | return matchSingleGlobPath(tester, testee); 13 | } 14 | 15 | // multi path 16 | if (Array.isArray(tester)) { 17 | if (tester.every(isStringPath)) { 18 | return matchMultiPath(tester, testee); 19 | } 20 | if (tester.every(isGlobPath)) { 21 | return matchMultiGlobPath(tester, testee); 22 | } 23 | 24 | throw new Error('Invalid interceptor filter.'); 25 | } 26 | 27 | // custom matching 28 | if (typeof tester === 'function') { 29 | return !!tester(testee); 30 | } 31 | 32 | // 最后相等 33 | return matchSingleStringPath(tester, testee); // eslint-disable-line eqeqeq 34 | }; 35 | 36 | function matchSingleStringPath(tester, testee) { 37 | return tester == testee; 38 | } 39 | 40 | function matchSingleGlobPath(pattern, testee) { 41 | const matches = micromatch([testee], pattern); 42 | return matches && matches.length > 0; 43 | } 44 | 45 | function matchMultiGlobPath(patternList, testee) { 46 | return matchSingleGlobPath(patternList, testee); 47 | } 48 | 49 | function matchMultiPath(arr, testee) { 50 | let isMultiPath = false; 51 | 52 | for (const tester of arr) { 53 | if (matchSingleStringPath(tester, testee)) { 54 | isMultiPath = true; 55 | break; 56 | } 57 | } 58 | 59 | return isMultiPath; 60 | } 61 | function isStringPath(context) { 62 | return typeof context === 'string' && !isGlob(context); 63 | } 64 | 65 | function isGlobPath(context) { 66 | return isGlob(context); 67 | } 68 | -------------------------------------------------------------------------------- /frontend/$_bridge/Bridge.js: -------------------------------------------------------------------------------- 1 | import {Capability, SDKModel, Target} from '../sdk/SDKModel.js'; 2 | 3 | Protocol.inspectorBackend.registerEvent('$Bridge.messageChannel', ['event', 'payload']); 4 | 5 | Protocol.inspectorBackend.registerCommand( 6 | '$Bridge.messageChannel', 7 | [ 8 | {name: 'event', type: 'string', optional: false}, 9 | {name: 'payload', type: 'object', optional: true} 10 | ], 11 | ['result'], 12 | false 13 | ); 14 | 15 | class Bridge { 16 | // agent发送消息 17 | // dispathcher 接受消息 18 | constructor(model, agent) { 19 | this._model = model; 20 | this._agent = agent; 21 | this._listeners = new Map(); 22 | } 23 | registerEvent(event, listener) { 24 | this._listeners.set(event, listener); 25 | } 26 | sendCommand(event, payload) { 27 | return this._agent.messageChannel(event, payload); 28 | } 29 | // backend事件接收触发 30 | messageChannel(event, data) { 31 | const handler = this._listeners.get(event); 32 | if (handler && typeof handler === 'function') { 33 | return handler(data) || {}; 34 | } 35 | throw Error(`${event} unimplemented`); 36 | } 37 | } 38 | 39 | export class BridgeModel extends SDKModel { 40 | static Events = { 41 | messageChannel: Symbol('bridge-messageChannel') 42 | }; 43 | constructor(target) { 44 | super(target); 45 | this._agent = target.$BridgeAgent(); 46 | const dispatcher = (this._dispatcher = new Bridge(this, this._agent)); 47 | // 给runtime加个方法 48 | runtime.bridge = dispatcher; 49 | // this._agent.messageChannel('ffff', {}).then(a => console.log(a)); 50 | this.target().register$BridgeDispatcher(this._dispatcher); 51 | } 52 | } 53 | export class Main extends Common.Object { 54 | run() { 55 | SDK.SDKModel.register(BridgeModel, Capability.None, true); 56 | SDK.targetManager.addModelListener(BridgeModel, BridgeModel.Events.messageChannel, {}); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/websocket/Manager.js: -------------------------------------------------------------------------------- 1 | const {nanoid} = require('nanoid'); 2 | const colorette = require('colorette'); 3 | const debug = require('../utils/createDebug')('websocket'); 4 | const Channel = require('./Channel'); 5 | const {getColorfulName} = require('../utils'); 6 | 7 | module.exports = class HomeChannel { 8 | constructor(wssInstance) { 9 | this.wssInstance = wssInstance; 10 | this.heartBeatsWs = []; 11 | this._channels = []; 12 | this._addListeners(); 13 | } 14 | createChannel(ws, id = nanoid()) { 15 | const channel = new Channel(ws); 16 | debug(`${getColorfulName('manager')} ${id} ${colorette.green('connected')}`); 17 | const channelData = { 18 | id, 19 | channel 20 | }; 21 | this._channels.push(channelData); 22 | 23 | channel.on('close', () => this.removeChannel(id)); 24 | } 25 | 26 | _addListeners() { 27 | const channelManager = this.wssInstance.getChannelManager(); 28 | // TODO update 29 | channelManager.on('backendUpdate', data => { 30 | this.send({event: 'backendUpdate', payload: data}); 31 | }); 32 | channelManager.on('backendConnected', data => { 33 | this.send({event: 'backendConnected', payload: data}); 34 | }); 35 | channelManager.on('backendDisconnected', data => { 36 | this.send({event: 'backendDisconnected', payload: data}); 37 | }); 38 | channelManager.on('updateFoxyInfo', data => { 39 | this.send({event: 'updateFoxyInfo', payload: data}); 40 | }); 41 | } 42 | removeChannel(id) { 43 | debug(`${getColorfulName('manager')} ${id} ${colorette.red('disconnected')}`); 44 | const idx = this._channels.findIndex(c => c.id === id); 45 | this._channels.splice(idx, 1); 46 | } 47 | // 广播事件 48 | send(message) { 49 | this._channels.forEach(c => c.channel.send(message)); 50 | } 51 | destroy() { 52 | this._channels.forEach(c => c.channel.destroy()); 53 | this._channels.length = 0; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /frontend/network-standalone/signedExchangeInfoTree.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .tree-outline { 8 | padding-left: 0; 9 | } 10 | 11 | .tree-outline > ol { 12 | padding-bottom: 5px; 13 | border-bottom: solid 1px #e0e0e0; 14 | } 15 | 16 | .tree-outline > .parent { 17 | user-select: none; 18 | font-weight: bold; 19 | color: #616161; 20 | margin-top: -1px; 21 | height: 20px; 22 | display: flex; 23 | align-items: center; 24 | height: 26px; 25 | } 26 | 27 | .tree-outline li { 28 | padding-left: 5px; 29 | line-height: 20px; 30 | } 31 | 32 | .tree-outline li:not(.parent) { 33 | display: block; 34 | margin-left: 10px; 35 | } 36 | 37 | .tree-outline li:not(.parent)::before { 38 | display: none; 39 | } 40 | 41 | .tree-outline .header-name { 42 | color: rgb(33%, 33%, 33%); 43 | display: inline-block; 44 | margin-right: 0.25em; 45 | font-weight: bold; 46 | vertical-align: top; 47 | white-space: pre-wrap; 48 | } 49 | 50 | .tree-outline .header-separator { 51 | user-select: none; 52 | } 53 | 54 | .tree-outline .header-value { 55 | display: inline; 56 | margin-right: 1em; 57 | white-space: pre-wrap; 58 | word-break: break-all; 59 | margin-top: 1px; 60 | } 61 | 62 | .tree-outline .header-toggle { 63 | display: inline; 64 | margin-left: 30px; 65 | font-weight: normal; 66 | color: rgb(45%, 45%, 45%); 67 | } 68 | 69 | .tree-outline .header-toggle:hover { 70 | color: rgb(20%, 20%, 45%); 71 | cursor: pointer; 72 | } 73 | 74 | .tree-outline .error-log { 75 | color: red; 76 | display: inline-block; 77 | margin-right: 0.25em; 78 | margin-left: 0.25em; 79 | font-weight: bold; 80 | vertical-align: top; 81 | white-space: pre-wrap; 82 | } 83 | 84 | .tree-outline .hex-data { 85 | display: block; 86 | word-break: break-word; 87 | margin-left: 20px; 88 | } 89 | 90 | .tree-outline .error-field { 91 | color: red; 92 | } 93 | -------------------------------------------------------------------------------- /frontend/network-standalone/networkConfigView.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .network-config { 8 | padding: 12px; 9 | display: block; 10 | } 11 | 12 | .network-config-group { 13 | display: flex; 14 | margin-bottom: 10px; 15 | flex-wrap: wrap; 16 | flex: 0 0 auto; 17 | min-height: 30px; 18 | } 19 | 20 | .network-config-title { 21 | margin-right: 16px; 22 | width: 130px; 23 | } 24 | 25 | .network-config-fields { 26 | flex: 2 0 200px; 27 | } 28 | 29 | .panel-section-separator { 30 | height: 1px; 31 | margin-bottom: 10px; 32 | background: #f0f0f0; 33 | } 34 | 35 | /* Disable cache */ 36 | 37 | .network-config-disable-cache { 38 | line-height: 28px; 39 | border-top: none; 40 | padding-top: 0; 41 | } 42 | 43 | .network-config-input-validation-error { 44 | color: var(--input-validation-error); 45 | margin: 5px 0; 46 | } 47 | 48 | /* Network throttling */ 49 | 50 | .network-config-throttling .chrome-select { 51 | width: 100%; 52 | max-width: 250px; 53 | } 54 | 55 | .network-config-throttling > .network-config-title { 56 | line-height: 24px; 57 | } 58 | 59 | /* User agent */ 60 | 61 | .network-config-ua > .network-config-title { 62 | line-height: 20px; 63 | } 64 | 65 | .network-config-ua span[is="dt-radio"].checked > * { 66 | display: none 67 | } 68 | 69 | .network-config-ua input { 70 | display: block; 71 | width: calc(100% - 20px); 72 | } 73 | 74 | .network-config-ua input[readonly] { 75 | background-color: rgb(235, 235, 228); 76 | } 77 | 78 | .network-config-ua input[type=text], .network-config-ua .chrome-select { 79 | margin-top: 8px; 80 | } 81 | 82 | .network-config-ua .chrome-select { 83 | width: calc(100% - 20px); 84 | max-width: 250px; 85 | } 86 | 87 | .network-config-ua span[is="dt-radio"] { 88 | display: block; 89 | } 90 | 91 | .network-config-ua-custom { 92 | opacity: 0.5; 93 | } 94 | 95 | .network-config-ua-custom.checked { 96 | opacity: 1; 97 | } 98 | -------------------------------------------------------------------------------- /frontend/network_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | {"name": "network-main", "type": "autostart"}, 4 | {"name": "$_bridge", "type": "autostart"}, 5 | 6 | {"name": "emulation", "type": "autostart"}, 7 | {"name": "mobile_throttling", "type": "autostart"}, 8 | {"name": "browser_debugger"}, 9 | {"name": "cookie_table"}, 10 | {"name": "har_importer"}, 11 | {"name": "layer_viewer"}, 12 | {"name": "persistence", "type": "autostart"}, 13 | {"name": "data_grid"}, 14 | {"name": "browser_sdk", "type": "autostart"}, 15 | {"name": "perf_ui"}, 16 | {"name": "common", "type": "autostart"}, 17 | {"name": "ui", "type": "autostart"}, 18 | {"name": "main-for-network-standalone", "type": "autostart"}, 19 | {"name": "search"}, 20 | {"name": "network-standalone"}, 21 | 22 | {"name": "bindings", "type": "autostart"}, 23 | {"name": "components", "type": "autostart"}, 24 | {"name": "console_counters", "type": "autostart"}, 25 | {"name": "console"}, 26 | {"name": "dom_extension", "type": "autostart"}, 27 | {"name": "extensions", "type": "autostart"}, 28 | {"name": "host", "type": "autostart"}, 29 | {"name": "platform", "type": "autostart"}, 30 | {"name": "protocol", "type": "autostart"}, 31 | {"name": "sdk", "type": "autostart"}, 32 | {"name": "services", "type": "autostart"}, 33 | {"name": "text_utils", "type": "autostart"}, 34 | {"name": "workspace", "type": "autostart"}, 35 | 36 | {"name": "changes"}, 37 | {"name": "cm"}, 38 | {"name": "cm_modes"}, 39 | {"name": "cm_web_modes"}, 40 | {"name": "diff"}, 41 | {"name": "formatter"}, 42 | {"name": "inline_editor"}, 43 | {"name": "javascript_metadata"}, 44 | {"name": "object_ui"}, 45 | {"name": "quick_open"}, 46 | {"name": "snippets"}, 47 | {"name": "source_frame"}, 48 | {"name": "sources"}, 49 | {"name": "text_editor"}, 50 | {"name": "workspace_diff"}, 51 | {"name": "input"} 52 | ], 53 | "has_html": true 54 | } 55 | -------------------------------------------------------------------------------- /frontend/network-standalone/requestHeadersTree.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The Chromium Authors. All rights reserved. 3 | * Use of this source code is governed by a BSD-style license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | .tree-outline { 8 | padding-left: 0; 9 | } 10 | 11 | .tree-outline > ol { 12 | padding-bottom: 5px; 13 | border-bottom: solid 1px #e0e0e0; 14 | } 15 | 16 | .tree-outline > .parent { 17 | user-select: none; 18 | font-weight: bold; 19 | color: #616161; 20 | margin-top: -1px; 21 | height: 20px; 22 | display: flex; 23 | align-items: center; 24 | height: 26px; 25 | } 26 | 27 | .tree-outline li { 28 | display: block; 29 | padding-left: 5px; 30 | line-height: 20px; 31 | } 32 | 33 | .tree-outline li:not(.parent) { 34 | margin-left: 10px; 35 | } 36 | 37 | .tree-outline li:not(.parent)::before { 38 | display: none; 39 | } 40 | 41 | .tree-outline .caution { 42 | margin-left: 4px; 43 | display: inline-block; 44 | font-weight: bold; 45 | } 46 | 47 | .tree-outline li.expanded .header-count { 48 | display: none; 49 | } 50 | 51 | .tree-outline li .header-toggle { 52 | display: none; 53 | } 54 | 55 | .tree-outline li .status-from-cache { 56 | color: gray; 57 | } 58 | 59 | .tree-outline li.expanded .header-toggle { 60 | display: inline; 61 | margin-left: 30px; 62 | font-weight: normal; 63 | color: rgb(45%, 45%, 45%); 64 | } 65 | 66 | .tree-outline li .header-toggle:hover { 67 | color: rgb(20%, 20%, 45%); 68 | cursor: pointer; 69 | } 70 | 71 | .tree-outline .header-name { 72 | color: rgb(33%, 33%, 33%); 73 | display: inline-block; 74 | margin-right: 0.25em; 75 | font-weight: bold; 76 | vertical-align: top; 77 | white-space: pre-wrap; 78 | } 79 | 80 | .tree-outline .header-separator { 81 | user-select: none; 82 | } 83 | 84 | .tree-outline .header-value { 85 | display: inline; 86 | margin-right: 1em; 87 | white-space: pre-wrap; 88 | word-break: break-all; 89 | margin-top: 1px; 90 | } 91 | 92 | .tree-outline .empty-request-header { 93 | color: rgba(33%, 33%, 33%, 0.5); 94 | } 95 | 96 | .request-headers-show-more-button { 97 | border: none; 98 | border-radius: 3px; 99 | display: inline-block; 100 | font-size: 12px; 101 | font-family: sans-serif; 102 | cursor: pointer; 103 | margin: 0 4px; 104 | padding: 2px 4px; 105 | } 106 | 107 | .header-highlight { 108 | background-color: #FFFF78 109 | } 110 | -------------------------------------------------------------------------------- /server/proxy/plugins/injectBackend.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs'); 2 | const getResourceType = require('../../utils/getResourceType'); 3 | const {injectAssetsIntoHtml} = require('../../utils/htmlUtils'); 4 | module.exports = ({request, response, websocketConnect}, proxyInstance) => { 5 | const rootInstance = proxyInstance.serverInstance; 6 | const port = rootInstance.getPort(); 7 | let hostname = rootInstance.getHostname(); 8 | if (hostname === '0.0.0.0' || hostname === '127.0.0.1') { 9 | hostname = 'devtools.pro'; 10 | } 11 | const protocol = rootInstance.isSSL() ? 'wss:' : 'ws:'; 12 | const backendjsUrl = `${ 13 | rootInstance.isSSL() ? 'https' : 'http' 14 | }://${hostname}:${port}/backend.js?hostname=devtools.pro&port=${port}&protocol=${protocol}`; 15 | const id = response.add(({request: req, response: res}) => { 16 | const type = res.type; 17 | const resourceType = getResourceType(type); 18 | if (resourceType === 'Document') { 19 | const body = res.body.toString(); 20 | const htmlRegExp = /(]*>)/i; 21 | if (!htmlRegExp.test(body)) { 22 | return; 23 | } 24 | const html = injectAssetsIntoHtml( 25 | body, 26 | {}, 27 | { 28 | headTags: [ 29 | { 30 | tagName: 'script', 31 | attributes: { 32 | src: backendjsUrl 33 | } 34 | } 35 | ] 36 | } 37 | ); 38 | res.body = Buffer.from(html); 39 | } 40 | }); 41 | const reqId = request.add( 42 | ({request: req, response: res}) => { 43 | // 转发到127.0.0.1:xxx/backend.js 44 | // 这里的js需要统一走127,不能主动拦截 45 | // 因为backend.js 还可能被devtools plugin添加了佐料 46 | req.host = '127.0.0.1'; 47 | // req.port = port; 48 | // res.end(fs.readFileSync(rootInstance.getDistPath() + '/backend.js')); 49 | req.rejectUnauthorized = false; 50 | // req.url = '/backend.js'; 51 | }, 52 | {host: 'devtools.pro'} 53 | ); 54 | 55 | const wsId = websocketConnect.add( 56 | ({request}) => { 57 | request.host = '127.0.0.1'; 58 | request.rejectUnauthorized = false; 59 | }, 60 | {host: 'devtools.pro'} 61 | ); 62 | 63 | return () => { 64 | response.remove(id); 65 | request.remove(reqId); 66 | websocketConnect.remove(wsId); 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /interceptors/pathRewrite.js: -------------------------------------------------------------------------------- 1 | // 修改自 https://github.com/chimurai/http-proxy-middleware/blob/master/src/path-rewriter.ts 2 | const isPlainObj = require('is-plain-obj'); 3 | 4 | const logger = require('../server/utils/logger'); 5 | 6 | module.exports = (rewriteConfig, filterOptions) => { 7 | const rewriteFn = createPathRewriter(rewriteConfig); 8 | return interceptor => { 9 | return interceptor.request.add(({request}) => { 10 | // TODO 这里是url重写 11 | // '^/api/old-path': '/api/new-path' 12 | const result = rewriteFn(request.url); 13 | if (result) { 14 | request.url = result; 15 | } 16 | }, filterOptions); 17 | }; 18 | }; 19 | /** 20 | * Create rewrite function, to cache parsed rewrite rules. 21 | * 22 | * @param {Object} rewriteConfig 23 | * @return {Function} Function to rewrite paths; This function should accept `path` (request.url) as parameter 24 | */ 25 | function createPathRewriter(rewriteConfig) { 26 | let rulesCache; 27 | 28 | if (!isValidRewriteConfig(rewriteConfig)) { 29 | return; 30 | } 31 | 32 | if (typeof rewriteConfig === 'function') { 33 | const customRewriteFn = rewriteConfig; 34 | return customRewriteFn; 35 | } 36 | rulesCache = parsePathRewriteRules(rewriteConfig); 37 | return rewritePath; 38 | 39 | function rewritePath(path) { 40 | let result = path; 41 | 42 | for (const rule of rulesCache) { 43 | if (rule.regex.test(path)) { 44 | result = result.replace(rule.regex, rule.value); 45 | logger.debug('Rewriting path from "%s" to "%s"', path, result); 46 | break; 47 | } 48 | } 49 | 50 | return result; 51 | } 52 | } 53 | 54 | function isValidRewriteConfig(rewriteConfig) { 55 | if (typeof rewriteConfig === 'function') { 56 | return true; 57 | } else if (isPlainObj(rewriteConfig)) { 58 | return Object.keys(rewriteConfig).length !== 0; 59 | } else if (rewriteConfig === undefined || rewriteConfig === null) { 60 | return false; 61 | } 62 | throw new Error('Invalid path-rewrite config'); 63 | } 64 | 65 | function parsePathRewriteRules(rewriteConfig) { 66 | const rules = []; 67 | 68 | if (isPlainObj(rewriteConfig)) { 69 | for (const [key] of Object.entries(rewriteConfig)) { 70 | rules.push({ 71 | regex: new RegExp(key), 72 | value: rewriteConfig[key] 73 | }); 74 | logger.info('Proxy rewrite rule created: "%s" ~> "%s"', key, rewriteConfig[key]); 75 | } 76 | } 77 | 78 | return rules; 79 | } 80 | -------------------------------------------------------------------------------- /frontend/network-standalone/NetworkFrameGrouper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import {NetworkGroupNode} from './NetworkDataGridNode.js'; 6 | import {GroupLookupInterface, NetworkLogView} from './NetworkLogView.js'; // eslint-disable-line no-unused-vars 7 | 8 | /** 9 | * @implements {GroupLookupInterface} 10 | */ 11 | export class NetworkFrameGrouper { 12 | /** 13 | * @param {!NetworkLogView} parentView 14 | */ 15 | constructor(parentView) { 16 | this._parentView = parentView; 17 | /** @type {!Map} */ 18 | this._activeGroups = new Map(); 19 | } 20 | 21 | /** 22 | * @override 23 | * @param {!SDK.NetworkRequest} request 24 | * @return {?NetworkGroupNode} 25 | */ 26 | groupNodeForRequest(request) { 27 | const frame = SDK.ResourceTreeModel.frameForRequest(request); 28 | if (!frame || frame.isTopFrame()) { 29 | return null; 30 | } 31 | let groupNode = this._activeGroups.get(frame); 32 | if (groupNode) { 33 | return groupNode; 34 | } 35 | groupNode = new FrameGroupNode(this._parentView, frame); 36 | this._activeGroups.set(frame, groupNode); 37 | return groupNode; 38 | } 39 | 40 | /** 41 | * @override 42 | */ 43 | reset() { 44 | this._activeGroups.clear(); 45 | } 46 | } 47 | 48 | export class FrameGroupNode extends NetworkGroupNode { 49 | /** 50 | * @param {!NetworkLogView} parentView 51 | * @param {!SDK.ResourceTreeFrame} frame 52 | */ 53 | constructor(parentView, frame) { 54 | super(parentView); 55 | this._frame = frame; 56 | } 57 | 58 | /** 59 | * @override 60 | */ 61 | displayName() { 62 | return new Common.ParsedURL(this._frame.url).domain() || this._frame.name || '