xiaojunnuo
4 years ago
commit
b7654d6ab0
80 changed files with 49170 additions and 0 deletions
@ -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使用yarn,publish用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 |
||||
// } |
||||
// } |
||||
// } |
||||
} |
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,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) |
@ -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" |
||||
] |
||||
} |
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> |
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,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' |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue