├── __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 | 
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 || '