├── README.md ├── src ├── layouts │ ├── index.less │ ├── Content │ │ ├── index.less │ │ └── index.tsx │ ├── Sider │ │ ├── index.less │ │ └── index.tsx │ ├── ErrorBoundary │ │ └── index.tsx │ └── index.tsx ├── types │ ├── index.d.ts │ ├── less.d.ts │ └── assets.d.ts ├── assets │ └── logo.png ├── pages │ ├── 404 │ │ ├── index.less │ │ └── index.tsx │ ├── ModifyHeader │ │ ├── index.less │ │ ├── helper.tsx │ │ ├── ModifyHeaderModal.tsx │ │ └── index.tsx │ └── Record │ │ ├── index.less │ │ ├── index.tsx │ │ ├── helper.tsx │ │ ├── VirtualTable.tsx │ │ └── RecordDetail.tsx ├── utils │ ├── lazyLoad.ts │ └── getSelectedKeyPath.ts ├── components │ ├── Icon │ │ └── index.tsx │ ├── Title │ │ ├── index.less │ │ └── index.tsx │ ├── hooks │ │ └── useBreadcrumb.tsx │ ├── AsyncButton │ │ └── index.tsx │ └── ButtonGroup │ │ └── index.tsx ├── styles │ ├── global.less │ ├── theme.less │ └── normalize.less ├── store │ ├── global.ts │ ├── index.ts │ └── view.ts ├── routes.ts ├── index.html └── index.tsx ├── .eslintignore ├── main ├── anyproxy │ ├── resource │ │ ├── neijuan.png │ │ ├── cert_error.pug │ │ ├── 502.pug │ │ └── cert_download.pug │ ├── module_sample │ │ ├── simple_use.js │ │ ├── https_config.js │ │ ├── core_reload.js │ │ └── normal_use.js │ ├── lib │ │ ├── configUtil.js │ │ ├── wsServerMgr.js │ │ ├── rule_default.js │ │ ├── ruleLoader.js │ │ ├── requestErrorHandler.js │ │ ├── log.js │ │ ├── certMgr.js │ │ ├── wsServer.js │ │ ├── httpsServerMgr.js │ │ ├── systemProxyMgr.js │ │ ├── util.js │ │ ├── webInterface.js │ │ ├── recorder.js │ │ └── requestHandler.js │ └── index.js ├── menu │ ├── index.js │ └── menu.js ├── helper │ └── index.js ├── config │ └── index.js ├── index.js ├── payload.js ├── rule │ ├── index.js │ └── beforeSendRequest.js ├── event │ ├── quit.js │ └── communication.js ├── window │ ├── loading.html │ └── index.js └── proxy │ └── index.js ├── .prettierignore ├── .stylelintrc ├── .editorconfig ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── .ols.config.ts ├── .eslintrc └── package.json /README.md: -------------------------------------------------------------------------------- 1 | ## 代理工具桌面版 2 | -------------------------------------------------------------------------------- /src/layouts/index.less: -------------------------------------------------------------------------------- 1 | .layout-wrap { 2 | padding: 8px; 3 | } -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | proxyAPI: any 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | web 4 | web2 5 | resource 6 | *.sh 7 | docs -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisestCoder/dyn-proxy/main/src/assets/logo.png -------------------------------------------------------------------------------- /main/anyproxy/resource/neijuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisestCoder/dyn-proxy/main/main/anyproxy/resource/neijuan.png -------------------------------------------------------------------------------- /src/types/less.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less' { 2 | const resource: { [key: string]: string } 3 | export = resource 4 | } 5 | -------------------------------------------------------------------------------- /src/types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpg' 3 | declare module '*.jpeg' 4 | declare module '*.gif' 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | dist/ 3 | yarn.lock 4 | yarn-error.log 5 | *.sh 6 | .gitignore 7 | .prettierignore 8 | .DS_Store 9 | .editorconfig 10 | .eslintignore 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": ["node_modules/**", "**/*.json"], 3 | "rules": { 4 | "no-descending-specificity": null, 5 | "no-duplicate-selectors": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 🎨 editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = tab 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/pages/404/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/theme.less'; 2 | 3 | .not-found-wrapper { 4 | margin: 15vh auto 0; 5 | color: @text-color-secondary; 6 | text-align: center; 7 | } 8 | 9 | .refresh-button { 10 | margin-left: 16px; 11 | } 12 | -------------------------------------------------------------------------------- /main/menu/index.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const Menu = electron.Menu; 3 | 4 | module.exports = function createMenu() { 5 | const menus = require('./menu') 6 | const menu = Menu.buildFromTemplate(menus) 7 | Menu.setApplicationMenu(menu) 8 | } 9 | -------------------------------------------------------------------------------- /main/anyproxy/module_sample/simple_use.js: -------------------------------------------------------------------------------- 1 | const AnyProxy = require('../proxy'); 2 | 3 | const options = { 4 | port: 8001, 5 | webInterface: { 6 | enable: true 7 | } 8 | }; 9 | const proxyServer = new AnyProxy.ProxyServer(options); 10 | proxyServer.start(); 11 | -------------------------------------------------------------------------------- /src/layouts/Content/index.less: -------------------------------------------------------------------------------- 1 | .content-wrapper { 2 | padding-left: 8px; 3 | } 4 | 5 | .main-content { 6 | height: 100%; 7 | padding: 12px; 8 | overflow: auto; 9 | background-color: #fff; 10 | } 11 | 12 | .breadcrumb-wrapper { 13 | margin-bottom: 20px; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/lazyLoad.ts: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable' 2 | import Loading from '@/components/RouteLoading' 3 | 4 | const lazyLoadComponent = (component) => { 5 | return Loadable({ 6 | loader: component, 7 | loading: Loading, 8 | }) 9 | } 10 | 11 | export default lazyLoadComponent 12 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IconClass { 4 | icon: string 5 | } 6 | 7 | const TyIcon = (props: IconClass) => { 8 | const { icon } = props 9 | return 10 | } 11 | 12 | export default TyIcon 13 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | @import './normalize.less'; 2 | 3 | 4 | /* flex 垂直居中,左右对齐 */ 5 | .flex-around-center { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | } 10 | /* flex 垂直居中,左对齐 */ 11 | .flex-start-center { 12 | display: flex; 13 | align-items: center; 14 | justify-content: flex-start; 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "consistent", 3 | "singleQuote": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "semi": false, 7 | "bracketSpacing": true, 8 | "endOfLine": "lf", 9 | "arrowParens": "always", 10 | "tabWidth": 2, 11 | "useTabs": false, 12 | "printWidth": 106, 13 | "trailingComma": "all" 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | build 4 | node_modules 5 | 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | -------------------------------------------------------------------------------- /main/helper/index.js: -------------------------------------------------------------------------------- 1 | const execSync = require('child_process').execSync; 2 | const util = require('../anyproxy/lib/util') 3 | 4 | exports.openCAFile = function openCAFile() { 5 | const certDir = util.getAnyProxyPath('certificates') 6 | const isWin = /^win/.test(process.platform); 7 | if (isWin) { 8 | execSync('start .', { cwd: certDir }); 9 | } else { 10 | execSync('open .', { cwd: certDir }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/store/global.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, makeObservable } from 'mobx' 2 | 3 | export type UserInfo = { 4 | nick?: string 5 | backUrl?: string 6 | } 7 | 8 | export default class GlobalStore { 9 | @observable 10 | collapsed: boolean = false 11 | 12 | constructor() { 13 | makeObservable(this) 14 | } 15 | 16 | @action 17 | setCollapsed = (collapsed: boolean) => { 18 | this.collapsed = collapsed 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Title/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | display: inline-block; 4 | color: #2a343b; 5 | font-weight: 500; 6 | font-size: 14px; 7 | line-height: 22px; 8 | } 9 | 10 | .sign { 11 | padding-left: 10px; 12 | 13 | &::before { 14 | position: absolute; 15 | top: 4px; 16 | left: 0; 17 | display: block; 18 | width: 3px; 19 | height: 14px; 20 | background: #1f86e1; 21 | content: ''; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | // 请求抓包 3 | { 4 | link: '/', 5 | component: () => import('@/pages/Record'), 6 | name: '请求抓包', 7 | showMenu: true, 8 | children: [], 9 | }, 10 | 11 | // 请求抓包 12 | { 13 | link: '/modifyheader', 14 | component: () => import('@/pages/ModifyHeader'), 15 | name: '修改请求头', 16 | showMenu: true, 17 | children: [], 18 | }, 19 | ] 20 | 21 | export type RouteTypes = typeof routes 22 | 23 | export default routes 24 | -------------------------------------------------------------------------------- /src/pages/ModifyHeader/index.less: -------------------------------------------------------------------------------- 1 | .header { 2 | border-bottom: 1px solid #f0f0f0; 3 | padding: 10px; 4 | margin: -12px -12px 10px -12px; 5 | } 6 | 7 | .table { 8 | height: calc(100vh - 83px); 9 | overflow-y: auto; 10 | 11 | :global { 12 | table { 13 | font-size: 12px; 14 | } 15 | td { 16 | word-break: break-all; 17 | } 18 | } 19 | } 20 | 21 | .button-group { 22 | :global(.ant-btn) { 23 | padding-right: 0; 24 | padding-left: 0; 25 | font-size: 12px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /main/anyproxy/lib/configUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * a util to set and get all configuable constant 3 | * 4 | */ 5 | const path = require('path'); 6 | 7 | const USER_HOME = process.env.HOME || process.env.USERPROFILE; 8 | const DEFAULT_ANYPROXY_HOME = path.join(USER_HOME, '/.dynproxy/'); 9 | 10 | /** 11 | * return DynProxy's home path 12 | */ 13 | module.exports.getAnyProxyHome = function () { 14 | const ENV_ANYPROXY_HOME = process.env.ANYPROXY_HOME || ''; 15 | return ENV_ANYPROXY_HOME || DEFAULT_ANYPROXY_HOME; 16 | } 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import GlobalStore from './global' 3 | 4 | const combine = { 5 | global: new GlobalStore(), 6 | } 7 | 8 | type Store = typeof combine 9 | 10 | export const storeContext = createContext(combine) 11 | 12 | export const useStore = (): Store => { 13 | return useContext(storeContext) 14 | } 15 | 16 | export function useSelector(selector: (state: Store) => TSelected): TSelected { 17 | const all = useStore() 18 | return selector(all) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, CSSProperties } from 'react' 2 | import cn from 'classnames' 3 | import styles from './index.less' 4 | 5 | export type TitleProps = { 6 | sign?: boolean 7 | style?: CSSProperties 8 | className?: string 9 | } 10 | 11 | const Title: FC = ({ children, style = {}, className = '', sign = false }) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | 19 | export default Title 20 | -------------------------------------------------------------------------------- /src/components/hooks/useBreadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | import getSelectedKeyPath from '@/utils/getSelectedKeyPath' 4 | import routes from '../../routes' 5 | 6 | export default function useBreadcrumb() { 7 | const location = useLocation() 8 | const [breadcrumb, setBreadcrumb] = useState([]) 9 | 10 | useEffect(() => { 11 | getSelectedKeyPath(routes, location.pathname, (paths: any[]) => { 12 | setBreadcrumb(paths) 13 | }) 14 | }, [location.pathname]) 15 | 16 | return breadcrumb 17 | } 18 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= htmlWebpackPlugin.options.title %> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from 'antd' 2 | import zhCN from 'antd/es/locale/zh_CN' 3 | import dayjs from 'dayjs' 4 | import 'dayjs/locale/zh-cn' 5 | import React, { FC } from 'react' 6 | import ReactDOM from 'react-dom' 7 | import Layout from './layouts' 8 | 9 | import '@/styles/global.less' 10 | import 'antd/dist/antd.css' 11 | 12 | dayjs.locale('zh-cn') 13 | 14 | const App: FC = () => { 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | ReactDOM.render(, document.getElementById('app')) 23 | -------------------------------------------------------------------------------- /src/styles/theme.less: -------------------------------------------------------------------------------- 1 | /* 主题变量 */ 2 | 3 | @primary-color: #1890ff; // 全局主色 4 | @link-color: #1890ff; // 链接色 5 | @success-color: #52c41a; // 成功色 6 | @warning-color: #faad14; // 警告色 7 | @error-color: #f5222d; // 错误色 8 | @font-size-base: 14px; // 主字号 9 | @heading-color: rgba(0, 0, 0, 0.85); // 标题色 10 | @text-color: rgba(0, 0, 0, 0.65); // 主文本色 11 | @text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色 12 | @disabled-color: rgba(0, 0, 0, 0.25); // 失效色 13 | @border-radius-base: 4px; // 组件/浮层圆角 14 | @border-color-base: #d9d9d9; // 边框色 15 | @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影 16 | @white-color: #fff; 17 | -------------------------------------------------------------------------------- /main/config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | /** 5 | * 获取配置信息 6 | * @returns config 7 | */ 8 | exports.getConfig = function getConfig() { 9 | let config 10 | try { 11 | config = JSON.parse(fs.readFileSync(path.join(global.rootDir, 'config.json'), 'utf-8')) 12 | } catch (e) { 13 | config = {} 14 | } 15 | 16 | return config 17 | } 18 | 19 | /** 20 | * 更新配置信息 21 | * @param {*} config 22 | */ 23 | exports.setConfig = function setConfig(config) { 24 | console.log(config) 25 | config.updates = { 26 | now: Date.now(), 27 | record: JSON.stringify(config) 28 | } 29 | fs.writeFileSync(path.join(global.rootDir, 'config.json'), JSON.stringify(config)) 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "lib": ["dom", "es2017"], 6 | "allowJs": true, 7 | "checkJs": true, 8 | "jsx": "preserve", 9 | "declaration": false, 10 | "sourceMap": true, 11 | "noImplicitAny": false, 12 | "strictNullChecks": false, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "baseUrl": "./", 17 | "moduleResolution": "node", 18 | "paths": { 19 | "*": ["*"], 20 | "@/*": ["src/*"] 21 | }, 22 | "esModuleInterop": true, 23 | "experimentalDecorators": true, 24 | "skipLibCheck": true 25 | }, 26 | "include": ["src", "src/types"] 27 | } 28 | -------------------------------------------------------------------------------- /src/layouts/Sider/index.less: -------------------------------------------------------------------------------- 1 | .sider-wrap { 2 | :global(.ant-layout-sider-trigger) { 3 | bottom: 8px; 4 | border-top: 1px solid rgba(66,65,65,.1); 5 | } 6 | } 7 | 8 | .logo { 9 | background: rgba(255, 255, 255, 0.2); 10 | position: relative; 11 | display: flex; 12 | align-items: center; 13 | padding: 16px; 14 | cursor: pointer; 15 | transition: padding .3s cubic-bezier(.645,.045,.355,1); 16 | 17 | img { 18 | width: 50px; 19 | } 20 | 21 | h1 { 22 | display: inline-block; 23 | height: 32px; 24 | margin: 0 0 0 12px; 25 | color: rgb(24, 144, 255); 26 | font-weight: 600; 27 | font-size: 18px; 28 | line-height: 32px; 29 | vertical-align: middle; 30 | animation: pro-layout-title-hide .3s; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.ols.config.ts: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | 3 | const publicPaths = { 4 | development: '', 5 | production: '/dist', 6 | }; 7 | 8 | module.exports = { 9 | // 类型:项目 | 组件 10 | type: 'project', 11 | // 配置全局变量 12 | define: { 13 | BASE_NAME: 'bs14/#' 14 | }, 15 | // webpack配置,可以是一个对象,或者一个函数 16 | configureWebpack(config, merge) { 17 | return merge(config, { 18 | output: { 19 | publicPath: publicPaths[process.env.NODE_ENV] 20 | }, 21 | plugins: [ 22 | ] 23 | }); 24 | }, 25 | // 链式操作webpack配置 26 | chainWebpack(chian, config) { 27 | }, 28 | // 插件,是一个个函数组成的数组Array<(ctx) => void>,插件默认透出ctx上下文,用户可以通过上下文完成一些操作 29 | plugins: [ 30 | { 31 | name: 'electron-run', 32 | fn() { 33 | child_process.exec('yarn run electron-run') 34 | }, 35 | 36 | } 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /main/anyproxy/module_sample/https_config.js: -------------------------------------------------------------------------------- 1 | const AnyProxy = require('../proxy'); 2 | const exec = require('child_process').exec; 3 | 4 | if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) { 5 | AnyProxy.utils.certMgr.generateRootCA((error, keyPath) => { 6 | // let users to trust this CA before using proxy 7 | if (!error) { 8 | const certDir = require('path').dirname(keyPath); 9 | console.log('The cert is generated at', certDir); 10 | const isWin = /^win/.test(process.platform); 11 | if (isWin) { 12 | exec('start .', { cwd: certDir }); 13 | } else { 14 | exec('open .', { cwd: certDir }); 15 | } 16 | } else { 17 | console.error('error when generating rootCA', error); 18 | } 19 | }); 20 | } else { 21 | // clear all the certificates 22 | // AnyProxy.utils.certMgr.clearCerts() 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/404/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Button } from 'antd' 3 | import { withRouter, RouteComponentProps } from 'react-router-dom' 4 | import styles from './index.less' 5 | 6 | const PageNotFound: FC = ({ history }) => { 7 | const linkToHomePage = () => { 8 | history.push('/') 9 | } 10 | const refreshPage = () => { 11 | window.location.reload() 12 | } 13 | 14 | return ( 15 | 16 | 19 | 对不起,您访问的页面已被删除或不存在 20 | 21 | 22 | 首页 23 | 24 | 25 | 刷新 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default withRouter(PageNotFound) 33 | -------------------------------------------------------------------------------- /src/components/AsyncButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useState } from 'react' 2 | import { Button } from 'antd' 3 | import { ButtonProps } from 'antd/lib/button' 4 | 5 | interface IProps extends ButtonProps { 6 | onClick: (p?: any) => void | Promise 7 | } 8 | 9 | const AsyncButton: FC = ({ children, onClick, ...otherProps }) => { 10 | const [loading, setLoading] = useState(false) 11 | 12 | const onButtonClick = useCallback( 13 | (...args) => { 14 | const promise = onClick(...args) 15 | 16 | if (promise && promise.finally) { 17 | setLoading(true) 18 | return promise.finally(() => { 19 | setLoading(false) 20 | }) 21 | } 22 | }, 23 | [onClick], 24 | ) 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ) 31 | } 32 | 33 | export default AsyncButton 34 | -------------------------------------------------------------------------------- /src/utils/getSelectedKeyPath.ts: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router' 2 | import { RouteTypes } from '../routes' 3 | 4 | // 获取指定节点的路径 5 | export default function getSelectedKeyPath(_menus: RouteTypes, _pathname: string, callback) { 6 | const paths: RouteTypes = [] 7 | try { 8 | const getNodePath = (nodes: RouteTypes) => { 9 | // eslint-disable-next-line no-restricted-syntax 10 | for (const node of nodes) { 11 | paths.push(node) 12 | if ( 13 | matchPath(_pathname, { 14 | path: node.link, 15 | exact: true, 16 | }) 17 | ) { 18 | throw new Error('get node') 19 | } 20 | if (node.children?.length) { 21 | getNodePath(node.children as any) 22 | paths.pop() 23 | } else { 24 | paths.pop() 25 | } 26 | } 27 | } 28 | getNodePath(_menus) 29 | } catch (err) { 30 | callback(paths) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /main/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const os = require('os') 4 | const electron = require('electron') 5 | const mkdirp = require('mkdirp') 6 | const app = electron.app 7 | 8 | const createWindow = require('./window') 9 | const createMenu = require('./menu') 10 | const handleQuit = require('./event/quit') 11 | const communication = require('./event/communication') 12 | const { startProxy } = require('./proxy') 13 | 14 | global.mainWindow = null 15 | global.rootDir = path.join(os.homedir(), '.dynproxy') 16 | 17 | if (!fs.existsSync(rootDir)) { 18 | mkdirp.sync(rootDir) 19 | } 20 | 21 | startProxy() 22 | 23 | app.on('ready', () => { 24 | createWindow() 25 | createMenu() 26 | handleQuit() 27 | communication() 28 | }); 29 | 30 | app.on('window-all-closed', () => { 31 | if (process.platform !== 'darwin') { 32 | app.quit(); 33 | } 34 | }); 35 | 36 | app.on('activate', () => { 37 | if (global.mainWindow === null) { 38 | createWindow() 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /main/payload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | 3 | function dispatch(eventName, args, callback) { 4 | if (typeof args === 'function' && typeof callback === 'undefined') { 5 | callback = args 6 | args = {} 7 | } 8 | 9 | if(callback) { 10 | ipcRenderer.on(`${eventName}Apply`, callback) 11 | } 12 | ipcRenderer.send(eventName, args); 13 | } 14 | 15 | contextBridge.exposeInMainWorld('proxyAPI', { 16 | // 监听请求更新 17 | updateListener(callback) { 18 | dispatch('onUpdate', (e, data) => { 19 | callback(data) 20 | }) 21 | }, 22 | 23 | // 获取请求体数据 24 | fetchRecordBody(id, callback) { 25 | dispatch('onFetchRecordBody', { id }, (e, data) => { 26 | callback(data) 27 | }) 28 | }, 29 | 30 | // 获取用户定义的请求头 31 | fetchHeaderRules(callback) { 32 | dispatch('onFetchHeaderRules', (e, data) => { 33 | callback(data) 34 | }) 35 | }, 36 | 37 | // 更新用户定义的请求头 38 | updateHeaderRules(newRecords) { 39 | dispatch('onUpdateHeaderRules', { newRecords }) 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /main/anyproxy/lib/wsServerMgr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * manage the websocket server 3 | * 4 | */ 5 | const ws = require('ws'); 6 | const logUtil = require('./log.js'); 7 | 8 | const WsServer = ws.Server; 9 | 10 | /** 11 | * get a new websocket server based on the server 12 | * @param @required {object} config 13 | {string} config.server 14 | {handler} config.handler 15 | */ 16 | function getWsServer(config) { 17 | const wss = new WsServer({ 18 | server: config.server 19 | }); 20 | 21 | wss.on('connection', config.connHandler); 22 | 23 | wss.on('headers', (headers) => { 24 | headers.push('x-anyproxy-websocket:true'); 25 | }); 26 | 27 | wss.on('error', e => { 28 | logUtil.error(`error in websocket proxy: ${e.message},\r\n ${e.stack}`); 29 | console.error('error happened in proxy websocket:', e) 30 | }); 31 | 32 | wss.on('close', e => { 33 | console.error('==> closing the ws server'); 34 | }); 35 | 36 | return wss; 37 | } 38 | 39 | module.exports.getWsServer = getWsServer; 40 | -------------------------------------------------------------------------------- /main/rule/index.js: -------------------------------------------------------------------------------- 1 | const beforeSendRequest = require('./beforeSendRequest') 2 | 3 | module.exports = { 4 | 5 | summary: 'my customized rule for DynProxy', 6 | 7 | beforeSendRequest, 8 | 9 | /** 10 | * 11 | * 12 | * @param {object} requestDetail 13 | * @param {object} responseDetail 14 | */ 15 | *beforeSendResponse(requestDetail, responseDetail) { 16 | return null; 17 | }, 18 | 19 | /** 20 | * 21 | * 22 | * @param {any} requestDetail 23 | * @param {any} error 24 | * @returns 25 | */ 26 | *onError(requestDetail, error) { 27 | return null; 28 | }, 29 | 30 | 31 | /** 32 | * 33 | * 34 | * @param {any} requestDetail 35 | * @param {any} error 36 | * @returns 37 | */ 38 | *onConnectError(requestDetail, error) { 39 | return null; 40 | }, 41 | 42 | 43 | /** 44 | * 45 | * 46 | * @param {any} requestDetail 47 | * @param {any} error 48 | * @returns 49 | */ 50 | *onClientSocketError(requestDetail, error) { 51 | return null; 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /main/anyproxy/resource/cert_error.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Security Vulnerable 5 | style. 6 | body { 7 | color: #666; 8 | line-height: 1.5; 9 | font-size: 13px; 10 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif; 11 | } 12 | 13 | body * { 14 | box-sizing: border-box; 15 | } 16 | 17 | .container { 18 | max-width: 1200px; 19 | padding: 20px; 20 | padding-top: 150px; 21 | margin: 0 auto; 22 | } 23 | 24 | .title { 25 | font-size: 20px; 26 | margin-bottom: 20px; 27 | } 28 | 29 | .explain { 30 | font-size: 14px; 31 | font-weight: 200; 32 | color: #666; 33 | } 34 | 35 | .explainCode { 36 | color: #999; 37 | margin-bottom: 10px; 38 | } 39 | body 40 | .container 41 | div.title 42 | | #{title} 43 | div.explainCode 44 | | #{code} 45 | div.explain 46 | div!= explain 47 | -------------------------------------------------------------------------------- /main/event/quit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 程序退出监控 3 | */ 4 | const { app, BrowserWindow, dialog, ipcMain } = require('electron') 5 | const { disableProxy } = require('../proxy') 6 | let hasQuit = false 7 | 8 | function checkQuit(mainWindow, event) { 9 | const options = { 10 | type: 'info', 11 | title: '关闭确认', 12 | message: '确认要关闭程序吗?', 13 | buttons: ['确认', '取消'] 14 | }; 15 | dialog.showMessageBox(options) 16 | .then(({ response }) => { 17 | if (response === 0) { 18 | disableProxy() 19 | hasQuit = true; 20 | mainWindow = null; 21 | app.exit(0); 22 | } 23 | }) 24 | } 25 | 26 | module.exports = function handleQuit() { 27 | const mainWindow = BrowserWindow.fromId(global.mainId); 28 | mainWindow.on('close', event => { 29 | event.preventDefault(); 30 | checkQuit(mainWindow, event); 31 | }); 32 | app.on('window-all-closed', () => { 33 | if (!hasQuit) { 34 | if (process.platform !== 'darwin') { 35 | disableProxy() 36 | hasQuit = true; 37 | ipcMain.removeAllListeners(); 38 | app.quit(); 39 | } 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button } from 'antd' 3 | 4 | type IState = { 5 | hasError: boolean 6 | } 7 | 8 | class ErrorBoundary extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | hasError: false, 13 | } 14 | } 15 | 16 | static getDerivedStateFromError() { 17 | // Update state so the next render will show the fallback UI. 18 | return { hasError: true } 19 | } 20 | 21 | componentDidCatch(error, errorInfo) { 22 | // You can also log the error to an error reporting service 23 | // eslint-disable-next-line no-console 24 | console.error(error, errorInfo) 25 | } 26 | 27 | retry = () => { 28 | window.location.reload() 29 | } 30 | 31 | render() { 32 | const { hasError } = this.state 33 | if (hasError) { 34 | // You can render any custom fallback UI 35 | return ( 36 | <> 37 | Something went wrong. 38 | Retry 39 | > 40 | ) 41 | } 42 | 43 | return this.props.children 44 | } 45 | } 46 | 47 | export default ErrorBoundary 48 | -------------------------------------------------------------------------------- /src/pages/Record/index.less: -------------------------------------------------------------------------------- 1 | .header { 2 | border-bottom: 1px solid #f0f0f0; 3 | padding: 10px; 4 | margin: -12px -12px 10px -12px; 5 | } 6 | 7 | .table { 8 | height: calc(100vh - 83px); 9 | overflow-y: auto; 10 | 11 | :global { 12 | table { 13 | font-size: 12px; 14 | } 15 | td { 16 | word-break: break-all; 17 | } 18 | } 19 | } 20 | 21 | .drawer { 22 | :global { 23 | .ant-drawer-body { 24 | padding: 0 24px; 25 | } 26 | .ant-drawer-header-title { 27 | justify-content: flex-end; 28 | } 29 | } 30 | } 31 | 32 | .disabled { 33 | color: #999; 34 | } 35 | 36 | .detailBox { 37 | font-size: 12px; 38 | padding: 10px 0; 39 | border-bottom: 1px solid #d9d9d9; 40 | 41 | &:last-child { 42 | border: none 43 | } 44 | } 45 | 46 | .detailLine { 47 | line-height: 26px; 48 | word-break: break-all; 49 | margin-left: 10px; 50 | font-size: 12px; 51 | 52 | :global(label) { 53 | font-weight: 700; 54 | padding-right: 10px; 55 | } 56 | } 57 | 58 | .bodyTab { 59 | margin-top: 12px; 60 | margin-left: 10px; 61 | 62 | :global(.ant-tabs-nav .ant-tabs-tab) { 63 | font-size: 12px; 64 | } 65 | } 66 | 67 | .bodyTabSource { 68 | line-height: 26px; 69 | word-break: break-all; 70 | font-size: 12px; 71 | } 72 | 73 | // .virtual-table-cell { 74 | // padding: 8px; 75 | // overflow: hidden; 76 | // text-overflow: ellipsis; 77 | // white-space: nowrap; 78 | // border-bottom: 1px solid rgba(0,0,0,.06); 79 | 80 | // &:hover { 81 | // background-color: #fafafa; 82 | // } 83 | // } 84 | -------------------------------------------------------------------------------- /main/anyproxy/module_sample/core_reload.js: -------------------------------------------------------------------------------- 1 | const AnyProxy = require('../proxy'); 2 | const exec = require('child_process').exec; 3 | 4 | const AnyProxyRecorder = require('../lib/recorder'); 5 | const WebInterfaceLite = require('../lib/webInterface'); 6 | 7 | /* 8 | ------------------------------- 9 | | ProxyServerA | ProxyServerB | 10 | ------------------------------- ---------------------------- 11 | | Common Recorder | -------(by events)------| WebInterfaceLite | 12 | ------------------------------- ---------------------------- 13 | */ 14 | 15 | 16 | const commonRecorder = new AnyProxyRecorder(); 17 | 18 | // web interface依赖recorder 19 | new WebInterfaceLite({ // common web interface 20 | webPort: 8002 21 | }, commonRecorder); 22 | 23 | // proxy core只依赖recorder,与webServer无关 24 | const optionsA = { 25 | port: 8001, 26 | recorder: commonRecorder, // use common recorder 27 | }; 28 | 29 | const optionsB = { 30 | port: 8005, 31 | recorder: commonRecorder, // use common recorder 32 | }; 33 | 34 | const proxyServerA = new AnyProxy.ProxyCore(optionsA); 35 | const proxyServerB = new AnyProxy.ProxyCore(optionsB); 36 | 37 | proxyServerA.start(); 38 | proxyServerB.start(); 39 | 40 | // after both ready 41 | setTimeout(() => { 42 | exec('curl http://www.qq.com --proxy http://127.0.0.1:8001'); 43 | exec('curl http://www.sina.com.cn --proxy http://127.0.0.1:8005'); 44 | }, 1000); 45 | 46 | // visit http://127.0.0.1 , there should be two records 47 | 48 | -------------------------------------------------------------------------------- /src/layouts/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Breadcrumb } from 'antd' 2 | import React, { useMemo } from 'react' 3 | import { Link } from 'react-router-dom' 4 | import useBreadcrumb from '@/components/hooks/useBreadcrumb' 5 | import styles from './index.less' 6 | 7 | const { Content } = Layout 8 | 9 | interface IProps { 10 | children?: React.ReactElement 11 | name: string 12 | showBreadcrumb?: boolean 13 | } 14 | 15 | const BgContent = (props: IProps) => { 16 | const { children, name, showBreadcrumb } = props 17 | const breadcrumb = useBreadcrumb() 18 | 19 | const breadcrumbs = useMemo(() => [...breadcrumb.slice(0, -1), { name }], [breadcrumb, name]) 20 | 21 | return ( 22 | 23 | 24 | {showBreadcrumb && breadcrumbs.length > 0 && ( 25 | 26 | {breadcrumbs.map(({ name: breadcrumbName, link }) => { 27 | if (link) { 28 | return ( 29 | 30 | {breadcrumbName} 31 | 32 | ) 33 | } 34 | return {breadcrumbName} 35 | })} 36 | 37 | )} 38 | {children} 39 | 40 | 41 | ) 42 | } 43 | 44 | export default BgContent 45 | -------------------------------------------------------------------------------- /src/components/ButtonGroup/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Button组 3 | */ 4 | 5 | import React, { FC, useCallback, CSSProperties } from 'react' 6 | import { Button } from 'antd' 7 | import { ButtonProps } from 'antd/lib/button' 8 | import cn from 'classnames' 9 | import AsyncButton from '@/components/AsyncButton' 10 | 11 | interface BProps extends ButtonProps { 12 | component?: React.ReactElement 13 | isAsync?: boolean 14 | } 15 | interface IProps { 16 | align?: 'left' | 'right' | 'center' 17 | buttons?: BProps[] 18 | style?: CSSProperties 19 | className?: string 20 | space?: number 21 | isAsync?: boolean 22 | } 23 | 24 | const ButtonGroup: FC = ({ 25 | align = 'left', 26 | style = {}, 27 | className, 28 | buttons = [], 29 | space = 20, 30 | isAsync = false, 31 | }) => { 32 | const renderCpt = useCallback( 33 | (item) => { 34 | const { children, component, isAsync: itemIsAsync, ...otherProps } = item 35 | if (component) { 36 | return component 37 | } 38 | 39 | if (isAsync || itemIsAsync) { 40 | return {children} 41 | } 42 | 43 | return {children} 44 | }, 45 | [isAsync], 46 | ) 47 | 48 | return ( 49 | 50 | {buttons.map((item, index) => ( 51 | 52 | {renderCpt(item)} 53 | 54 | ))} 55 | 56 | ) 57 | } 58 | 59 | export default ButtonGroup 60 | -------------------------------------------------------------------------------- /src/layouts/Sider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { Layout, Menu, } from 'antd' 3 | import { Link } from 'react-router-dom' 4 | import { observer } from 'mobx-react-lite' 5 | import { useSelector } from '@/store/index' 6 | import routes, { RouteTypes } from '../../routes' 7 | import logo from '../../assets/logo.png' 8 | 9 | import styles from './index.less' 10 | 11 | const { Sider } = Layout 12 | const { SubMenu } = Menu 13 | 14 | const BgSider = () => { 15 | const { collapsed, setCollapsed } = useSelector(state => state.global) 16 | 17 | const renderMenus = (menus: RouteTypes) => { 18 | return menus.map((menu) => ( 19 | 20 | { 21 | menu.showMenu 22 | ? ( 23 | 24 | {menu.name} 25 | 26 | ) 27 | : null 28 | } 29 | { 30 | Array.isArray(menu.children) && menu.children.length > 0 31 | ? ( 32 | 33 | {renderMenus(menu.children || [])} 34 | 35 | ) 36 | : null 37 | } 38 | 39 | )) 40 | } 41 | 42 | return ( 43 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {renderMenus(routes)} 58 | 59 | 60 | ) 61 | } 62 | 63 | export default observer(BgSider) 64 | -------------------------------------------------------------------------------- /src/pages/Record/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useEffect, useRef, useState } from 'react' 2 | import { Table, Button } from 'antd' 3 | import { CloseOutlined } from '@ant-design/icons' 4 | import { updateHandler, COLUMNS } from './helper' 5 | import VirtualTable from './VirtualTable' 6 | import RecordDetail from './RecordDetail' 7 | 8 | import styles from './index.less' 9 | 10 | const Record: FC = () => { 11 | const [records, setRecords] = useState([]) 12 | const [visible, setVisible] = useState(false) 13 | const dataRef = useRef({}) 14 | 15 | const onClickItem = useCallback((record) => { 16 | dataRef.current = record 17 | setVisible(true) 18 | }, []) 19 | 20 | const onCloseItem = useCallback(() => { 21 | setVisible(false) 22 | }, []) 23 | 24 | const onEmpty = useCallback(() => { 25 | setRecords([]) 26 | }, []) 27 | 28 | useEffect(() => { 29 | const { updateListener } = window.proxyAPI; 30 | updateListener((data) => { 31 | setRecords((origin) => updateHandler(data, origin)) 32 | }) 33 | }, []) 34 | 35 | return ( 36 | <> 37 | 38 | 清空 39 | 40 | ({ 47 | onClick: () => onClickItem(record), 48 | className: record.method === 'CONNECT' ? styles.disabled : '' 49 | })} 50 | rowKey="id" 51 | /> 52 | 57 | > 58 | ) 59 | } 60 | 61 | export default Record 62 | -------------------------------------------------------------------------------- /main/menu/menu.js: -------------------------------------------------------------------------------- 1 | const { app } = require('electron') 2 | const { checkIsOpenProxy, toggleProxyOpen } = require('../proxy') 3 | const AnyProxy = require('../anyproxy') 4 | const { openCAFile } = require('../helper') 5 | 6 | const menus = [ 7 | { 8 | label: 'DynProxy', 9 | submenu: [ 10 | { 11 | label: '开启代理', 12 | type: 'checkbox', 13 | checked: checkIsOpenProxy(), 14 | click: function (item, focusedWindow) { 15 | toggleProxyOpen() 16 | }, 17 | }, 18 | { 19 | label: '安装证书', 20 | click: function (item, focusedWindow) { 21 | if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) { 22 | AnyProxy.utils.certMgr.generateRootCA((error) => { 23 | if (!error) { 24 | openCAFile() 25 | } else { 26 | console.error('error when generating rootCA', error); 27 | } 28 | }); 29 | } else { 30 | openCAFile() 31 | } 32 | }, 33 | } 34 | ] 35 | }, 36 | { 37 | label: '设置', 38 | role: 'window', 39 | submenu: [{ 40 | label: '切换全屏', 41 | accelerator: (function () { 42 | if (process.platform === 'darwin') { 43 | return 'Ctrl+Command+F' 44 | } else { 45 | return 'F11' 46 | } 47 | })(), 48 | click: function (item, focusedWindow) { 49 | if (focusedWindow) { 50 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) 51 | } 52 | } 53 | }, { 54 | label: '最小化', 55 | accelerator: 'CmdOrCtrl+M', 56 | role: 'minimize' 57 | }, { 58 | label: '关闭', 59 | accelerator: 'CmdOrCtrl+W', 60 | role: 'close' 61 | }, { 62 | type: 'separator' 63 | }] 64 | } 65 | ] 66 | 67 | module.exports = menus 68 | -------------------------------------------------------------------------------- /main/window/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 48 | 49 | 50 | 51 | 52 | 53 | Dynproxy 54 | proxy and more... 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/pages/Record/helper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cloneDeep from 'lodash/cloneDeep' 3 | import moment from 'moment' 4 | 5 | export function renderStatusCode(v) { 6 | if (String(v).startsWith('2')) { 7 | return {v} 8 | } 9 | 10 | if (String(v).startsWith('3')) { 11 | return v 12 | } 13 | 14 | return {v} 15 | } 16 | 17 | export const COLUMNS = [ 18 | { key: 'id', dataIndex: 'id', title: '#', width: 40 }, 19 | { key: 'method', dataIndex: 'method', title: 'Method', width: 80 }, 20 | { key: 'statusCode', 21 | dataIndex: 'statusCode', 22 | title: 'Status', 23 | width: 60, 24 | render: renderStatusCode, 25 | }, 26 | { key: 'protocol', dataIndex: 'protocol', title: 'Protocol', width: 60 }, 27 | { key: 'host', dataIndex: 'host', title: 'Host', width: 200 }, 28 | { key: 'path', dataIndex: 'path', title: 'Path', width: 200 }, 29 | { key: 'mime', dataIndex: 'mime', title: 'Mime', width: 100 }, 30 | { 31 | key: 'startTime', 32 | dataIndex: 'startTime', 33 | title: 'Start', 34 | width: 80, 35 | render(v) { 36 | if (!v) { 37 | return '' 38 | } 39 | 40 | return moment(v).format('HH:mm:ss') 41 | } 42 | }, 43 | ] 44 | 45 | export function updateHandler(newRecord, records): any[] { 46 | const newRecords = cloneDeep(records) 47 | const findIndex = newRecords.findIndex(x => x.id === newRecord.id) 48 | 49 | // 新进的数据 50 | if (findIndex < 0) { 51 | return newRecords.concat(newRecord) 52 | } 53 | 54 | // 替换旧数据 55 | newRecords[findIndex] = newRecord 56 | return newRecords 57 | } 58 | 59 | export function safeJsonParser(jsonStr, defaultV?) { 60 | try { 61 | return JSON.parse(jsonStr) 62 | } catch (error) { 63 | return defaultV || {} 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /main/anyproxy/module_sample/normal_use.js: -------------------------------------------------------------------------------- 1 | const AnyProxy = require('../proxy'); 2 | 3 | const options = { 4 | type: 'http', 5 | port: 8001, 6 | rule: null, 7 | webInterface: { 8 | enable: true, 9 | webPort: 8002 10 | }, 11 | throttle: 10000, 12 | forceProxyHttps: true, 13 | silent: false 14 | }; 15 | const proxyServer = new AnyProxy.ProxyServer(options); 16 | 17 | proxyServer.on('ready', () => { 18 | console.log('ready'); 19 | // set as system proxy 20 | proxyServer.close().then(() => { 21 | const proxyServerB = new AnyProxy.ProxyServer(options); 22 | proxyServerB.start(); 23 | }); 24 | 25 | console.log('closed'); 26 | // setTimeout(() => { 27 | 28 | // }, 2000); 29 | 30 | 31 | // AnyProxy.utils.systemProxyMgr.enableGlobalProxy('127.0.0.1', '8001'); 32 | }); 33 | 34 | proxyServer.on('error', (e) => { 35 | console.log('proxy error'); 36 | console.log(e); 37 | }); 38 | 39 | process.on('SIGINT', () => { 40 | // AnyProxy.utils.systemProxyMgr.disableGlobalProxy(); 41 | proxyServer.close(); 42 | process.exit(); 43 | }); 44 | 45 | 46 | proxyServer.start(); 47 | 48 | 49 | // const WebSocketServer = require('ws').Server; 50 | // const wsServer = new WebSocketServer({ port: 8003 },function(){ 51 | // console.log('ready'); 52 | 53 | // try { 54 | // const serverB = new WebSocketServer({ port: 8003 }, function (e, result) { 55 | // console.log('---in B---'); 56 | // console.log(e); 57 | // console.log(result); 58 | // }); 59 | // } catch(e) { 60 | // console.log(e); 61 | // console.log('e'); 62 | // } 63 | 64 | // // wsServer.close(function (e, result) { 65 | // // console.log('in close'); 66 | // // console.log(e); 67 | // // console.log(result); 68 | // // }); 69 | // }); 70 | -------------------------------------------------------------------------------- /main/window/index.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const BrowserWindow = electron.BrowserWindow; 3 | const path = require('path') 4 | const { WEB_INTERFACE_PORT } = require('../proxy') 5 | 6 | let loadingScreen 7 | 8 | function createLoading() { 9 | loadingScreen = new BrowserWindow({ 10 | width: 600, 11 | height: 200, 12 | frame: false, 13 | show: false, 14 | parent: global.mainWindow 15 | }); 16 | 17 | loadingScreen.loadURL(`file://${path.resolve(__dirname)}/loading.html`); 18 | 19 | loadingScreen.on('closed', () => loadingScreen = null); 20 | loadingScreen.webContents.on('did-finish-load', () => { 21 | loadingScreen.show(); 22 | }); 23 | 24 | return loadingScreen 25 | } 26 | 27 | function createWindow() { 28 | createLoading(loadingScreen) 29 | 30 | global.mainWindow = new BrowserWindow({ 31 | width: 1300, 32 | height: 800, 33 | icon: path.resolve(__dirname, '../../icon.png'), 34 | backgroundColor: '#fff', 35 | show: false, 36 | title: '代理工具', // todo 为啥不生效 37 | webPreferences: { 38 | preload: path.resolve(__dirname, '../payload.js') 39 | } 40 | }); 41 | global.mainId = global.mainWindow.id; 42 | global.mainWindow.setTitle('代理工具'); 43 | 44 | if (process.env.NODE_ENV === 'development') { 45 | global.mainWindow.loadURL(`http://localhost:${WEB_INTERFACE_PORT}/`); 46 | global.mainWindow.webContents.openDevTools(); // 打开调试工具 47 | } else { 48 | global.mainWindow.loadURL(`file://${__dirname}/client/index.html`); 49 | } 50 | 51 | global.mainWindow.webContents.on('did-finish-load', () => { 52 | global.mainWindow.show(); 53 | if (loadingScreen) { 54 | loadingScreen.close(); 55 | } 56 | }); 57 | 58 | global.mainWindow.on('closed', () => { 59 | global.mainWindow = null; 60 | }); 61 | } 62 | 63 | module.exports = createWindow 64 | -------------------------------------------------------------------------------- /main/anyproxy/lib/rule_default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | summary: 'the default rule for AnyProxy', 6 | 7 | /** 8 | * 9 | * 10 | * @param {object} requestDetail 11 | * @param {string} requestDetail.protocol 12 | * @param {object} requestDetail.requestOptions 13 | * @param {object} requestDetail.requestData 14 | * @param {object} requestDetail.response 15 | * @param {number} requestDetail.response.statusCode 16 | * @param {object} requestDetail.response.header 17 | * @param {buffer} requestDetail.response.body 18 | * @returns 19 | */ 20 | *beforeSendRequest(requestDetail) { 21 | return null; 22 | }, 23 | 24 | 25 | /** 26 | * 27 | * 28 | * @param {object} requestDetail 29 | * @param {object} responseDetail 30 | */ 31 | *beforeSendResponse(requestDetail, responseDetail) { 32 | return null; 33 | }, 34 | 35 | 36 | /** 37 | * default to return null 38 | * the user MUST return a boolean when they do implement the interface in rule 39 | * 40 | * @param {any} requestDetail 41 | * @returns 42 | */ 43 | *beforeDealHttpsRequest(requestDetail) { 44 | return null; 45 | }, 46 | 47 | /** 48 | * 49 | * 50 | * @param {any} requestDetail 51 | * @param {any} error 52 | * @returns 53 | */ 54 | *onError(requestDetail, error) { 55 | return null; 56 | }, 57 | 58 | 59 | /** 60 | * 61 | * 62 | * @param {any} requestDetail 63 | * @param {any} error 64 | * @returns 65 | */ 66 | *onConnectError(requestDetail, error) { 67 | return null; 68 | }, 69 | 70 | 71 | /** 72 | * 73 | * 74 | * @param {any} requestDetail 75 | * @param {any} error 76 | * @returns 77 | */ 78 | *onClientSocketError(requestDetail, error) { 79 | return null; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, lazy, Suspense } from 'react' 2 | import { Layout } from 'antd' 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' 4 | import { observer } from 'mobx-react-lite' 5 | import PageNotFound from '@/pages/404' 6 | import routes, { RouteTypes } from '../routes' 7 | import Content from './Content' 8 | import styles from './index.less' 9 | import Sider from './Sider' 10 | import ErrorBoundary from './ErrorBoundary' 11 | 12 | interface RouteProps { 13 | link: string 14 | component?: () => any 15 | showHeader?: boolean 16 | showSider?: boolean 17 | children?: any[] 18 | name?: string 19 | } 20 | 21 | const BgLayout = () => { 22 | const Routers = useMemo(() => { 23 | function renderRouter(menus: RouteTypes) { 24 | return menus.map((item: RouteProps) => { 25 | const { link, component, name, children } = item 26 | const Component = lazy(component) 27 | 28 | return [ 29 | { 34 | return ( 35 | 36 | 37 | Loading...}> 38 | 39 | 40 | 41 | 42 | ) 43 | }} 44 | />, 45 | ...renderRouter(children || []), 46 | ] 47 | }) 48 | } 49 | 50 | return renderRouter(routes) 51 | }, []) 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | {Routers} 59 | 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | export default observer(BgLayout) 67 | -------------------------------------------------------------------------------- /main/rule/beforeSendRequest.js: -------------------------------------------------------------------------------- 1 | const { getConfig } = require('../config') 2 | const { json2Buffer } = require('../anyproxy/lib/util') 3 | 4 | /** 5 | * 6 | * 7 | * @param {object} requestDetail 8 | * @param {string} requestDetail.protocol 9 | * @param {object} requestDetail.requestOptions 10 | * @param {object} requestDetail.requestData 11 | * @param {object} requestDetail.response 12 | * @param {number} requestDetail.response.statusCode 13 | * @param {object} requestDetail.response.header 14 | * @param {buffer} requestDetail.response.body 15 | * @returns 16 | */ 17 | module.exports = function *beforeSendRequest(requestDetail) { 18 | const config = getConfig() 19 | const newRequestOptions = requestDetail.requestOptions; 20 | 21 | /** 22 | * 修改请求头 23 | * [ 24 | * { 25 | * uniqKey: 'fjweofjwo' 26 | * name: "User-Agent", 27 | * value: "DynProxy/1.0", 28 | * active: true, 29 | * proxyRule: 'all' | 'contains' | 'regexp' 30 | * contains: "www.baidu.com" 31 | * } 32 | * ] 33 | */ 34 | const headerRules = config.headerRules || [] 35 | 36 | console.log('headerRules', headerRules) 37 | 38 | if (headerRules.length) { 39 | headerRules.forEach(({ name, value, active, proxyRule, contains, regexp }) => { 40 | if (!active) { 41 | return 42 | } 43 | 44 | if (proxyRule === 'all') { 45 | newRequestOptions.headers[name] = value 46 | } else if (proxyRule === 'contains' && contains && requestDetail.url.includes(contains)) { 47 | newRequestOptions.headers[name] = value 48 | } else if (proxyRule === 'regexp' && regexp && new RegExp(regexp).test(requestDetail.url)) { 49 | newRequestOptions.headers[name] = value 50 | } 51 | }) 52 | } 53 | 54 | /** 55 | * 修改请求数据 56 | */ 57 | 58 | /** 59 | * 修改请求协议 60 | */ 61 | 62 | /** 63 | * 修改返回头 64 | */ 65 | 66 | /** 67 | * 修改返回数据 68 | */ 69 | 70 | return { 71 | requestOptions: newRequestOptions, 72 | // requestData: json2Buffer({ limit: 20, offset: 0 }) 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/ModifyHeader/helper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ButtonGroup from '@/components/ButtonGroup' 3 | import { Switch } from 'antd' 4 | import styles from './index.less' 5 | 6 | export const getColumns = ({ onEdit, onDelete, onSwitch }) => ([ 7 | { 8 | key: 'active', 9 | dataIndex: 'active', 10 | title: '开关', 11 | width: 80, 12 | render: (value, record) => { 13 | return ( 14 | { 20 | onSwitch(isChecked, record) 21 | }} 22 | /> 23 | ) 24 | } 25 | }, 26 | { key: 'name', dataIndex: 'name', title: 'name', width: 300 }, 27 | { key: 'name', dataIndex: 'value', title: 'value', width: 300 }, 28 | { 29 | title: '操作', 30 | dataIndex: 'action', 31 | width: 100, 32 | fixed: 'right', 33 | render: (_, record) => { 34 | return ( 35 | { 44 | onEdit(record) 45 | }, 46 | }, 47 | { 48 | type: 'link', 49 | size: 'small', 50 | children: '删除', 51 | onClick: () => { 52 | onDelete(record) 53 | }, 54 | }, 55 | ]} 56 | /> 57 | ) 58 | }, 59 | } 60 | ]) 61 | 62 | getRandomID.ids = [] 63 | export function getRandomID() { 64 | // window.crypto.randomUUID() 兼容性有问题 65 | const create = () => 66 | Array.prototype.map 67 | .call(window.crypto.getRandomValues(new Uint8Array(12)), (v) => v.toString(16)) 68 | .join('') 69 | 70 | let id = create() 71 | while (getRandomID.ids.includes(id)) { 72 | id = create() 73 | } 74 | 75 | getRandomID.ids.push(id) 76 | 77 | return id 78 | } 79 | -------------------------------------------------------------------------------- /main/anyproxy/lib/ruleLoader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const proxyUtil = require('./util'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const request = require('request'); 7 | 8 | const cachePath = proxyUtil.getAnyProxyTmpPath(); 9 | 10 | /** 11 | * download a file and cache 12 | * 13 | * @param {any} url 14 | * @returns {string} cachePath 15 | */ 16 | function cacheRemoteFile(url) { 17 | return new Promise((resolve, reject) => { 18 | request(url, (error, response, body) => { 19 | if (error) { 20 | return reject(error); 21 | } else if (response.statusCode !== 200) { 22 | return reject(`failed to load with a status code ${response.statusCode}`); 23 | } else { 24 | const fileCreatedTime = proxyUtil.formatDate(new Date(), 'YYYY_MM_DD_hh_mm_ss'); 25 | const random = Math.ceil(Math.random() * 500); 26 | const fileName = `remote_rule_${fileCreatedTime}_r${random}.js`; 27 | const filePath = path.join(cachePath, fileName); 28 | fs.writeFileSync(filePath, body); 29 | resolve(filePath); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | 36 | /** 37 | * load a local npm module 38 | * 39 | * @param {any} filePath 40 | * @returns module 41 | */ 42 | function loadLocalPath(filePath) { 43 | return new Promise((resolve, reject) => { 44 | const ruleFilePath = path.resolve(process.cwd(), filePath); 45 | if (fs.existsSync(ruleFilePath)) { 46 | resolve(require(ruleFilePath)); 47 | } else { 48 | resolve(require(filePath)); 49 | } 50 | }); 51 | } 52 | 53 | 54 | /** 55 | * load a module from url or local path 56 | * 57 | * @param {any} urlOrPath 58 | * @returns module 59 | */ 60 | function requireModule(urlOrPath) { 61 | return new Promise((resolve, reject) => { 62 | if (/^http/i.test(urlOrPath)) { 63 | resolve(cacheRemoteFile(urlOrPath)); 64 | } else { 65 | resolve(urlOrPath); 66 | } 67 | }).then(localPath => loadLocalPath(localPath)); 68 | } 69 | 70 | module.exports = { 71 | cacheRemoteFile, 72 | loadLocalPath, 73 | requireModule, 74 | }; 75 | -------------------------------------------------------------------------------- /main/anyproxy/resource/502.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title AnyProxy Inner Error 5 | style. 6 | body { 7 | color: #666; 8 | line-height: 1.5; 9 | font-size: 13px; 10 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif; 11 | } 12 | 13 | body * { 14 | box-sizing: border-box; 15 | } 16 | 17 | .stackError { 18 | border-radius: 5px; 19 | padding: 20px; 20 | border: 1px solid #fdc; 21 | background-color: #ffeee6; 22 | color: #666; 23 | } 24 | .stackError li { 25 | list-style-type: none; 26 | } 27 | .infoItem { 28 | position: relative; 29 | overflow: hidden; 30 | border: 1px solid #d5f1fd; 31 | background-color: #eaf8fe; 32 | border-radius: 4px; 33 | margin-bottom: 5px; 34 | padding-left: 70px; 35 | } 36 | .infoItem .label { 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | bottom: 0; 41 | display: flex; 42 | justify-content: flex-start; 43 | align-items: center; 44 | width: 70px; 45 | font-weight: 300; 46 | background-color: #76abc1; 47 | color: #fff; 48 | padding: 5px; 49 | } 50 | .infoItem .value { 51 | overflow:hidden; 52 | padding: 5px; 53 | } 54 | 55 | .tipItem .label { 56 | background-color: #ecf6fd; 57 | } 58 | .tip { 59 | color: #808080; 60 | } 61 | body 62 | h1 # AnyProxy Inner Error 63 | h3 Oops! Error happend when AnyProxy handle the request. 64 | p.tip This is an error occurred inside AnyProxy, not from your target website. 65 | .infoItem 66 | .label 67 | | Error: 68 | .value #{error} 69 | .infoItem 70 | .label 71 | | URL: 72 | .value #{url} 73 | if tipMessage 74 | .infoItem 75 | .label 76 | | TIP: 77 | .value!= tipMessage 78 | p 79 | ul.stackError 80 | each item in errorStack 81 | li= item -------------------------------------------------------------------------------- /main/anyproxy/lib/requestErrorHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * handle all request error here, 5 | * 6 | */ 7 | const pug = require('pug'); 8 | const path = require('path'); 9 | 10 | const error502PugFn = pug.compileFile(path.join(__dirname, '../resource/502.pug')); 11 | const certPugFn = pug.compileFile(path.join(__dirname, '../resource/cert_error.pug')); 12 | 13 | /** 14 | * get error content for certification issues 15 | */ 16 | function getCertErrorContent(error, fullUrl) { 17 | let content; 18 | const title = 'The connection is not private. '; 19 | let explain = 'There are error with the certfication of the site.'; 20 | switch (error.code) { 21 | case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': { 22 | explain = 'The certfication of the site you are visiting is not issued by a known agency, ' 23 | + 'It usually happenes when the cert is a self-signed one.' 24 | + 'If you know and trust the site, you can run DynProxy with option -ignore-unauthorized-ssl to continue.' 25 | 26 | break; 27 | } 28 | default: { 29 | explain = '' 30 | break; 31 | } 32 | } 33 | 34 | try { 35 | content = certPugFn({ 36 | title: title, 37 | explain: explain, 38 | code: error.code 39 | }); 40 | } catch (parseErro) { 41 | content = error.stack; 42 | } 43 | 44 | return content; 45 | } 46 | 47 | /* 48 | * get the default error content 49 | */ 50 | function getDefaultErrorCotent(error, fullUrl) { 51 | let content; 52 | 53 | try { 54 | content = error502PugFn({ 55 | error, 56 | url: fullUrl, 57 | errorStack: error.stack.split(/\n/) 58 | }); 59 | } catch (parseErro) { 60 | content = error.stack; 61 | } 62 | 63 | return content; 64 | } 65 | 66 | /* 67 | * get mapped error content for each error 68 | */ 69 | module.exports.getErrorContent = function (error, fullUrl) { 70 | let content = ''; 71 | error = error || {}; 72 | switch (error.code) { 73 | case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': { 74 | content = getCertErrorContent(error, fullUrl); 75 | break; 76 | } 77 | default: { 78 | content = getDefaultErrorCotent(error, fullUrl); 79 | break; 80 | } 81 | } 82 | 83 | return content; 84 | } 85 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "jest": true 9 | }, 10 | "globals": { 11 | "React": true, 12 | "ReactDOM": true, 13 | "Zepto": true, 14 | "JsBridgeUtil": true 15 | }, 16 | "rules": { 17 | "semi": [0], 18 | "comma-dangle": [0], 19 | "global-require": [0], 20 | "no-alert": [0], 21 | "no-console": [0], 22 | "no-param-reassign": [0], 23 | "max-len": [0], 24 | "func-names": [0], 25 | "no-underscore-dangle": [0], 26 | "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }], 27 | "object-shorthand": [0], 28 | "arrow-body-style": [0], 29 | "no-new": [0], 30 | "strict": [0], 31 | "no-script-url": [0], 32 | "spaced-comment": [0], 33 | "no-empty": [0], 34 | "no-constant-condition": [0], 35 | "no-else-return": [0], 36 | "no-use-before-define": [0], 37 | "no-unused-expressions": [0], 38 | "no-class-assign": [0], 39 | "new-cap": [0], 40 | "array-callback-return": [0], 41 | "prefer-template": [0], 42 | "no-restricted-syntax": [0], 43 | "no-trailing-spaces": [0], 44 | "import/no-unresolved": [0], 45 | "jsx-a11y/img-has-alt": [0], 46 | "camelcase": [0], 47 | "consistent-return": [0], 48 | "guard-for-in": [0], 49 | "one-var": [0], 50 | "react/wrap-multilines": [0], 51 | "react/no-multi-comp": [0], 52 | "react/jsx-no-bind": [0], 53 | "react/prop-types": [0], 54 | "react/prefer-stateless-function": [0], 55 | "react/jsx-first-prop-new-line": [0], 56 | "react/sort-comp": [0], 57 | "import/no-extraneous-dependencies": [0], 58 | "import/extensions": [0], 59 | "react/forbid-prop-types": [0], 60 | "react/require-default-props": [0], 61 | "class-methods-use-this": [0], 62 | "jsx-a11y/no-static-element-interactions": [0], 63 | "react/no-did-mount-set-state": [0], 64 | "jsx-a11y/alt-text": [0], 65 | "import/no-dynamic-require": [0], 66 | "no-extra-boolean-cast": [0], 67 | "no-lonely-if": [0], 68 | "no-plusplus": [0], 69 | "generator-star-spacing": ["error", {"before": true, "after": false}], 70 | "require-yield": [0], 71 | "arrow-parens": [0], 72 | "no-template-curly-in-string": [0], 73 | "no-mixed-operators": [0] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/store/view.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, makeObservable } from 'mobx' 2 | 3 | export type SettingType = 'global' | 'form' | 'table' | 'tableField' | 'formField' 4 | 5 | export type EntityAdd = { 6 | entityId?: string 7 | dataMap: Record 8 | } 9 | 10 | export type Setting = { 11 | type?: SettingType 12 | preview?: boolean 13 | form?: Record 14 | global?: Record 15 | table?: Record 16 | tableField?: Record 17 | formField?: Record 18 | } 19 | 20 | export default class ViewStore { 21 | static DefaultValue = { 22 | entityAdd: { 23 | dataMap: {}, 24 | } as EntityAdd, 25 | setting: { 26 | type: 'global', 27 | preview: false, 28 | form: { 29 | showExpand: true, 30 | expandCount: 9, 31 | alignCount: 3, 32 | }, 33 | table: { 34 | settable: true, 35 | }, 36 | tableField: { 37 | columns: [], 38 | }, 39 | formField: { 40 | fields: [], 41 | }, 42 | } as Setting, 43 | } 44 | 45 | @observable 46 | setting: Setting = ViewStore.DefaultValue.setting 47 | 48 | @observable 49 | entityAdd: EntityAdd = ViewStore.DefaultValue.entityAdd 50 | 51 | constructor() { 52 | makeObservable(this) 53 | } 54 | 55 | @action 56 | setSettingType = (newType: SettingType) => { 57 | this.setting.type = newType 58 | } 59 | 60 | @action 61 | setSettingPreview = (newPreview: boolean) => { 62 | this.setting.preview = newPreview 63 | } 64 | 65 | @action 66 | setSettingDataByType = ({ type, data }: { type: SettingType; data: Record }) => { 67 | this.setting[type] = data 68 | } 69 | 70 | @action 71 | setSettingData = (newData: Setting) => { 72 | this.setting = { 73 | ...this.setting, 74 | ...newData, 75 | } 76 | } 77 | 78 | @action 79 | resetFormAndTable = () => { 80 | this.setting = { 81 | ...this.setting, 82 | table: ViewStore.DefaultValue.setting.table, 83 | tableField: ViewStore.DefaultValue.setting.tableField, 84 | form: ViewStore.DefaultValue.setting.form, 85 | formField: ViewStore.DefaultValue.setting.formField, 86 | } 87 | } 88 | 89 | @action 90 | setEntityId = (newId) => { 91 | this.entityAdd.entityId = newId 92 | } 93 | 94 | @action 95 | setEntityDataMap = (dataMap) => { 96 | this.entityAdd.dataMap = dataMap 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /main/anyproxy/resource/cert_download.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Download rootCA 5 | meta(name='viewport', content='initial-scale=1, maximum-scale=0.5, minimum-scale=1, user-scalable=no') 6 | style. 7 | body { 8 | color: #666; 9 | line-height: 1.5; 10 | font-size: 16px; 11 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif; 12 | } 13 | 14 | body * { 15 | box-sizing: border-box; 16 | } 17 | 18 | .logo { 19 | font-size: 36px; 20 | margin-bottom: 40px; 21 | text-align: center; 22 | } 23 | 24 | .any { 25 | font-weight: 500; 26 | } 27 | 28 | .proxy { 29 | font-weight: 100; 30 | } 31 | 32 | .title { 33 | font-weight: bold; 34 | margin: 20px 0 6px; 35 | } 36 | 37 | .button { 38 | text-align: center; 39 | padding: 4px 15px 5px 15px; 40 | font-size: 14px; 41 | font-weight: 500; 42 | border-radius: 4px; 43 | height: 32px; 44 | margin-bottom: 10px; 45 | display: block; 46 | text-decoration: none; 47 | border-color: #108ee9; 48 | color: rgba(0, 0, 0, .65); 49 | background-color: #fff; 50 | border-style: solid; 51 | border-width: 1px; 52 | border-style: solid; 53 | border-color: #d9d9d9; 54 | } 55 | 56 | .primary { 57 | color: #fff; 58 | background-color: #108ee9; 59 | border-color: #108ee9; 60 | } 61 | 62 | .more { 63 | text-align: center; 64 | font-size: 14px; 65 | } 66 | 67 | .content { 68 | word-break: break-all; 69 | font-size: 14px; 70 | line-height: 1.2; 71 | margin-bottom: 10px; 72 | } 73 | body 74 | .logo 75 | span.any Any 76 | span.proxy Proxy 77 | .title Download: 78 | .content Select a CA file to download, the .crt file is commonly used. 79 | a(href="/fetchCrtFile?type=crt").button.primary rootCA.crt 80 | a(href="/fetchCrtFile?type=cer").button rootCA.cer 81 | .more More 82 | .buttons(style='display: none') 83 | a(href="/fetchCrtFile?type=pem").button rootCA.pem 84 | a(href="/fetchCrtFile?type=der").button rootCA.der 85 | .title User-Agent: 86 | .content #{ua} 87 | script(type='text/javascript'). 88 | window.document.querySelector('.more').addEventListener('click', function (e) { 89 | e.target.style.display = 'none'; 90 | window.document.querySelector('.buttons').style.display = 'block'; 91 | }); -------------------------------------------------------------------------------- /main/event/communication.js: -------------------------------------------------------------------------------- 1 | const { ipcMain } = require('electron') 2 | const { getConfig, setConfig } = require('../config') 3 | const util = require('../anyproxy/lib/util') 4 | 5 | const MAX_CONTENT_SIZE = 1024 * 2000; // 2000kb 6 | 7 | /** 8 | * 通用监听事件,监听渲染进程的消息,并回执 9 | * @param {*} eventName 10 | * @param {*} callback 11 | */ 12 | function listener(eventName, callback) { 13 | ipcMain.on(eventName, (evt, args) => { 14 | callback({ 15 | ...evt, 16 | reply: (res) => evt.reply(`${eventName}Apply`, res) 17 | }, args) 18 | }) 19 | } 20 | 21 | 22 | module.exports = function communication() { 23 | /** 24 | * 监听请求数据更新 25 | */ 26 | listener('onUpdate', (evt) => { 27 | global.recorder.on('update', (data) => { 28 | evt.reply(data) 29 | }); 30 | }) 31 | 32 | /** 33 | * 监听获取请求体数据 34 | */ 35 | listener('onFetchRecordBody', (evt, args) => { 36 | const { id } = args 37 | let res 38 | 39 | recorder.getDecodedBody(id, (err, result) => { 40 | // 返回下载信息 41 | const _resDownload = function (isDownload) { 42 | isDownload = typeof isDownload === 'boolean' ? isDownload : true; 43 | return { 44 | id, 45 | type: result.type, 46 | method: result.meethod, 47 | fileName: result.fileName, 48 | }; 49 | }; 50 | 51 | // 返回内容 52 | const _resContent = () => { 53 | if (util.getByteSize(result.content || '') > MAX_CONTENT_SIZE) { 54 | return _resDownload(true); 55 | } 56 | 57 | return { 58 | id, 59 | type: result.type, 60 | method: result.method, 61 | resBody: result.content 62 | }; 63 | }; 64 | 65 | if (err || !result) { 66 | res = {}; 67 | } else if (result.statusCode === 200 && result.mime) { 68 | // deal with 'application/x-javascript' and 'application/javascript' 69 | if (/json|text|javascript/.test(result.mime)) { 70 | res = _resContent(); 71 | } else if (result.type === 'image') { 72 | res = _resDownload(false); 73 | } else { 74 | res = _resDownload(true); 75 | } 76 | } else { 77 | res = _resContent(); 78 | } 79 | 80 | evt.reply(res) 81 | }); 82 | }) 83 | 84 | /** 85 | * 监听获取请求头 86 | */ 87 | listener('onFetchHeaderRules', (evt) => { 88 | const config = getConfig() 89 | evt.reply(config.headerRules || []) 90 | }) 91 | 92 | /** 93 | * 监听更新请求头 94 | */ 95 | listener('onUpdateHeaderRules', (evt, args) => { 96 | const { newRecords } = args 97 | const config = getConfig() 98 | setConfig({ 99 | ...config, 100 | headerRules: newRecords 101 | }) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /main/anyproxy/lib/log.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const color = require('colorful'); 4 | const util = require('./util'); 5 | 6 | let ifPrint = true; 7 | let logLevel = 0; 8 | const LogLevelMap = { 9 | tip: 0, 10 | system_error: 1, 11 | rule_error: 2, 12 | warn: 3, 13 | debug: 4, 14 | }; 15 | 16 | function setPrintStatus(status) { 17 | ifPrint = !!status; 18 | } 19 | 20 | function setLogLevel(level) { 21 | logLevel = parseInt(level, 10); 22 | } 23 | 24 | function printLog(content, type) { 25 | if (!ifPrint) { 26 | return; 27 | } 28 | 29 | const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'); 30 | switch (type) { 31 | case LogLevelMap.tip: { 32 | if (logLevel > 0) { 33 | return; 34 | } 35 | console.log(color.cyan(`[DynProxy Log][${timeString}]: ` + content)); 36 | break; 37 | } 38 | 39 | case LogLevelMap.system_error: { 40 | if (logLevel > 1) { 41 | return; 42 | } 43 | console.error(color.red(`[DynProxy ERROR][${timeString}]: ` + content)); 44 | break; 45 | } 46 | 47 | case LogLevelMap.rule_error: { 48 | if (logLevel > 2) { 49 | return; 50 | } 51 | 52 | console.error(color.red(`[DynProxy RULE_ERROR][${timeString}]: ` + content)); 53 | break; 54 | } 55 | 56 | case LogLevelMap.warn: { 57 | if (logLevel > 3) { 58 | return; 59 | } 60 | 61 | console.error(color.yellow(`[DynProxy WARN][${timeString}]: ` + content)); 62 | break; 63 | } 64 | 65 | case LogLevelMap.debug: { 66 | console.log(color.cyan(`[DynProxy Log][${timeString}]: ` + content)); 67 | return; 68 | } 69 | 70 | default : { 71 | console.log(color.cyan(`[DynProxy Log][${timeString}]: ` + content)); 72 | break; 73 | } 74 | } 75 | } 76 | 77 | module.exports.printLog = printLog; 78 | 79 | module.exports.debug = (content) => { 80 | printLog(content, LogLevelMap.debug); 81 | }; 82 | 83 | module.exports.info = (content) => { 84 | printLog(content, LogLevelMap.tip); 85 | }; 86 | 87 | module.exports.warn = (content) => { 88 | printLog(content, LogLevelMap.warn); 89 | }; 90 | 91 | module.exports.error = (content) => { 92 | printLog(content, LogLevelMap.system_error); 93 | }; 94 | 95 | module.exports.ruleError = (content) => { 96 | printLog(content, LogLevelMap.rule_error); 97 | }; 98 | 99 | module.exports.setPrintStatus = setPrintStatus; 100 | module.exports.setLogLevel = setLogLevel; 101 | module.exports.T_TIP = LogLevelMap.tip; 102 | module.exports.T_ERR = LogLevelMap.system_error; 103 | module.exports.T_RULE_ERROR = LogLevelMap.rule_error; 104 | module.exports.T_WARN = LogLevelMap.warn; 105 | module.exports.T_DEBUG = LogLevelMap.debug; 106 | -------------------------------------------------------------------------------- /main/proxy/index.js: -------------------------------------------------------------------------------- 1 | const AnyProxy = require('../anyproxy') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const { getConfig, setConfig } = require('../config') 5 | const customizedRule = require('../rule') 6 | 7 | const PROXY_PORT = 5050 8 | const WEB_INTERFACE_PORT = 5051 9 | 10 | let proxyServer 11 | const options = { 12 | port: PROXY_PORT, 13 | rule: customizedRule, 14 | throttle: 10000, 15 | forceProxyHttps: true, 16 | wsIntercept: true, 17 | silent: false, 18 | webInterface: false, 19 | // webInterface: { 20 | // enable: true, 21 | // webPort: WEB_INTERFACE_PORT 22 | // } 23 | }; 24 | 25 | exports.PROXY_PORT = PROXY_PORT 26 | exports.WEB_INTERFACE_PORT = WEB_INTERFACE_PORT 27 | 28 | function checkIsOpenProxy() { 29 | return getConfig().isOpen 30 | } 31 | 32 | function startProxy() { 33 | const execSync = require('child_process').execSync; 34 | 35 | if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) { 36 | AnyProxy.utils.certMgr.generateRootCA((error, keyPath) => { 37 | // let users to trust this CA before using proxy 38 | if (!error) { 39 | const certDir = require('path').dirname(keyPath); 40 | console.log('The cert is generated at', certDir); 41 | const isWin = /^win/.test(process.platform); 42 | if (isWin) { 43 | execSync('start .', { cwd: certDir }); 44 | } else { 45 | execSync('open .', { cwd: certDir }); 46 | } 47 | } else { 48 | console.error('error when generating rootCA', error); 49 | } 50 | }); 51 | } 52 | 53 | proxyServer = proxyServer || new AnyProxy.ProxyServer(options); 54 | 55 | proxyServer.on('ready', () => { 56 | const isOpenProxy = checkIsOpenProxy() 57 | if (isOpenProxy) { 58 | enableProxy() 59 | } 60 | }); 61 | 62 | proxyServer.on('error', (e) => { 63 | if (proxyServer) { 64 | proxyServer.close() 65 | } 66 | }); 67 | 68 | proxyServer.start(); 69 | } 70 | function enableProxy() { 71 | // 开启全局代理服务器 72 | AnyProxy.utils.systemProxyMgr.enableGlobalProxy('127.0.0.1', options.port, 'http'); 73 | AnyProxy.utils.systemProxyMgr.enableGlobalProxy('127.0.0.1', options.port, 'https'); 74 | } 75 | 76 | function disableProxy() { 77 | // 关闭全局代理服务器 78 | AnyProxy.utils.systemProxyMgr.disableGlobalProxy('http'); 79 | AnyProxy.utils.systemProxyMgr.disableGlobalProxy('https'); 80 | } 81 | 82 | function toggleProxyOpen() { 83 | const proxyInfo = getConfig() 84 | proxyInfo.isOpen = !proxyInfo.isOpen 85 | 86 | if (proxyInfo.isOpen) { 87 | // 开启全局代理服务器 88 | enableProxy() 89 | } else { 90 | // 关闭全局代理服务器 91 | disableProxy() 92 | } 93 | 94 | setConfig(proxyInfo) 95 | } 96 | 97 | exports.startProxy = startProxy 98 | exports.getConfig = getConfig 99 | exports.checkIsOpenProxy = checkIsOpenProxy 100 | exports.toggleProxyOpen = toggleProxyOpen 101 | exports.enableProxy = enableProxy 102 | exports.disableProxy = disableProxy 103 | -------------------------------------------------------------------------------- /src/pages/ModifyHeader/ModifyHeaderModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useEffect } from 'react' 2 | import { Modal, Form, Input, Select } from 'antd' 3 | import { ModalProps } from 'antd/lib/modal' 4 | import { getRandomID } from './helper' 5 | 6 | interface IProps extends ModalProps { 7 | onOk?: (p: any) => void 8 | mode?: 'add' | 'edit' 9 | data: Record 10 | } 11 | 12 | const ModifyHeaderModal: FC = ({ onOk = () => {}, mode, data, visible, ...otherProps }) => { 13 | const [form] = Form.useForm() 14 | 15 | const onModalOk = useCallback(() => { 16 | form.validateFields() 17 | .then((values) => { 18 | onOk({ 19 | uniqKey: getRandomID(), 20 | ...values, 21 | }) 22 | }) 23 | }, [onOk]) 24 | 25 | useEffect(() => { 26 | if (visible) { 27 | if (mode === 'add') { 28 | form.setFieldsValue({ 29 | proxyRule: 'all' 30 | }) 31 | } else { 32 | form.setFieldsValue(data) 33 | } 34 | } 35 | }, [visible]) 36 | 37 | return ( 38 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 61 | 62 | 63 | {({ getFieldValue }) => { 64 | return getFieldValue('proxyRule') !== 'contains' 65 | ? null 66 | : ( 67 | 68 | 69 | 70 | ) 71 | }} 72 | 73 | 74 | {({ getFieldValue }) => { 75 | return getFieldValue('proxyRule') !== 'regexp' 76 | ? null 77 | : ( 78 | 79 | 80 | 81 | ) 82 | }} 83 | 84 | 85 | 86 | ) 87 | } 88 | 89 | export default ModifyHeaderModal 90 | /** 91 | * 修改请求头 92 | * [ 93 | * { 94 | * uniqKey: 'fjweofjwo' 95 | * name: "User-Agent", 96 | * value: "DynProxy/1.0", 97 | * active: true, 98 | * proxyRule: 'all' | 'contains' | 'regexp' 99 | * contains: "www.baidu.com" 100 | * } 101 | * ] 102 | */ 103 | -------------------------------------------------------------------------------- /src/pages/ModifyHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useEffect, useState, useRef } from 'react' 2 | import { Table } from 'antd' 3 | import ButtonGroup from '@/components/ButtonGroup' 4 | import cloneDeep from 'lodash/cloneDeep' 5 | import ModifyHeaderModal from './ModifyHeaderModal' 6 | import { getColumns } from './helper' 7 | 8 | import styles from './index.less' 9 | 10 | /** 11 | * 修改请求头 12 | * [ 13 | * { 14 | * name: "User-Agent", 15 | * value: "DynProxy/1.0", 16 | * active: true, 17 | * proxyRule: 'all' | 'contains' | 'regexp' 18 | * contains: "www.baidu.com" 19 | * } 20 | * ] 21 | */ 22 | 23 | const ModifyHeader = () => { 24 | const [record, setRecords] = useState([]) 25 | const [visible, setVisible] = useState(false) 26 | const modalRef = useRef({}) 27 | 28 | const updateRecord = useCallback((values) => { 29 | const { mode, data } = modalRef.current 30 | 31 | setRecords((origin) => { 32 | const newRecords = cloneDeep(origin) 33 | if (mode === 'add') { 34 | newRecords.push(values) 35 | } else { 36 | const findIndex = newRecords.findIndex(x => x.uniqKey === data.uniqKey) 37 | newRecords[findIndex] = values 38 | } 39 | 40 | const { updateHeaderRules } = window.proxyAPI 41 | updateHeaderRules(newRecords) 42 | 43 | return newRecords 44 | }) 45 | }, []) 46 | 47 | const onAdd = useCallback(() => { 48 | setVisible(true) 49 | modalRef.current = { 50 | mode: 'add', 51 | data: {} 52 | } 53 | }, []) 54 | 55 | const onEdit = useCallback((record) => { 56 | setVisible(true) 57 | modalRef.current = { 58 | mode: 'edit', 59 | data: record 60 | } 61 | }, []) 62 | 63 | const onSwitch = useCallback((active, record) => { 64 | modalRef.current = { 65 | mode: 'edit', 66 | data: record 67 | } 68 | 69 | updateRecord({ 70 | ...record, 71 | active, 72 | }) 73 | }, [updateRecord]) 74 | 75 | const onDelete = useCallback(() => {}, []) 76 | 77 | useEffect(() => { 78 | const { fetchHeaderRules } = window.proxyAPI 79 | fetchHeaderRules((records) => { 80 | setRecords(records) 81 | }) 82 | }, []) 83 | 84 | const columns = useMemo(() => getColumns({ onEdit, onDelete, onSwitch }), [onEdit, onDelete, onSwitch]) 85 | 86 | return ( 87 | <> 88 | 89 | 97 | 98 | 106 | { 111 | setVisible(false) 112 | }} 113 | onOk={(values) => { 114 | updateRecord(values) 115 | setVisible(false) 116 | }} 117 | /> 118 | > 119 | ) 120 | } 121 | 122 | export default ModifyHeader 123 | -------------------------------------------------------------------------------- /main/anyproxy/lib/certMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EasyCert = require('node-easy-cert'); 4 | const co = require('co'); 5 | const os = require('os'); 6 | const inquirer = require('inquirer'); 7 | 8 | const util = require('./util'); 9 | const logUtil = require('./log'); 10 | 11 | const options = { 12 | rootDirPath: util.getAnyProxyPath('certificates'), 13 | inMemory: false, 14 | defaultCertAttrs: [ 15 | { name: 'countryName', value: 'CN' }, 16 | { name: 'organizationName', value: 'DynProxy' }, 17 | { shortName: 'ST', value: 'SH' }, 18 | { shortName: 'OU', value: 'DynProxy SSL Proxy' } 19 | ] 20 | }; 21 | 22 | const easyCert = new EasyCert(options); 23 | const crtMgr = util.merge({}, easyCert); 24 | 25 | // rename function 26 | crtMgr.ifRootCAFileExists = easyCert.isRootCAFileExists; 27 | 28 | crtMgr.generateRootCA = function (cb) { 29 | doGenerate(false); 30 | 31 | // set default common name of the cert 32 | function doGenerate(overwrite) { 33 | const rootOptions = { 34 | commonName: 'DynProxy', 35 | overwrite: !!overwrite 36 | }; 37 | 38 | easyCert.generateRootCA(rootOptions, (error, keyPath, crtPath) => { 39 | cb(error, keyPath, crtPath); 40 | }); 41 | } 42 | }; 43 | 44 | crtMgr.getCAStatus = function *() { 45 | return co(function *() { 46 | const result = { 47 | exist: false, 48 | }; 49 | const ifExist = easyCert.isRootCAFileExists(); 50 | if (!ifExist) { 51 | return result; 52 | } else { 53 | result.exist = true; 54 | if (!/^win/.test(process.platform)) { 55 | result.trusted = yield easyCert.ifRootCATrusted; 56 | } 57 | return result; 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * trust the root ca by command 64 | */ 65 | crtMgr.trustRootCA = function *() { 66 | const platform = os.platform(); 67 | const rootCAPath = crtMgr.getRootCAFilePath(); 68 | const trustInquiry = [ 69 | { 70 | type: 'list', 71 | name: 'trustCA', 72 | message: 'The rootCA is not trusted yet, install it to the trust store now?', 73 | choices: ['Yes', "No, I'll do it myself"] 74 | } 75 | ]; 76 | 77 | if (platform === 'darwin') { 78 | const answer = yield inquirer.prompt(trustInquiry); 79 | if (answer.trustCA === 'Yes') { 80 | logUtil.info('About to trust the root CA, this may requires your password'); 81 | // https://ss64.com/osx/security-cert.html 82 | const result = util.execScriptSync(`sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain ${rootCAPath}`); 83 | if (result.status === 0) { 84 | logUtil.info('Root CA install, you are ready to intercept the https now'); 85 | } else { 86 | console.error(result); 87 | logUtil.info('Failed to trust the root CA, please trust it manually'); 88 | util.guideToHomePage(); 89 | } 90 | } else { 91 | logUtil.info('Please trust the root CA manually so https interception works'); 92 | util.guideToHomePage(); 93 | } 94 | } 95 | 96 | 97 | if (/^win/.test(process.platform)) { 98 | logUtil.info('You can install the root CA manually.'); 99 | } 100 | logUtil.info('The root CA file path is: ' + crtMgr.getRootCAFilePath()); 101 | } 102 | 103 | module.exports = crtMgr; 104 | -------------------------------------------------------------------------------- /src/pages/Record/VirtualTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { VariableSizeGrid as Grid } from 'react-window'; 3 | import ResizeObserver from 'rc-resize-observer'; 4 | import classNames from 'classnames'; 5 | import { Table } from 'antd'; 6 | import styles from './index.less' 7 | 8 | export default function VirtualTable(props: Parameters[0]) { 9 | const { columns, scroll } = props; 10 | const [tableWidth, setTableWidth] = useState(0); 11 | 12 | const widthColumnCount = columns!.filter(({ width }) => !width).length; 13 | const mergedColumns = columns!.map(column => { 14 | if (column.width) { 15 | return column; 16 | } 17 | 18 | return { 19 | ...column, 20 | width: Math.floor(tableWidth / widthColumnCount), 21 | }; 22 | }); 23 | 24 | const gridRef = useRef(); 25 | const [connectObject] = useState(() => { 26 | const obj = {}; 27 | Object.defineProperty(obj, 'scrollLeft', { 28 | get: () => null, 29 | set: (scrollLeft: number) => { 30 | if (gridRef.current) { 31 | gridRef.current.scrollTo({ scrollLeft }); 32 | } 33 | }, 34 | }); 35 | 36 | return obj; 37 | }); 38 | 39 | const resetVirtualGrid = () => { 40 | gridRef.current.resetAfterIndices({ 41 | columnIndex: 0, 42 | shouldForceUpdate: true, 43 | }); 44 | }; 45 | 46 | useEffect(() => resetVirtualGrid, [tableWidth]); 47 | 48 | const renderVirtualList = (rawData: object[], { scrollbarSize, ref, onScroll }: any) => { 49 | ref.current = connectObject; 50 | const totalHeight = rawData.length * 34; 51 | 52 | return ( 53 | { 58 | const { width } = mergedColumns[index]; 59 | return totalHeight > scroll!.y! && index === mergedColumns.length - 1 60 | ? (width as number) - scrollbarSize - 1 61 | : (width as number); 62 | }} 63 | height={scroll!.y as number} 64 | rowCount={rawData.length} 65 | rowHeight={() => 34} 66 | width={tableWidth} 67 | onScroll={({ scrollLeft }: { scrollLeft: number }) => { 68 | onScroll({ scrollLeft }); 69 | }} 70 | > 71 | {({ 72 | columnIndex, 73 | rowIndex, 74 | style, 75 | }: { 76 | columnIndex: number; 77 | rowIndex: number; 78 | style: React.CSSProperties; 79 | }) => ( 80 | 86 | {(rawData[rowIndex] as any)[(mergedColumns as any)[columnIndex].dataIndex]} 87 | 88 | )} 89 | 90 | ); 91 | }; 92 | 93 | return ( 94 | { 96 | setTableWidth(width); 97 | }} 98 | > 99 | 107 | 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/pages/Record/RecordDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState, memo } from 'react' 2 | import { Drawer, DrawerProps, Tabs } from 'antd' 3 | import ReactJson from 'react-json-view' 4 | import Title from '@/components/Title' 5 | import { renderStatusCode, safeJsonParser } from './helper' 6 | import styles from './index.less' 7 | 8 | interface IProps extends DrawerProps { 9 | data?: any 10 | } 11 | 12 | const RecordDetail: FC = ({ data, visible, ...otherProps }) => { 13 | const [detail, setDetail] = useState({}) 14 | 15 | useEffect(() => { 16 | if (visible && data?.id) { 17 | const { fetchRecordBody } = window.proxyAPI 18 | fetchRecordBody(data.id, (record) => { 19 | setDetail({ 20 | ...data, 21 | ...record 22 | }) 23 | }) 24 | } 25 | }, [visible]) 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | General 33 | URL:{detail.url} 34 | Method:{detail.method} 35 | Protocol:{detail.protocol} 36 | 37 | 38 | Header 39 | { 40 | Object.keys(detail.reqHeader || {}).map((headerKey) => ( 41 | {headerKey}:{detail.reqHeader[headerKey]} 42 | )) 43 | } 44 | 45 | 46 | Body 47 | 48 | 49 | {detail.reqBody} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | General 60 | 61 | Status Code: 62 | {renderStatusCode(detail.statusCode)} 63 | 64 | 65 | 66 | Header 67 | { 68 | Object.keys(detail.resHeader || {}).map((headerKey) => ( 69 | {headerKey}:{detail.resHeader[headerKey]} 70 | )) 71 | } 72 | 73 | 74 | Body 75 | 76 | 77 | {detail.resBody} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default memo(RecordDetail) 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dyn-proxy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "dev": "ols dev --port 5051", 7 | "electron-run": "cross-env NODE_ENV=development electron ./main/index.js", 8 | "electron-pack": "cross-env NODE_ENV=production electron-packager ./main/index.js --out=pack", 9 | "build": "ols build", 10 | "commit": "git-cz", 11 | "lint:script": "eslint --ext .tsx,.ts,.js,.jsx --fix ./src", 12 | "lint:style": "stylelint --fix 'src/**/*.{less,css}' --syntax less" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged" 17 | } 18 | }, 19 | "lint-staged": { 20 | "*.{json,md,yml}": "prettier --write", 21 | "*.{less,css}": [ 22 | "yarn lint:style" 23 | ], 24 | "*.{tsx,ts,js,jsx}": [ 25 | "yarn lint:script" 26 | ] 27 | }, 28 | "sideEffects": [ 29 | "dist/*", 30 | "*.less", 31 | "*.css" 32 | ], 33 | "author": "", 34 | "license": "ISC", 35 | "dependencies": { 36 | "@ant-design/icons": "^4.2.2", 37 | "antd": "^4.0.2", 38 | "async": "~0.9.0", 39 | "async-task-mgr": ">=1.1.0", 40 | "axios": "^0.19.2", 41 | "body-parser": "^1.13.1", 42 | "brotli": "^1.3.2", 43 | "classnames": "^2.2.6", 44 | "clipboard-js": "^0.3.3", 45 | "co": "^4.6.0", 46 | "colorful": "^2.1.0", 47 | "commander": "~2.11.0", 48 | "component-emitter": "^1.2.1", 49 | "compression": "^1.4.4", 50 | "copy-to-clipboard": "^3.3.1", 51 | "cross-env": "^7.0.3", 52 | "electron": "^17.1.2", 53 | "es6-promise": "^3.3.1", 54 | "express": "^4.8.5", 55 | "fast-json-stringify": "^0.17.0", 56 | "iconv-lite": "^0.4.6", 57 | "inquirer": "^5.2.0", 58 | "ip": "^0.3.2", 59 | "juicer": "^0.6.6-stable", 60 | "lodash": "^4.17.20", 61 | "mime-types": "2.1.11", 62 | "mkdirp": "^1.0.4", 63 | "mobx": "^6.5.0", 64 | "mobx-react-lite": "^3.3.0", 65 | "moment": "^2.15.1", 66 | "nedb": "^1.8.0", 67 | "node-easy-cert": "^1.3.3", 68 | "pug": "^2.0.0-beta6", 69 | "qrcode-npm": "0.0.3", 70 | "query-string": "^6.13.7", 71 | "react-json-view": "^1.21.3", 72 | "react-window": "^1.8.6", 73 | "request": "^2.74.0", 74 | "stream-throttle": "^0.1.3", 75 | "svg-inline-react": "^1.0.2", 76 | "thunkify": "^2.1.2", 77 | "whatwg-fetch": "^1.0.0", 78 | "ws": "^5.1.0" 79 | }, 80 | "devDependencies": { 81 | "@ols-scripts/cli": "^0.0.2", 82 | "@types/node": "^13.7.4", 83 | "@types/prop-types": "^15.7.1", 84 | "@types/react": "^16.9.22", 85 | "@types/react-dom": "^16.9.5", 86 | "@types/react-redux": "^7.1.9", 87 | "antd": "^2.5.0", 88 | "electron-packager": "^15.4.0", 89 | "eslint-config-airbnb": "^15.1.0", 90 | "eslint-plugin-import": "^2.7.0", 91 | "eslint-plugin-jsx-a11y": "^5.1.1", 92 | "eslint-plugin-react": "^7.4.0", 93 | "husky": "^4.2.5", 94 | "lint-staged": "^10.1.3", 95 | "prettier": "^2.0.1", 96 | "react": "^16.12.0", 97 | "react-addons-perf": "^15.4.0", 98 | "react-dom": "^16.12.0", 99 | "react-json-tree": "^0.10.0", 100 | "react-redux": "^4.4.5", 101 | "react-router": "^5.1.2", 102 | "react-router-dom": "^5.2.0", 103 | "react-tap-event-plugin": "^1.0.0", 104 | "redux": "^3.6.0", 105 | "redux-saga": "^0.11.1", 106 | "stream-equal": "0.1.8", 107 | "style-loader": "^0.13.1", 108 | "svg-inline-loader": "^0.7.1", 109 | "tunnel": "^0.0.6", 110 | "url-loader": "^0.5.7", 111 | "urllib": "^2.34.2" 112 | }, 113 | "browserslist": [ 114 | "last 2 versions", 115 | "Firefox ESR", 116 | "> 1%", 117 | "ie >= 11" 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /main/anyproxy/lib/wsServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //websocket server manager 4 | 5 | const WebSocketServer = require('ws').Server; 6 | const logUtil = require('./log'); 7 | 8 | function resToMsg(msg, recorder, cb) { 9 | let result = {}, 10 | jsonData; 11 | 12 | try { 13 | jsonData = JSON.parse(msg); 14 | } catch (e) { 15 | result = { 16 | type: 'error', 17 | error: 'failed to parse your request : ' + e.toString() 18 | }; 19 | cb && cb(result); 20 | return; 21 | } 22 | 23 | if (jsonData.reqRef) { 24 | result.reqRef = jsonData.reqRef; 25 | } 26 | 27 | if (jsonData.type === 'reqBody' && jsonData.id) { 28 | result.type = 'body'; 29 | recorder.getBody(jsonData.id, (err, data) => { 30 | if (err) { 31 | result.content = { 32 | id: null, 33 | body: null, 34 | error: err.toString() 35 | }; 36 | } else { 37 | result.content = { 38 | id: jsonData.id, 39 | body: data.toString() 40 | }; 41 | } 42 | cb && cb(result); 43 | }); 44 | } else { // more req handler here 45 | return null; 46 | } 47 | } 48 | 49 | //config.server 50 | 51 | class wsServer { 52 | constructor(config, recorder) { 53 | if (!recorder) { 54 | throw new Error('proxy recorder is required'); 55 | } else if (!config || !config.server) { 56 | throw new Error('config.server is required'); 57 | } 58 | 59 | const self = this; 60 | self.config = config; 61 | self.recorder = recorder; 62 | self.checkBroadcastFlagTimer = null; 63 | } 64 | 65 | start() { 66 | const self = this; 67 | const config = self.config; 68 | const recorder = self.recorder; 69 | return new Promise((resolve, reject) => { 70 | //web socket interface 71 | const wss = new WebSocketServer({ 72 | server: config.server, 73 | clientTracking: true, 74 | }); 75 | resolve(); 76 | 77 | // the queue of the messages to be delivered 78 | let messageQueue = []; 79 | // the flat to indicate wheter to broadcast the record 80 | let broadcastFlag = true; 81 | 82 | self.checkBroadcastFlagTimer = setInterval(() => { 83 | broadcastFlag = true; 84 | sendMultipleMessage(); 85 | }, 50); 86 | 87 | function sendMultipleMessage(data) { 88 | // if the flag goes to be true, and there are records to send 89 | if (broadcastFlag && messageQueue.length > 0) { 90 | wss && wss.broadcast({ 91 | type: 'updateMultiple', 92 | content: messageQueue 93 | }); 94 | messageQueue = []; 95 | broadcastFlag = false; 96 | } else { 97 | data && messageQueue.push(data); 98 | } 99 | } 100 | 101 | wss.broadcast = function (data) { 102 | if (typeof data === 'object') { 103 | try { 104 | data = JSON.stringify(data); 105 | } catch (e) { 106 | console.error('==> error when do broadcast ', e, data); 107 | } 108 | } 109 | for (const client of wss.clients) { 110 | try { 111 | client.send(data); 112 | } catch (e) { 113 | logUtil.printLog('websocket failed to send data, ' + e, logUtil.T_ERR); 114 | } 115 | } 116 | }; 117 | 118 | wss.on('connection', (ws) => { 119 | ws.on('message', (msg) => { 120 | resToMsg(msg, recorder, (res) => { 121 | res && ws.send(JSON.stringify(res)); 122 | }); 123 | }); 124 | 125 | ws.on('error', (e) => { 126 | console.error('error in ws:', e); 127 | }); 128 | }); 129 | 130 | wss.on('error', (e) => { 131 | logUtil.printLog('websocket error, ' + e, logUtil.T_ERR); 132 | }); 133 | 134 | wss.on('close', () => { }); 135 | 136 | recorder.on('update', (data) => { 137 | try { 138 | sendMultipleMessage(data); 139 | } catch (e) { 140 | console.log('ws error'); 141 | console.log(e); 142 | } 143 | }); 144 | 145 | recorder.on('updateLatestWsMsg', (data) => { 146 | try { 147 | // console.info('==> update latestMsg ', data); 148 | wss && wss.broadcast({ 149 | type: 'updateLatestWsMsg', 150 | content: data 151 | }); 152 | } catch (e) { 153 | logUtil.error(e.message); 154 | logUtil.error(e.stack); 155 | console.error(e); 156 | } 157 | }); 158 | 159 | self.wss = wss; 160 | }); 161 | } 162 | 163 | closeAll() { 164 | const self = this; 165 | if (self.checkBroadcastFlagTimer) { 166 | clearInterval(self.checkBroadcastFlagTimer); 167 | } 168 | return new Promise((resolve, reject) => { 169 | self.wss.close((e) => { 170 | if (e) { 171 | reject(e); 172 | } else { 173 | resolve(); 174 | } 175 | }); 176 | }); 177 | } 178 | } 179 | 180 | module.exports = wsServer; 181 | -------------------------------------------------------------------------------- /main/anyproxy/lib/httpsServerMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //manage https servers 4 | const async = require('async'), 5 | https = require('https'), 6 | tls = require('tls'), 7 | assert = require('assert'), 8 | crypto = require('crypto'), 9 | color = require('colorful'), 10 | certMgr = require('./certMgr'), 11 | logUtil = require('./log'), 12 | util = require('./util'), 13 | wsServerMgr = require('./wsServerMgr'), 14 | constants = require('constants'), 15 | asyncTask = require('async-task-mgr'); 16 | 17 | /** 18 | * Create an https server 19 | * 20 | * @param {object} config 21 | * @param {number} config.port 22 | * @param {function} config.handler 23 | */ 24 | function createHttpsSNIServer(port, handler) { 25 | assert(port && handler, 'invalid param for https SNI server'); 26 | 27 | const createSecureContext = tls.createSecureContext || crypto.createSecureContext; 28 | function SNIPrepareCert(serverName, SNICallback) { 29 | let keyContent, 30 | crtContent, 31 | ctx; 32 | 33 | async.series([ 34 | (callback) => { 35 | certMgr.getCertificate(serverName, (err, key, crt) => { 36 | if (err) { 37 | callback(err); 38 | } else { 39 | keyContent = key; 40 | crtContent = crt; 41 | callback(); 42 | } 43 | }); 44 | }, 45 | (callback) => { 46 | try { 47 | ctx = createSecureContext({ 48 | key: keyContent, 49 | cert: crtContent 50 | }); 51 | callback(); 52 | } catch (e) { 53 | callback(e); 54 | } 55 | } 56 | ], (err) => { 57 | if (!err) { 58 | const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName); 59 | logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText)); 60 | SNICallback(null, ctx); 61 | } else { 62 | logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR); 63 | logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR); 64 | SNICallback(err); 65 | } 66 | }); 67 | } 68 | 69 | return new Promise((resolve) => { 70 | const server = https.createServer({ 71 | secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, 72 | SNICallback: SNIPrepareCert, 73 | }, handler).listen(port); 74 | resolve(server); 75 | }); 76 | } 77 | 78 | function createHttpsIPServer(ip, port, handler) { 79 | assert(ip && port && handler, 'invalid param for https IP server'); 80 | 81 | return new Promise((resolve, reject) => { 82 | certMgr.getCertificate(ip, (err, keyContent, crtContent) => { 83 | if (err) return reject(err); 84 | const server = https.createServer({ 85 | secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, 86 | key: keyContent, 87 | cert: crtContent, 88 | }, handler).listen(port); 89 | 90 | resolve(server); 91 | }); 92 | }); 93 | } 94 | 95 | class httpsServerMgr { 96 | constructor(config) { 97 | if (!config || !config.handler) { 98 | throw new Error('handler is required'); 99 | } 100 | this.httpsAsyncTask = new asyncTask(); 101 | this.handler = config.handler; 102 | this.wsHandler = config.wsHandler 103 | this.asyncSNITaskName = `https_SNI_${Math.random()}`; 104 | this.activeServers = []; 105 | } 106 | 107 | getSharedHttpsServer(hostname) { 108 | const self = this; 109 | const ifIPHost = hostname && util.isIp(hostname); 110 | const serverHost = '127.0.0.1'; 111 | 112 | function prepareServer(callback) { 113 | let port; 114 | Promise.resolve(util.getFreePort()) 115 | .then(freePort => { 116 | port = freePort; 117 | if (ifIPHost) { 118 | return createHttpsIPServer(hostname, port, self.handler); 119 | } else { 120 | return createHttpsSNIServer(port, self.handler); 121 | } 122 | }) 123 | .then(httpsServer => { 124 | self.activeServers.push(httpsServer); 125 | 126 | wsServerMgr.getWsServer({ 127 | server: httpsServer, 128 | connHandler: self.wsHandler 129 | }); 130 | 131 | httpsServer.on('upgrade', (req, cltSocket, head) => { 132 | logUtil.debug('will let WebSocket server to handle the upgrade event'); 133 | }); 134 | 135 | const result = { 136 | host: serverHost, 137 | port, 138 | }; 139 | callback(null, result); 140 | }) 141 | .catch(e => { 142 | callback(e); 143 | }); 144 | } 145 | 146 | // same server for same host 147 | return new Promise((resolve, reject) => { 148 | self.httpsAsyncTask.addTask(ifIPHost ? hostname : serverHost, prepareServer, (error, serverInfo) => { 149 | if (error) { 150 | reject(error); 151 | } else { 152 | resolve(serverInfo); 153 | } 154 | }); 155 | }); 156 | } 157 | 158 | close() { 159 | this.activeServers.forEach(server => { 160 | server.close(); 161 | }); 162 | } 163 | } 164 | 165 | module.exports = httpsServerMgr; 166 | -------------------------------------------------------------------------------- /main/anyproxy/lib/systemProxyMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const child_process = require('child_process'); 4 | 5 | const networkTypes = ['Ethernet', 'Thunderbolt Ethernet', 'Wi-Fi']; 6 | 7 | function execSync(cmd) { 8 | let stdout, 9 | status = 0; 10 | try { 11 | stdout = child_process.execSync(cmd); 12 | } catch (err) { 13 | stdout = err.stdout; 14 | status = err.status; 15 | } 16 | 17 | return { 18 | stdout: stdout.toString(), 19 | status 20 | }; 21 | } 22 | 23 | /** 24 | * proxy for CentOs 25 | * ------------------------------------------------------------------------ 26 | * 27 | * file: ~/.bash_profile 28 | * 29 | * http_proxy=http://proxy_server_address:port 30 | * export no_proxy=localhost,127.0.0.1,192.168.0.34 31 | * export http_proxy 32 | * ------------------------------------------------------------------------ 33 | */ 34 | 35 | /** 36 | * proxy for Ubuntu 37 | * ------------------------------------------------------------------------ 38 | * 39 | * file: /etc/environment 40 | * more info: http://askubuntu.com/questions/150210/how-do-i-set-systemwide-proxy-servers-in-xubuntu-lubuntu-or-ubuntu-studio 41 | * 42 | * http_proxy=http://proxy_server_address:port 43 | * export no_proxy=localhost,127.0.0.1,192.168.0.34 44 | * export http_proxy 45 | * ------------------------------------------------------------------------ 46 | */ 47 | 48 | /** 49 | * ------------------------------------------------------------------------ 50 | * mac proxy manager 51 | * ------------------------------------------------------------------------ 52 | */ 53 | 54 | const macProxyManager = {}; 55 | 56 | macProxyManager.getNetworkType = () => { 57 | for (let i = 0; i < networkTypes.length; i++) { 58 | const type = networkTypes[i], 59 | result = execSync('networksetup -getwebproxy ' + type); 60 | 61 | if (result.status === 0) { 62 | macProxyManager.networkType = type; 63 | return type; 64 | } 65 | } 66 | 67 | throw new Error('Unknown network type'); 68 | }; 69 | 70 | 71 | macProxyManager.enableGlobalProxy = (ip, port, proxyType) => { 72 | if (!ip || !port) { 73 | console.log('failed to set global proxy server.\n ip and port are required.'); 74 | return; 75 | } 76 | 77 | proxyType = proxyType || 'http'; 78 | 79 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 80 | 81 | return /^http$/i.test(proxyType) ? 82 | 83 | // set http proxy 84 | execSync( 85 | 'networksetup -setwebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost' 86 | .replace(/\${networkType}/g, networkType) 87 | .replace('${ip}', ip) 88 | .replace('${port}', port)) : 89 | 90 | // set https proxy 91 | execSync('networksetup -setsecurewebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost' 92 | .replace(/\${networkType}/g, networkType) 93 | .replace('${ip}', ip) 94 | .replace('${port}', port)); 95 | }; 96 | 97 | macProxyManager.disableGlobalProxy = (proxyType) => { 98 | proxyType = proxyType || 'http'; 99 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 100 | return /^http$/i.test(proxyType) ? 101 | 102 | // set http proxy 103 | execSync( 104 | 'networksetup -setwebproxystate ${networkType} off' 105 | .replace('${networkType}', networkType)) : 106 | 107 | // set https proxy 108 | execSync( 109 | 'networksetup -setsecurewebproxystate ${networkType} off' 110 | .replace('${networkType}', networkType)); 111 | }; 112 | 113 | macProxyManager.getProxyState = () => { 114 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 115 | const result = execSync('networksetup -getwebproxy ${networkType}'.replace('${networkType}', networkType)); 116 | 117 | return result; 118 | }; 119 | 120 | /** 121 | * ------------------------------------------------------------------------ 122 | * windows proxy manager 123 | * 124 | * netsh does not alter the settings for IE 125 | * ------------------------------------------------------------------------ 126 | */ 127 | 128 | const winProxyManager = {}; 129 | 130 | winProxyManager.enableGlobalProxy = (ip, port) => { 131 | if (!ip && !port) { 132 | console.log('failed to set global proxy server.\n ip and port are required.'); 133 | return; 134 | } 135 | 136 | return execSync( 137 | // set proxy 138 | 'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer /t REG_SZ /d ${ip}:${port} /f & ' 139 | .replace('${ip}', ip) 140 | .replace('${port}', port) + 141 | 142 | // enable proxy 143 | 'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f'); 144 | }; 145 | 146 | winProxyManager.disableGlobalProxy = () => execSync('reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f'); 147 | 148 | winProxyManager.getProxyState = () => '' 149 | 150 | winProxyManager.getNetworkType = () => '' 151 | 152 | module.exports = /^win/.test(process.platform) ? winProxyManager : macProxyManager; 153 | -------------------------------------------------------------------------------- /src/styles/normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | margin: 0.67em 0; 42 | font-size: 2em; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-size: 1em; /* 2 */ 66 | font-family: monospace; /* 1 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | text-decoration: underline; /* 2 */ 87 | text-decoration: underline dotted; /* 2 */ 88 | border-bottom: none; /* 1 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-size: 1em; /* 2 */ 109 | font-family: monospace; /* 1 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | position: relative; 128 | font-size: 75%; 129 | line-height: 0; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | margin: 0; /* 2 */ 166 | font-size: 100%; /* 1 */ 167 | font-family: inherit; /* 1 */ 168 | line-height: 1.15; /* 1 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | padding: 0; 211 | border-style: none; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | display: table; /* 1 */ 242 | box-sizing: border-box; /* 1 */ 243 | max-width: 100%; /* 1 */ 244 | padding: 0; /* 3 */ 245 | color: inherit; /* 2 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | outline-offset: -2px; /* 2 */ 292 | -webkit-appearance: textfield; /* 1 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | font: inherit; /* 2 */ 310 | -webkit-appearance: button; /* 1 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /main/anyproxy/lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'), 4 | path = require('path'), 5 | mime = require('mime-types'), 6 | color = require('colorful'), 7 | child_process = require('child_process'), 8 | os = require('os'), 9 | Buffer = require('buffer').Buffer, 10 | logUtil = require('./log'); 11 | 12 | const networkInterfaces = os.networkInterfaces(); 13 | 14 | // {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"} 15 | module.exports.lower_keys = (obj) => { 16 | for (const key in obj) { 17 | const val = obj[key]; 18 | delete obj[key]; 19 | 20 | obj[key.toLowerCase()] = val; 21 | } 22 | 23 | return obj; 24 | }; 25 | 26 | module.exports.merge = function (baseObj, extendObj) { 27 | for (const key in extendObj) { 28 | baseObj[key] = extendObj[key]; 29 | } 30 | 31 | return baseObj; 32 | }; 33 | 34 | function getUserHome() { 35 | return process.env.HOME || process.env.USERPROFILE; 36 | } 37 | module.exports.getUserHome = getUserHome; 38 | 39 | function getAnyProxyHome() { 40 | const home = path.join(getUserHome(), '/.dynproxy/'); 41 | if (!fs.existsSync(home)) { 42 | fs.mkdirSync(home); 43 | } 44 | return home; 45 | } 46 | module.exports.getAnyProxyHome = getAnyProxyHome; 47 | 48 | module.exports.getAnyProxyPath = function (pathName) { 49 | const home = getAnyProxyHome(); 50 | const targetPath = path.join(home, pathName); 51 | if (!fs.existsSync(targetPath)) { 52 | fs.mkdirSync(targetPath); 53 | } 54 | return targetPath; 55 | } 56 | 57 | module.exports.getAnyProxyTmpPath = function () { 58 | const targetPath = path.join(os.tmpdir(), 'anyproxy', 'cache'); 59 | if (!fs.existsSync(targetPath)) { 60 | fs.mkdirSync(targetPath, { recursive: true }); 61 | } 62 | return targetPath; 63 | } 64 | 65 | module.exports.simpleRender = function (str, object, regexp) { 66 | return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), (match, name) => { 67 | if (match.charAt(0) === '\\') { 68 | return match.slice(1); 69 | } 70 | return (object[name] != null) ? object[name] : ''; 71 | }); 72 | }; 73 | 74 | module.exports.filewalker = function (root, cb) { 75 | root = root || process.cwd(); 76 | 77 | const ret = { 78 | directory: [], 79 | file: [] 80 | }; 81 | 82 | fs.readdir(root, (err, list) => { 83 | if (list && list.length) { 84 | list.map((item) => { 85 | const fullPath = path.join(root, item), 86 | stat = fs.lstatSync(fullPath); 87 | 88 | if (stat.isFile()) { 89 | ret.file.push({ 90 | name: item, 91 | fullPath 92 | }); 93 | } else if (stat.isDirectory()) { 94 | ret.directory.push({ 95 | name: item, 96 | fullPath 97 | }); 98 | } 99 | }); 100 | } 101 | 102 | cb && cb.apply(null, [null, ret]); 103 | }); 104 | }; 105 | 106 | /* 107 | * 获取文件所对应的content-type以及content-length等信息 108 | * 比如在useLocalResponse的时候会使用到 109 | */ 110 | module.exports.contentType = function (filepath) { 111 | return mime.contentType(path.extname(filepath)); 112 | }; 113 | 114 | /* 115 | * 读取file的大小,以byte为单位 116 | */ 117 | module.exports.contentLength = function (filepath) { 118 | try { 119 | const stat = fs.statSync(filepath); 120 | return stat.size; 121 | } catch (e) { 122 | logUtil.printLog(color.red('\nfailed to ready local file : ' + filepath)); 123 | logUtil.printLog(color.red(e)); 124 | return 0; 125 | } 126 | }; 127 | 128 | /* 129 | * remove the cache before requiring, the path SHOULD BE RELATIVE TO UTIL.JS 130 | */ 131 | module.exports.freshRequire = function (modulePath) { 132 | delete require.cache[require.resolve(modulePath)]; 133 | return require(modulePath); 134 | }; 135 | 136 | /* 137 | * format the date string 138 | * @param date Date or timestamp 139 | * @param formatter YYYYMMDDHHmmss 140 | */ 141 | module.exports.formatDate = function (date, formatter) { 142 | if (typeof date !== 'object') { 143 | date = new Date(date); 144 | } 145 | const transform = function (value) { 146 | return value < 10 ? '0' + value : value; 147 | }; 148 | return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, (match) => { 149 | switch (match) { 150 | case 'YYYY': 151 | return transform(date.getFullYear()); 152 | case 'MM': 153 | return transform(date.getMonth() + 1); 154 | case 'mm': 155 | return transform(date.getMinutes()); 156 | case 'DD': 157 | return transform(date.getDate()); 158 | case 'hh': 159 | return transform(date.getHours()); 160 | case 'ss': 161 | return transform(date.getSeconds()); 162 | default: 163 | return '' 164 | } 165 | }); 166 | }; 167 | 168 | 169 | /** 170 | * get headers(Object) from rawHeaders(Array) 171 | * @param rawHeaders [key, value, key2, value2, ...] 172 | 173 | */ 174 | 175 | module.exports.getHeaderFromRawHeaders = function (rawHeaders) { 176 | const headerObj = {}; 177 | const _handleSetCookieHeader = function (key, value) { 178 | if (headerObj[key].constructor === Array) { 179 | headerObj[key].push(value); 180 | } else { 181 | headerObj[key] = [headerObj[key], value]; 182 | } 183 | }; 184 | 185 | if (!!rawHeaders) { 186 | for (let i = 0; i < rawHeaders.length; i += 2) { 187 | const key = rawHeaders[i]; 188 | let value = rawHeaders[i + 1]; 189 | 190 | if (typeof value === 'string') { 191 | value = value.replace(/\0+$/g, ''); // 去除 \u0000的null字符串 192 | } 193 | 194 | if (!headerObj[key]) { 195 | headerObj[key] = value; 196 | } else { 197 | // headers with same fields could be combined with comma. Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 198 | // set-cookie should NOT be combined. Ref: https://tools.ietf.org/html/rfc6265 199 | if (key.toLowerCase() === 'set-cookie') { 200 | _handleSetCookieHeader(key, value); 201 | } else { 202 | headerObj[key] = headerObj[key] + ',' + value; 203 | } 204 | } 205 | } 206 | } 207 | return headerObj; 208 | }; 209 | 210 | module.exports.getAllIpAddress = function getAllIpAddress() { 211 | const allIp = []; 212 | 213 | Object.keys(networkInterfaces).map((nic) => { 214 | networkInterfaces[nic].filter((detail) => { 215 | if (detail.family.toLowerCase() === 'ipv4') { 216 | allIp.push(detail.address); 217 | } 218 | }); 219 | }); 220 | 221 | return allIp.length ? allIp : ['127.0.0.1']; 222 | }; 223 | 224 | function deleteFolderContentsRecursive(dirPath, ifClearFolderItself) { 225 | if (!dirPath.trim() || dirPath === '/') { 226 | throw new Error('can_not_delete_this_dir'); 227 | } 228 | 229 | if (fs.existsSync(dirPath)) { 230 | fs.readdirSync(dirPath).forEach((file) => { 231 | const curPath = path.join(dirPath, file); 232 | if (fs.lstatSync(curPath).isDirectory()) { 233 | deleteFolderContentsRecursive(curPath, true); 234 | } else { // delete all files 235 | fs.unlinkSync(curPath); 236 | } 237 | }); 238 | 239 | if (ifClearFolderItself) { 240 | try { 241 | // ref: https://github.com/shelljs/shelljs/issues/49 242 | const start = Date.now(); 243 | while (true) { 244 | try { 245 | fs.rmdirSync(dirPath); 246 | break; 247 | } catch (er) { 248 | if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM')) { 249 | // Retry on windows, sometimes it takes a little time before all the files in the directory are gone 250 | if (Date.now() - start > 1000) throw er; 251 | } else if (er.code === 'ENOENT') { 252 | break; 253 | } else { 254 | throw er; 255 | } 256 | } 257 | } 258 | } catch (e) { 259 | throw new Error('could not remove directory (code ' + e.code + '): ' + dirPath); 260 | } 261 | } 262 | } 263 | } 264 | 265 | module.exports.deleteFolderContentsRecursive = deleteFolderContentsRecursive; 266 | 267 | module.exports.getFreePort = function () { 268 | return new Promise((resolve, reject) => { 269 | const server = require('net').createServer(); 270 | server.unref(); 271 | server.on('error', reject); 272 | server.listen(0, () => { 273 | const port = server.address().port; 274 | server.close(() => { 275 | resolve(port); 276 | }); 277 | }); 278 | }); 279 | } 280 | 281 | module.exports.collectErrorLog = function (error) { 282 | if (error && error.code && error.toString()) { 283 | return error.toString(); 284 | } else { 285 | let result = [error, error.stack].join('\n'); 286 | try { 287 | const errorString = error.toString(); 288 | if (errorString.indexOf('You may only yield a function') >= 0) { 289 | result = 'Function is not yieldable. Did you forget to provide a generator or promise in rule file ? \nFAQ http://anyproxy.io/4.x/#faq'; 290 | } 291 | } catch (e) {} 292 | return result 293 | } 294 | } 295 | 296 | module.exports.isFunc = function (source) { 297 | return source && Object.tostring.call(source) === '[object Function]'; 298 | }; 299 | 300 | /** 301 | * @param {object} content 302 | * @returns the size of the content 303 | */ 304 | module.exports.getByteSize = function (content) { 305 | return Buffer.byteLength(content); 306 | }; 307 | 308 | /* 309 | * identify whether the 310 | */ 311 | module.exports.isIp = function (domain) { 312 | if (!domain) { 313 | return false; 314 | } 315 | const ipReg = /^\d+?\.\d+?\.\d+?\.\d+?$/; 316 | 317 | return ipReg.test(domain); 318 | }; 319 | 320 | module.exports.execScriptSync = function (cmd) { 321 | let stdout, 322 | status = 0; 323 | try { 324 | stdout = child_process.execSync(cmd); 325 | } catch (err) { 326 | stdout = err.stdout; 327 | status = err.status; 328 | } 329 | 330 | return { 331 | stdout: stdout.toString(), 332 | status 333 | }; 334 | }; 335 | 336 | module.exports.guideToHomePage = function () { 337 | logUtil.info('Refer to http://anyproxy.io for more detail'); 338 | }; 339 | 340 | 341 | module.exports.json2Buffer = function json2Buffer(json) { 342 | let jsonObj = json 343 | if (typeof jsonObj === 'string') { 344 | jsonObj = JSON.parse(jsonObj) 345 | } 346 | const jsonStr = JSON.stringify(jsonObj) 347 | return Buffer.from(jsonStr) 348 | } 349 | -------------------------------------------------------------------------------- /main/anyproxy/lib/webInterface.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'), 4 | url = require('url'), 5 | bodyParser = require('body-parser'), 6 | fs = require('fs'), 7 | path = require('path'), 8 | events = require('events'), 9 | qrCode = require('qrcode-npm'), 10 | util = require('./util'), 11 | certMgr = require('./certMgr'), 12 | wsServer = require('./wsServer'), 13 | juicer = require('juicer'), 14 | ip = require('ip'), 15 | compress = require('compression'), 16 | pug = require('pug'); 17 | 18 | const DEFAULT_WEB_PORT = 8002; // port for web interface 19 | 20 | const packageJson = require('../../../package.json'); 21 | 22 | const MAX_CONTENT_SIZE = 1024 * 2000; // 2000kb 23 | 24 | const certFileTypes = ['crt', 'cer', 'pem', 'der']; 25 | /** 26 | * 27 | * 28 | * @class webInterface 29 | * @extends {events.EventEmitter} 30 | */ 31 | class webInterface extends events.EventEmitter { 32 | /** 33 | * Creates an instance of webInterface. 34 | * 35 | * @param {object} config 36 | * @param {number} config.webPort 37 | * @param {object} recorder 38 | * 39 | * @memberOf webInterface 40 | */ 41 | constructor(config, recorder) { 42 | if (!recorder) { 43 | throw new Error('recorder is required for web interface'); 44 | } 45 | super(); 46 | const self = this; 47 | self.webPort = config.webPort || DEFAULT_WEB_PORT; 48 | self.recorder = recorder; 49 | self.config = config || {}; 50 | 51 | self.app = this.getServer(); 52 | self.server = null; 53 | self.wsServer = null; 54 | } 55 | 56 | /** 57 | * get the express server 58 | */ 59 | getServer() { 60 | const self = this; 61 | const recorder = self.recorder; 62 | const ipAddress = ip.address(), 63 | // userRule = proxyInstance.proxyRule, 64 | webBasePath = 'web'; 65 | let ruleSummary = ''; 66 | let customMenu = []; 67 | 68 | try { 69 | ruleSummary = ''; //userRule.summary(); 70 | customMenu = ''; // userRule._getCustomMenu(); 71 | } catch (e) { } 72 | 73 | const staticDir = path.join(__dirname, '../', webBasePath); 74 | const app = express(); 75 | 76 | app.use(compress()); //invoke gzip 77 | app.use((req, res, next) => { 78 | res.setHeader('note', 'THIS IS A REQUEST FROM ANYPROXY WEB INTERFACE'); 79 | return next(); 80 | }); 81 | app.use(bodyParser.json()); 82 | 83 | app.get('/latestLog', (req, res) => { 84 | res.setHeader('Access-Control-Allow-Origin', '*'); 85 | recorder.getRecords(null, 10000, (err, docs) => { 86 | if (err) { 87 | res.end(err.toString()); 88 | } else { 89 | res.json(docs); 90 | } 91 | }); 92 | }); 93 | 94 | app.get('/downloadBody', (req, res) => { 95 | const query = req.query; 96 | recorder.getDecodedBody(query.id, (err, result) => { 97 | if (err || !result || !result.content) { 98 | res.json({}); 99 | } else if (result.mime) { 100 | if (query.raw === 'true') { 101 | //TODO : cache query result 102 | res.type(result.mime).end(result.content); 103 | } else if (query.download === 'true') { 104 | res.setHeader('Content-disposition', `attachment; filename=${result.fileName}`); 105 | res.setHeader('Content-type', result.mime); 106 | res.end(result.content); 107 | } 108 | } else { 109 | res.json({ 110 | 111 | }); 112 | } 113 | }); 114 | }); 115 | 116 | app.get('/fetchBody', (req, res) => { 117 | res.setHeader('Access-Control-Allow-Origin', '*'); 118 | const query = req.query; 119 | if (query && query.id) { 120 | recorder.getDecodedBody(query.id, (err, result) => { 121 | // 返回下载信息 122 | const _resDownload = function (isDownload) { 123 | isDownload = typeof isDownload === 'boolean' ? isDownload : true; 124 | res.json({ 125 | id: query.id, 126 | type: result.type, 127 | method: result.meethod, 128 | fileName: result.fileName, 129 | ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}` 130 | }); 131 | }; 132 | 133 | // 返回内容 134 | const _resContent = () => { 135 | if (util.getByteSize(result.content || '') > MAX_CONTENT_SIZE) { 136 | _resDownload(true); 137 | return; 138 | } 139 | 140 | res.json({ 141 | id: query.id, 142 | type: result.type, 143 | method: result.method, 144 | resBody: result.content 145 | }); 146 | }; 147 | 148 | if (err || !result) { 149 | res.json({}); 150 | } else if (result.statusCode === 200 && result.mime) { 151 | // deal with 'application/x-javascript' and 'application/javascript' 152 | if (/json|text|javascript/.test(result.mime)) { 153 | _resContent(); 154 | } else if (result.type === 'image') { 155 | _resDownload(false); 156 | } else { 157 | _resDownload(true); 158 | } 159 | } else { 160 | _resContent(); 161 | } 162 | }); 163 | } else { 164 | res.end(''); 165 | } 166 | }); 167 | 168 | app.get('/fetchReqBody', (req, res) => { 169 | const query = req.query; 170 | if (query && query.id) { 171 | recorder.getSingleRecord(query.id, (err, doc) => { 172 | if (err || !doc[0]) { 173 | console.error(err); 174 | res.end(''); 175 | return; 176 | } 177 | 178 | res.setHeader('Content-disposition', `attachment; filename=request_${query.id}_body.txt`); 179 | res.setHeader('Content-type', 'text/plain'); 180 | res.end(doc[0].reqBody); 181 | }); 182 | } else { 183 | res.end(''); 184 | } 185 | }); 186 | 187 | app.get('/fetchWsMessages', (req, res) => { 188 | const query = req.query; 189 | if (query && query.id) { 190 | recorder.getDecodedWsMessage(query.id, (err, messages) => { 191 | if (err) { 192 | console.error(err); 193 | res.json([]); 194 | return; 195 | } 196 | res.json(messages); 197 | }); 198 | } else { 199 | res.json([]); 200 | } 201 | }); 202 | 203 | app.get('/downloadCrt', (req, res) => { 204 | const pageFn = pug.compileFile(path.join(__dirname, '../resource/cert_download.pug')); 205 | res.end(pageFn({ ua: req.get('user-agent') })); 206 | }); 207 | 208 | app.get('/fetchCrtFile', (req, res) => { 209 | res.setHeader('Access-Control-Allow-Origin', '*'); 210 | const _crtFilePath = certMgr.getRootCAFilePath(); 211 | if (_crtFilePath) { 212 | const fileType = certFileTypes.indexOf(req.query.type) !== -1 ? req.query.type : 'crt'; 213 | res.setHeader('Content-Type', 'application/x-x509-ca-cert'); 214 | res.setHeader('Content-Disposition', `attachment; filename="rootCA.${fileType}"`); 215 | res.end(fs.readFileSync(_crtFilePath, { encoding: null })); 216 | } else { 217 | res.setHeader('Content-Type', 'text/html'); 218 | res.end('can not file rootCA ,plase use anyproxy --root to generate one'); 219 | } 220 | }); 221 | 222 | app.get('/api/getQrCode', (req, res) => { 223 | res.setHeader('Access-Control-Allow-Origin', '*'); 224 | 225 | const qr = qrCode.qrcode(4, 'M'); 226 | const targetUrl = req.protocol + '://' + req.get('host') + '/downloadCrt'; 227 | const isRootCAFileExists = certMgr.isRootCAFileExists(); 228 | 229 | qr.addData(targetUrl); 230 | qr.make(); 231 | 232 | res.json({ 233 | status: 'success', 234 | url: targetUrl, 235 | isRootCAFileExists, 236 | qrImgDom: qr.createImgTag(4) 237 | }); 238 | }); 239 | 240 | // response init data 241 | app.get('/api/getInitData', (req, res) => { 242 | res.setHeader('Access-Control-Allow-Origin', '*'); 243 | const rootCAExists = certMgr.isRootCAFileExists(); 244 | const rootDirPath = certMgr.getRootDirPath(); 245 | const interceptFlag = false; //proxyInstance.getInterceptFlag(); TODO 246 | const globalProxyFlag = false; // TODO: proxyInstance.getGlobalProxyFlag(); 247 | res.json({ 248 | status: 'success', 249 | rootCAExists, 250 | rootCADirPath: rootDirPath, 251 | currentInterceptFlag: interceptFlag, 252 | currentGlobalProxyFlag: globalProxyFlag, 253 | ruleSummary: ruleSummary || '', 254 | ipAddress: util.getAllIpAddress(), 255 | port: '', //proxyInstance.proxyPort, // TODO 256 | appVersion: packageJson.version 257 | }); 258 | }); 259 | 260 | app.post('/api/generateRootCA', (req, res) => { 261 | res.setHeader('Access-Control-Allow-Origin', '*'); 262 | const rootExists = certMgr.isRootCAFileExists(); 263 | if (!rootExists) { 264 | certMgr.generateRootCA(() => { 265 | res.json({ 266 | status: 'success', 267 | code: 'done' 268 | }); 269 | }); 270 | } else { 271 | res.json({ 272 | status: 'success', 273 | code: 'root_ca_exists' 274 | }); 275 | } 276 | }); 277 | 278 | app.use((req, res, next) => { 279 | const indexTpl = fs.readFileSync(path.join(staticDir, '/index.html'), { encoding: 'utf8' }), 280 | opt = { 281 | rule: ruleSummary || '', 282 | customMenu: customMenu || [], 283 | ipAddress: ipAddress || '127.0.0.1' 284 | }; 285 | 286 | if (url.parse(req.url).pathname === '/') { 287 | res.setHeader('Content-Type', 'text/html'); 288 | res.end(juicer(indexTpl, opt)); 289 | } else { 290 | next(); 291 | } 292 | }); 293 | app.use(express.static(staticDir)); 294 | return app; 295 | } 296 | 297 | start() { 298 | const self = this; 299 | return new Promise((resolve, reject) => { 300 | self.server = self.app.listen(self.webPort); 301 | self.wsServer = new wsServer({ 302 | server: self.server 303 | }, self.recorder); 304 | self.wsServer.start(); 305 | resolve(); 306 | }) 307 | } 308 | 309 | close() { 310 | const self = this; 311 | return new Promise((resolve, reject) => { 312 | self.server && self.server.close(); 313 | self.wsServer && self.wsServer.closeAll(); 314 | self.server = null; 315 | self.wsServer = null; 316 | self.proxyInstance = null; 317 | resolve(); 318 | }); 319 | } 320 | } 321 | 322 | module.exports = webInterface; 323 | -------------------------------------------------------------------------------- /main/anyproxy/lib/recorder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //start recording and share a list when required 4 | const Datastore = require('nedb'), 5 | path = require('path'), 6 | fs = require('fs'), 7 | logUtil = require('./log'), 8 | events = require('events'), 9 | iconv = require('iconv-lite'), 10 | fastJson = require('fast-json-stringify'), 11 | proxyUtil = require('./util'); 12 | 13 | const wsMessageStingify = fastJson({ 14 | title: 'ws message stringify', 15 | type: 'object', 16 | properties: { 17 | time: { 18 | type: 'integer' 19 | }, 20 | message: { 21 | type: 'string' 22 | }, 23 | isToServer: { 24 | type: 'boolean' 25 | } 26 | } 27 | }); 28 | 29 | const BODY_FILE_PRFIX = 'res_body_'; 30 | const WS_MESSAGE_FILE_PRFIX = 'ws_message_'; 31 | const CACHE_DIR_PREFIX = 'cache_r'; 32 | function getCacheDir() { 33 | const rand = Math.floor(Math.random() * 1000000), 34 | cachePath = path.join(proxyUtil.getAnyProxyTmpPath(), './' + CACHE_DIR_PREFIX + rand); 35 | 36 | fs.mkdirSync(cachePath); 37 | return cachePath; 38 | } 39 | 40 | function normalizeInfo(id, info) { 41 | const singleRecord = {}; 42 | 43 | //general 44 | singleRecord._id = id; 45 | singleRecord.id = id; 46 | singleRecord.url = info.url; 47 | singleRecord.host = info.host; 48 | singleRecord.path = info.path; 49 | singleRecord.method = info.method; 50 | 51 | //req 52 | singleRecord.reqHeader = info.req.headers; 53 | singleRecord.startTime = info.startTime; 54 | singleRecord.reqBody = info.reqBody || ''; 55 | singleRecord.protocol = info.protocol || ''; 56 | 57 | //res 58 | if (info.endTime) { 59 | singleRecord.statusCode = info.statusCode; 60 | singleRecord.endTime = info.endTime; 61 | singleRecord.resHeader = info.resHeader; 62 | singleRecord.length = info.length; 63 | const contentType = info.resHeader['content-type'] || info.resHeader['Content-Type']; 64 | if (contentType) { 65 | singleRecord.mime = contentType.split(';')[0]; 66 | } else { 67 | singleRecord.mime = ''; 68 | } 69 | 70 | singleRecord.duration = info.endTime - info.startTime; 71 | } else { 72 | singleRecord.statusCode = ''; 73 | singleRecord.endTime = ''; 74 | singleRecord.resHeader = ''; 75 | singleRecord.length = ''; 76 | singleRecord.mime = ''; 77 | singleRecord.duration = ''; 78 | } 79 | 80 | return singleRecord; 81 | } 82 | 83 | class Recorder extends events.EventEmitter { 84 | constructor(config) { 85 | super(config); 86 | this.globalId = 1; 87 | this.cachePath = getCacheDir(); 88 | this.db = new Datastore(); 89 | 90 | this.recordBodyMap = []; // id - body 91 | } 92 | 93 | setDbAutoCompact() { 94 | this.db.persistence.setAutocompactionInterval(5001); 95 | } 96 | 97 | stopDbAutoCompact() { 98 | try { 99 | this.db.persistence.stopAutocompaction(); 100 | } catch (e) { 101 | logUtil.printLog(e, logUtil.T_ERR); 102 | } 103 | } 104 | 105 | emitUpdate(id, info) { 106 | const self = this; 107 | if (info) { 108 | self.emit('update', info); 109 | } else { 110 | self.getSingleRecord(id, (err, doc) => { 111 | if (!err && !!doc && !!doc[0]) { 112 | self.emit('update', doc[0]); 113 | } 114 | }); 115 | } 116 | } 117 | 118 | emitUpdateLatestWsMessage(id, message) { 119 | this.emit('updateLatestWsMsg', message); 120 | } 121 | 122 | updateRecord(id, info) { 123 | if (id < 0) return; 124 | const self = this; 125 | const db = self.db; 126 | 127 | const finalInfo = normalizeInfo(id, info); 128 | 129 | db.update({ _id: id }, finalInfo); 130 | self.updateRecordBody(id, info); 131 | 132 | self.emitUpdate(id, finalInfo); 133 | } 134 | 135 | /** 136 | * This method shall be called at each time there are new message 137 | * 138 | */ 139 | updateRecordWsMessage(id, message) { 140 | if (id < 0) return; 141 | try { 142 | this.getCacheFile(WS_MESSAGE_FILE_PRFIX + id, (err, recordWsMessageFile) => { 143 | if (err) return; 144 | fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', () => {}); 145 | }); 146 | } catch (e) { 147 | console.error(e); 148 | logUtil.error(e.message + e.stack); 149 | } 150 | 151 | this.emitUpdateLatestWsMessage(id, { 152 | id: id, 153 | message: message 154 | }); 155 | } 156 | 157 | updateExtInfo(id, extInfo) { 158 | const self = this; 159 | const db = self.db; 160 | 161 | db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => { 162 | if (!err) { 163 | self.emitUpdate(id); 164 | } 165 | }); 166 | } 167 | 168 | appendRecord(info) { 169 | if (info.req.headers.anyproxy_web_req) { //TODO request from web interface 170 | return -1; 171 | } 172 | const self = this; 173 | const db = self.db; 174 | 175 | const thisId = self.globalId++; 176 | const finalInfo = normalizeInfo(thisId, info); 177 | db.insert(finalInfo); 178 | self.updateRecordBody(thisId, info); 179 | 180 | self.emitUpdate(thisId, finalInfo); 181 | return thisId; 182 | } 183 | 184 | updateRecordBody(id, info) { 185 | const self = this; 186 | 187 | if (id === -1) return; 188 | 189 | if (!id || typeof info.resBody === 'undefined') return; 190 | //add to body map 191 | //ignore image data 192 | self.getCacheFile(BODY_FILE_PRFIX + id, (err, bodyFile) => { 193 | if (err) return; 194 | fs.writeFile(bodyFile, info.resBody, () => {}); 195 | }); 196 | } 197 | 198 | /** 199 | * get body and websocket file 200 | * 201 | */ 202 | getBody(id, cb) { 203 | const self = this; 204 | 205 | if (id < 0) { 206 | cb && cb(''); 207 | return; 208 | } 209 | self.getCacheFile(BODY_FILE_PRFIX + id, (error, bodyFile) => { 210 | if (error) { 211 | cb && cb(error); 212 | return; 213 | } 214 | fs.access(bodyFile, fs.F_OK || fs.R_OK, (err) => { 215 | if (err) { 216 | cb && cb(err); 217 | } else { 218 | fs.readFile(bodyFile, cb); 219 | } 220 | }); 221 | }); 222 | } 223 | 224 | getDecodedBody(id, cb) { 225 | const self = this; 226 | const result = { 227 | method: '', 228 | type: 'unknown', 229 | mime: '', 230 | content: '' 231 | }; 232 | self.getSingleRecord(id, (err, doc) => { 233 | //check whether this record exists 234 | if (!doc || !doc[0]) { 235 | cb(new Error('failed to find record for this id')); 236 | return; 237 | } 238 | 239 | // also put the `method` back, so the client can decide whether to load ws messages 240 | result.method = doc[0].method; 241 | 242 | self.getBody(id, (error, bodyContent) => { 243 | if (error) { 244 | cb(error); 245 | } else if (!bodyContent) { 246 | cb(null, result); 247 | } else { 248 | const record = doc[0], 249 | resHeader = record.resHeader || {}; 250 | try { 251 | const headerStr = JSON.stringify(resHeader), 252 | charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/), 253 | contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']); 254 | 255 | if (charsetMatch && charsetMatch.length) { 256 | const currentCharset = charsetMatch[1].toLowerCase(); 257 | if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) { 258 | bodyContent = iconv.decode(bodyContent, currentCharset); 259 | } 260 | 261 | result.content = bodyContent.toString(); 262 | result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text'; 263 | } else if (contentType && /image/i.test(contentType)) { 264 | result.type = 'image'; 265 | result.content = bodyContent; 266 | } else { 267 | result.type = contentType; 268 | result.content = bodyContent.toString(); 269 | } 270 | result.mime = contentType; 271 | result.fileName = path.basename(record.path); 272 | result.statusCode = record.statusCode; 273 | } catch (e) { 274 | console.error(e); 275 | } 276 | cb(null, result); 277 | } 278 | }); 279 | }); 280 | } 281 | 282 | /** 283 | * get decoded WebSoket messages 284 | * 285 | */ 286 | getDecodedWsMessage(id, cb) { 287 | if (id < 0) { 288 | cb && cb([]); 289 | return; 290 | } 291 | 292 | this.getCacheFile(WS_MESSAGE_FILE_PRFIX + id, (outError, wsMessageFile) => { 293 | if (outError) { 294 | cb && cb(outError); 295 | return; 296 | } 297 | fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => { 298 | if (err) { 299 | cb && cb(err); 300 | } else { 301 | fs.readFile(wsMessageFile, 'utf8', (error, content) => { 302 | if (error) { 303 | cb && cb(err); 304 | } 305 | 306 | try { 307 | // remove the last dash "," if it has, since it's redundant 308 | // and also add brackets to make it a complete JSON structure 309 | content = `[${content.replace(/,$/, '')}]`; 310 | const messages = JSON.parse(content); 311 | cb(null, messages); 312 | } catch (e) { 313 | console.error(e); 314 | logUtil.error(e.message + e.stack); 315 | cb(e); 316 | } 317 | }); 318 | } 319 | }); 320 | }); 321 | } 322 | 323 | getSingleRecord(id, cb) { 324 | const self = this; 325 | const db = self.db; 326 | db.find({ _id: parseInt(id, 10) }, cb); 327 | } 328 | 329 | getSummaryList(cb) { 330 | const self = this; 331 | const db = self.db; 332 | db.find({}, cb); 333 | } 334 | 335 | getRecords(idStart, limit, cb) { 336 | const self = this; 337 | const db = self.db; 338 | limit = limit || 10; 339 | idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit); 340 | db.find({ _id: { $gte: parseInt(idStart, 10) } }) 341 | .sort({ _id: 1 }) 342 | .limit(limit) 343 | .exec(cb); 344 | } 345 | 346 | clear() { 347 | logUtil.printLog('clearing cache file...'); 348 | const self = this; 349 | proxyUtil.deleteFolderContentsRecursive(self.cachePath, true); 350 | } 351 | 352 | getCacheFile(fileName, cb) { 353 | const self = this; 354 | const cachePath = self.cachePath; 355 | const filepath = path.join(cachePath, fileName); 356 | 357 | if (filepath.indexOf(cachePath) !== 0) { 358 | cb && cb(new Error('invalid cache file path')); 359 | } else { 360 | cb && cb(null, filepath); 361 | return filepath; 362 | } 363 | } 364 | } 365 | 366 | module.exports = Recorder; 367 | -------------------------------------------------------------------------------- /main/anyproxy/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'), 4 | https = require('https'), 5 | async = require('async'), 6 | color = require('colorful'), 7 | certMgr = require('./lib/certMgr'), 8 | Recorder = require('./lib/recorder'), 9 | logUtil = require('./lib/log'), 10 | util = require('./lib/util'), 11 | events = require('events'), 12 | co = require('co'), 13 | WebInterface = require('./lib/webInterface'), 14 | wsServerMgr = require('./lib/wsServerMgr'), 15 | ThrottleGroup = require('stream-throttle').ThrottleGroup; 16 | 17 | const T_TYPE_HTTP = 'http', 18 | T_TYPE_HTTPS = 'https', 19 | DEFAULT_TYPE = T_TYPE_HTTP; 20 | 21 | const PROXY_STATUS_INIT = 'INIT'; 22 | const PROXY_STATUS_READY = 'READY'; 23 | const PROXY_STATUS_CLOSED = 'CLOSED'; 24 | 25 | /** 26 | * 27 | * @class ProxyCore 28 | * @extends {events.EventEmitter} 29 | */ 30 | class ProxyCore extends events.EventEmitter { 31 | /** 32 | * Creates an instance of ProxyCore. 33 | * 34 | * @param {object} config - configs 35 | * @param {number} config.port - port of the proxy server 36 | * @param {object} [config.rule=null] - rule module to use 37 | * @param {string} [config.type=http] - type of the proxy server, could be 'http' or 'https' 38 | * @param {strign} [config.hostname=localhost] - host name of the proxy server, required when this is an https proxy 39 | * @param {number} [config.throttle] - speed limit in kb/s 40 | * @param {boolean} [config.forceProxyHttps=false] - if proxy all https requests 41 | * @param {boolean} [config.silent=false] - if keep the console silent 42 | * @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response 43 | * @param {object} [config.recorder] - recorder to use 44 | * @param {boolean} [config.wsIntercept] - whether intercept websocket 45 | * 46 | * @memberOf ProxyCore 47 | */ 48 | constructor(config) { 49 | super(); 50 | config = config || {}; 51 | 52 | this.status = PROXY_STATUS_INIT; 53 | this.proxyPort = config.port; 54 | this.proxyType = /https/i.test(config.type || DEFAULT_TYPE) ? T_TYPE_HTTPS : T_TYPE_HTTP; 55 | this.proxyHostName = config.hostname || 'localhost'; 56 | global.recorder = config.recorder; 57 | 58 | if (parseInt(process.versions.node.split('.')[0], 10) < 4) { 59 | throw new Error('node.js >= v4.x is required for dynproxy'); 60 | } else if (config.forceProxyHttps && !certMgr.ifRootCAFileExists()) { 61 | logUtil.printLog('You can run `dynproxy-ca` to generate one root CA and then re-run this command'); 62 | throw new Error('root CA not found. Please run `dynproxy-ca` to generate one first.'); 63 | } else if (this.proxyType === T_TYPE_HTTPS && !config.hostname) { 64 | throw new Error('hostname is required in https proxy'); 65 | } else if (!this.proxyPort) { 66 | throw new Error('proxy port is required'); 67 | } else if (!global.recorder) { 68 | throw new Error('recorder is required'); 69 | } else if (config.forceProxyHttps && config.rule && config.rule.beforeDealHttpsRequest) { 70 | logUtil.printLog('both "-i(--intercept)" and rule.beforeDealHttpsRequest are specified, the "-i" option will be ignored.', logUtil.T_WARN); 71 | config.forceProxyHttps = false; 72 | } 73 | 74 | this.httpProxyServer = null; 75 | this.requestHandler = null; 76 | 77 | // copy the rule to keep the original proxyRule independent 78 | this.proxyRule = config.rule || {}; 79 | 80 | if (config.silent) { 81 | logUtil.setPrintStatus(false); 82 | } 83 | 84 | if (config.throttle) { 85 | logUtil.printLog('throttle :' + config.throttle + 'kb/s'); 86 | const rate = parseInt(config.throttle, 10); 87 | if (rate < 1) { 88 | throw new Error('Invalid throttle rate value, should be positive integer'); 89 | } 90 | global._throttle = new ThrottleGroup({ rate: 1024 * rate }); // rate - byte/sec 91 | } 92 | 93 | // init request handler 94 | const RequestHandler = util.freshRequire('./requestHandler'); 95 | this.requestHandler = new RequestHandler({ 96 | wsIntercept: config.wsIntercept, 97 | httpServerPort: config.port, // the http server port for http proxy 98 | forceProxyHttps: !!config.forceProxyHttps, 99 | dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized 100 | }, this.proxyRule, global.recorder); 101 | } 102 | 103 | /** 104 | * manage all created socket 105 | * for each new socket, we put them to a map; 106 | * if the socket is closed itself, we remove it from the map 107 | * when the `close` method is called, we'll close the sockes before the server closed 108 | * 109 | * @param {Socket} the http socket that is creating 110 | * @returns undefined 111 | * @memberOf ProxyCore 112 | */ 113 | handleExistConnections(socket) { 114 | const self = this; 115 | self.socketIndex++; 116 | const key = `socketIndex_${self.socketIndex}`; 117 | self.socketPool[key] = socket; 118 | 119 | // if the socket is closed already, removed it from pool 120 | socket.on('close', () => { 121 | delete self.socketPool[key]; 122 | }); 123 | } 124 | /** 125 | * start the proxy server 126 | * 127 | * @returns ProxyCore 128 | * 129 | * @memberOf ProxyCore 130 | */ 131 | start() { 132 | const self = this; 133 | self.socketIndex = 0; 134 | self.socketPool = {}; 135 | 136 | if (self.status !== PROXY_STATUS_INIT) { 137 | throw new Error('server status is not PROXY_STATUS_INIT, can not run start()'); 138 | } 139 | async.series( 140 | [ 141 | //creat proxy server 142 | function (callback) { 143 | if (self.proxyType === T_TYPE_HTTPS) { 144 | certMgr.getCertificate(self.proxyHostName, (err, keyContent, crtContent) => { 145 | if (err) { 146 | callback(err); 147 | } else { 148 | self.httpProxyServer = https.createServer({ 149 | key: keyContent, 150 | cert: crtContent 151 | }, self.requestHandler.userRequestHandler); 152 | callback(null); 153 | } 154 | }); 155 | } else { 156 | self.httpProxyServer = http.createServer(self.requestHandler.userRequestHandler); 157 | callback(null); 158 | } 159 | }, 160 | 161 | //handle CONNECT request for https over http 162 | function (callback) { 163 | self.httpProxyServer.on('connect', self.requestHandler.connectReqHandler); 164 | 165 | callback(null); 166 | }, 167 | 168 | function (callback) { 169 | wsServerMgr.getWsServer({ 170 | server: self.httpProxyServer, 171 | connHandler: self.requestHandler.wsHandler 172 | }); 173 | // remember all sockets, so we can destory them when call the method 'close'; 174 | self.httpProxyServer.on('connection', (socket) => { 175 | self.handleExistConnections.call(self, socket); 176 | }); 177 | callback(null); 178 | }, 179 | 180 | //start proxy server 181 | function (callback) { 182 | self.httpProxyServer.listen(self.proxyPort); 183 | callback(null); 184 | }, 185 | ], 186 | 187 | //final callback 188 | (err, result) => { 189 | if (!err) { 190 | const tipText = (self.proxyType === T_TYPE_HTTP ? 'Http' : 'Https') + ' proxy started on port ' + self.proxyPort; 191 | logUtil.printLog(color.green(tipText)); 192 | 193 | if (self.webServerInstance) { 194 | const webTip = 'web interface started on port ' + self.webServerInstance.webPort; 195 | logUtil.printLog(color.green(webTip)); 196 | } 197 | 198 | let ruleSummaryString = ''; 199 | const ruleSummary = this.proxyRule.summary; 200 | if (ruleSummary) { 201 | co(function *() { 202 | if (typeof ruleSummary === 'string') { 203 | ruleSummaryString = ruleSummary; 204 | } else { 205 | ruleSummaryString = yield ruleSummary(); 206 | } 207 | 208 | logUtil.printLog(color.green(`Active rule is: ${ruleSummaryString}`)); 209 | }); 210 | } 211 | 212 | self.status = PROXY_STATUS_READY; 213 | self.emit('ready'); 214 | } else { 215 | const tipText = 'err when start proxy server :('; 216 | logUtil.printLog(color.red(tipText), logUtil.T_ERR); 217 | logUtil.printLog(err, logUtil.T_ERR); 218 | self.emit('error', { 219 | error: err 220 | }); 221 | } 222 | } 223 | ); 224 | 225 | return self; 226 | } 227 | 228 | /** 229 | * close the proxy server 230 | * 231 | * @returns ProxyCore 232 | * 233 | * @memberOf ProxyCore 234 | */ 235 | close() { 236 | // clear recorder cache 237 | return new Promise((resolve) => { 238 | if (this.httpProxyServer) { 239 | // destroy conns & cltSockets when closing proxy server 240 | for (const connItem of this.requestHandler.conns) { 241 | const key = connItem[0]; 242 | const conn = connItem[1]; 243 | logUtil.printLog(`destorying https connection : ${key}`); 244 | conn.end(); 245 | } 246 | 247 | for (const cltSocketItem of this.requestHandler.cltSockets) { 248 | const key = cltSocketItem[0]; 249 | const cltSocket = cltSocketItem[1]; 250 | logUtil.printLog(`closing https cltSocket : ${key}`); 251 | cltSocket.end(); 252 | } 253 | 254 | if (this.requestHandler.httpsServerMgr) { 255 | this.requestHandler.httpsServerMgr.close(); 256 | } 257 | 258 | if (this.socketPool) { 259 | for (const key in this.socketPool) { 260 | this.socketPool[key].destroy(); 261 | } 262 | } 263 | 264 | this.httpProxyServer.close((error) => { 265 | if (error) { 266 | console.error(error); 267 | logUtil.printLog(`proxy server close FAILED : ${error.message}`, logUtil.T_ERR); 268 | } else { 269 | this.httpProxyServer = null; 270 | 271 | this.status = PROXY_STATUS_CLOSED; 272 | logUtil.printLog(`proxy server closed at ${this.proxyHostName}:${this.proxyPort}`); 273 | } 274 | resolve(error); 275 | }); 276 | } else { 277 | resolve(); 278 | } 279 | }) 280 | } 281 | } 282 | 283 | /** 284 | * start proxy server as well as recorder and webInterface 285 | */ 286 | class ProxyServer extends ProxyCore { 287 | /** 288 | * 289 | * @param {object} config - config 290 | * @param {object} [config.webInterface] - config of the web interface 291 | * @param {boolean} [config.webInterface.enable=false] - if web interface is enabled 292 | * @param {number} [config.webInterface.webPort=8002] - http port of the web interface 293 | */ 294 | constructor(config) { 295 | // prepare a recorder 296 | const recorder = new Recorder(); 297 | const configForCore = Object.assign({ 298 | recorder, 299 | }, config); 300 | 301 | super(configForCore); 302 | 303 | this.proxyWebinterfaceConfig = config.webInterface; 304 | global.recorder = recorder; 305 | this.webServerInstance = null; 306 | } 307 | 308 | start() { 309 | if (global.recorder) { 310 | global.recorder.setDbAutoCompact(); 311 | } 312 | 313 | // start web interface if neeeded 314 | if (this.proxyWebinterfaceConfig && this.proxyWebinterfaceConfig.enable) { 315 | this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, global.recorder); 316 | // start web server 317 | this.webServerInstance.start() 318 | // start proxy core 319 | .then(() => { 320 | super.start(); 321 | }) 322 | .catch((e) => { 323 | this.emit('error', e); 324 | }); 325 | } else { 326 | super.start(); 327 | } 328 | } 329 | 330 | close() { 331 | const self = this; 332 | // release recorder 333 | if (global.recorder) { 334 | global.recorder.stopDbAutoCompact(); 335 | global.recorder.clear(); 336 | } 337 | global.recorder = null; 338 | 339 | // close ProxyCore 340 | return super.close() 341 | // release webInterface 342 | .then(() => { 343 | if (self.webServerInstance) { 344 | const tmpWebServer = self.webServerInstance; 345 | self.webServerInstance = null; 346 | logUtil.printLog('closing webInterface...'); 347 | return tmpWebServer.close(); 348 | } 349 | }); 350 | } 351 | } 352 | 353 | module.exports.ProxyCore = ProxyCore; 354 | module.exports.ProxyServer = ProxyServer; 355 | module.exports.ProxyRecorder = Recorder; 356 | module.exports.ProxyWebServer = WebInterface; 357 | module.exports.utils = { 358 | systemProxyMgr: require('./lib/systemProxyMgr'), 359 | certMgr, 360 | }; 361 | -------------------------------------------------------------------------------- /main/anyproxy/lib/requestHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'), 4 | https = require('https'), 5 | net = require('net'), 6 | url = require('url'), 7 | zlib = require('zlib'), 8 | color = require('colorful'), 9 | Buffer = require('buffer').Buffer, 10 | util = require('./util'), 11 | Stream = require('stream'), 12 | logUtil = require('./log'), 13 | co = require('co'), 14 | WebSocket = require('ws'), 15 | HttpsServerMgr = require('./httpsServerMgr'), 16 | brotliTorb = require('brotli'), 17 | Readable = require('stream').Readable; 18 | 19 | const requestErrorHandler = require('./requestErrorHandler'); 20 | 21 | // to fix issue with TLS cache, refer to: https://github.com/nodejs/node/issues/8368 22 | https.globalAgent.maxCachedSessions = 0; 23 | 24 | const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb 25 | 26 | class CommonReadableStream extends Readable { 27 | constructor(config) { 28 | super({ 29 | highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5 30 | }); 31 | } 32 | _read(size) { 33 | 34 | } 35 | } 36 | 37 | /* 38 | * get error response for exception scenarios 39 | */ 40 | const getErrorResponse = (error, fullUrl) => { 41 | // default error response 42 | const errorResponse = { 43 | statusCode: 500, 44 | header: { 45 | 'Content-Type': 'text/html; charset=utf-8', 46 | 'Proxy-Error': true, 47 | 'Proxy-Error-Message': error ? JSON.stringify(error) : 'null' 48 | }, 49 | body: requestErrorHandler.getErrorContent(error, fullUrl) 50 | }; 51 | 52 | return errorResponse; 53 | } 54 | 55 | /** 56 | * fetch remote response 57 | * 58 | * @param {string} protocol 59 | * @param {object} options options of http.request 60 | * @param {buffer} reqData request body 61 | * @param {object} config 62 | * @param {boolean} config.dangerouslyIgnoreUnauthorized 63 | * @param {boolean} config.chunkSizeThreshold 64 | * @returns 65 | */ 66 | function fetchRemoteResponse(protocol, options, reqData, config) { 67 | reqData = reqData || ''; 68 | return new Promise((resolve, reject) => { 69 | delete options.headers['content-length']; // will reset the content-length after rule 70 | delete options.headers['Content-Length']; 71 | delete options.headers['Transfer-Encoding']; 72 | delete options.headers['transfer-encoding']; 73 | 74 | if (config.dangerouslyIgnoreUnauthorized) { 75 | options.rejectUnauthorized = false; 76 | } 77 | 78 | if (!config.chunkSizeThreshold) { 79 | throw new Error('chunkSizeThreshold is required'); 80 | } 81 | 82 | //send request 83 | const proxyReq = (/https/i.test(protocol) ? https : http).request(options, (res) => { 84 | res.headers = util.getHeaderFromRawHeaders(res.rawHeaders); 85 | //deal response header 86 | const statusCode = res.statusCode; 87 | const resHeader = res.headers; 88 | let resDataChunks = []; // array of data chunks or stream 89 | const rawResChunks = []; // the original response chunks 90 | let resDataStream = null; 91 | let resSize = 0; 92 | const finishCollecting = () => { 93 | new Promise((fulfill, rejectParsing) => { 94 | if (resDataStream) { 95 | fulfill(resDataStream); 96 | } else { 97 | const serverResData = Buffer.concat(resDataChunks); 98 | const originContentLen = util.getByteSize(serverResData); 99 | // remove gzip related header, and ungzip the content 100 | // note there are other compression types like deflate 101 | const contentEncoding = resHeader['content-encoding'] || resHeader['Content-Encoding']; 102 | const ifServerGzipped = /gzip/i.test(contentEncoding); 103 | const isServerDeflated = /deflate/i.test(contentEncoding); 104 | const isBrotlied = /br/i.test(contentEncoding); 105 | 106 | /** 107 | * when the content is unzipped, update the header content 108 | */ 109 | const refactContentEncoding = () => { 110 | if (contentEncoding) { 111 | resHeader['x-anyproxy-origin-content-encoding'] = contentEncoding; 112 | delete resHeader['content-encoding']; 113 | delete resHeader['Content-Encoding']; 114 | } 115 | } 116 | 117 | // set origin content length into header 118 | resHeader['x-anyproxy-origin-content-length'] = originContentLen; 119 | 120 | // only do unzip when there is res data 121 | if (ifServerGzipped && originContentLen) { 122 | refactContentEncoding(); 123 | zlib.gunzip(serverResData, (err, buff) => { 124 | if (err) { 125 | rejectParsing(err); 126 | } else { 127 | fulfill(buff); 128 | } 129 | }); 130 | } else if (isServerDeflated && originContentLen) { 131 | refactContentEncoding(); 132 | zlib.inflate(serverResData, (err, buff) => { 133 | if (err) { 134 | rejectParsing(err); 135 | } else { 136 | fulfill(buff); 137 | } 138 | }); 139 | } else if (isBrotlied && originContentLen) { 140 | refactContentEncoding(); 141 | 142 | try { 143 | // an Unit8Array returned by decompression 144 | const result = brotliTorb.decompress(serverResData); 145 | fulfill(Buffer.from(result)); 146 | } catch (e) { 147 | rejectParsing(e); 148 | } 149 | } else { 150 | fulfill(serverResData); 151 | } 152 | } 153 | }).then((serverResData) => { 154 | resolve({ 155 | statusCode, 156 | header: resHeader, 157 | body: serverResData, 158 | rawBody: rawResChunks, 159 | _res: res, 160 | }); 161 | }).catch((e) => { 162 | reject(e); 163 | }); 164 | }; 165 | 166 | //deal response data 167 | res.on('data', (chunk) => { 168 | rawResChunks.push(chunk); 169 | if (resDataStream) { // stream mode 170 | resDataStream.push(chunk); 171 | } else { // dataChunks 172 | resSize += chunk.length; 173 | resDataChunks.push(chunk); 174 | 175 | // stop collecting, convert to stream mode 176 | if (resSize >= config.chunkSizeThreshold) { 177 | resDataStream = new CommonReadableStream(); 178 | while (resDataChunks.length) { 179 | resDataStream.push(resDataChunks.shift()); 180 | } 181 | resDataChunks = null; 182 | finishCollecting(); 183 | } 184 | } 185 | }); 186 | 187 | res.on('end', () => { 188 | if (resDataStream) { 189 | resDataStream.push(null); // indicate the stream is end 190 | } else { 191 | finishCollecting(); 192 | } 193 | }); 194 | res.on('error', (error) => { 195 | logUtil.printLog('error happend in response:' + error, logUtil.T_ERR); 196 | reject(error); 197 | }); 198 | }); 199 | 200 | proxyReq.on('error', reject); 201 | proxyReq.end(reqData); 202 | }); 203 | } 204 | 205 | /** 206 | * get request info from the ws client, includes: 207 | host 208 | port 209 | path 210 | protocol ws/wss 211 | 212 | @param @required wsClient the ws client of WebSocket 213 | * 214 | */ 215 | function getWsReqInfo(wsReq) { 216 | const headers = wsReq.headers || {}; 217 | const host = headers.host; 218 | const hostName = host.split(':')[0]; 219 | const port = host.split(':')[1]; 220 | 221 | // TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出 222 | const path = wsReq.url || '/'; 223 | 224 | const isEncript = wsReq.connection && wsReq.connection.encrypted; 225 | /** 226 | * construct the request headers based on original connection, 227 | * but delete the `sec-websocket-*` headers as they are already consumed by AnyProxy 228 | */ 229 | const getNoWsHeaders = () => { 230 | const originHeaders = Object.assign({}, headers); 231 | const originHeaderKeys = Object.keys(originHeaders); 232 | originHeaderKeys.forEach((key) => { 233 | // if the key matchs 'sec-websocket', delete it 234 | if (/sec-websocket/ig.test(key)) { 235 | delete originHeaders[key]; 236 | } 237 | }); 238 | 239 | delete originHeaders.connection; 240 | delete originHeaders.upgrade; 241 | return originHeaders; 242 | } 243 | 244 | 245 | return { 246 | headers: headers, // the full headers of origin ws connection 247 | noWsHeaders: getNoWsHeaders(), 248 | hostName: hostName, 249 | port: port, 250 | path: path, 251 | protocol: isEncript ? 'wss' : 'ws' 252 | }; 253 | } 254 | /** 255 | * get a request handler for http/https server 256 | * 257 | * @param {RequestHandler} reqHandlerCtx 258 | * @param {object} userRule 259 | * @param {Recorder} recorder 260 | * @returns 261 | */ 262 | function getUserReqHandler(userRule, recorder) { 263 | const reqHandlerCtx = this 264 | 265 | return function (req, userRes) { 266 | /* 267 | note 268 | req.url is wired 269 | in http server: http://www.example.com/a/b/c 270 | in https server: /a/b/c 271 | */ 272 | 273 | const host = req.headers.host; 274 | const protocol = (!!req.connection.encrypted && !(/^http:/).test(req.url)) ? 'https' : 'http'; 275 | 276 | // try find fullurl https://github.com/alibaba/anyproxy/issues/419 277 | let fullUrl = protocol + '://' + host + req.url; 278 | if (protocol === 'http') { 279 | const reqUrlPattern = url.parse(req.url); 280 | if (reqUrlPattern.host && reqUrlPattern.protocol) { 281 | fullUrl = req.url; 282 | } 283 | } 284 | 285 | const urlPattern = url.parse(fullUrl); 286 | const path = urlPattern.path; 287 | const chunkSizeThreshold = DEFAULT_CHUNK_COLLECT_THRESHOLD; 288 | 289 | let resourceInfo = null; 290 | let resourceInfoId = -1; 291 | let reqData; 292 | let requestDetail; 293 | 294 | // refer to https://github.com/alibaba/anyproxy/issues/103 295 | // construct the original headers as the reqheaders 296 | req.headers = util.getHeaderFromRawHeaders(req.rawHeaders); 297 | 298 | logUtil.printLog(color.green(`received request to: ${req.method} ${host}${path}`)); 299 | 300 | /** 301 | * fetch complete req data 302 | */ 303 | const fetchReqData = () => new Promise((resolve) => { 304 | const postData = []; 305 | req.on('data', (chunk) => { 306 | postData.push(chunk); 307 | }); 308 | req.on('end', () => { 309 | reqData = Buffer.concat(postData); 310 | resolve(); 311 | }); 312 | }); 313 | 314 | /** 315 | * prepare detailed request info 316 | */ 317 | const prepareRequestDetail = () => { 318 | const options = { 319 | hostname: urlPattern.hostname || req.headers.host, 320 | port: urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80), 321 | path, 322 | method: req.method, 323 | headers: req.headers 324 | }; 325 | 326 | requestDetail = { 327 | requestOptions: options, 328 | protocol, 329 | url: fullUrl, 330 | requestData: reqData, 331 | _req: req 332 | }; 333 | 334 | return Promise.resolve(); 335 | }; 336 | 337 | /** 338 | * send response to client 339 | * 340 | * @param {object} finalResponseData 341 | * @param {number} finalResponseData.statusCode 342 | * @param {object} finalResponseData.header 343 | * @param {buffer|string} finalResponseData.body 344 | */ 345 | const sendFinalResponse = (finalResponseData) => { 346 | const responseInfo = finalResponseData.response; 347 | const resHeader = responseInfo.header; 348 | const responseBody = responseInfo.body || ''; 349 | 350 | const transferEncoding = resHeader['transfer-encoding'] || resHeader['Transfer-Encoding'] || ''; 351 | const contentLength = resHeader['content-length'] || resHeader['Content-Length']; 352 | const connection = resHeader.Connection || resHeader.connection; 353 | if (contentLength) { 354 | delete resHeader['content-length']; 355 | delete resHeader['Content-Length']; 356 | } 357 | 358 | // set proxy-connection 359 | if (connection) { 360 | resHeader['x-anyproxy-origin-connection'] = connection; 361 | delete resHeader.connection; 362 | delete resHeader.Connection; 363 | } 364 | 365 | if (!responseInfo) { 366 | throw new Error('failed to get response info'); 367 | } else if (!responseInfo.statusCode) { 368 | throw new Error('failed to get response status code') 369 | } else if (!responseInfo.header) { 370 | throw new Error('filed to get response header'); 371 | } 372 | // if there is no transfer-encoding, set the content-length 373 | if (!global._throttle 374 | && transferEncoding !== 'chunked' 375 | && !(responseBody instanceof CommonReadableStream) 376 | ) { 377 | resHeader['Content-Length'] = util.getByteSize(responseBody); 378 | } 379 | 380 | userRes.writeHead(responseInfo.statusCode, resHeader); 381 | 382 | if (global._throttle) { 383 | if (responseBody instanceof CommonReadableStream) { 384 | responseBody.pipe(global._throttle.throttle()).pipe(userRes); 385 | } else { 386 | const thrStream = new Stream(); 387 | thrStream.pipe(global._throttle.throttle()).pipe(userRes); 388 | thrStream.emit('data', responseBody); 389 | thrStream.emit('end'); 390 | } 391 | } else { 392 | if (responseBody instanceof CommonReadableStream) { 393 | responseBody.pipe(userRes); 394 | } else { 395 | userRes.end(responseBody); 396 | } 397 | } 398 | 399 | return responseInfo; 400 | } 401 | 402 | // fetch complete request data 403 | co(fetchReqData) 404 | .then(prepareRequestDetail) 405 | 406 | .then(() => { 407 | // record request info 408 | if (recorder) { 409 | resourceInfo = { 410 | host, 411 | method: req.method, 412 | path, 413 | protocol, 414 | url: protocol + '://' + host + path, 415 | req, 416 | startTime: new Date().getTime() 417 | }; 418 | resourceInfoId = recorder.appendRecord(resourceInfo); 419 | } 420 | 421 | try { 422 | resourceInfo.reqBody = reqData.toString(); //TODO: deal reqBody in webInterface.js 423 | recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 424 | } catch (e) { } 425 | }) 426 | 427 | // invoke rule before sending request 428 | .then(co.wrap(function *() { 429 | const userModifiedInfo = (yield userRule.beforeSendRequest(Object.assign({}, requestDetail))) || {}; 430 | const finalReqDetail = {}; 431 | ['protocol', 'requestOptions', 'requestData', 'response'].map((key) => { 432 | finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key] 433 | }); 434 | return finalReqDetail; 435 | })) 436 | 437 | // route user config 438 | .then(co.wrap(function *(userConfig) { 439 | if (userConfig.response) { 440 | // user-assigned local response 441 | userConfig._directlyPassToRespond = true; 442 | return userConfig; 443 | } else if (userConfig.requestOptions) { 444 | const remoteResponse = yield fetchRemoteResponse(userConfig.protocol, userConfig.requestOptions, userConfig.requestData, { 445 | dangerouslyIgnoreUnauthorized: reqHandlerCtx.dangerouslyIgnoreUnauthorized, 446 | chunkSizeThreshold, 447 | }); 448 | return { 449 | response: { 450 | statusCode: remoteResponse.statusCode, 451 | header: remoteResponse.header, 452 | body: remoteResponse.body, 453 | rawBody: remoteResponse.rawBody 454 | }, 455 | _res: remoteResponse._res, 456 | }; 457 | } else { 458 | throw new Error('lost response or requestOptions, failed to continue'); 459 | } 460 | })) 461 | 462 | // invoke rule before responding to client 463 | .then(co.wrap(function *(responseData) { 464 | if (responseData._directlyPassToRespond) { 465 | return responseData; 466 | } else if (responseData.response.body && responseData.response.body instanceof CommonReadableStream) { // in stream mode 467 | return responseData; 468 | } else { 469 | // TODO: err etimeout 470 | return (yield userRule.beforeSendResponse(Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData; 471 | } 472 | })) 473 | 474 | .catch(co.wrap(function *(error) { 475 | logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR); 476 | 477 | let errorResponse = getErrorResponse(error, fullUrl); 478 | 479 | // call user rule 480 | try { 481 | const userResponse = yield userRule.onError(Object.assign({}, requestDetail), error); 482 | if (userResponse && userResponse.response && userResponse.response.header) { 483 | errorResponse = userResponse.response; 484 | } 485 | } catch (e) {} 486 | 487 | return { 488 | response: errorResponse 489 | }; 490 | })) 491 | .then(sendFinalResponse) 492 | 493 | //update record info 494 | .then((responseInfo) => { 495 | resourceInfo.endTime = new Date().getTime(); 496 | resourceInfo.res = { //construct a self-defined res object 497 | statusCode: responseInfo.statusCode, 498 | headers: responseInfo.header, 499 | }; 500 | 501 | resourceInfo.statusCode = responseInfo.statusCode; 502 | resourceInfo.resHeader = responseInfo.header; 503 | resourceInfo.resBody = responseInfo.body instanceof CommonReadableStream ? '(big stream)' : (responseInfo.body || ''); 504 | resourceInfo.length = resourceInfo.resBody.length; 505 | 506 | // console.info('===> resbody in record', resourceInfo); 507 | 508 | recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 509 | }) 510 | .catch((e) => { 511 | logUtil.printLog(color.green('Send final response failed:' + e.message), logUtil.T_ERR); 512 | }); 513 | } 514 | } 515 | 516 | /** 517 | * get a handler for CONNECT request 518 | * 519 | * @param {RequestHandler} reqHandlerCtx 520 | * @param {object} userRule 521 | * @param {Recorder} recorder 522 | * @param {object} httpsServerMgr 523 | * @returns 524 | */ 525 | function getConnectReqHandler(userRule, recorder, httpsServerMgr) { 526 | const reqHandlerCtx = this; reqHandlerCtx.conns = new Map(); reqHandlerCtx.cltSockets = new Map() 527 | 528 | return function (req, cltSocket, head) { 529 | const host = req.url.split(':')[0], 530 | targetPort = req.url.split(':')[1]; 531 | let shouldIntercept; 532 | let interceptWsRequest = false; 533 | let requestDetail; 534 | let resourceInfo = null; 535 | let resourceInfoId = -1; 536 | const requestStream = new CommonReadableStream(); 537 | 538 | /* 539 | 1. write HTTP/1.1 200 to client 540 | 2. get request data 541 | 3. tell if it is a websocket request 542 | 4.1 if (websocket || do_not_intercept) --> pipe to target server 543 | 4.2 else --> pipe to local server and do man-in-the-middle attack 544 | */ 545 | co(function *() { 546 | // determine whether to use the man-in-the-middle server 547 | logUtil.printLog(color.green('received https CONNECT request ' + host)); 548 | requestDetail = { 549 | host: req.url, 550 | _req: req 551 | }; 552 | // the return value in default rule is null 553 | // so if the value is null, will take it as final value 554 | shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail); 555 | 556 | // otherwise, will take the passed in option 557 | if (shouldIntercept === null) { 558 | shouldIntercept = reqHandlerCtx.forceProxyHttps; 559 | } 560 | }) 561 | .then(() => { 562 | return new Promise((resolve) => { 563 | // mark socket connection as established, to detect the request protocol 564 | cltSocket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', resolve); 565 | }); 566 | }) 567 | .then(() => { 568 | return new Promise((resolve, reject) => { 569 | let resolved = false; 570 | cltSocket.on('data', (chunk) => { 571 | requestStream.push(chunk); 572 | if (!resolved) { 573 | resolved = true; 574 | try { 575 | const chunkString = chunk.toString(); 576 | if (chunkString.indexOf('GET ') === 0) { 577 | shouldIntercept = false; // websocket, do not intercept 578 | 579 | // if there is '/do-not-proxy' in the request, do not intercept the websocket 580 | // to avoid AnyProxy itself be proxied 581 | if (reqHandlerCtx.wsIntercept && chunkString.indexOf('GET /do-not-proxy') !== 0) { 582 | interceptWsRequest = true; 583 | } 584 | } 585 | } catch (e) { 586 | console.error(e); 587 | } 588 | resolve(); 589 | } 590 | }); 591 | cltSocket.on('error', (error) => { 592 | logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR); 593 | co.wrap(function *() { 594 | try { 595 | yield userRule.onClientSocketError(requestDetail, error); 596 | } catch (e) { } 597 | }); 598 | }); 599 | cltSocket.on('end', () => { 600 | requestStream.push(null); 601 | }); 602 | }); 603 | }) 604 | .then((result) => { 605 | // log and recorder 606 | if (shouldIntercept) { 607 | logUtil.printLog('will forward to local https server'); 608 | } else { 609 | logUtil.printLog('will bypass the man-in-the-middle proxy'); 610 | } 611 | 612 | //record 613 | if (recorder) { 614 | resourceInfo = { 615 | host, 616 | method: req.method, 617 | path: '', 618 | url: 'https://' + host, 619 | req, 620 | startTime: new Date().getTime() 621 | }; 622 | resourceInfoId = recorder.appendRecord(resourceInfo); 623 | } 624 | }) 625 | .then(() => { 626 | // determine the request target 627 | if (!shouldIntercept) { 628 | // server info from the original request 629 | const originServer = { 630 | host, 631 | port: (targetPort === 80) ? 443 : targetPort 632 | } 633 | 634 | const localHttpServer = { 635 | host: 'localhost', 636 | port: reqHandlerCtx.httpServerPort 637 | } 638 | 639 | // for ws request, redirect them to local ws server 640 | return interceptWsRequest ? localHttpServer : originServer; 641 | } else { 642 | return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port })); 643 | } 644 | }) 645 | .then((serverInfo) => { 646 | if (!serverInfo.port || !serverInfo.host) { 647 | throw new Error('failed to get https server info'); 648 | } 649 | 650 | return new Promise((resolve, reject) => { 651 | const conn = net.connect(serverInfo.port, serverInfo.host, () => { 652 | //throttle for direct-foward https 653 | if (global._throttle && !shouldIntercept) { 654 | requestStream.pipe(conn); 655 | conn.pipe(global._throttle.throttle()).pipe(cltSocket); 656 | } else { 657 | requestStream.pipe(conn); 658 | conn.pipe(cltSocket); 659 | } 660 | 661 | resolve(); 662 | }); 663 | 664 | conn.on('error', (e) => { 665 | reject(e); 666 | }); 667 | 668 | reqHandlerCtx.conns.set(serverInfo.host + ':' + serverInfo.port, conn) 669 | reqHandlerCtx.cltSockets.set(serverInfo.host + ':' + serverInfo.port, cltSocket) 670 | }); 671 | }) 672 | .then(() => { 673 | if (recorder) { 674 | resourceInfo.endTime = new Date().getTime(); 675 | resourceInfo.statusCode = '200'; 676 | resourceInfo.resHeader = {}; 677 | resourceInfo.resBody = ''; 678 | resourceInfo.length = 0; 679 | 680 | recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 681 | } 682 | }) 683 | .catch(co.wrap(function *(error) { 684 | logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR); 685 | 686 | try { 687 | yield userRule.onConnectError(requestDetail, error); 688 | } catch (e) { } 689 | 690 | try { 691 | let errorHeader = 'Proxy-Error: true\r\n'; 692 | errorHeader += 'Proxy-Error-Message: ' + (error || 'null') + '\r\n'; 693 | errorHeader += 'Content-Type: text/html\r\n'; 694 | cltSocket.write('HTTP/1.1 502\r\n' + errorHeader + '\r\n\r\n'); 695 | } catch (e) { } 696 | })); 697 | } 698 | } 699 | 700 | /** 701 | * get a websocket event handler 702 | @param @required {object} wsClient 703 | */ 704 | function getWsHandler(userRule, recorder, wsClient, wsReq) { 705 | const self = this; 706 | try { 707 | let resourceInfoId = -1; 708 | const resourceInfo = { 709 | wsMessages: [] // all ws messages go through AnyProxy 710 | }; 711 | const clientMsgQueue = []; 712 | const serverInfo = getWsReqInfo(wsReq); 713 | const serverInfoPort = serverInfo.port ? `:${serverInfo.port}` : ''; 714 | const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}${serverInfoPort}${serverInfo.path}`; 715 | const proxyWs = new WebSocket(wsUrl, '', { 716 | rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized, 717 | headers: serverInfo.noWsHeaders 718 | }); 719 | 720 | if (recorder) { 721 | Object.assign(resourceInfo, { 722 | host: serverInfo.hostName, 723 | method: 'WebSocket', 724 | path: serverInfo.path, 725 | url: wsUrl, 726 | req: wsReq, 727 | startTime: new Date().getTime() 728 | }); 729 | resourceInfoId = recorder.appendRecord(resourceInfo); 730 | } 731 | 732 | /** 733 | * store the messages before the proxy ws is ready 734 | */ 735 | const sendProxyMessage = (event) => { 736 | const message = event.data; 737 | if (proxyWs.readyState === 1) { 738 | // if there still are msg queue consuming, keep it going 739 | if (clientMsgQueue.length > 0) { 740 | clientMsgQueue.push(message); 741 | } else { 742 | proxyWs.send(message); 743 | } 744 | } else { 745 | clientMsgQueue.push(message); 746 | } 747 | } 748 | 749 | /** 750 | * consume the message in queue when the proxy ws is not ready yet 751 | * will handle them from the first one-by-one 752 | */ 753 | const consumeMsgQueue = () => { 754 | while (clientMsgQueue.length > 0) { 755 | const message = clientMsgQueue.shift(); 756 | proxyWs.send(message); 757 | } 758 | } 759 | 760 | /** 761 | * When the source ws is closed, we need to close the target websocket. 762 | * If the source ws is normally closed, that is, the code is reserved, we need to transfrom them 763 | */ 764 | const getCloseFromOriginEvent = (event) => { 765 | const code = event.code || ''; 766 | const reason = event.reason || ''; 767 | let targetCode = ''; 768 | let targetReason = ''; 769 | if (code >= 1004 && code <= 1006) { 770 | targetCode = 1000; // normal closure 771 | targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`; 772 | } else { 773 | targetCode = code; 774 | targetReason = reason; 775 | } 776 | 777 | return { 778 | code: targetCode, 779 | reason: targetReason 780 | } 781 | } 782 | 783 | /** 784 | * consruct a message Record from message event 785 | * @param @required {event} messageEvent the event from websockt.onmessage 786 | * @param @required {boolean} isToServer whether the message is to or from server 787 | * 788 | */ 789 | const recordMessage = (messageEvent, isToServer) => { 790 | const message = { 791 | time: Date.now(), 792 | message: messageEvent.data, 793 | isToServer: isToServer 794 | }; 795 | 796 | // resourceInfo.wsMessages.push(message); 797 | recorder && recorder.updateRecordWsMessage(resourceInfoId, message); 798 | }; 799 | 800 | proxyWs.onopen = () => { 801 | consumeMsgQueue(); 802 | } 803 | 804 | // this event is fired when the connection is build and headers is returned 805 | proxyWs.on('upgrade', (response) => { 806 | resourceInfo.endTime = new Date().getTime(); 807 | const headers = response.headers; 808 | resourceInfo.res = { //construct a self-defined res object 809 | statusCode: response.statusCode, 810 | headers: headers, 811 | }; 812 | 813 | resourceInfo.statusCode = response.statusCode; 814 | resourceInfo.resHeader = headers; 815 | resourceInfo.resBody = ''; 816 | resourceInfo.length = resourceInfo.resBody.length; 817 | 818 | recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 819 | }); 820 | 821 | proxyWs.onerror = (e) => { 822 | // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes 823 | wsClient.close(1001, e.message); 824 | proxyWs.close(1001); 825 | } 826 | 827 | proxyWs.onmessage = (event) => { 828 | recordMessage(event, false); 829 | wsClient.readyState === 1 && wsClient.send(event.data); 830 | } 831 | 832 | proxyWs.onclose = (event) => { 833 | logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`); 834 | const targetCloseInfo = getCloseFromOriginEvent(event); 835 | wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason); 836 | } 837 | 838 | wsClient.onmessage = (event) => { 839 | recordMessage(event, true); 840 | sendProxyMessage(event); 841 | } 842 | 843 | wsClient.onclose = (event) => { 844 | logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`); 845 | const targetCloseInfo = getCloseFromOriginEvent(event); 846 | proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason); 847 | } 848 | } catch (e) { 849 | logUtil.debug('WebSocket Proxy Error:' + e.message); 850 | logUtil.debug(e.stack); 851 | console.error(e); 852 | } 853 | } 854 | 855 | class RequestHandler { 856 | /** 857 | * Creates an instance of RequestHandler. 858 | * 859 | * @param {object} config 860 | * @param {boolean} config.forceProxyHttps proxy all https requests 861 | * @param {boolean} config.dangerouslyIgnoreUnauthorized 862 | @param {number} config.httpServerPort the http port AnyProxy do the proxy 863 | * @param {object} rule 864 | * @param {Recorder} recorder 865 | * 866 | * @memberOf RequestHandler 867 | */ 868 | constructor(config, rule, recorder) { 869 | const reqHandlerCtx = this; 870 | this.forceProxyHttps = false; 871 | this.dangerouslyIgnoreUnauthorized = false; 872 | this.httpServerPort = ''; 873 | this.wsIntercept = false; 874 | 875 | if (config.forceProxyHttps) { 876 | this.forceProxyHttps = true; 877 | } 878 | 879 | if (config.dangerouslyIgnoreUnauthorized) { 880 | this.dangerouslyIgnoreUnauthorized = true; 881 | } 882 | 883 | if (config.wsIntercept) { 884 | this.wsIntercept = config.wsIntercept; 885 | } 886 | 887 | this.httpServerPort = config.httpServerPort; 888 | const default_rule = util.freshRequire('./rule_default'); 889 | const userRule = util.merge(default_rule, rule); 890 | 891 | reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]); 892 | reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder); 893 | 894 | reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({ 895 | handler: reqHandlerCtx.userRequestHandler, 896 | wsHandler: reqHandlerCtx.wsHandler, // websocket 897 | hostname: '127.0.0.1', 898 | }); 899 | 900 | this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]); 901 | } 902 | } 903 | 904 | module.exports = RequestHandler; 905 | --------------------------------------------------------------------------------
对不起,您访问的页面已被删除或不存在
Dynproxy
proxy and more...