pull/180/head
xiaojunnuo 4 years ago
commit b7654d6ab0

6
.gitignore vendored

@ -0,0 +1,6 @@
# IntelliJ project files
.idea
*.iml
out
gen
node_modules/

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

@ -0,0 +1,105 @@
# dev-sidecar
开发者边车命名取自service-mesh的service-sidecar意为为开发者打辅助的边车工具
通过本地代理的方式将请求代理到一些国内的加速通道上
解决一些网站和库无法访问或访问速度慢的问题
## 特性
### 1、 解决git push某些情况下需要临时输入账号密码的问题
### 2、 github的release、source、zip下载加速
可解决npm install 时某些安装包下载不下来的问题
### 3、 github的源代码查看raw/blame查看
### 4、 Stack Overflow 加速
### 5、 google cdn 加速
### 6、 gist.github.com 加速
## 快速开始
### 1、安装与启动
```shell
git clone https://gitee.com/docmirror/dev-sidecar.git
cd ./dev-sidecar/packages/core
npm install
npm run start
#或使用cnpm
cnpm install
npm run start
#或使用yarn
yarn install
npm run start
```
输出
```
CA Cert saved in: C:\Users\Administrator\.dev-sidecar\dev-sidecar.ca.crt
CA private key saved in: C:\Users\Administrator\.dev-sidecar\dev-sidecar.ca.key.pem
dev-sidecar启动端口: 1181
代理已开启, 127.0.0.1 1181
```
启动后会自动设置系统代理、npm代理
### 2、设置信任根证书
第一次启动时会本地随机生成一个根证书放在此目录下(由于此证书是本地随机生成的,所以信任它是安全的)
```
# 你的Home路径如果有修改输出会不一样请按照实际日志输出路径查看
CA Cert saved in: C:\Users\Administrator\.dev-sidecar\dev-sidecar.ca.crt
```
windows用户安装根证书
```
start %HOMEPATH%/.dev-sidecar/dev-sidecar.ca.crt
或者
打开`C:\Users\Administrator\.dev-sidecar\`文件夹,双击`dev-sidecar.ca.crt`
```
依次点击安装证书->所有用户->将所有证书都放入下列存储->受信任的根证书颁发机构->确定,下一步,确定即可
![](./doc/setup.png)
Mac 用户安装根证书
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.dev-sidecar/dev-sidecar.ca.crt
```
### 开始加速吧
去github上`Download ZIP`、`Release` 下载试试,体验秒下的感觉
比如去下载它: https://github.com/greper/d2-crud-plus/archive/master.zip
## 最佳实践
把dev-sidecar一直开着就行了
### npm加速
1. yarn 设置淘宝镜像registry
2. npm设置官方registry。
3. 项目install使用yarnpublish用npm互不影响
### 其他加速
1. git clone 加速 [fgit-go](https://github.com/FastGitORG/fgit-go)
2. github.com代理网站(不能登录) [hub.fastgit.org](https://hub.fastgit.org/)
## 开发计划
1. 桌面端,右下角小图标
2. √ google cdn加速
## 感谢
本项目参考如下开源项目
* [node-mitmproxy](https://github.com/wuchangming/node-mitmproxy)
* [ReplaceGoogleCDN](https://github.com/justjavac/ReplaceGoogleCDN)
本项目加速资源由如下组织提供
* [fastgit](https://fastgit.org/)

@ -0,0 +1,28 @@
{
server: {
port: 1181
},
"intercepts": {
'notify3.note.youdao.com': [
{
regexp: '.*',
redirect: 'https://localhost:99999'
}
]
},
"dns": {
"mapping": {
//"avatars*.githubusercontent.com": "usa"
}
},
// setting: {
// startup: {
// // 开机启动
// server: true,
// proxy: {
// system: true,
// npm: true
// }
// }
// }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

@ -0,0 +1,19 @@
{
"packages": [
"packages/*"
],
"command": {
"publish": {
"ignoreChanges": [
"*.md",
"config",
"doc"
]
},
"bootstrap": {
"ignore": [
]
}
},
"version": "1.0.0"
}

@ -0,0 +1,7 @@
{
"name": "dev-sidecar-parent",
"private": true,
"devDependencies": {
"lerna": "^3.22.1"
}
}

@ -0,0 +1 @@
module.exports = require('./src')

@ -0,0 +1,84 @@
{
"name": "@docmirror/dev-sidecar",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"depedencies": {},
"keywords": [],
"author": "docmirror.cn",
"license": "MPL2.0",
"private": false,
"scripts": {
"start": "node start.js",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"agentkeepalive": "^2.1.1",
"babel-core": "^6.8.0",
"babel-plugin-transform-async-to-generator": "^6.7.4",
"babel-polyfill": "^6.8.0",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.8.0",
"charset": "^1.0.0",
"child_process": "^1.0.2",
"colors": "^1.1.2",
"commander": "^2.9.0",
"core-js": "^3.6.5",
"debug": "^4.1.1",
"dns-over-http": "^0.2.0",
"dns-over-tls": "^0.0.8",
"iconv-lite": "^0.4.13",
"is-browser": "^2.1.0",
"jschardet": "^1.4.1",
"json5": "^2.1.3",
"lodash": "^4.7.0",
"lru-cache": "^6.0.0",
"mkdirp": "^0.5.1",
"node-cmd": "^3.0.0",
"node-forge": "^0.8.2",
"node-mitmproxy": "^3.1.1",
"node-powershell": "^4.0.0",
"require-context": "^1.1.0",
"through2": "^2.0.1",
"tunnel-agent": "^0.4.3",
"util": "^0.12.3",
"validator": "^13.1.17",
"vue": "^2.6.11",
"winreg": "^1.2.4"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

@ -0,0 +1,20 @@
const lodash = require('lodash')
const defConfig = require('./config/index.js')
let configTarget = defConfig
module.exports = {
get () {
return configTarget
},
set (newConfig) {
const clone = lodash.cloneDeep(defConfig)
lodash.merge(clone, newConfig)
configTarget = clone
return configTarget
},
getDefault () {
return defConfig
},
resetDefault () {
configTarget = defConfig
}
}

@ -0,0 +1,126 @@
module.exports = {
server: {
port: 1181
},
intercepts: {
'github.com': [
{
// "release archive 下载链接替换",
regexp: [
'/.*/.*/releases/download/',
'/.*/.*/archive/'
],
redirect: 'https://download.fastgit.org'
},
{
regexp: [
'/.*/.*/raw/',
'/.*/.*/blame/'
],
redirect: 'https://hub.fastgit.org'
}
],
// 'codeload.github.com': [
// {
// regexp: '.*',
// redirect:"https://download.fastgit.org"
// }
// ],
'raw.githubusercontent.com': [
{
regexp: '.*',
proxy: 'https://raw.fastgit.org'
}
],
'github.githubassets.com': [
{
regexp: '.*',
proxy: 'https://assets.fastgit.org'
}
],
'customer-stories-feed.github.com': [
{
regexp: '.*',
proxy: 'https://customer-stories-feed.fastgit.org'
}
],
// google cdn
'ajax.googleapis.com': [
{
regexp: '.*',
proxy: 'https://ajax.loli.net'
}
],
'fonts.googleapis.com': [
{
regexp: '.*',
proxy: 'https://fonts.loli.net'
}
],
'themes.googleapis.com': [
{
regexp: '.*',
proxy: 'https://themes.loli.net'
}
],
'fonts.gstatic.com': [
{
regexp: '.*',
proxy: 'https://gstatic.loli.net'
}
],
'www.google.com': [
{
regexp: '/recaptcha/.*',
proxy: 'https://www.recaptcha.net'
}
],
'secure.gravatar.com': [
{
regexp: '.*',
redirect: 'https://gravatar.loli.net'
}
],
'clients*.google.com': [
{
regexp: '.*',
redirect: 'https://localhost:99999'
}
],
'lh*.googleusercontent.com': [
{
regexp: '.*',
redirect: 'https://localhost:99999'
}
]
},
dns: {
providers: {
aliyun: {
type: 'https',
server: 'https://dns.alidns.com/dns-query',
cacheSize: 1000
},
usa: {
type: 'https',
server: 'https://cloudflare-dns.com/dns-query',
cacheSize: 1000
}
},
mapping: {
// "解决push的时候需要输入密码的问题",
'api.github.com': 'usa',
'gist.github.com': 'usa'
// "avatars*.githubusercontent.com": "usa"
}
},
setting: {
startup: { // 开机启动
server: true,
proxy: {
system: true,
npm: true
}
}
}
}

@ -0,0 +1,39 @@
const listener = {}
let index = 1
function register (channel, handle, order = 10) {
let handles = listener[channel]
if (handles == null) {
handles = listener[channel] = []
}
handles.push({ id: index, handle, order })
handles.sort((a, b) => { return a.order - b.order })
return index++
}
function fire (channel, event) {
const handles = listener[channel]
if (handles == null) {
return
}
for (const item of handles) {
item.handle(event)
}
}
function unregister (id) {
for (const key in listener) {
const handlers = listener[key]
for (let i = 0; i < handlers.length; i++) {
const handle = handlers[i]
if (handle.id === id) {
handlers.splice(i)
return
}
}
}
}
const EventHub = {
register,
fire,
unregister
}
module.exports = EventHub

@ -0,0 +1,53 @@
const server = require('./server/index.js')
const proxy = require('./switch/proxy/index.js')
const status = require('./status')
const config = require('./config')
const event = require('./event')
async function proxyStartup ({ ip, port }) {
for (const key in proxy) {
if (config.get().setting.startup.proxy[key]) {
await proxy[key].open({ ip, port })
}
}
}
async function proxyShutdown () {
for (const key in proxy) {
console.log('status', status)
if (status.proxy[key] === false) {
continue
}
await proxy[key].close()
}
}
module.exports = {
status,
api: {
server,
proxy,
config,
startup: async (newConfig) => {
config.set(newConfig)
try {
if (config.get().setting.startup.server) {
server.start(newConfig)
}
await proxyStartup({ ip: '127.0.0.1', port: config.get().server.port })
} catch (error) {
console.log(error)
}
},
shutdown: async () => {
try {
await proxyShutdown()
return new Promise(resolve => {
server.close()
resolve()
})
} catch (error) {
console.log(error)
}
},
event
}
}

@ -0,0 +1,19 @@
const expose = require('./expose.js')
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
// 避免异常崩溃
process.on('uncaughtException', function (err) {
if (err.code === 'ECONNABORTED') {
// console.error(err.errno)
return
}
console.error(err)
})
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason)
// application specific logging, throwing an error, or other logic here
})
module.exports = expose

@ -0,0 +1,45 @@
const LRU = require('lru-cache')
const { isIP } = require('validator')
const getLogger = require('../utils/logger')
const logger = getLogger('dns')
const cacheSize = 1024
function _isIP (v) {
return v && isIP(v)
}
module.exports = class BaseDNS {
constructor () {
this.cache = new LRU(cacheSize)
}
async lookup (hostname) {
try {
let ip = this.cache.get(hostname)
if (ip) {
return ip
}
const t = new Date()
ip = hostname
for (let depth = 0; !_isIP(ip) && depth < 5; depth++) {
ip = await this._lookup(ip).catch(error => {
logger.debug(error)
return ip
})
}
if (!_isIP(ip)) {
throw new Error(`BAD IP FORMAT (${ip})`)
}
logger.debug(`[DNS] ${hostname} -> ${ip} (${new Date() - t} ms)`)
this.cache.set(hostname, ip)
return ip
} catch (error) {
console.error(error)
logger.debug(`[DNS] cannot resolve hostname ${hostname} (${error})`)
}
}
}

@ -0,0 +1,17 @@
const { promisify } = require('util')
const doh = require('dns-over-http')
const BaseDNS = require('./base')
const dohQueryAsync = promisify(doh.query)
module.exports = class DNSOverHTTPS extends BaseDNS {
constructor (dnsServer) {
super()
this.dnsServer = dnsServer
}
async _lookup (hostname) {
const result = await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }])
return result.answers[0].data
}
}

@ -0,0 +1,31 @@
const DNSOverTLS = require('./tls.js')
const DNSOverHTTPS = require('./https.js')
module.exports = {
initDNS (dnsProviders) {
const dnsMap = {}
for (const provider in dnsProviders) {
const conf = dnsProviders[provider]
dnsMap[provider] = conf.type === 'https' ? new DNSOverHTTPS(conf.server) : new DNSOverTLS(conf.server)
}
return dnsMap
},
hasDnsLookup (dnsConfig, hostname) {
let providerName = dnsConfig.mapping[hostname]
if (!providerName) {
for (const target in dnsConfig.mapping) {
if (target.indexOf('*') < 0) {
continue
}
const regexp = target.replace('.', '\\.')
.replace('*', '.*')
if (hostname.match(regexp)) {
providerName = dnsConfig.mapping[target]
}
}
}
if (providerName) {
console.log('匹配到dns:', providerName, hostname)
return dnsConfig.providers[providerName]
}
}
}

@ -0,0 +1,14 @@
const dnstls = require('dns-over-tls')
const BaseDNS = require('./base')
module.exports = class DNSOverTLS extends BaseDNS {
async _lookup (hostname) {
const { answers } = await dnstls.query(hostname)
const answer = answers.find(answer => answer.type === 'A' && answer.class === 'IN')
if (answer) {
return answer.data
}
}
}

@ -0,0 +1,10 @@
const url = require('url')
module.exports = {
requestInterceptor (interceptOpt, rOptions, req, res, ssl) {
req.abort()
console.log('abort:', rOptions.hostname, req.url)
},
is (interceptOpt) {
return !!interceptOpt.abort
}
}

@ -0,0 +1,19 @@
const url = require('url')
module.exports = {
requestInterceptor (interceptOpt, rOptions, req, res, ssl) {
// eslint-disable-next-line node/no-deprecated-api
const URL = url.parse(interceptOpt.proxy)
rOptions.protocol = URL.protocol
rOptions.hostname = URL.host
rOptions.host = URL.host
rOptions.headers.host = URL.host
if (URL.port == null) {
rOptions.port = rOptions.protocol === 'https:' ? 443 : 80
}
console.log('proxy:', rOptions.hostname, req.url, interceptOpt.proxy)
},
is (interceptOpt) {
return !!interceptOpt.proxy
}
}

@ -0,0 +1,18 @@
module.exports = {
requestInterceptor (interceptOpt, rOptions, req, res, ssl) {
const url = req.url
let redirect
if (typeof interceptOpt.redirect === 'string') {
redirect = interceptOpt.redirect + url
} else {
redirect = interceptOpt.redirect(url)
}
console.log('请求重定向:', rOptions.hostname, url, redirect)
res.writeHead(302, { Location: redirect })
res.end()
return true
},
is (interceptOpt) {
return interceptOpt.redirect // 如果配置中有redirect那么这个配置是需要redirect拦截的
}
}

@ -0,0 +1,6 @@
const proxy = require('./impl/proxy')
const redirect = require('./impl/redirect')
const modules = [proxy, redirect]
module.exports = modules

@ -0,0 +1,32 @@
#!/usr/bin/env node
require('babel-polyfill')
const mitmproxy = require('../mitmproxy')
const program = require('commander')
const packageJson = require('../../package.json')
// const tlsUtils = require('../tls/tlsUtils')
const fs = require('fs')
const path = require('path')
const colors = require('colors')
fs.existsSync = fs.existsSync || path.existsSync
program
.version(packageJson.version)
.option('-c, --config [value]', 'config file path')
.parse(process.argv)
console.log(program.config)
const configPath = path.resolve(program.config)
if (fs.existsSync(configPath)) {
const configObject = require(configPath)
if (typeof configObject !== 'object') {
console.error(colors.red(`Config Error in ${configPath}`))
} else {
mitmproxy.createProxy(configObject)
}
} else {
console.error(colors.red(`Can not find \`config file\` file: ${configPath}`))
}

@ -0,0 +1,13 @@
const AgentOrigin = require('agentkeepalive')
module.exports = class Agent extends AgentOrigin {
// Hacky
getName (option) {
let name = AgentOrigin.prototype.getName.call(this, option)
name += ':'
if (option.customSocketId) {
name += option.customSocketId
}
return name
}
}

@ -0,0 +1,13 @@
const HttpsAgentOrigin = require('agentkeepalive').HttpsAgent
module.exports = class HttpsAgent extends HttpsAgentOrigin {
// Hacky
getName (option) {
let name = HttpsAgentOrigin.prototype.getName.call(this, option)
name += ':'
if (option.customSocketId) {
name += option.customSocketId
}
return name
}
}

@ -0,0 +1,24 @@
const path = require('path')
const config = exports
config.caCertFileName = 'dev-sidecar.ca.crt'
config.caKeyFileName = 'dev-sidecar.ca.key.pem'
config.defaultPort = 1181
config.caName = 'Dev-Sidecar CA'
config.getDefaultCABasePath = function () {
const userHome = process.env.HOME || process.env.USERPROFILE
return path.resolve(userHome, './.dev-sidecar')
}
config.getDefaultCACertPath = function () {
return path.resolve(config.getDefaultCABasePath(), config.caCertFileName)
}
config.getDefaultCAKeyPath = function () {
return path.resolve(config.getDefaultCABasePath(), config.caKeyFileName)
}

@ -0,0 +1,143 @@
const url = require('url')
const Agent = require('./ProxyHttpAgent')
const HttpsAgent = require('./ProxyHttpsAgent')
const tunnelAgent = require('tunnel-agent')
const util = exports
const httpsAgent = new HttpsAgent({
keepAlive: true,
timeout: 60000,
keepAliveTimeout: 30000, // free socket keepalive for 30 seconds
rejectUnauthorized: false
})
const httpAgent = new Agent({
keepAlive: true,
timeout: 60000,
keepAliveTimeout: 30000 // free socket keepalive for 30 seconds
})
let socketId = 0
let httpsOverHttpAgent, httpOverHttpsAgent, httpsOverHttpsAgent
util.getOptionsFormRequest = (req, ssl, externalProxy = null) => {
// eslint-disable-next-line node/no-deprecated-api
const urlObject = url.parse(req.url)
const defaultPort = ssl ? 443 : 80
const protocol = ssl ? 'https:' : 'http:'
const headers = Object.assign({}, req.headers)
let externalProxyUrl = null
if (externalProxy) {
if (typeof externalProxy === 'string') {
externalProxyUrl = externalProxy
} else if (typeof externalProxy === 'function') {
try {
externalProxyUrl = externalProxy(req, ssl)
} catch (e) {
console.error(e)
}
}
}
delete headers['proxy-connection']
let agent = false
if (!externalProxyUrl) {
// keepAlive
if (headers.connection !== 'close') {
if (protocol === 'https:') {
agent = httpsAgent
} else {
agent = httpAgent
}
headers.connection = 'keep-alive'
}
} else {
agent = util.getTunnelAgent(protocol === 'https:', externalProxyUrl)
}
const options = {
protocol: protocol,
hostname: req.headers.host.split(':')[0],
method: req.method,
port: req.headers.host.split(':')[1] || defaultPort,
path: urlObject.path,
headers: req.headers,
agent: agent
}
// eslint-disable-next-line node/no-deprecated-api
if (protocol === 'http:' && externalProxyUrl && (url.parse(externalProxyUrl)).protocol === 'http:') {
// eslint-disable-next-line node/no-deprecated-api
const externalURL = url.parse(externalProxyUrl)
options.hostname = externalURL.hostname
options.port = externalURL.port
// support non-transparent proxy
options.path = `http://${urlObject.host}${urlObject.path}`
}
// mark a socketId for Agent to bind socket for NTLM
if (req.socket.customSocketId) {
options.customSocketId = req.socket.customSocketId
} else if (headers.authorization) {
options.customSocketId = req.socket.customSocketId = socketId++
}
return options
}
util.getTunnelAgent = (requestIsSSL, externalProxyUrl) => {
// eslint-disable-next-line node/no-deprecated-api
const urlObject = url.parse(externalProxyUrl)
const protocol = urlObject.protocol || 'http:'
let port = urlObject.port
if (!port) {
port = protocol === 'http:' ? 80 : 443
}
const hostname = urlObject.hostname || 'localhost'
if (requestIsSSL) {
if (protocol === 'http:') {
if (!httpsOverHttpAgent) {
httpsOverHttpAgent = tunnelAgent.httpsOverHttp({
proxy: {
host: hostname,
port: port
}
})
}
return httpsOverHttpAgent
} else {
if (!httpsOverHttpsAgent) {
httpsOverHttpsAgent = tunnelAgent.httpsOverHttps({
proxy: {
host: hostname,
port: port
}
})
}
return httpsOverHttpsAgent
}
} else {
if (protocol === 'http:') {
// if (!httpOverHttpAgent) {
// httpOverHttpAgent = tunnelAgent.httpOverHttp({
// proxy: {
// host: hostname,
// port: port
// }
// });
// }
return false
} else {
if (!httpOverHttpsAgent) {
httpOverHttpsAgent = tunnelAgent.httpOverHttps({
proxy: {
host: hostname,
port: port
}
})
}
return httpOverHttpsAgent
}
}
}

@ -0,0 +1,2 @@
require('babel-polyfill')
module.exports = require('./mitmproxy')

@ -0,0 +1,98 @@
const through = require('through2')
const zlib = require('zlib')
// eslint-disable-next-line no-unused-vars
const url = require('url')
const httpUtil = {}
httpUtil.isGzip = function (res) {
const contentEncoding = res.headers['content-encoding']
return !!(contentEncoding && contentEncoding.toLowerCase() === 'gzip')
}
httpUtil.isHtml = function (res) {
const contentType = res.headers['content-type']
return (typeof contentType !== 'undefined') && /text\/html|application\/xhtml\+xml/.test(contentType)
}
// eslint-disable-next-line no-unused-vars
function injectContentIntoHtmlHead (html, content) {
html = html.replace(/(<\/head>)/i, function (match) {
return content + match
})
return html
}
function injectScriptIntoHtmlHead (html, content) {
return html
}
function injectContentIntoHtmlBody (html, content) {
html = html.replace(/(<\/body>)/i, function (match) {
return content + match
})
return html
}
function chunkReplace (_this, chunk, enc, callback, headContent, bodyContent) {
let chunkString = chunk.toString()
if (headContent) {
chunkString = injectScriptIntoHtmlHead(chunkString, headContent)
}
if (bodyContent) {
chunkString = injectContentIntoHtmlBody(chunkString, bodyContent)
}
_this.push(Buffer.alloc(chunkString))
callback()
}
module.exports = class InjectHtmlPlugin {
constructor ({
head,
body
}) {
this.head = head
this.body = body
}
responseInterceptor (req, res, proxyReq, proxyRes, ssl, next) {
if (!this.head && !this.body) {
next()
return
}
const isHtml = httpUtil.isHtml(proxyRes)
const contentLengthIsZero = (() => {
return proxyRes.headers['content-length'] === 0
})()
if (!isHtml || contentLengthIsZero) {
next()
} else {
Object.keys(proxyRes.headers).forEach(function (key) {
if (proxyRes.headers[key] !== undefined) {
let newkey = key.replace(/^[a-z]|-[a-z]/g, (match) => {
return match.toUpperCase()
})
newkey = key
if (isHtml && key === 'content-length') {
// do nothing
} else {
res.setHeader(newkey, proxyRes.headers[key])
}
}
})
res.writeHead(proxyRes.statusCode)
const isGzip = httpUtil.isGzip(proxyRes)
if (isGzip) {
proxyRes.pipe(new zlib.Gunzip())
.pipe(through(function (chunk, enc, callback) {
chunkReplace(this, chunk, enc, callback, this.head, this.body)
})).pipe(new zlib.Gzip()).pipe(res)
} else {
proxyRes.pipe(through(function (chunk, enc, callback) {
chunkReplace(this, chunk, enc, callback, this.head, this.body)
})).pipe(res)
}
}
next()
}
}

@ -0,0 +1,58 @@
const net = require('net')
const url = require('url')
// const colors = require('colors')
const DnsUtil = require('../../dns/index')
const localIP = '127.0.0.1'
// create connectHandler function
module.exports = function createConnectHandler (sslConnectInterceptor, fakeServerCenter, dnsConfig) {
// return
return function connectHandler (req, cltSocket, head) {
// eslint-disable-next-line node/no-deprecated-api
const srvUrl = url.parse(`https://${req.url}`)
const hostname = srvUrl.hostname
if (typeof sslConnectInterceptor === 'function' && sslConnectInterceptor(req, cltSocket, head)) {
fakeServerCenter.getServerPromise(hostname, srvUrl.port).then((serverObj) => {
connect(req, cltSocket, head, localIP, serverObj.port)
}, (e) => {
console.error(e)
})
} else {
if (dnsConfig) {
const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)
if (dns) {
dns.lookup(hostname).then(ip => {
connect(req, cltSocket, head, ip, srvUrl.port)
})
return
}
}
connect(req, cltSocket, head, hostname, srvUrl.port)
}
}
}
function connect (req, cltSocket, head, hostname, port) {
// tunneling https
// console.log('connect:', hostname, port)
try {
const proxySocket = net.connect(port, hostname, () => {
cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: dev-sidecar\r\n' +
'\r\n')
proxySocket.write(head)
proxySocket.pipe(cltSocket)
cltSocket.pipe(proxySocket)
})
proxySocket.on('error', (e) => {
// 连接失败可能被GFW拦截或者服务端拥挤
console.error('代理连接失败:', e.errno, hostname, port)
cltSocket.destroy()
})
return proxySocket
} catch (error) {
console.log('err', error)
}
}

@ -0,0 +1,35 @@
const fs = require('fs')
const forge = require('node-forge')
const FakeServersCenter = require('../tls/FakeServersCenter')
const colors = require('colors')
module.exports = function createFakeServerCenter ({
caCertPath,
caKeyPath,
requestHandler,
upgradeHandler,
getCertSocketTimeout
}) {
let caCert
let caKey
try {
fs.accessSync(caCertPath, fs.F_OK)
fs.accessSync(caKeyPath, fs.F_OK)
const caCertPem = fs.readFileSync(caCertPath)
const caKeyPem = fs.readFileSync(caKeyPath)
caCert = forge.pki.certificateFromPem(caCertPem)
caKey = forge.pki.privateKeyFromPem(caKeyPem)
} catch (e) {
console.log(colors.red('Can not find `CA certificate` or `CA key`.'), e)
process.exit(1)
}
return new FakeServersCenter({
caCert,
caKey,
maxLength: 100,
requestHandler,
upgradeHandler,
getCertSocketTimeout
})
}

@ -0,0 +1,180 @@
const http = require('http')
const https = require('https')
const commonUtil = require('../common/util')
// const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i
const DnsUtil = require('../../dns/index')
// create requestHandler function
module.exports = function createRequestHandler (requestInterceptor, responseInterceptor, middlewares, externalProxy, dnsConfig) {
// return
return function requestHandler (req, res, ssl) {
let proxyReq
const rOptions = commonUtil.getOptionsFormRequest(req, ssl, externalProxy)
if (rOptions.headers.connection === 'close') {
req.socket.setKeepAlive(false)
} else if (rOptions.customSocketId != null) { // for NTLM
req.socket.setKeepAlive(true, 60 * 60 * 1000)
} else {
req.socket.setKeepAlive(true, 30000)
}
const requestInterceptorPromise = () => {
return new Promise((resolve, reject) => {
const next = () => {
resolve()
}
try {
if (typeof requestInterceptor === 'function') {
requestInterceptor(rOptions, req, res, ssl, next)
} else {
resolve()
}
} catch (e) {
reject(e)
}
})
}
const proxyRequestPromise = async () => {
rOptions.host = rOptions.hostname || rOptions.host || 'localhost'
if (dnsConfig) {
const dns = DnsUtil.hasDnsLookup(dnsConfig, rOptions.host)
if (dns) {
const ip = await dns.lookup(rOptions.host)
console.log('使用自定义dns:', rOptions.host, ip, dns.dnsServer)
rOptions.host = ip
}
}
return new Promise((resolve, reject) => {
// use the binded socket for NTLM
if (rOptions.agent && rOptions.customSocketId != null && rOptions.agent.getName) {
const socketName = rOptions.agent.getName(rOptions)
const bindingSocket = rOptions.agent.sockets[socketName]
if (bindingSocket && bindingSocket.length > 0) {
bindingSocket[0].once('free', onFree)
return
}
}
onFree()
function onFree () {
const url = `${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}`
const start = new Date().getTime()
if (rOptions.protocol === 'https:') {
console.log('代理请求:', url)
}
proxyReq = (rOptions.protocol === 'https:' ? https : http).request(rOptions, (proxyRes) => {
const end = new Date().getTime()
if (rOptions.protocol === 'https:') {
console.log('代理请求返回:', url, (end - start) + 'ms')
}
resolve(proxyRes)
})
proxyReq.on('timeout', () => {
console.error('代理请求超时', rOptions.host, rOptions.path)
reject(new Error(`${rOptions.host}:${rOptions.port}, 代理请求超时`))
})
proxyReq.on('error', (e, req, res) => {
console.error('代理请求错误', e.errno, rOptions.host, rOptions.path)
reject(e)
if (res) {
res.end()
}
})
proxyReq.on('aborted', () => {
console.error('代理请求被取消', rOptions.host, rOptions.path)
reject(new Error('代理请求被取消'))
req.abort()
})
req.on('aborted', function () {
console.error('请求被取消', rOptions.host, rOptions.path)
proxyReq.abort()
reject(new Error('请求被取消'))
})
req.on('error', function (e, req, res) {
console.error('请求错误:', e.errno, rOptions.host, rOptions.path)
reject(e)
if (res) {
res.end()
}
})
req.on('timeout', () => {
console.error('请求超时', rOptions.host, rOptions.path)
reject(new Error(`${rOptions.host}:${rOptions.port}, 请求超时`))
})
req.pipe(proxyReq)
}
})
}
// workflow control
(async () => {
await requestInterceptorPromise()
if (res.finished) {
return false
}
const proxyRes = await proxyRequestPromise()
const responseInterceptorPromise = new Promise((resolve, reject) => {
const next = () => {
resolve()
}
try {
if (typeof responseInterceptor === 'function') {
responseInterceptor(req, res, proxyReq, proxyRes, ssl, next)
} else {
resolve()
}
} catch (e) {
reject(e)
}
})
await responseInterceptorPromise
if (res.finished) {
return false
}
if (!res.headersSent) { // prevent duplicate set headers
Object.keys(proxyRes.headers).forEach(function (key) {
if (proxyRes.headers[key] !== undefined) {
// https://github.com/nodejitsu/node-http-proxy/issues/362
if (/^www-authenticate$/i.test(key)) {
if (proxyRes.headers[key]) {
proxyRes.headers[key] = proxyRes.headers[key] && proxyRes.headers[key].split(',')
}
key = 'www-authenticate'
}
res.setHeader(key, proxyRes.headers[key])
}
})
res.writeHead(proxyRes.statusCode)
proxyRes.pipe(res)
}
})().then(
(flag) => {
// do nothing
},
(e) => {
if (!res.finished) {
res.writeHead(500)
res.write(`Dev-Sidecar Warning:\n\n ${e.toString()}`)
res.end()
}
console.error(e)
}
)
}
}

@ -0,0 +1,60 @@
const http = require('http')
const https = require('https')
const util = require('../common/util')
// copy from node-http-proxy. ^_^
// create connectHandler function
module.exports = function createUpgradeHandler () {
// return
return function upgradeHandler (req, cltSocket, head, ssl) {
const clientOptions = util.getOptionsFormRequest(req, ssl)
const proxyReq = (ssl ? https : http).request(clientOptions)
proxyReq.on('error', (e) => {
console.error(e)
})
proxyReq.on('response', function (res) {
// if upgrade event isn't going to happen, close the socket
if (!res.upgrade) cltSocket.end()
})
proxyReq.on('upgrade', function (proxyRes, proxySocket, proxyHead) {
proxySocket.on('error', (e) => {
console.log('error-----1111')
console.error(e)
})
cltSocket.on('error', function () {
console.log('error-----2222')
proxySocket.end()
})
proxySocket.setTimeout(0)
proxySocket.setNoDelay(true)
proxySocket.setKeepAlive(true, 0)
if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead)
cltSocket.write(
Object.keys(proxyRes.headers).reduce(function (head, key) {
const value = proxyRes.headers[key]
if (!Array.isArray(value)) {
head.push(key + ': ' + value)
return head
}
for (let i = 0; i < value.length; i++) {
head.push(key + ': ' + value[i])
}
return head
}, ['HTTP/1.1 101 Switching Protocols'])
.join('\r\n') + '\r\n\r\n'
)
proxySocket.pipe(cltSocket).pipe(proxySocket)
})
proxyReq.end()
}
}

@ -0,0 +1,90 @@
const tlsUtils = require('../tls/tlsUtils')
const http = require('http')
const config = require('../common/config')
const colors = require('colors')
const createRequestHandler = require('./createRequestHandler')
const createConnectHandler = require('./createConnectHandler')
const createFakeServerCenter = require('./createFakeServerCenter')
const createUpgradeHandler = require('./createUpgradeHandler')
module.exports = {
createProxy ({
port = config.defaultPort,
caCertPath,
caKeyPath,
sslConnectInterceptor,
requestInterceptor,
responseInterceptor,
getCertSocketTimeout = 1 * 1000,
middlewares = [],
externalProxy,
dnsConfig
}, callback) {
// Don't reject unauthorized
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
if (!caCertPath && !caKeyPath) {
const rs = this.createCA()
caCertPath = rs.caCertPath
caKeyPath = rs.caKeyPath
if (rs.create) {
console.log(colors.cyan(`CA Cert saved in: ${caCertPath}`))
console.log(colors.cyan(`CA private key saved in: ${caKeyPath}`))
}
}
port = ~~port
const requestHandler = createRequestHandler(
requestInterceptor,
responseInterceptor,
middlewares,
externalProxy,
dnsConfig
)
const upgradeHandler = createUpgradeHandler()
const fakeServersCenter = createFakeServerCenter({
caCertPath,
caKeyPath,
requestHandler,
upgradeHandler,
getCertSocketTimeout
})
const connectHandler = createConnectHandler(
sslConnectInterceptor,
fakeServersCenter,
dnsConfig
)
const server = new http.Server()
server.listen(port, () => {
console.log(colors.green(`dev-sidecar启动端口: ${port}`))
server.on('error', (e) => {
console.error(colors.red(e))
})
server.on('request', (req, res) => {
const ssl = false
requestHandler(req, res, ssl)
})
// tunneling for https
server.on('connect', (req, cltSocket, head) => {
connectHandler(req, cltSocket, head)
})
// TODO: handler WebSocket
server.on('upgrade', function (req, socket, head) {
const ssl = false
upgradeHandler(req, socket, head, ssl)
})
if (callback) {
callback(server)
}
})
return server
},
createCA (caBasePath = config.getDefaultCABasePath()) {
return tlsUtils.initCA(caBasePath)
}
}

@ -0,0 +1,20 @@
const _ = require('lodash')
module.exports = (middlewares) => {
if (middlewares) {
if (Object.prototype.toString.call(middlewares) !== '[object Array]') {
throw new TypeError('middlewares must be a array')
}
}
//
// const sslConnectInterceptors = []
// const requestInterceptors = []
// const responseInterceptors = []
_.each(middlewares, (m) => {
if (m.buildIn === false || m.buildIn === 'false') {
} else {
// m.name
}
})
}

@ -0,0 +1,107 @@
const tlsUtils = require('./tlsUtils')
const https = require('https')
module.exports = class CertAndKeyContainer {
constructor ({
maxLength = 1000,
getCertSocketTimeout = 2 * 1000,
caCert,
caKey
}) {
this.queue = []
this.maxLength = maxLength
this.getCertSocketTimeout = getCertSocketTimeout
this.caCert = caCert
this.caKey = caKey
}
addCertPromise (certPromiseObj) {
if (this.queue.length >= this.maxLength) {
this.queue.shift()
}
this.queue.push(certPromiseObj)
return certPromiseObj
}
getCertPromise (hostname, port) {
for (let i = 0; i < this.queue.length; i++) {
const _certPromiseObj = this.queue[i]
const mappingHostNames = _certPromiseObj.mappingHostNames
for (let j = 0; j < mappingHostNames.length; j++) {
const DNSName = mappingHostNames[j]
if (tlsUtils.isMappingHostName(DNSName, hostname)) {
this.reRankCert(i)
return _certPromiseObj.promise
}
}
}
const certPromiseObj = {
mappingHostNames: [hostname] // temporary hostname
}
const promise = new Promise((resolve, reject) => {
let once = true
const _resolve = (_certObj) => {
if (once) {
once = false
const mappingHostNames = tlsUtils.getMappingHostNamesFormCert(_certObj.cert)
certPromiseObj.mappingHostNames = mappingHostNames // change
resolve(_certObj)
}
}
let certObj
const fast = true
if (fast) {
certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, hostname)
_resolve(certObj)
} else {
// 这个太慢了
const preReq = https.request({
port: port,
hostname: hostname,
path: '/',
method: 'HEAD'
}, (preRes) => {
try {
const realCert = preRes.socket.getPeerCertificate()
if (realCert) {
try {
certObj = tlsUtils.createFakeCertificateByCA(this.caKey, this.caCert, realCert)
} catch (error) {
certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, hostname)
}
} else {
certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, hostname)
}
_resolve(certObj)
} catch (e) {
reject(e)
}
})
preReq.setTimeout(~~this.getCertSocketTimeout, () => {
if (!certObj) {
certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, hostname)
_resolve(certObj)
}
})
preReq.on('error', (e) => {
if (!certObj) {
certObj = tlsUtils.createFakeCertificateByDomain(this.caKey, this.caCert, hostname)
_resolve(certObj)
}
})
preReq.end()
}
})
certPromiseObj.promise = promise
return (this.addCertPromise(certPromiseObj)).promise
}
reRankCert (index) {
// index ==> queue foot
this.queue.push((this.queue.splice(index, 1))[0])
}
}

@ -0,0 +1,113 @@
const https = require('https')
const tlsUtils = require('./tlsUtils')
const CertAndKeyContainer = require('./CertAndKeyContainer')
const forge = require('node-forge')
const pki = forge.pki
// const colors = require('colors')
const tls = require('tls')
module.exports = class FakeServersCenter {
constructor ({ maxLength = 256, requestHandler, upgradeHandler, caCert, caKey, getCertSocketTimeout }) {
this.queue = []
this.maxLength = maxLength
this.requestHandler = requestHandler
this.upgradeHandler = upgradeHandler
this.certAndKeyContainer = new CertAndKeyContainer({
getCertSocketTimeout,
caCert,
caKey
})
}
addServerPromise (serverPromiseObj) {
if (this.queue.length >= this.maxLength) {
const delServerObj = this.queue.shift()
try {
console.log('超过最大服务数量,删除旧服务', delServerObj)
delServerObj.serverObj.server.close()
} catch (e) {
console.log(e)
}
}
console.log('add server promise:', serverPromiseObj)
this.queue.push(serverPromiseObj)
return serverPromiseObj
}
getServerPromise (hostname, port) {
for (let i = 0; i < this.queue.length; i++) {
const serverPromiseObj = this.queue[i]
const mappingHostNames = serverPromiseObj.mappingHostNames
for (let j = 0; j < mappingHostNames.length; j++) {
const DNSName = mappingHostNames[j]
if (tlsUtils.isMappingHostName(DNSName, hostname)) {
this.reRankServer(i)
return serverPromiseObj.promise
}
}
}
const serverPromiseObj = {
mappingHostNames: [hostname] // temporary hostname
}
const promise = new Promise((resolve, reject) => {
(async () => {
const certObj = await this.certAndKeyContainer.getCertPromise(hostname, port)
const cert = certObj.cert
const key = certObj.key
const certPem = pki.certificateToPem(cert)
const keyPem = pki.privateKeyToPem(key)
const fakeServer = new https.Server({
key: keyPem,
cert: certPem,
SNICallback: (hostname, done) => {
(async () => {
const certObj = await this.certAndKeyContainer.getCertPromise(hostname, port)
done(null, tls.createSecureContext({
key: pki.privateKeyToPem(certObj.key),
cert: pki.certificateToPem(certObj.cert)
}))
})()
}
})
const serverObj = {
cert,
key,
server: fakeServer,
port: 0 // if prot === 0 ,should listen server's `listening` event.
}
serverPromiseObj.serverObj = serverObj
fakeServer.listen(0, () => {
const address = fakeServer.address()
serverObj.port = address.port
})
fakeServer.on('request', (req, res) => {
const ssl = true
this.requestHandler(req, res, ssl)
})
fakeServer.on('error', (e) => {
console.error(e)
})
fakeServer.on('listening', () => {
const mappingHostNames = tlsUtils.getMappingHostNamesFormCert(certObj.cert)
serverPromiseObj.mappingHostNames = mappingHostNames
resolve(serverObj)
})
fakeServer.on('upgrade', (req, socket, head) => {
const ssl = true
this.upgradeHandler(req, socket, head, ssl)
})
})()
})
serverPromiseObj.promise = promise
return (this.addServerPromise(serverPromiseObj)).promise
}
reRankServer (index) {
// index ==> queue foot
this.queue.push((this.queue.splice(index, 1))[0])
}
}

@ -0,0 +1,264 @@
const forge = require('node-forge')
const fs = require('fs')
const path = require('path')
const config = require('../common/config')
const _ = require('lodash')
const mkdirp = require('mkdirp')
// const colors = require('colors')
const utils = exports
const pki = forge.pki
utils.createCA = function (CN) {
const keys = pki.rsa.generateKeyPair(2046)
const cert = pki.createCertificate()
cert.publicKey = keys.publicKey
cert.serialNumber = (new Date()).getTime() + ''
cert.validity.notBefore = new Date()
cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 5)
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 20)
const attrs = [{
name: 'commonName',
value: CN
}, {
name: 'countryName',
value: 'CN'
}, {
shortName: 'ST',
value: 'GuangDong'
}, {
name: 'localityName',
value: 'ShenZhen'
}, {
name: 'organizationName',
value: 'dev-sidecar'
}, {
shortName: 'OU',
value: 'https://github.com/docmirror/dev-sidecar'
}]
cert.setSubject(attrs)
cert.setIssuer(attrs)
cert.setExtensions([{
name: 'basicConstraints',
critical: true,
cA: true
}, {
name: 'keyUsage',
critical: true,
keyCertSign: true
}, {
name: 'subjectKeyIdentifier'
}])
// self-sign certificate
cert.sign(keys.privateKey, forge.md.sha256.create())
return {
key: keys.privateKey,
cert: cert
}
}
utils.covertNodeCertToForgeCert = function (originCertificate) {
const obj = forge.asn1.fromDer(originCertificate.raw.toString('binary'))
return forge.pki.certificateFromAsn1(obj)
}
utils.createFakeCertificateByDomain = function (caKey, caCert, domain) {
const keys = pki.rsa.generateKeyPair(2046)
const cert = pki.createCertificate()
cert.publicKey = keys.publicKey
cert.serialNumber = (new Date()).getTime() + ''
cert.validity.notBefore = new Date()
cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1)
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1)
const attrs = [{
name: 'commonName',
value: domain
}, {
name: 'countryName',
value: 'CN'
}, {
shortName: 'ST',
value: 'GuangDong'
}, {
name: 'localityName',
value: 'ShengZhen'
}, {
name: 'organizationName',
value: 'dev-sidecar'
}, {
shortName: 'OU',
value: 'https://github.com/docmirror/dev-sidecar'
}]
cert.setIssuer(caCert.subject.attributes)
cert.setSubject(attrs)
cert.setExtensions([{
name: 'basicConstraints',
critical: true,
cA: false
},
{
name: 'keyUsage',
critical: true,
digitalSignature: true,
contentCommitment: true,
keyEncipherment: true,
dataEncipherment: true,
keyAgreement: true,
keyCertSign: true,
cRLSign: true,
encipherOnly: true,
decipherOnly: true
},
{
name: 'subjectAltName',
altNames: [{
type: 2,
value: domain
}]
},
{
name: 'subjectKeyIdentifier'
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true
},
{
name: 'authorityKeyIdentifier'
}])
cert.sign(caKey, forge.md.sha256.create())
return {
key: keys.privateKey,
cert: cert
}
}
utils.createFakeCertificateByCA = function (caKey, caCert, originCertificate) {
const certificate = utils.covertNodeCertToForgeCert(originCertificate)
const keys = pki.rsa.generateKeyPair(2046)
const cert = pki.createCertificate()
cert.publicKey = keys.publicKey
cert.serialNumber = certificate.serialNumber
cert.validity.notBefore = new Date()
cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1)
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1)
cert.setSubject(certificate.subject.attributes)
cert.setIssuer(caCert.subject.attributes)
certificate.subjectaltname && (cert.subjectaltname = certificate.subjectaltname)
const subjectAltName = _.find(certificate.extensions, { name: 'subjectAltName' })
cert.setExtensions([{
name: 'basicConstraints',
critical: true,
cA: false
},
{
name: 'keyUsage',
critical: true,
digitalSignature: true,
contentCommitment: true,
keyEncipherment: true,
dataEncipherment: true,
keyAgreement: true,
keyCertSign: true,
cRLSign: true,
encipherOnly: true,
decipherOnly: true
},
{
name: 'subjectAltName',
altNames: subjectAltName.altNames
},
{
name: 'subjectKeyIdentifier'
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true
},
{
name: 'authorityKeyIdentifier'
}])
cert.sign(caKey, forge.md.sha256.create())
return {
key: keys.privateKey,
cert: cert
}
}
utils.isBrowserRequest = function (userAgent) {
return /Mozilla/i.test(userAgent)
}
//
// /^[^.]+\.a\.com$/.test('c.a.com')
//
utils.isMappingHostName = function (DNSName, hostname) {
let reg = DNSName.replace(/\./g, '\\.').replace(/\*/g, '[^.]+')
reg = '^' + reg + '$'
return (new RegExp(reg)).test(hostname)
}
utils.getMappingHostNamesFormCert = function (cert) {
let mappingHostNames = []
mappingHostNames.push(cert.subject.getField('CN') ? cert.subject.getField('CN').value : '')
const altNames = cert.getExtension('subjectAltName') ? cert.getExtension('subjectAltName').altNames : []
mappingHostNames = mappingHostNames.concat(_.map(altNames, 'value'))
return mappingHostNames
}
// sync
utils.initCA = function (basePath = config.getDefaultCABasePath()) {
const caCertPath = path.resolve(basePath, config.caCertFileName)
const caKeyPath = path.resolve(basePath, config.caKeyFileName)
try {
fs.accessSync(caCertPath, fs.F_OK)
fs.accessSync(caKeyPath, fs.F_OK)
// has exist
return {
caCertPath,
caKeyPath,
create: false
}
} catch (e) {
const caObj = utils.createCA(config.caName)
const caCert = caObj.cert
const cakey = caObj.key
const certPem = pki.certificateToPem(caCert)
const keyPem = pki.privateKeyToPem(cakey)
mkdirp.sync(path.dirname(caCertPath))
fs.writeFileSync(caCertPath, certPem)
fs.writeFileSync(caKeyPath, keyPem)
}
return {
caCertPath,
caKeyPath,
create: true
}
}

@ -0,0 +1,87 @@
const cmd = require('node-cmd')
const util = require('util')
const winExec = util.promisify(cmd.get, { multiArgs: true, context: cmd })
const os = require('os')
class SystemProxy {
static async setProxy (ip, port) {
throw new Error('You have to implement the method setProxy!')
}
static async unsetProxy () {
throw new Error('You have to implement the method unsetProxy!')
}
}
class DarwinSystemProxy extends SystemProxy {
}
class LinuxSystemProxy extends SystemProxy {
}
class WindowsSystemProxy extends SystemProxy {
static async setProxy (ip, port) {
let ret = await winExec(`yarn config set proxy=http://${ip}:${port}`)
console.log('yarn http proxy set success', ret)
ret = await winExec(`yarn config set https-proxy=http://${ip}:${port}`)
console.log('yarn https proxy set success', ret)
// ret = await winExec(`yarn config set cafile ${config.getDefaultCAKeyPath()}`)
// console.log('yarn cafile set success', ret)
ret = await winExec('yarn config set strict-ssl false')
console.log('yarn strict-ssl false success', ret)
}
static async unsetProxy () {
await winExec('yarn config delete proxy')
console.log('yarn https proxy unset success')
await winExec('yarn config delete https-proxy')
console.log('yarn https proxy unset success')
// await winExec(`yarn config delete cafile`)
// console.log('yarn cafile unset success')
await winExec(' yarn config delete strict-ssl')
console.log('yarn strict-ssl true success')
}
static _asyncRegSet (regKey, name, type, value) {
return new Promise((resolve, reject) => {
regKey.set(name, type, value, e => {
if (e) {
reject(e)
} else {
resolve()
}
})
})
}
}
function getSystemProxy () {
switch (os.platform()) {
case 'darwin':
return DarwinSystemProxy
case 'linux':
return LinuxSystemProxy
case 'win32':
case 'win64':
return WindowsSystemProxy
case 'unknown os':
default:
throw new Error(`UNKNOWN OS TYPE ${os.platform()}`)
}
}
module.exports = {
async setProxy (ip, port) {
const systemProxy = getSystemProxy()
await systemProxy.setProxy(ip, port)
},
async unsetProxy () {
const systemProxy = getSystemProxy()
await systemProxy.unsetProxy()
}
}

@ -0,0 +1,11 @@
const debug = require('debug')
module.exports = function getLogger (name) {
return {
debug: debug(`dev-sidecar:${name}:debug`),
info: debug(`dev-sidecar:${name}:info`),
error: debug(`dev-sidecar:${name}:error`)
}
}
debug.enable('dev-sidecar:*')

@ -0,0 +1,33 @@
const ProxyOptions = require('./options')
const mitmproxy = require('../lib/proxy')
const getLogger = require('../lib/utils/logger')
const logger = getLogger('proxy')
const config = require('../config')
const event = require('../event')
let server
module.exports = {
async start (newConfig) {
config.set(newConfig)
const proxyOptions = ProxyOptions(config.get())
server = mitmproxy.createProxy(proxyOptions, () => {
event.fire('status', { key: 'server', value: true })
server.on('close', () => {
event.fire('status', { key: 'server', value: false })
})
})
server.config = config.get()
return server.config
},
close () {
try {
if (server) {
server.close()
}
} catch (err) {
logger.error(err)
}
},
getServer () {
return server
}
}

@ -0,0 +1,82 @@
const getLogger = require('../lib/utils/logger')
const logger = getLogger('proxy')
const interceptors = require('../lib/interceptor')
const dnsUtil = require('../lib/dns')
function matchHostname (intercepts, hostname) {
const interceptOpts = intercepts[hostname]
if (interceptOpts) {
return interceptOpts
}
if (!interceptOpts) { // 该域名没有配置拦截器,直接过
for (const target in intercepts) {
if (target.indexOf('*') < 0) {
continue
}
// 正则表达式匹配
const regexp = target.replace('.', '\\.').replace('*', '.*')
if (hostname.match(regexp)) {
return intercepts[target]
}
}
}
}
function isMatched (url, regexp) {
return url.match(regexp)
}
module.exports = (config) => {
console.log('config', config)
return {
port: config.server.port,
dnsConfig: {
providers: dnsUtil.initDNS(config.dns.providers), mapping: config.dns.mapping
},
sslConnectInterceptor: (req, cltSocket, head) => {
const hostname = req.url.split(':')[0]
return !!matchHostname(config.intercepts, hostname) // 配置了拦截的域名,将会被代理
},
requestInterceptor: (rOptions, req, res, ssl, next) => {
const hostname = rOptions.hostname
const interceptOpts = matchHostname(config.intercepts, hostname)
if (!interceptOpts) { // 该域名没有配置拦截器,直接过
next()
return
}
for (const interceptOpt of interceptOpts) { // 遍历拦截配置
let regexpList
if (interceptOpt.regexp instanceof Array) {
regexpList = interceptOpt.regexp
} else {
regexpList = [interceptOpt.regexp]
}
for (const regexp of regexpList) { // 遍历regexp配置
if (!isMatched(req.url, regexp)) {
continue
}
for (const interceptImpl of interceptors) {
// 根据拦截配置挑选合适的拦截器来处理
if (!interceptImpl.is(interceptOpt) && interceptImpl.requestInterceptor) {
continue
}
try {
const result = interceptImpl.requestInterceptor(interceptOpt, rOptions, req, res, ssl)
if (result) { // 拦截成功,其他拦截器就不处理了
return
}
} catch (err) {
// 拦截失败
logger.error(err)
}
}
}
}
next()
},
responseInterceptor: (req, res, proxyReq, proxyRes, ssl, next) => {
next()
}
}
}

@ -0,0 +1,16 @@
const event = require('./event')
const lodash = require('lodash')
const status = {
server: false,
proxy: {
system: false,
npm: false,
git: false
}
}
event.register('status', (event) => {
lodash.set(status, event.key, event.value)
console.log('status changed:', event)
}, -999)
module.exports = status

@ -0,0 +1,87 @@
const cmd = require('node-cmd')
const util = require('util')
const winExec = util.promisify(cmd.get, { multiArgs: true, context: cmd })
const os = require('os')
class SystemProxy {
static async setProxy (ip, port) {
throw new Error('You have to implement the method setProxy!')
}
static async unsetProxy () {
throw new Error('You have to implement the method unsetProxy!')
}
}
class DarwinSystemProxy extends SystemProxy {
}
class LinuxSystemProxy extends SystemProxy {
}
class WindowsSystemProxy extends SystemProxy {
static async setProxy (ip, port) {
let ret = await winExec(`npm config set proxy=http://${ip}:${port}`)
console.log('npm http proxy set success', ret)
ret = await winExec(`npm config set https-proxy=http://${ip}:${port}`)
console.log('npm https proxy set success', ret)
// ret = await winExec(`npm config set cafile ${config.getDefaultCAKeyPath()}`)
// console.log('npm cafile set success', ret)
ret = await winExec('npm config set strict-ssl false')
console.log('npm strict-ssl false success', ret)
}
static async unsetProxy () {
await winExec('npm config delete proxy')
console.log('npm https proxy unset success')
await winExec('npm config delete https-proxy')
console.log('npm https proxy unset success')
// await winExec(`npm config delete cafile`)
// console.log('npm cafile unset success')
await winExec(' npm config delete strict-ssl')
console.log('npm strict-ssl true success')
}
static _asyncRegSet (regKey, name, type, value) {
return new Promise((resolve, reject) => {
regKey.set(name, type, value, e => {
if (e) {
reject(e)
} else {
resolve()
}
})
})
}
}
function getSystemProxy () {
switch (os.platform()) {
case 'darwin':
return DarwinSystemProxy
case 'linux':
return LinuxSystemProxy
case 'win32':
case 'win64':
return WindowsSystemProxy
case 'unknown os':
default:
throw new Error(`UNKNOWN OS TYPE ${os.platform()}`)
}
}
module.exports = {
async setProxy (ip, port) {
const systemProxy = getSystemProxy()
await systemProxy.setProxy(ip, port)
},
async unsetProxy () {
const systemProxy = getSystemProxy()
await systemProxy.unsetProxy()
}
}

@ -0,0 +1,14 @@
const script = `
$signature = @'
[DllImport("wininet.dll", SetLastError = true, CharSet=CharSet.Auto)]
public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);
'@
$INTERNET_OPTION_SETTINGS_CHANGED = 39
$INTERNET_OPTION_REFRESH = 37
$type = Add-Type -MemberDefinition $signature -Name wininet -Namespace pinvoke -PassThru
$a = $type::InternetSetOption(0, $INTERNET_OPTION_SETTINGS_CHANGED, 0, 0)
$b = $type::InternetSetOption(0, $INTERNET_OPTION_REFRESH, 0, 0)
$a -and $b
`
module.exports = script

@ -0,0 +1,61 @@
const script = `
Function Set-InternetProxy
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String[]]$Proxy,
[Parameter(Mandatory=$False,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[AllowEmptyString()]
[String[]]$acs
)
Begin
{
$regKey="HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"
}
Process
{
Set-ItemProperty -path $regKey ProxyEnable -value 1
Set-ItemProperty -path $regKey ProxyServer -value $proxy
if($acs)
{
Set-ItemProperty -path $regKey AutoConfigURL -Value $acs
}
}
End
{
Write-Output "Proxy is now enabled"
Write-Output "Proxy Server : $proxy"
if ($acs)
{
Write-Output "Automatic Configuration Script : $acs"
}
else
{
Write-Output "Automatic Configuration Script : Not Defined"
}
}
}
`
module.exports = script

@ -0,0 +1,196 @@
const util = require('util')
const os = require('os')
const path = require('path')
const childProcess = require('child_process')
const _exec = childProcess.exec
const spawn = childProcess.spawn
const Registry = require('winreg')
// const cmd = require('node-cmd')
console.log('childProcess', childProcess)
const exec = util.promisify(_exec)
const setproxyPs = require('./set-internet-proxy')
const refreshInternetPs = require('./refresh-internet')
const Shell = require('node-powershell')
const _lanIP = [
'localhost',
'127.*',
'10.*',
'172.16.*',
'172.17.*',
'172.18.*',
'172.19.*',
'172.20.*',
'172.21.*',
'172.22.*',
'172.23.*',
'172.24.*',
'172.25.*',
'172.26.*',
'172.27.*',
'172.28.*',
'172.29.*',
'172.30.*',
'172.31.*',
'192.168.*',
'<local>'
]
class SystemProxy {
static async setProxy (ip, port) {
throw new Error('You have to implement the method setProxy!')
}
static async unsetProxy () {
throw new Error('You have to implement the method unsetProxy!')
}
}
// TODO: Add path http_proxy and https_proxy
// TODO: Support for non-gnome
class LinuxSystemProxy extends SystemProxy {
static async setProxy (ip, port) {
await exec('gsettings set org.gnome.system.proxy mode manual')
await exec(`gsettings set org.gnome.system.proxy.http host ${ip}`)
await exec(`gsettings set org.gnome.system.proxy.http port ${port}`)
}
static async unsetProxy () {
await exec('gsettings set org.gnome.system.proxy mode none')
}
}
// TODO: Support for lan connections too
// TODO: move scripts to ../scripts/darwin
class DarwinSystemProxy extends SystemProxy {
static async setProxy (ip, port) {
const wifiAdaptor = (await exec('sh -c "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \'interface\' | cut -d \':\' -f2` -B 1 | head -n 1 | cut -d \' \' -f2"')).stdout.trim()
await exec(`networksetup -setwebproxy '${wifiAdaptor}' ${ip} ${port}`)
await exec(`networksetup -setsecurewebproxy '${wifiAdaptor}' ${ip} ${port}`)
}
static async unsetProxy () {
const wifiAdaptor = (await exec('sh -c "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \'interface\' | cut -d \':\' -f2` -B 1 | head -n 1 | cut -d \' \' -f2"')).stdout.trim()
await exec(`networksetup -setwebproxystate '${wifiAdaptor}' off`)
await exec(`networksetup -setsecurewebproxystate '${wifiAdaptor}' off`)
}
}
class WindowsSystemProxy extends SystemProxy {
static async setProxy (ip, port) {
const regKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
})
let lanIpStr = ''
for (const string of _lanIP) {
lanIpStr += string + ';'
}
console.log('lanIps:', lanIpStr, ip, port)
await Promise.all([
WindowsSystemProxy._asyncRegSet(regKey, 'MigrateProxy', Registry.REG_DWORD, 1),
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyEnable', Registry.REG_DWORD, 1),
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyHttp1.1', Registry.REG_DWORD, 0),
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyServer', Registry.REG_SZ, `${ip}:${port}`),
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyOverride', Registry.REG_SZ, lanIpStr)
])
await WindowsSystemProxy._resetWininetProxySettings('echo refreshing') // 要执行以下这个才能生效
await WindowsSystemProxy._resetWininetProxySettings(refreshInternetPs)
}
static async unsetProxy () {
const regKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
})
await Promise.all([
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyEnable', Registry.REG_DWORD, 0),
WindowsSystemProxy._asyncRegSet(regKey, 'ProxyServer', Registry.REG_SZ, '')
])
await WindowsSystemProxy._resetWininetProxySettings(refreshInternetPs)
}
static _asyncRegSet (regKey, name, type, value) {
return new Promise((resolve, reject) => {
regKey.set(name, type, value, e => {
if (e) {
reject(e)
} else {
resolve()
}
})
})
}
static _resetWininetProxySettings (script) {
return new Promise((resolve, reject) => {
const ps = new Shell({
executionPolicy: 'Bypass',
noProfile: true
})
// ps.addCommand(setproxyPs)
// ps.addCommand(`Set-InternetProxy -Proxy "${ip}:${port}"`)
ps.addCommand(script)
ps.invoke()
.then(output => {
console.log(output)
resolve()
})
.catch(err => {
console.log(err)
reject(err)
})
// const scriptPath = path.join(__dirname, '..', 'scripts', 'windows', 'wininet-reset-settings.ps1')
// const child = spawn('powershell.exe', [scriptPath])
// child.stdout.setEncoding('utf8')
// child.stdout.on('data', (data) => {
// console.log('data', data)
// if (data.includes('True')) {
// resolve()
// } else {
// reject(data)
// }
// })
//
// child.stderr.on('data', (err) => {
// console.log('data', err)
// reject(err)
// })
//
// child.stdin.end()
})
}
}
function getSystemProxy () {
switch (os.platform()) {
case 'darwin':
return DarwinSystemProxy
case 'linux':
return LinuxSystemProxy
case 'win32':
case 'win64':
return WindowsSystemProxy
case 'unknown os':
default:
throw new Error(`UNKNOWN OS TYPE ${os.platform()}`)
}
}
module.exports = {
async setProxy (ip, port) {
const systemProxy = getSystemProxy()
await systemProxy.setProxy(ip, port)
},
async unsetProxy () {
const systemProxy = getSystemProxy()
await systemProxy.unsetProxy()
}
}

@ -0,0 +1,32 @@
const systemProxy = require('./impl/system-proxy')
const npmProxy = require('./impl/npm-proxy')
const event = require('../../event')
const config = require('../../config')
function createProxyApi (type, impl) {
return {
async open (conf = { ip: '127.0.0.1', port: config.get().server.port }) {
try {
const { ip, port } = conf
await impl.setProxy(ip, port)
event.fire('status', { key: 'proxy.' + type, value: true })
console.info(`开启【${type}】代理成功`)
} catch (e) {
console.error(`开启【${type}】代理失败`, e)
}
},
async close () {
try {
await impl.unsetProxy()
event.fire('status', { key: 'proxy.' + type, value: false })
console.info(`关闭【${type}】代理成功`)
} catch (e) {
console.error(`关闭【${type}】代理失败`, e)
}
}
}
}
module.exports = {
system: createProxyApi('system', systemProxy),
npm: createProxyApi('npm', npmProxy)
}

@ -0,0 +1,74 @@
<#
.Synopsis
This function will set the proxy settings provided as input to the cmdlet.
.Description
This function will set the proxy server and (optinal) Automatic configuration script.
.Parameter ProxyServer
This parameter is set as the proxy for the system.
Data from. This parameter is Mandatory
.Example
Setting proxy information
Set-InternetProxy -proxy "proxy:7890"
.Example
Setting proxy information and (optinal) Automatic Configuration Script
Set-InternetProxy -proxy "proxy:7890" -acs "http://proxy:7892"
#>
Function Set-InternetProxy
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String[]]$Proxy,
[Parameter(Mandatory=$False,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[AllowEmptyString()]
[String[]]$acs
)
Begin
{
$regKey="HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
}
Process
{
Set-ItemProperty -path $regKey ProxyEnable -value 1
Set-ItemProperty -path $regKey ProxyServer -value $proxy
if($acs)
{
Set-ItemProperty -path $regKey AutoConfigURL -Value $acs
}
}
End
{
Write-Output "Proxy is now enabled"
Write-Output "Proxy Server : $proxy"
if ($acs)
{
Write-Output "Automatic Configuration Script : $acs"
}
else
{
Write-Output "Automatic Configuration Script : Not Defined"
}
}
}

@ -0,0 +1,14 @@
$signature = @'
[DllImport("wininet.dll", SetLastError = true, CharSet=CharSet.Auto)]
public static extern bool InternetSetOption(IntPtr hInternet, int
dwOption, IntPtr lpBuffer, int dwBufferLength);
'@
$interopHelper = Add-Type -MemberDefinition $signature -Name MyInteropHelper -PassThru
$INTERNET_OPTION_SETTINGS_CHANGED = 39
$INTERNET_OPTION_REFRESH = 37
$result1 = $interopHelper::InternetSetOption(0, $INTERNET_OPTION_SETTINGS_CHANGED, 0, 0)
$result2 = $interopHelper::InternetSetOption(0, $INTERNET_OPTION_REFRESH, 0, 0)
$result1 -and $result2

@ -0,0 +1,12 @@
const DevSidercar = require('.')
require('json5/lib/register')
const config = require('../../config/index.json5')
// 启动服务
DevSidercar.api.startup(config)
async function onClose () {
console.log('on sigint ')
await DevSidercar.api.shutdown()
console.log('on closed ')
process.exit(0)
}
process.on('SIGINT', onClose)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

@ -0,0 +1,26 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron

@ -0,0 +1,24 @@
# dev-sidecar-gui
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,61 @@
{
"name": "dev-sidecar-gui",
"version": "1.0.0",
"private": false,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"ant-design-vue": "^1.6.5",
"core-js": "^3.6.5",
"lodash": "^4.17.20",
"vue": "^2.6.11",
"vue-json-editor": "^1.4.2",
"dev-sidecar": "1.0.0",
"json5": "^2.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"electron": "^10.1.3",
"electron-devtools-installer": "^3.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"json5-loader": "^4.0.1",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,71 @@
<svg id="svg_canvas" viewBox="0 0 300 180" width="300" height="180" version="1.1" xmlns="http://www.w3.org/2000/svg" >
<g transform="translate(100,-20) scale(1,1)">
<path fill="#368FB1" d="M102.423,165.089H62.778c-2.534,0-4.589-2.056-4.589-4.589s2.055-4.589,4.589-4.589h39.645
c15.916,0,32.379-11.18,32.379-29.892c0-22.1-21.58-25.871-22.498-26.02l-4.244-0.693l0.397-4.281
c0.686-7.096-1.867-14.598-6.831-20.063c-5.45-6.002-13.351-9.175-22.85-9.175c-23.706,0-27.28,23.842-27.42,24.858l-0.565,4.081
l-4.122-0.111c-23.445-0.729-30.104,8.127-30.168,8.216c-1.457,2.02-4.294,2.558-6.348,1.147c-2.054-1.407-2.644-4.15-1.287-6.238
c0.837-1.288,8.623-12.082,34.18-12.339c2.982-11.745,13.044-28.793,35.731-28.793c15.36,0,24.6,6.624,29.645,12.184
c5.737,6.319,9.076,14.694,9.309,23.038c10.814,3.128,26.25,12.961,26.25,34.189C143.98,150.476,122.851,165.089,102.423,165.089z
"></path>
<rect x="140.167" y="74.342" fill="#368FB1" width="25.821" height="25.821"></rect>
<path fill="#368FB1" d="M137.108,98.083c-4.099-4.038-9.485-7.74-16.337-10.891c0,0-4.895-41.302-54.795-35.062
c0,0,49.319-22.548,64.248,26.77c0,0,2.907,1.439,6.884,4.314V98.083z"></path>
<path fill="#368FB1" d="M153.537,100.164h-13.37V85.509C144.543,89.027,149.59,93.922,153.537,100.164z"></path>
<path fill="#368FB1" d="M159.747,129.932c-4.803,29.615-33.316,34.602-35.091,34.877c1.071-0.581,12.789-7.129,19.58-20.774
c5.599-11.197,7.404-27.167-2.601-40.812h13.676C159.197,110.535,161.461,119.438,159.747,129.932z"></path>
<rect x="160.828" y="51.603" fill="#368FB1" width="17.177" height="17.177"></rect>
<rect x="173.065" y="75.186" fill="#368FB1" width="12.556" height="12.556"></rect>
</g>
<g transform="translate(10,90)">
<path fill="rgb(54, 143, 177)"
d="M2.52 0L2.52-24.68L12.99-24.68Q16.91-24.68 19.05-23.90L19.05-23.90Q23.01-22.46 24.35-18.61L24.35-18.61Q25.34-15.65 25.34-12.28L25.34-12.28Q25.34-8.95 24.38-5.99L24.38-5.99Q23.42-2.96 21.20-1.52L21.20-1.52Q19.79-0.63 18.11-0.31Q16.43 0 12.99 0L12.99 0L2.52 0ZM12.99-20.28L7.77-20.28L7.77-4.40L12.99-4.40Q16.43-4.40 17.76-5.92L17.76-5.92Q18.57-6.88 19.07-8.62Q19.57-10.36 19.57-12.32L19.57-12.32Q19.57-14.50 18.98-16.32Q18.39-18.13 17.43-19.02L17.43-19.02Q16.02-20.28 12.99-20.28L12.99-20.28Z"
transform="translate(5 31.782999999999998)" ></path>
<path fill="rgb(54, 143, 177)"
d="M6.70-10.95L17.13-10.95L17.13-7.10L6.70-7.10Q6.84-5.33 7.84-4.59Q8.84-3.85 11.10-3.85L11.10-3.85L17.13-3.85L17.13 0L10.54 0Q8.40 0 7.05-0.39Q5.70-0.78 4.51-1.66L4.51-1.66Q1.22-4.22 1.22-9.25L1.22-9.25Q1.22-12.51 2.96-14.95L2.96-14.95Q4.14-16.61 5.81-17.32Q7.47-18.02 10.17-18.02L10.17-18.02L17.13-18.02L17.13-14.17L10.54-14.17Q8.51-14.17 7.70-13.49Q6.88-12.80 6.70-10.95L6.70-10.95Z"
transform="translate(31.491999999999997 31.782999999999998)" ></path>
<path fill="rgb(54, 143, 177)"
d="M5.62-18.02L10.21-5.62L15.24-18.02L20.50-18.02L12.73 0L7.40 0L0.22-18.02L5.62-18.02Z"
transform="translate(50.251 31.782999999999998)" ></path>
<path fill="rgb(54, 143, 177)" d="M0-11.77L8.62-11.77L8.62-7.33L0-7.33L0-11.77Z"
transform="translate(71.008 31.782999999999998)"></path>
<path fill="rgb(54, 143, 177)"
d="M14.80 0L2.40 0L2.40-4.40L13.69-4.40Q16.39-4.40 17.28-4.92L17.28-4.92Q18.57-5.70 18.57-7.25L18.57-7.25Q18.57-9.06 17.06-9.88L17.06-9.88Q16.21-10.36 14.32-10.36L14.32-10.36L9.73-10.36Q5.55-10.36 3.70-11.88L3.70-11.88Q2.55-12.84 1.91-14.28Q1.26-15.72 1.26-17.39L1.26-17.39Q1.26-19.98 2.77-22.13L2.77-22.13Q4.25-24.20 7.51-24.57L7.51-24.57Q8.62-24.68 10.80-24.68L10.80-24.68L23.05-24.68L23.05-20.28L11.99-20.28Q9.51-20.24 8.70-20.05L8.70-20.05Q7.03-19.65 7.03-17.54L7.03-17.54Q7.03-15.76 8.44-15.17L8.44-15.17Q9.36-14.73 11.54-14.73L11.54-14.73L15.50-14.73Q18.54-14.73 20.02-14.21L20.02-14.21Q22.64-13.32 23.64-11.10L23.64-11.10Q24.42-9.32 24.42-7.29L24.42-7.29Q24.42-5.03 23.38-3.26L23.38-3.26Q21.94-0.74 19.05-0.22L19.05-0.22Q17.65 0 14.80 0L14.80 0Z"
transform="translate(79.62899999999999 31.782999999999998)" ></path>
<path fill="rgb(54, 143, 177)"
d="M2.40 0L2.40-18.02L7.36-18.02L7.36 0L2.40 0ZM7.36-19.87L2.40-19.87L2.40-24.68L7.36-24.68L7.36-19.87Z"
transform="translate(105.344 31.782999999999998)" ></path>
<path fill="rgb(54, 143, 177)"
d="M9.84-18.02L14.50-18.02L14.50-24.68L19.43-24.68L19.43 0L10.91 0Q8.32 0 6.99-0.41L6.99-0.41Q3.85-1.41 2.29-4.33L2.29-4.33Q1.22-6.29 1.22-9.14L1.22-9.14Q1.22-13.76 4.33-16.32L4.33-16.32Q6.36-18.02 9.84-18.02L9.84-18.02ZM10.91-3.85L14.50-3.85L14.50-14.17L10.91-14.17Q8.55-14.17 7.33-12.58L7.33-12.58Q6.25-11.25 6.25-9.14L6.25-9.14Q6.25-6.07 8.03-4.74L8.03-4.74Q9.25-3.85 10.91-3.85L10.91-3.85Z"
transform="translate(115.18599999999999 31.782999999999998)"
></path>
<path fill="rgb(54, 143, 177)"
d="M6.70-10.95L17.13-10.95L17.13-7.10L6.70-7.10Q6.84-5.33 7.84-4.59Q8.84-3.85 11.10-3.85L11.10-3.85L17.13-3.85L17.13 0L10.54 0Q8.40 0 7.05-0.39Q5.70-0.78 4.51-1.66L4.51-1.66Q1.22-4.22 1.22-9.25L1.22-9.25Q1.22-12.51 2.96-14.95L2.96-14.95Q4.14-16.61 5.81-17.32Q7.47-18.02 10.17-18.02L10.17-18.02L17.13-18.02L17.13-14.17L10.54-14.17Q8.51-14.17 7.70-13.49Q6.88-12.80 6.70-10.95L6.70-10.95Z"
transform="translate(137.053 31.782999999999998)"></path>
<path fill="rgb(54, 143, 177)"
d="M10.69-18.02L17.46-18.02L17.46-14.17L11.54-14.17Q8.55-14.17 7.40-12.84L7.40-12.84Q6.29-11.58 6.29-8.99L6.29-8.99Q6.29-6.10 8.10-4.77L8.10-4.77Q8.84-4.25 9.71-4.05Q10.58-3.85 12.14-3.85L12.14-3.85L17.46-3.85L17.46 0L10.69 0Q7.95 0 6.36-0.55Q4.77-1.11 3.55-2.48L3.55-2.48Q1.22-5.07 1.22-9.06L1.22-9.06Q1.22-13.88 4.00-16.24L4.00-16.24Q5.11-17.20 6.66-17.61Q8.21-18.02 10.69-18.02L10.69-18.02Z"
transform="translate(155.812 31.782999999999998)"></path>
<path fill="rgb(54, 143, 177)"
d="M8.77-10.95L14.84-10.95Q14.84-12.84 14.08-13.50Q13.32-14.17 11.06-14.17L11.06-14.17L2.70-14.17L2.70-18.02L11.06-18.02Q13.32-18.02 14.17-17.93Q15.02-17.83 15.91-17.57L15.91-17.57Q20.02-16.02 19.79-10.14L19.79-10.14L19.79 0L9.36 0Q6.55 0 5.48-0.17Q4.40-0.33 3.59-0.85L3.59-0.85Q1.41-2.33 1.41-5.33L1.41-5.33Q1.41-7.14 2.28-8.57Q3.15-9.99 4.59-10.51L4.59-10.51Q5.85-10.95 8.77-10.95L8.77-10.95ZM14.84-3.85L14.84-7.10L9.14-7.10L8.29-7.10Q7.44-7.10 6.94-6.66Q6.44-6.22 6.44-5.44L6.44-5.44Q6.44-4.59 7.05-4.22Q7.66-3.85 9.14-3.85L9.14-3.85L14.84-3.85Z"
transform="translate(174.645 31.782999999999998)"></path>
<path fill="rgb(54, 143, 177)"
d="M2.40 0L2.40-18.02L9.73-18.02Q11.99-18.02 13.23-17.70Q14.47-17.39 15.28-16.65L15.28-16.65Q16.09-15.91 16.43-14.80Q16.76-13.69 16.76-11.62L16.76-11.62L16.76-9.88L11.99-9.88L11.99-10.84Q11.99-12.76 11.32-13.47Q10.66-14.17 8.77-14.17L8.77-14.17L7.36-14.17L7.36 0L2.40 0Z"
transform="translate(196.66000000000003 31.782999999999998)"></path>
</g>
<g transform="translate(40,130)">
<path fill="rgb(54, 143, 177)"
d="M18.56-12.98L15.44-12.98L15.44-8.21L19.05-8.21L18.56-7.22L15.44-7.22L15.44-1.13L12.94-1.13L12.94-7.22L8.11-7.22L8.11-6.26L5.84-1.13L3.11-1.13L5.69-6.55L5.69-7.22L1.93-7.22L1.93-8.21L5.69-8.21L5.69-12.98L1.93-12.98L1.93-13.97L5.69-13.97L5.69-13.99L15.44-13.99L15.44-13.97L19.05-13.97L18.56-12.98ZM12.94-8.21L12.94-12.98L8.11-12.98L8.11-8.21L12.94-8.21Z"
transform="translate(5 18.060000000000002)" ></path>
<path fill="rgb(54, 143, 177)"
d="M13.13-2.69L10.08-1.09L6.47-1.09L11.40-3.59L7.41-5.71L10.37-5.71L12.92-4.37L15.94-5.90L15.94-6.72L7.16-6.72L4.22-1.18L1.53-1.18L6.07-9.74L1.49-9.74L3.30-14.03L5.86-14.03L4.43-10.65L6.55-10.65L8.36-14.03L11.07-14.03L9.26-10.65L16.09-10.65L15.29-12.89L17.70-12.89L18.52-10.65L19.49-10.65L19.49-9.74L8.78-9.74L7.67-7.64L18.33-7.64L18.33-5.44L14.62-3.47L19.15-1.09L16.17-1.09L13.13-2.69Z"
transform="translate(26 18.060000000000002)" ></path>
<path fill="rgb(54, 143, 177)"
d="M11.47-12.92L18.71-12.92L18.08-11.99L11.47-11.99L11.47-9.87L13.17-9.87L14.09-11.40L17.07-11.40L16.15-9.87L19.30-9.87L18.67-8.95L7.27-8.95L6.24-7.64L15.81-7.64L15.81-7.67L18.19-7.67L18.19-1.32L3.42-1.32L3.42-5.80L1.81-5.80L4.28-8.95L1.70-8.95L1.70-9.87L9.07-9.87L9.07-11.99L2.71-11.99L2.71-12.92L9.07-12.92L9.07-13.80L11.47-13.80L11.47-12.92ZM15.25-4.12L5.82-4.12L5.82-2.25L15.81-2.25L15.81-4.79L15.25-4.12ZM15.33-6.68L5.82-6.68L5.82-5.06L15.81-5.06L15.81-7.27L15.33-6.68Z"
transform="translate(47 18.060000000000002)" ></path>
<path fill="rgb(54, 143, 177)"
d="M11.03-6.11L8.93-2.67L6.49-2.67L8.59-6.11L8.59-11.28L6.41-11.28L6.41-12.26L8.59-12.26L8.59-13.86L11.03-13.86L11.03-12.26L18.44-12.26L18.44-2.67L13.57-2.67L13.02-3.65L16.00-3.65L16.00-11.28L11.03-11.28L11.03-6.11ZM1.47-9.91L4.96-9.91L4.96-2.25L19.53-2.25L19.13-1.28L5.71-1.28L4.89-2.10L4.26-1.28L1.47-1.28L1.47-2.25L2.56-2.25L2.56-8.93L1.47-8.93L1.47-9.91ZM4.31-13.80L5.40-11.05L2.83-11.05L1.72-13.80L4.31-13.80Z"
transform="translate(68 18.060000000000002)" ></path>
<path fill="rgb(54, 143, 177)"
d="M11.87-1.07L9.37-1.07L9.37-3.49L2.46-3.49L2.46-4.45L9.37-4.45L9.37-6.53L2.92-6.53L6.13-11.32L2.50-11.32L2.50-12.29L6.78-12.29L7.96-14.05L11.05-14.05L9.87-12.29L17.07-12.29L16.53-11.32L9.22-11.32L6.66-7.48L9.37-7.48L9.37-10.40L11.87-10.40L11.87-7.48L17.07-7.48L16.53-6.53L11.87-6.53L11.87-4.45L18.54-4.45L18.00-3.49L11.87-3.49L11.87-1.07Z"
transform="translate(89 18.060000000000002)" ></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,143 @@
'use strict'
import path from 'path'
import { app, protocol, BrowserWindow, Menu, Tray } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import bridge from './bridge/index'
// import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
let forceClose = false
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
// 隐藏主窗口,并创建托盘,绑定关闭事件
function setTray (app) {
// 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区
// 通常被添加到一个 context menu 上.
// 系统托盘右键菜单
const trayMenuTemplate = [
{
// 系统托盘图标目录
label: '退出',
click: () => {
forceClose = true
app.quit()
}
}
]
// 设置系统托盘图标
const iconPath = path.join(__dirname, '../public/favicon.ico')
const appTray = new Tray(iconPath)
// 图标的上下文菜单
const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
// 设置托盘悬浮提示
appTray.setToolTip('DevSidecar-开发者边车辅助工具')
// 设置托盘菜单
appTray.setContextMenu(contextMenu)
// 单击托盘小图标显示应用
appTray.on('click', () => {
// 显示主程序
win.show()
})
return appTray
}
function createWindow () {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
enableRemoteModule: true,
// preload: path.join(__dirname, 'preload.js'),
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: true// process.env.ELECTRON_NODE_INTEGRATION
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
win.on('closed', (e) => {
win = null
bridge.devSidecar.expose.api.shutdown()
})
win.on('close', (e) => {
if (!forceClose) {
e.preventDefault()
win.hide()
}
})
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
// try {
// await installExtension(VUEJS_DEVTOOLS)
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
}
createWindow()
// 最小化到托盘
setTray(app)
bridge.init(win)
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}

@ -0,0 +1,101 @@
import lodash from 'lodash'
import DevSidecar from 'dev-sidecar'
import { ipcMain } from 'electron'
import fs from 'fs'
import JSON5 from 'json5'
const localApi = {
config: {
/**
* 保存自定义的 config
* @param newConfig
*/
save (newConfig) {
// 对比默认config的异同
const defConfig = DevSidecar.api.config.get()
const saveConfig = {}
_merge(defConfig, newConfig, saveConfig, 'intercepts')
_merge(defConfig, newConfig, saveConfig, 'dns.mapping')
_merge(defConfig, newConfig, saveConfig, 'setting.startup.server', true)
_merge(defConfig, newConfig, saveConfig, 'setting.startup.proxy')
// TODO 保存到文件
console.log('save config ', saveConfig)
fs.writeFileSync('./config.json5', JSON5.stringify(saveConfig, null, 2))
return saveConfig
},
reload () {
const file = fs.readFileSync('./config.json5')
const userConfig = JSON5.parse(file.toString())
DevSidecar.api.config.set(userConfig)
}
}
}
function _merge (defConfig, newConfig, saveConfig, target, self = false) {
if (self) {
const defValue = lodash.get(defConfig, target)
const newValue = lodash.get(newConfig, target)
if (newValue != null && newValue !== defValue) {
lodash.set(saveConfig, newValue, target)
}
return
}
const saveObj = _mergeConfig(lodash.get(defConfig, target), lodash.get(newConfig, target))
lodash.set(saveConfig, target, saveObj)
}
function _mergeConfig (defObj, newObj) {
for (const key in defObj) {
// 从默认里面提取对比,是否有被删除掉的
if (newObj[key] == null) {
newObj[key] = false
}
}
for (const key in newObj) {
const newItem = newObj[key]
const defItem = defObj[key]
if (newItem && !defItem) {
continue
}
// 深度对比 是否有修改
if (lodash.isEqual(newItem, defItem)) {
console.log('equle', key, newItem, defItem)
// 没有修改则删除
delete newObj[key]
}
}
return newObj
}
export default {
init (win) {
// 接收view的方法调用
ipcMain.handle('apiInvoke', async (event, args) => {
const api = args[0]
let target = lodash.get(DevSidecar.api, api)
if (target == null) {
console.log('get local api')
target = lodash.get(localApi, api)
}
if (target == null) {
console.log('找不到此接口方法:', api)
}
let param
if (args.length >= 2) {
param = args[1]
}
const ret = target(param)
console.log('api:', api, 'ret:', ret)
return ret
})
// 注册从core里来的事件并转发给view
DevSidecar.api.event.register('status', (event) => {
console.log('bridge on status', event)
win.webContents.send('status', { ...event })
})
},
devSidecar: DevSidecar
}

@ -0,0 +1,2 @@
import config from '../../../config/index.json5'
export default config

@ -0,0 +1,11 @@
import Vue from 'vue'
import App from './view/components/App.vue'
import antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import './view'
Vue.config.productionTip = false
Vue.use(antd)
new Vue({
render: h => h(App)
}).$mount('#app')

@ -0,0 +1 @@
window.ipcRenderer = require('electron').ipcRenderer

@ -0,0 +1,36 @@
import lodash from 'lodash'
import { ipcRenderer } from 'electron'
const doInvoke = (api, args) => {
return ipcRenderer.invoke('apiInvoke', [api, args])
}
const bindApi = (api, param1) => {
lodash.set(apiObj, api, (param2) => {
return doInvoke(api, param2 || param1)
})
}
const apiObj = {
on (channel, callback) {
ipcRenderer.on(channel, callback)
},
doInvoke
}
bindApi('startup')
bindApi('shutdown')
bindApi('config.set')
bindApi('config.get')
bindApi('config.save')
bindApi('config.reload')
bindApi('server.start')
bindApi('server.close')
bindApi('proxy.system.open')
bindApi('proxy.system.close')
bindApi('proxy.npm.open')
bindApi('proxy.npm.close')
export default apiObj

@ -0,0 +1,183 @@
<template>
<div id="app">
<template>
<div style="margin:auto">
<div style="text-align: center"><img height="80px" src="/logo.svg"></div>
<a-card title="DevSidecar-开发者辅助工具 " style="width: 500px;margin:auto">
<div style="display: flex; align-items:center;justify-content:space-around;flex-direction: row">
<div style="text-align: center">
<div class="big_button" >
<a-button shape="circle" icon="poweroff" :type="startup.type()" :loading="startup.loading" @click="startup.doClick" ></a-button>
<div style="margin-top: 10px">{{status.server?'已开启':'已关闭'}}</div>
</div>
</div>
<div :span="12">
<a-form style="margin-top:20px" :label-col="{ span: 12 }" :wrapper-col="{ span: 12 }" >
<a-form-item label="代理服务">
<a-switch :loading="server.loading" v-model="status.server" default-checked v-on:click="server.doClick">
<a-icon slot="checkedChildren" type="check" />
<a-icon slot="unCheckedChildren" type="close" />
</a-switch>
</a-form-item>
<a-form-item v-for=" (item, key) in proxy" :key="key" :label="_lang(key,langSetting.proxy) ">
<a-switch :loading="item.loading" v-model="status.proxy[key]" default-checked v-on:click="item.doClick">
<a-icon slot="checkedChildren" type="check" />
<a-icon slot="unCheckedChildren" type="close" />
</a-switch>
</a-form-item>
</a-form>
</div>
</div>
<span slot="extra" >
<a-button v-if="config" @click="openSettings" icon="setting" ></a-button>
</span>
</a-card>
<settings v-if="config" title="设置" :config="config" :visible.sync="settings.visible" @change="onConfigChanged"></settings>
</div>
</template>
</div>
</template>
<script>
import api from '../api'
import status from '../status'
import lodash from 'lodash'
import Settings from './settings'
export default {
name: 'App',
components: {
Settings
},
data () {
return {
langSetting: {
proxy: {
system: '系统代理',
npm: 'npm代理',
yarn: 'yarn代理'
}
},
status: status,
startup: {
loading: false,
type: () => {
return this.status.server ? 'primary' : 'default'
},
doClick: () => {
if (this.status.server) {
this.apiCall(this.startup, api.shutdown)
} else {
this.apiCall(this.startup, api.startup)
}
}
},
server: {
key: '代理服务',
loading: false,
doClick: (checked) => {
this.onSwitchClick(this.server, api.server.start, api.server.close, checked)
}
},
proxy: undefined,
config: undefined,
settings: {
visible: false
}
}
},
computed: {
_intercepts () {
return this.config.intercepts
}
},
created () {
api.config.set().then(() => {
return api.config.get().then(ret => {
this.config = ret
this.start(true)
})
}).then(() => {
this.proxy = this.createProxyBtns()
console.log('proxy', this.proxy)
})
},
methods: {
_lang (key, parent) {
const label = parent ? lodash.get(parent, key) : lodash.get(this.langSetting, key)
if (label) {
return label
}
return key
},
createProxyBtns () {
const btns = {}
console.log('api.proxy', api.proxy, api)
for (const type in api.proxy) {
btns[type] = {
loading: false,
key: type,
doClick: (checked) => {
this.onSwitchClick(this.proxy[type], api.proxy[type].open, api.proxy[type].close, checked)
}
}
}
return btns
},
async apiCall (btn, api, param) {
btn.loading = true
try {
const ret = await api(param)
return ret
} catch (err) {
console.log('api invoke error:', err)
} finally {
btn.loading = false
}
},
onSwitchClick (btn, openApi, closeApi, checked) {
if (checked) {
this.apiCall(btn, openApi)
} else {
this.apiCall(btn, closeApi)
}
},
start (checked) {
this.apiCall(this.startup, api.startup)
},
openSettings () {
this.settings.visible = true
},
onConfigChanged (newConfig) {
console.log('config chagned', newConfig)
api.config.save(newConfig).then(() => {
api.config.reload()
})
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
padding-top:60px;
}
.big_button >button{
width:100px;
height:100px;
border-radius: 100px;
}
.big_button >button i{
size:40px
}
</style>

@ -0,0 +1,155 @@
<template>
<a-drawer
:title="title"
placement="right"
:closable="false"
:visible="visible"
:after-visible-change="afterVisibleChange"
@close="onClose"
width="650px"
height="100%"
wrapClassName="json-wrapper"
>
<a-tabs
default-active-key="1"
tab-position="left"
:style="{ height: '100%' }"
>
<a-tab-pane tab="拦截设置" key="1" >
<vue-json-editor style="height:100%;" ref="editor" v-model="targetConfig.intercepts" :show-btns="false" :expandedOnStart="true" @json-change="onJsonChange" ></vue-json-editor>
</a-tab-pane>
<a-tab-pane tab="DNS设置" key="2">
<div>
<div>某些域名有时候需要通过其他DNS服务器获取到的IP才可以访问</div>
<a-row :gutter="10" style="margin-top: 10px" v-for="item in dnsMappings" :key = 'item.key'>
<a-col :span="18">
<a-input v-model="item.key"></a-input>
</a-col>
<a-col :span="6">
<a-select v-model="item.value">
<a-select-option value="usa">USA</a-select-option>
<a-select-option value="aliyun">Aliyun</a-select-option>
</a-select>
</a-col>
</a-row>
</div>
</a-tab-pane>
<a-tab-pane tab="启动设置" key="3" >
<div>启动应用程序后自动启动</div>
<a-form style="margin-top: 20px" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }" >
<a-form-item label="代理服务" style="margin-bottom: 10px">
<a-switch v-model="targetConfig.setting.startup.server" default-checked v-on:click="(checked)=>{targetConfig.setting.startup.server = checked}">
<a-icon slot="checkedChildren" type="check" />
<a-icon slot="unCheckedChildren" type="close" />
</a-switch>
</a-form-item>
<a-form-item style="margin-bottom: 10px" v-for="(item,key) in targetConfig.setting.startup.proxy" :key="key" :label="key">
<a-switch v-model="targetConfig.setting.startup.proxy[key]" default-checked v-on:click="(checked)=>{targetConfig.setting.startup.proxy[key] = checked}">
<a-icon slot="checkedChildren" type="check" />
<a-icon slot="unCheckedChildren" type="close" />
</a-switch>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script>
import vueJsonEditor from 'vue-json-editor'
import lodash from 'lodash'
export default {
name: 'App',
components: {
vueJsonEditor
},
props: {
config: {
type: Object
},
title: {
type: String,
default: '编辑'
},
visible: {
type: Boolean
}
},
data () {
return {
targetConfig: {},
dnsMappings: [],
changed: false
}
},
created () {
this.resetConfig()
},
methods: {
resetConfig () {
this.targetConfig = lodash.cloneDeep(this.config)
console.log('targetConfig', this.targetConfig)
this.dnsMappings = []
for (const key in this.targetConfig.dns.mapping) {
const value = this.targetConfig.dns.mapping[key]
this.dnsMappings.push({
key, value
})
}
},
onJsonChange (config) {
this.changed = true
},
afterVisibleChange (val) {
console.log('visible', val)
if (val === true) {
this.resetConfig()
}
},
showDrawer () {
this.$emit('update:visible', true)
},
onClose () {
if (this.changed) {
this.$confirm({
title: '提示',
content: '是否需要保存?',
onOk: () => {
this.$emit('change', this.targetConfig)
},
onCancel () {}
})
}
this.$emit('update:visible', false)
}
}
}
</script>
<style>
.json-wrapper .ant-drawer-wrapper-body{
display: flex;
flex-direction: column;
}
.json-wrapper .ant-drawer-wrapper-body .ant-drawer-body{
flex: 1;
height: 0;
}
.json-wrapper .jsoneditor-vue{
height:100%
}
.json-wrapper .ant-tabs{
height: 100%;
}
.json-wrapper .ant-tabs-content{
height: 100%;
}
.json-wrapper .ant-tabs-tabpane-active{
height: 100%;
}
</style>

@ -0,0 +1 @@
import './status'

@ -0,0 +1,17 @@
import api from './api'
import lodash from 'lodash'
const status = {
server: false,
proxy: {
system: false,
npm: false
}
}
api.on('status', (event, message) => {
console.log('view on status', event, message)
const value = message.value
const key = message.key
lodash.set(status, key, value)
})
export default status

@ -0,0 +1,39 @@
module.exports = {
configureWebpack: config => {
const configNew = {
module: {
rules: [
{
test: /\.json5$/i,
loader: 'json5-loader',
options: {
esModule: false
},
type: 'javascript/auto'
}
]
}
}
return configNew
},
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
// Provide an array of files that, when changed, will recompile the main process and restart Electron
// Your main process file will be added by default
mainProcessWatch: ['src/bridge', 'src/*.js', 'node_modules/dev-sidecar/src'],
builderOptions: {
extraResources: [
{
from: 'src/config.json5',
to: 'app/config.json5'
},
{
from: 'public',
to: 'public'
}
]
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save