Compare commits

...

175 commits

Author SHA1 Message Date
Zachary Boyd
6ab973e359 Update CNAME 2021-08-06 12:43:00 -04:00
Zachary Boyd
0a414024c2 Update CNAME 2021-08-06 12:39:22 -04:00
Zachary Boyd
a261baeed3
bump version 2019-11-23 22:44:19 -05:00
Zachary Boyd
6040db22dc
merge 2019-11-23 22:41:28 -05:00
Zachary Boyd
37a58f929a
Merge pull request #14 from jogli5er/upstream_master
Pool getPort race condition
2019-11-23 22:40:13 -05:00
Zachary Boyd
c1b7a124c4
Merge branch 'master' into upstream_master 2019-11-23 22:39:55 -05:00
Zachary Boyd
5617f2bfdb
Switches the granax package to the new package location 2019-11-21 23:13:31 -05:00
Zachary Boyd
6b7054500d
Merge pull request #16 from znetstar/dependabot/npm_and_yarn/merge-1.2.1
Bump merge from 1.2.0 to 1.2.1
2019-11-21 22:33:39 -05:00
dependabot[bot]
bf0327ad66
Bump merge from 1.2.0 to 1.2.1
Bumps [merge](https://github.com/yeikos/js.merge) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/yeikos/js.merge/releases)
- [Commits](https://github.com/yeikos/js.merge/compare/v1.2.0...v1.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-22 03:26:08 +00:00
Zachary Boyd
fbdbf525df
Added a entrypoint file so the node.js process is not the root process, so it responds to SIGINT calls (Ctrl+C). 2019-11-21 22:25:01 -05:00
Roman Brunner
f89e4b20c5 Pool getPort race condition
It appears that when requesting multiple ports asynchronously,
that a port is given out multiple times, which crashes single
Tor processes. Such a crash brings down the whole system as of
now, which might or might not be desired.
This is caused by a race condition on the port numbers. This
commit fixes the issue if a number of Tor instances for the pool
is requested. If the pool is populated with an array of definitions,
the user has to make sure to include the ports in the definition,
otherwise the race condition still persists. If this should still be
allowed is up for discussion or if a check should be included in the
TorPool create function.
2019-11-21 16:27:48 +01:00
Zachary Boyd
15408206a3 Update changelog 2019-01-15 16:07:26 -05:00
Zachary Boyd
76ab9ae829 Fixes #11 2019-01-15 12:11:49 -05:00
Zachary Boyd
aca5498435 updates multi-rpc 2018-12-29 11:14:05 -08:00
Zachary Boyd
5195b576be Updatets to multi-rpc to 1.4.0 2018-12-23 21:17:07 -05:00
Zachary Boyd
08c0b7687e Updates multi-rpc 2018-12-23 20:56:55 -05:00
Zachary Boyd
089e9bbad1 updates multi-rpc 2018-12-16 17:53:18 -05:00
Zachary Boyd
c1dfa465d5 Updates multi-rpc 2018-12-14 19:02:39 -05:00
Zachary Boyd
7b90edabd0 Prevents errors that occur when connecting to the destination in the SOCKS Proxy and HTTP Proxy from crashing the applications 2018-10-15 17:36:32 -04:00
Zachary Boyd
1261821ab3 Updates mutli-rpc 2018-09-26 22:37:17 -04:00
Zachary Boyd
459aef5ca3 Updates multi-rpc 2018-09-25 21:25:06 -04:00
Zachary Boyd
90adbe3d55 move jsdoc to devdependencies 2018-09-25 12:54:50 -04:00
Zachary Boyd
eb95142b6a update docs 2018-09-25 12:53:29 -04:00
Zachary Boyd
6389835989 updates multi-rpc version 2018-09-25 12:52:13 -04:00
Zachary Boyd
419e78b727 Replaces 'jrpc2' with 'multi-rpc'. No application or API changes. 2018-09-25 12:40:15 -04:00
Zachary Boyd
55ee9b9765 Merge branch 'master' of github.com:znetstar/tor-router 2018-09-19 15:08:20 -04:00
Zachary Boyd
0fa8bca118 removes docs from node_modules 2018-09-19 15:08:10 -04:00
Zachary Boyd
7076bb06f7 Update CNAME 2018-09-19 11:33:38 -04:00
Zachary Boyd
c716aa8b03 Create CNAME 2018-09-19 11:23:45 -04:00
Zachary Boyd
f1e3767bcc adds docs folder to sc 2018-09-19 11:22:38 -04:00
Zachary Boyd
a9e8baa27f references 'granax' to comply with licensing requirements 2018-09-15 13:54:28 -04:00
Zachary Boyd
d0fe557036 add fossa shield 2018-09-15 13:39:57 -04:00
Zachary Boyd
ffa6f66d77 remove README from dockerignore 2018-09-15 10:15:32 -04:00
Zachary Boyd
3d1877d851 add online docs link 2018-09-15 03:00:49 -04:00
Zachary Boyd
96537af59a merge 2018-09-15 02:29:21 -04:00
Zachary Boyd
3a869f46bf fixes a small bug in tests 2018-09-15 02:27:19 -04:00
Zachary Boyd
0b82ad9cb5 finishes documentation. minor bug fixes and some code clean-up 2018-09-14 20:09:36 -04:00
Zachary Boyd
5b598f612a Adds jsdoc for TorPool. 2018-09-13 22:35:40 -04:00
Zachary Boyd
4aec5c3d0f Move most of the README to the wiki. Add grunt and jsdoc and documentation for TorProcess 2018-09-13 15:00:51 -04:00
Zachary Boyd
bcace28669
Update README.md 2018-09-12 18:35:22 -04:00
Zachary Boyd
a61a2adb34
Update README.md 2018-09-12 18:34:38 -04:00
Zachary Boyd
c539d47e11 update readme and changelog 2018-09-11 19:52:45 -04:00
Zachary Boyd
e50a392eb5
Update README.md 2018-09-11 19:51:36 -04:00
Zachary Boyd
d2b64ad796 "proxyByName" is now disabled by default because most browsers do not support SOCKS authentication 2018-09-11 19:44:41 -04:00
Zachary Boyd
2f57082394 "proxyByName" is now disabled by default because most browsers do not support SOCKS authentication 2018-09-11 19:37:13 -04:00
Zachary Boyd
e2e43215ad Update README and CHANGELOG. Small bug fixes 2018-09-11 19:21:36 -04:00
Zachary Boyd
c8bc7b81ac Adds setConfig, getConfig, saveConfig and loadConfig methods to the RPC interface. 2018-09-11 17:44:32 -04:00
Zachary Boyd
f97272b5c0 Finishes proxy by name for groups. 2018-09-11 15:53:52 -04:00
Zachary Boyd
be52091af1 Adds a rotate function to groups 2018-09-11 13:52:42 -04:00
Zachary Boyd
998185d31e Adds methods to the RPC Interface to interact with groups. Adds tests for those methods 2018-09-11 02:48:56 -04:00
Zachary Boyd
99c59541e0 Adds tests for the group feature. Isolates individual tests 2018-09-11 01:08:06 -04:00
Zachary Boyd
f7537bbf8e Adds tests for the group feature. 2018-09-10 22:36:21 -04:00
Zachary Boyd
22908b7bba Adds group functionality to TorPool 2018-09-10 20:53:33 -04:00
Zachary Boyd
94a6511fd5 Fixes a bug where exceptions caused by the inbound socket on HTTPServer are not caught 2018-09-10 13:25:39 -04:00
Zachary Boyd
c43e6cf14f Finishes proxying through a specific instance 2018-09-10 13:03:02 -04:00
Zachary Boyd
55cb0a9566 update README.md 2018-09-10 00:53:06 -04:00
Zachary Boyd
ece4d4ea02 Can now connect directly to an instance via http 2018-09-09 23:44:24 -04:00
Zachary Boyd
4553bcc82b Replaced socksv5 with a version that supports user context. Locks down the versions of some packages 2018-09-09 22:26:55 -04:00
Zachary Boyd
c379350de7 The control server can now accept websocket connections. All servers can be bound to a specific hostname 2018-09-09 22:13:39 -04:00
Zachary Boyd
a571d2caf9 removes 'async' as a dependency. adds npm badge 2018-09-09 17:03:43 -04:00
Zachary Boyd
ba837dca3e Rasies the node version from 8 to 10 in the dockerfile 2018-09-09 15:45:46 -04:00
Zachary Boyd
b65fa27ba8 Breaks the mocha test into individual files so tests can be run independently. Uses Promises in the mocha test. Adds a listen method to all servers 2018-09-09 15:42:50 -04:00
Zachary Boyd
1c29679024 Splits the mocha tests into individual files 2018-09-09 03:00:29 -04:00
Zachary Boyd
a74409ddb7 Replaces callbacks with Promises throughout the rest of the application. 2018-09-09 01:45:12 -04:00
Zachary Boyd
d1265253d2 Uses bluebird for Promises. All functions in TorProcess return Promises instead of callbacks 2018-09-09 00:44:39 -04:00
Zachary Boyd
723a227f26 replaces eventemitter2 with eventemitter3 2018-09-09 00:28:43 -04:00
Zachary Boyd
c1515969b4 Adds a sepreate file for a silent winston logger to be used whenever a logger is not specified. Replaces all callbacks with Promises in TorPool. 2018-09-09 00:25:46 -04:00
Zachary Boyd
72aa9193d2 Moves 'tor-router' executable code to 'src/launch.js' 2018-09-08 23:56:34 -04:00
Zachary Boyd
0b60f6f949 removes winston-null-transport and uses the 'silent' option built-in winston instead 2018-09-08 23:47:10 -04:00
Zachary Boyd
07b06082e2
Merge pull request #6 from knoxcard/patch-1
corrected typo
2018-09-08 23:39:43 -04:00
Indospace.io
964227fc33
update
update
2018-09-08 18:55:24 -07:00
Zachary Boyd
e79c5bf3e2 remove cloudbuild 2018-08-15 21:41:10 -04:00
Zachary Boyd
607b74d294 Merge branch 'master' of github.com:znetstar/tor-router 2018-08-15 21:40:54 -04:00
Zachary Boyd
a48ea78cee add copyright holder 2018-08-15 21:40:41 -04:00
Zachary Boyd
9c92883082 simplifies dockerfile 2018-08-12 12:03:23 -04:00
Zachary Boyd
150e1e8af6 modify cloudbuild 2018-08-12 11:55:42 -04:00
Zachary Boyd
422790bee1 modify cloudbuild 2018-08-12 11:54:48 -04:00
Zachary Boyd
784ab58e84 modify cloudbuild 2018-08-12 11:51:45 -04:00
Zachary Boyd
129136aa69 switches to node:8 repo 2018-08-12 11:46:06 -04:00
Zachary Boyd
ac421e3897 Remove external tor repo from dockerfile 2018-08-12 11:42:36 -04:00
Zachary Boyd
e0e5d50c35 modify cloudbuild.yaml 2018-08-12 11:40:21 -04:00
Zachary Boyd
b0705d129d modify cloudbuild.yaml 2018-08-12 11:36:58 -04:00
Zachary Boyd
1fa52e50e8 modify cloudbuild.yaml 2018-08-12 11:31:33 -04:00
Zachary Boyd
eff827600a Adds cloudbuild.yaml 2018-08-12 11:20:38 -04:00
Zachary Boyd
51467b2e72 Adds cloudbuild.yaml 2018-08-12 11:20:30 -04:00
Zachary Boyd
fb1840b90d updates changelog 2018-08-10 15:47:42 -04:00
Zachary Boyd
56b34f2133 Launches tor with the '--quiet' flag when attempting to get the hashed password for the control port. Uses 'os.EOL' instead of '\n' when creating the config file for compatibility with windows 2018-08-10 15:43:28 -04:00
Zachary Boyd
7d996ffa81 Adds "queryInstanceByName" and "queryInstanceAt" 2018-08-10 13:06:40 -04:00
Zachary Boyd
58d3d1da17 Adds a changelog with changes going back to 3.0.0 2018-08-10 12:31:41 -04:00
Zachary Boyd
bd77890930 Adds a changelog with changes going back to 3.0.0 2018-08-10 12:29:26 -04:00
Zachary Boyd
1f822af2bd fixes typo 2018-08-10 11:58:48 -04:00
Zachary Boyd
82d0e48df5 ensures instance names are sorted for test 2018-08-10 11:50:58 -04:00
Zachary Boyd
2716786c61 Add bitbucket pipelines again 2018-08-10 11:20:05 -04:00
Zachary Boyd
9c9c7cb324 begins work on the CHANGELOG 2018-08-10 10:59:02 -04:00
Zachary Boyd
1f93a82abd Add travis yml 2018-08-10 10:25:28 -04:00
Zachary Boyd
a52789be41 Ensures that data directories are not included in the instance definitions when being compared 2018-08-10 10:08:46 -04:00
Zachary Boyd
9df61fa743 add changelog file 2018-08-10 01:06:25 -04:00
Zachary Boyd
8abdd0b5c7 Upgrade mocha 2018-08-10 01:03:15 -04:00
Zachary Boyd
a3917f7870 Merge branch 'master' of bitbucket.org:znetstar/tor-router 2018-08-10 00:58:10 -04:00
Zachary Boyd
ba24667009 add nconf env loader 2018-08-10 00:57:47 -04:00
Zachary Boyd
79b005bccb Initial Bitbucket Pipelines configuration 2018-08-10 04:55:49 +00:00
Zachary Boyd
09605e36c5 add bitbucket pipelines 2018-08-10 00:54:30 -04:00
Zachary Boyd
f77d6bf3b5 fixes typo 2018-08-10 00:53:43 -04:00
Zachary Boyd
a8b68ef5ec Removes domains 2018-08-10 00:46:34 -04:00
Zachary Boyd
04f185f85b Cleans up existing test suites and adds suites for new methods 2018-08-10 00:44:07 -04:00
Zachary Boyd
4c148e2832 completes mocha test for TorProcess 2018-08-09 16:13:48 -04:00
Zachary Boyd
7b89981059 Uses a 'null' transport if 'logLevel' is set to 'null' 2018-08-09 15:16:45 -04:00
Zachary Boyd
b3a03b4a3e Removes refrences to 'nconf' within classes to make testing easier 2018-08-09 15:03:51 -04:00
Zachary Boyd
f08aa6d23f removes all dependency on domains since they're being deprecated 2018-08-09 13:42:11 -04:00
Zachary Boyd
41aea76a95 fixes bug with port numbers not being set in nconf 2018-07-19 10:33:18 -04:00
Zachary Boyd
ecef462bde cleans up README 2018-05-12 17:55:32 -07:00
Zachary Boyd
797bdee4dd Fixes typos in readme 2018-05-11 13:27:50 -07:00
Zachary Boyd
ea1c2b85c1 Updates the README with information about using external tor executables 2018-05-11 13:21:53 -07:00
Zachary Boyd
e6ad6204b8 Reverts back to ubuntu dockerfile and uses system Tor 2018-05-11 13:16:23 -07:00
Zachary Boyd
18d047b933 removes empty space in dockerfile 2018-05-11 08:56:49 -07:00
Zachary Boyd
dd93c4a97f Amends README.md 2018-05-11 08:56:11 -07:00
Zachary Boyd
be4274a46b Updates rpc-methods.md with changes 2018-05-10 22:56:55 -07:00
Zachary Boyd
1273316e7d Allows signals to be sent to Tor instances using the control protocol from the rpc interface 2018-05-10 22:41:15 -07:00
Zachary Boyd
48373e3c71 Allows Tor configuration to be modified at runtime via the rpc interface 2018-05-10 22:25:26 -07:00
Zachary Boyd
38bf4d7c6a Adds remove_by_name and new_identity_by_name methods to TorPool 2018-05-10 22:08:38 -07:00
Zachary Boyd
5f8fde9039 Uses the Control Protocol to request a new identity. Replaces 'new_ip' with 'new_identity' and deprecates 'new_ip' 2018-05-10 21:44:30 -07:00
Zachary Boyd
4bc4c15dd7 Connects to the Tor instance via the control port when the instance starts 2018-05-10 21:17:00 -07:00
Zachary Boyd
b3700e0202 Bundles the Tor executable 2018-05-09 22:42:30 -07:00
Zachary Boyd
c0026c4411 Removes the default values on socksPort and instances 2018-05-09 22:09:31 -07:00
Zachary Boyd
adb27ab9dd fixes bug in README 2018-05-09 22:03:37 -07:00
Zachary Boyd
ba543cdfe3 fixes bug in README 2018-05-09 22:02:54 -07:00
Zachary Boyd
b14e8fa079 fixes bug in Dockerfile 2018-05-09 22:01:13 -07:00
Zachary Boyd
62495b1e86 Adds documentation on all of the RPC methods 2018-05-09 21:56:00 -07:00
Zachary Boyd
12c00baecc Updates README to reflect changes. Uses instance name when reporting process info 2018-05-09 21:30:51 -07:00
Zachary Boyd
da1d543d03 Allows for the load balance method to be set/retrived via the control server 2018-05-09 21:11:05 -07:00
Zachary Boyd
6878081797 Allows for multiple types of load-balancing methods to be selected and allows for instances to be load-balanced by weight. Allows for instances to be named. Fixes bug with instance cleanup 2018-05-09 21:09:38 -07:00
Zachary Boyd
4bf03cb299 Amends README 2018-05-09 18:22:41 -07:00
Zachary Boyd
bfde78268a Removes the data directory when the tor process exits 2018-05-09 18:19:06 -07:00
Zachary Boyd
369ffa5d4f Allows to default config of the Tor instances to be specified at startup and during runtime. Allows for the configuration of individual Tor instances to be specified at startup and during runtime 2018-05-09 18:15:58 -07:00
Zachary Boyd
b3546799da Switches from "commander" to "nconf/yargs" for configuration management and argv/env variable processing 2018-05-09 15:31:58 -07:00
Zachary Boyd
70f2002d61 Move sources for Tor Project deb repository into the Dockerfile 2018-05-09 14:00:39 -07:00
Zachary Boyd
cd9ca3bb3e Remove socket.io-client from dev-dependencies 2018-05-09 08:01:35 -07:00
Zachary Boyd
9f5f26d277 removes redundant reference to "this.torPool" 2018-05-09 07:39:54 -07:00
Zachary Boyd
fadde58af2 Runs tor router as an unprivileged user 2018-05-07 22:56:48 -07:00
Zachary Boyd
8ccbd3a1f7
Merge pull request #5 from jogli5er/master
Fix issue for dynamically created Tor instances
2018-05-07 08:23:50 -07:00
Roman Brunner
4ccc9e0e8f Fix issue for dynamically created Tor instances
The root cause was that the "this" binding was missing
for the calls that dynamically create the tor instances
and update the references within the _instances array.
[#4]
2018-05-07 09:50:52 +02:00
Zachary Boyd
cb02b756ff Updates the README to reflect some of the changes 2018-05-06 22:55:10 -07:00
Zachary Boyd
f2abcbb429 Makes the log entires that describe network traffic "verbose" instead of "debug". Sets the default logLevel to "info". Fixes a bug with setting the logLevel via the command line 2018-05-06 22:54:21 -07:00
Zachary Boyd
3b91c9a5b5 Add package-lock.json 2018-05-06 22:41:13 -07:00
Zachary Boyd
3c3449eab1 Upgrades to the latest version of "winston" (logging library) 2018-05-06 22:40:49 -07:00
Zachary Boyd
c08ef63867 Replaces 'new Buffer' with 'Buffer.from' 2018-05-06 09:29:18 -07:00
Zachary Boyd
8275a462dc Upgrade the version of ubuntu in the dockerfile 2018-05-06 09:26:49 -07:00
Zachary Boyd
ddaa185d17 fixes bug in control server 2017-12-07 22:35:13 -08:00
Zachary Boyd
f8698b1541 Uses JSON-RPC 2 for the control server. Removes socket.io 2017-12-07 18:53:16 -08:00
Zachary Boyd
e4f081304e remove docker compose file 2017-12-07 17:58:00 -08:00
Zachary Boyd
9a3091504a remove env variables from dockerfile 2017-12-07 17:57:44 -08:00
Zachary Boyd
54fa83bdf5 Adds HTTP Connect proxy 2017-12-07 17:52:10 -08:00
Zachary Boyd
4620732805 finished http proxy 2017-12-07 17:21:42 -08:00
Zachary Boyd
cecdd2d9fd uses entrypoint and cmd in docker file 2017-12-07 15:50:37 -08:00
Zachary Boyd
b7af228e94 remove bitbucket pipelines 2017-12-07 15:48:18 -08:00
Zachary Boyd
f081dbd2fd adds query instances function 2017-11-12 14:54:47 -08:00
Zachary Boyd
de6ce4b936 grabs tor repository public key in dockerfile 2017-11-12 14:49:19 -08:00
Zachary Boyd
737c052ac4 increases package version 2017-11-12 14:16:02 -08:00
Zachary Boyd
3577ea7fee upgrades npm packages 2017-11-12 14:15:40 -08:00
Zachary Boyd
d98a945620 updates ubuntu version to 17.10 in dockerfile. uses nodejs 8. 2017-11-12 14:15:06 -08:00
Zachary Boyd
67e3d9ba32 removed reference to missing env file 2017-11-12 14:07:42 -08:00
Zachary Boyd
840f889693 Updates package version 2017-07-25 08:49:05 -07:00
Zachary Boyd
ce0be3c1f5 Fixes issue with file descriptor on OSX 2017-07-25 08:41:57 -07:00
Zachary Boyd
cb49f4d08e fixes error with callback 2017-04-05 23:41:41 -04:00
Zachary Boyd
2aed4257df better handling of docker cache 2017-04-05 20:58:31 -04:00
Zachary Boyd
87a8479a53 fixes bug 2017-04-05 20:54:37 -04:00
Zachary Boyd
2bae741aeb fixes bug 2017-04-05 20:54:31 -04:00
Zachary Boyd
2144ebf4b0 fixes bug 2017-04-05 20:53:57 -04:00
Zachary Boyd
0237d1dc3f adds the apache licence 2017-03-25 23:55:07 -04:00
Zachary Boyd
5dc6bf92cf adds the apache licence 2017-03-25 23:54:41 -04:00
Zachary Boyd
25c3b0b8e1 fixes typo 2017-03-25 21:16:10 -04:00
Zachary Boyd
1cc2a57394 starts filling the buffer before a tor instance is live 2017-03-25 21:14:46 -04:00
Zachary Boyd
a8841a2f40 waits for a tor instance to come online before making a connection 2017-03-25 21:05:24 -04:00
Zachary Boyd
7d0aba7d60 waits for a tor instance to come online before making a connection 2017-03-25 21:02:56 -04:00
Zachary Boyd
fd1c08ff16 adds line about global installation 2017-03-24 20:36:23 -04:00
82 changed files with 32950 additions and 680 deletions

View file

@ -1,8 +1,9 @@
.git
node_modules
Dockerfile
yarn.lock
npm-debug.log
docker-compose.yml
.env
README.md
.vscode
.DS_Store
docs

8
.gitignore vendored
View file

@ -1,4 +1,6 @@
node_modules
yarn.lock
npm-debug.log
.env
*.log
.env
.vscode
.DS_Store

View file

@ -1,4 +1,8 @@
node_modules
yarn.lock
npm-debug.log
.env
.env
.vscode
.DS_Store
docs
test
Gruntfile.js

197
CHANGELOG.md Normal file
View file

@ -0,0 +1,197 @@
# Changelog
## [4.0.13] - 2019-11-23
### Added
- Added a mechanism to handle dynamically allocating numerous ports at the same time without collisions per [jogli5er's](https://github.com/jogli5er) suggestion in [PR 15](https://zb.gy/3a).
### Changed
- Switched 'granax' package to '@deadcanaries/granax' as suggested in [issue 12](https://zb.gy/2z).
## [4.0.12] - 2019-11-21
### Added
- Added a entrypoint file so the node.js process is not the root process, so it responds to SIGINT calls (Ctrl+C).
## [4.0.11] - 2019-01-15
### Changed
- Updates `multi-rpc` to version 1.5.5.
## [4.0.10] - 2018-12-14
### Changed
- Updates `multi-rpc` to version 1.4.1.
## [4.0.9] - 2018-12-14
### Changed
- Updates `multi-rpc` to version 1.4.0.
## [4.0.7] - 2018-12-14
### Changed
- Updates `multi-rpc` to version 1.1.9.
## [4.0.6] - 2018-12-14
### Changed
- Updates `multi-rpc` to version 1.1.1.
## [4.0.5] - 2018-10-15
### Changed
- Prevents errors that occur when connecting to the destination in the SOCKS Proxy and HTTP Proxy from crashing the applications
## [4.0.4] - 2018-09-24
### Changed
- Replaces `jrpc2` with `multi-rpc` for providing the RPC Interface. No changes to the application or API
## [4.0.3] - 2018-09-15
### Changed
- References granax in `default_config.js` to comply with licensing requirements
## [4.0.2] - 2018-09-15
### Added
- Adds API documentation. To generate run `npm run docs` and open under `docs/index.html`
### Changed
- Much of the README has been moved to [the wiki](https://github.com/znetstar/tor-router/wiki)
- Updates granax to 3.1.4 which fixes a bug on MacOS
- The constructor on `ControlServer` now takes an nconf instance as the first argument and a logger as the second
## [4.0.1] - 2018-09-11
## [4.0.0] - 2018-09-09
### Added
- Instances can now added to one or more groups by setting the `Group` field in the instance definition to a single string or array
- You can now proxy through a specific instance using the username field when connecting to a proxy by setting `--proxyByName` or `-n` to "individual" or true. For example: to connect to an instance named `instance-1` via http use `http://instance-1:@localhost:9080`
- You can also connect to a specific group of instances by setting `--proxyByName` or `-n` to "group". If enabled, requests made to `://foo:@localhost:9080` would be routed to instances in the `foo` group in round-robin fashion
- The control server will accept WebSocket connections if the `--websocketControlHost` or `-w` argument is set. If the argument is used without a hostname it will default to 9078 on all interfaces
- All servers (DNS, HTTP, SOCKS and Control) all have a `listen` method which takes a port and optionally a host. It will return a Promise that will resolve when the server is listening
- Application configuration can be changed at runtime using the `getConfig` and `setConfig` RPC methods
- Application configuration can be saved and loaded from disk using the `saveConfig` and `loadConfig` RPC methods
### Changes
- All "Port" config options (e.g. socksPort) have been replaced with "Host", and can take a full host (e.g. 127.0.0.1:9050) for its value. This allows you to bind Tor Router to a specific hostname. If just a port is given it will bind to all interfaces
- All methods now return promises instead of accepting callbacks
- The `logger` argument to the constructor of all classes is now optional
- The `Config` property of instance definitions will now inherit all properties from `TorPool.default_tor_config`
- The mocha test has been split into individual files all under `test/`
- DNS shows the source/destination hostname/port in logs instead of what the query was resolved to
- `TorProcess` takes an instance definition as the second argument in its constructor
### Removes
- The `new_ips` and `new_ip_at` TorPool and `new_ip` TorProcess have been removed. Use `new_identites`, `new_identity_at` and `new_identity` instead.
- The `getDefaultTorConfig` and `setDefaultTorConfig` RPC methods have removed. Use `getConfig('torConfig')` and `setConfig('torConfig', value)` instead.
## [3.4.3] - 2018-08-10
### Added
- Adds a changelog
- Adds `queryInstanceByName` and `queryInstanceAt` RPC methods to retrieve individual instances
### Changes
- Makes changes to ensure compatibility on Windows
## [3.4.2] - 2018-08-09
### Added
- Test suites for `DNSServer`, `HTTPServer`, `SOCKSServer` and `ControlServer`
- Added `TorPool.set_config_all` method to change configuration of all active instances
### Changed
- Cleans up test suites for `TorPool` and `TorProcess`
- Removes potential security vulnerability
- `setTorConfig` rpc method will now change the configuration of active instances, `setDefaultTorConfig` and `getDefaultTorConfig` will set or get the configuration of future instances
- The default Tor Configuration will be applied to instances when the `Config` property on the instance definition is not set
## [3.4.1] - 2018-07-19
### Changed
- Fixes bug with the application not binding to port numbers specified on the command line
## [3.4.0] - 2018-05-11
### Added
- Bundles the Tor executable with the application. Tor will be downloaded during `npm install`
- Signals and Configuration changes can be sent to live Tor instances via the Tor Control Protocol. Serveral RPC and `TorPool` methods have been added.
### Changed
- By default Tor Router will use the Tor executable bundled with the application, to override use the `TOR_PATH` environment variable
- Deprecates the `TorPool.new_ips` and `TorProcess.new_ip` functions use `TorPool.new_identites`cand `TorProcess.new_identity` function respectively.
## [3.3.0] - 2018-05-10
### Added
- Adds documentation on all available RPC Methods
- Allows different load-balance methods to be defined, and changed at runtime and via RPC
- Each instance can have started with a specific configuration (torrc) by setting the `Config` property in the definition
### Changed
- If the `Name` property in the definition was not set the data directory will be deleted when the Tor Process exits
- Switches from "commander" to "nconf"/"yargs" for command line processing, switches however will remain the same
## [3.2.2] - 2018-05-08
### Changed
- Tor Router and child Tor processes are run as an unprivilieged user in the Docker container
- Fixes [Issue #4](https://github.com/znetstar/tor-router/issues/4) which affects dynamically created Tor Instances
- Network traffic is only logged when `logLevel` is set to "verbose"
## [3.2.1] - 2017-12-08
### Changed
- Fixes typo bug in Control Server
## [3.2.0] - 2017-12-07
### Changed
- Replaced socket.io with JSON-RPC 2 as the RPC protocol
- The Dockerfile includes an `ENTRYPOINT` for Tor Router
## [3.1.0] - 2017-12-07
### Added
- Adds an HTTP Proxy Server `HTTPServer` which can HTTP-Connect requests (HTTPS traffic).
- Adds a `queryInstances` RPC function as requested in [Issue #3](https://github.com/znetstar/tor-router/issues/3)
### Removed
- Removes the "docker-compose.yml" file
## [3.0.7] - 2017-11-12
### Changes
- The Dockerfile will now use Node.js version 8
## [3.0.6] - 2017-07-25
### Changes
- Fixes a bug that occures on OS X
## [3.0.5] - 2017-04-05
### Changes
- Fixes a bug with an RPC Method
## [3.0.4] - 2017-04-05
### Changes
- Changes the arguments tor-router will be launched with when run using npm start
## [3.0.3] - 2017-03-25
### Added
- Adds the Apache Licence Version 2 as the project's licence
## [3.0.2] - 2017-03-25
### Changed
- Data sent in a request before a Tor instance comes online is held in a buffer.
## [3.0.1] - 2017-03-25
### Added
- In `SOCKSServer` waits for a Tor instance in the pool to come online before making a connection.
## [3.0.0] - 2017-03-24
### Changed
- Rewrites the application as a proxy server written as a node.js app.

View file

@ -1,35 +1,35 @@
FROM ubuntu:16.10
EXPOSE 9050
EXPOSE 53
EXPOSE 9077
ENV DNS_PORT 53
ENV SOCKS_PORT 9050
ENV CONTROL_PORT 9077
ENV PATH $PATH:/app/bin
ADD http://public.zacharyboyd.nyc/columbia-ubuntu-sources.list /etc/apt/sources.list
ADD tor-sources.list /etc/apt/sources.list.d/tor.list
ADD https://deb.nodesource.com/setup_6.x /tmp/nodejs_install
RUN bash /tmp/nodejs_install
RUN apt install -y --allow-unauthenticated deb.torproject.org-keyring nodejs tor git
ADD . /app
FROM node:10-jessie
WORKDIR /app
RUN npm install
ENV PARENT_DATA_DIRECTORTY /var/lib/tor-router
# Grab the current local timezone from an external api and save it into /etc/timezone, otherwise Tor will complain and won't start
ENV TOR_PATH /usr/bin/tor
CMD bash /app/bin/get-timezone.sh > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata && npm start
ENV NODE_ENV production
ENV PATH $PATH:/app/bin
RUN apt-get update && apt-get install -y tor && rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash tor_router
RUN chown -hR tor_router:tor_router /app
USER tor_router
ADD package.json /app/package.json
ADD package-lock.json /app/package-lock.json
RUN npm ci
ADD . /app
ENV HOME /home/tor_router
EXPOSE 9050 9053 9077
ENTRYPOINT [ "/bin/bash", "/app/docker-entrypoint.sh" ]
CMD [ "-s", "-d", "-j", "1" ]

20
Gruntfile.js Normal file
View file

@ -0,0 +1,20 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jsdoc : {
dist : {
src: ['src/*.js', 'test/*.js', 'README.md'],
options: {
destination : 'docs',
template : "node_modules/docdash"
}
}
}
});
grunt.loadNpmTasks('grunt-jsdoc');
grunt.registerTask('default', []);
grunt.registerTask('docs', ['jsdoc']);
};

201
LICENSE.txt Normal file
View file

@ -0,0 +1,201 @@
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 2018 Zachary Boyd
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.

View file

@ -1,52 +1,58 @@
# Tor Router
*Tor Router* is a simple SOCKS5 forward proxy for distributing traffic across multiple instances of Tor. At startup Tor Router will run an arbitrary number of instances Tor an each request will be sent to a different instance in round-robin fashion. This can be used to increase anonymity, because each request will be sent on a different circut and will most likely use a different exit-node, and also to increase performance since outbound traffic is now split across several instances of Tor.
[![NPM](https://nodei.co/npm/tor-router.png)](https://nodei.co/npm/tor-router/)
Tor Router also includes a DNS forward proxy as well, which like the SOCKS proxy will distribute traffic across multiple instances of Tor in round-robin fashion.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fznetstar%2Ftor-router.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fznetstar%2Ftor-router?ref=badge_shield)
*Tor Router* is a SOCKS5, DNS and HTTP proxy server for distributing traffic across multiple instances of Tor. At startup Tor Router will run an arbitrary number of instances Tor and each request will be sent to a different instance in round-robin fashion. This can be used to increase anonymity, because each request will be sent on a different circuit and will most likely use a different exit-node, and also to increase performance since outbound traffic is now split across several instances of Tor.
A list of changes can be [found here](https://github.com/znetstar/tor-router/blob/master/CHANGELOG.md).
## Building and Running
Installation requirements are node.js and tor. Make sure "tor" is in your PATH.
The only installation requirement is node.js. Tor is bundled with the application. To use an external Tor executable use the `--torPath` command line switch or set the `TOR_PATH` environment variable.
To install run: `npm install`
To start run: `bin/tor-router`
To install globally run: `npm install -g tor-router`
Alternatively docker can be used. The build will retrieve the latest version of Tor from the offical Tor Project repository.
To build run: `docker build -t znetstar/tor-router .`
To start run: `docker run --rm -it -p 9050:9050 znetstar/tor-router tor-router --help`
To start run: `docker run --rm -it -p 9050:9050 znetstar/tor-router`
## Usage
The following command line switches and their environment variable equivalents are available for use:
If just a port number is passed in place of a host, it will bind to all interfaces.
|Command line switch|Environment Variable|Description|
|-------------------|--------------------|-----------|
|-c, --controlPort |CONTROL_PORT |Port the control server will bind to (see below)|
|-j, --instances |INSTANCES |Number of Tor instances to spawn|
|-s, --socksPort |SOCKS_PORT |Port the SOCKS proxy will bind to|
|-d, --dnsPort |DNS_PORT |Port the DNS proxy will bind to|
|-l, --logLevel |LOG_LEVEL |The log level, "info" by default. Set to "null" to disable logging|
|---------------------------|--------------------|-----------|
|-f, --config | |Path to a JSON configuration file to use|
|-c, --controlHost |CONTROL_HOST |Host the control server will bind to and listen for TCP traffic (see below)|
|-w, --websocketControlHost |WEBSOCKET_CONTROL_HOST |Host the control server will bind to and listen for WebSocket traffic|
|-j, --instances |INSTANCES |Number of Tor instances to spawn|
|-s, --socksHost |SOCKS_HOST |Host the SOCKS proxy will bind to|
|-d, --dnsHost |DNS_HOST |Host the DNS proxy will bind to|
|-h, --httpHost |HTTP_HOST |Host the HTTP proxy will bind to|
|-l, --logLevel |LOG_LEVEL |Log level (defaults to "info") set to "null" to disable logging. To see a log of all network traffic set logLevel to "verbose"|
|-p, --parentDataDirectory |PARENT_DATA_DIRECTORY|Parent directory that will contain the data directories for the instances|
|-b, --loadBalanceMethod |LOAD_BALANCE_METHOD |Method that will be used to sort the instances between each request. Currently supports "round_robin" and "weighted". |
|-t, --torPath |TOR_PATH |Provide the path for the Tor executable that will be used|
|-n, --proxyByName |PROXY_BY_NAME |Controls how authenticated requests will be handled. Can be set to "individual", "group" or false to disable|
A full list of all available configuration options and their defaults can be found in [default_config.js](https://github.com/znetstar/tor-router/blob/master/src/default_config.js)
For example: `tor-router -j 3 -s 9050` would start the proxy with 3 tor instances and listen for SOCKS connections on 9050.
For example: `tor-router -j 3 -s 127.0.0.1:9050` would start the proxy with 3 tor instances and listen for SOCKS connections on localhost:9050.
## Control Server
## Documentation
A socket.io server included will listen on port 9077 by default. Using the socket.io server the client can add/remove Tor instances and get a new identity (which includes a new ip address) while Tor Router is running.
For detailed examples and insturctions on using Tor Router [see the wiki](https://github.com/znetstar/tor-router/wiki).
Example (in node):
Documentation is available in `docs/`. An online version of the documentation is also [available here](https://tor-router.docs.zacharyboyd.nyc/).
```
var client = require('socket.io-client').connect('ws://localhost:9077');
client.emit('createInstances', 3, (error) => {
if (error) return;
console.log('three instances created!');
client.emit('newIps');
console.log('clients have new ips!')
});
```
## Testing
## Test
Tests are written in mocha, just run `npm test`
Tests are written in mocha and can be found under `test/` and can be run with `npm test`

View file

@ -1 +0,0 @@
curl -sL http://ip-api.com/json | node -e "process.stdin.resume(); process.stdin.on('data', (data) => { process.stdout.write(JSON.parse(data.toString('utf8')).timezone); process.exit(0); });"

View file

@ -1,60 +1,5 @@
#!/usr/bin/env node
var program = require('commander');
var TorRouter = require('../');
var SOCKSServer = TorRouter.SOCKSServer;
var DNSServer = TorRouter.DNSServer;
var TorPool = TorRouter.TorPool;
var ControlServer = TorRouter.ControlServer;
var winston = require('winston')
let { nconf, logger, main } = require('../src/launch');
process.title = 'tor-router';
program
.version(JSON.parse(require('fs').readFileSync(`${__dirname}/../package.json`, 'utf8')).version)
.option('-c, --controlPort [9077]', 'Control Server port', Number)
.option('-j, --instances <1>', 'Number of tor instances', Number)
.option('-s, --socksPort [9050]', 'SOCKS Server port', Number)
.option('-d, --dnsPort [9053]', 'DNS Server port', Number)
.option('-l, --logLevel [info]', 'Log level (defaults to "info") set to "null" to disable logging', Number)
.parse(process.argv);
var logger = new (winston.Logger)({
transports: [
new (winston.transports.Console)({ level: (program.logLevel || 'info') })
]
});
let instances = program.instances || Number(process.env.INSTANCES);
let log_level = program.logLevel || process.env.LOG_LEVEL;
let socks_port = (program.socksPort === true ? 9050 : program.socksPort) || Number(process.env.SOCKS_PORT);
let dns_port = (program.dnsPort === true ? 9053 : program.dnsPort) || Number(process.env.DNS_PORT);
let control_port = (program.controlPort === true ? 9077 : program.controlPort) || Number(process.env.CONTROL_PORT) || 9077;
if (log_level === 'null')
logger = void(0);
let control = new ControlServer(logger);
process.on('SIGHUP', () => {
control.torPool.new_ips();
});
if (socks_port) {
let socks = control.createSOCKSServer(socks_port);
}
if (dns_port) {
let dns = control.createDNSServer(dns_port);
}
if (instances) {
logger && logger.info(`[tor]: starting ${instances} tor instances...`)
control.torPool.create(instances, (err) => {
logger && logger.info('[tor]: tor started');
});
}
control.server.listen(control_port, () => {
logger && logger.info(`[control]: Control Server listening on ${control_port}`);
})
main(nconf, logger);

View file

@ -1,12 +0,0 @@
image: ubuntu:16.10
pipelines:
default:
- step:
script:
- apt update && apt install -y curl tor git
- curl -sL https://deb.nodesource.com/setup_6.x > /tmp/node_install
- bash /tmp/node_install && apt install -y nodejs
- npm install
- bash /app/bin/get-timezone.sh > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata
- npm test

View file

@ -1,9 +0,0 @@
version: '2'
services:
tor_router:
env_file: .env
build: .
ports:
- "9050:9050"
- "53:53/udp"
- "9077:9077"

3
docker-entrypoint.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/bash
/app/bin/tor-router $@

1
docs/CNAME Normal file
View file

@ -0,0 +1 @@
tor-router.gh.zb.gy

2400
docs/ControlServer.html Normal file

File diff suppressed because one or more lines are too long

506
docs/ControlServer.js.html Normal file

File diff suppressed because one or more lines are too long

947
docs/DNSServer.html Normal file

File diff suppressed because one or more lines are too long

201
docs/DNSServer.js.html Normal file

File diff suppressed because one or more lines are too long

916
docs/HTTPServer.html Normal file

File diff suppressed because one or more lines are too long

407
docs/HTTPServer.js.html Normal file

File diff suppressed because one or more lines are too long

1076
docs/SOCKSServer.html Normal file

File diff suppressed because one or more lines are too long

308
docs/SOCKSServer.js.html Normal file

File diff suppressed because one or more lines are too long

6771
docs/TorPool.html Normal file

File diff suppressed because one or more lines are too long

848
docs/TorPool.js.html Normal file

File diff suppressed because one or more lines are too long

2869
docs/TorProcess.html Normal file

File diff suppressed because one or more lines are too long

494
docs/TorProcess.js.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2105
docs/global.html Normal file

File diff suppressed because one or more lines are too long

148
docs/hex.html Normal file

File diff suppressed because one or more lines are too long

164
docs/index.html Normal file

File diff suppressed because one or more lines are too long

77
docs/index.js.html Normal file

File diff suppressed because one or more lines are too long

330
docs/launch.js.html Normal file

File diff suppressed because one or more lines are too long

178
docs/module-tor-router.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

128
docs/nconf_load_env.js.html Normal file

File diff suppressed because one or more lines are too long

150
docs/rpcMethods.html Normal file

File diff suppressed because one or more lines are too long

11
docs/scripts/collapse.js Normal file
View file

@ -0,0 +1,11 @@
function hideAllButCurrent(){
//by default all submenut items are hidden
$("nav > ul > li > ul li").hide();
//only current page (if it exists) should be opened
var file = window.location.pathname.split("/").pop();
$("nav > ul > li > a[href^='"+file+"']").parent().find("> ul li").show();
}
$( document ).ready(function() {
hideAllButCurrent();
});

4
docs/scripts/jquery-3.1.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
/*global document */
(function() {
var source = document.getElementsByClassName('prettyprint source linenums');
var i = 0;
var lineNumber = 0;
var lineId;
var lines;
var totalLines;
var anchorHash;
if (source && source[0]) {
anchorHash = document.location.hash.substring(1);
lines = source[0].getElementsByTagName('li');
totalLines = lines.length;
for (; i < totalLines; i++) {
lineNumber++;
lineId = 'line' + lineNumber;
lines[i].id = lineId;
if (lineId === anchorHash) {
lines[i].className += ' selected';
}
}
}
})();

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.

View file

@ -0,0 +1,2 @@
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com",
/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);

View file

@ -0,0 +1,28 @@
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();

42
docs/scripts/search.js Normal file
View file

@ -0,0 +1,42 @@
$( document ).ready(function() {
jQuery.expr[':'].Contains = function(a,i,m){
return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0;
};
//on search
$("#nav-search").on("keyup", function(event) {
var search = $(this).val();
if (!search) {
//no search, show all results
$("nav > ul > li").show();
if(typeof hideAllButCurrent === "function"){
//let's do what ever collapse wants to do
hideAllButCurrent();
}
else{
//menu by default should be opened
$("nav > ul > li > ul li").show();
}
}
else{
//we are searching
//show all parents
$("nav > ul > li").show();
//hide all results
$("nav > ul > li > ul li").hide();
//show results matching filter
$("nav > ul > li > ul").find("a:Contains("+search+")").parent().show();
//hide parents without children
$("nav > ul > li").each(function(){
if($(this).find("a:Contains("+search+")").length == 0 && $(this).children("ul").length === 0){
//has no child at all and does not contain text
$(this).hide();
}
else if($(this).find("a:Contains("+search+")").length == 0 && $(this).find("ul").children(':visible').length == 0){
//has no visible child and does not contain text
$(this).hide();
}
});
}
});
});

654
docs/styles/jsdoc.css Normal file
View file

@ -0,0 +1,654 @@
@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700);
* {
box-sizing: border-box
}
html, body {
height: 100%;
width: 100%;
}
body {
color: #4d4e53;
background-color: white;
margin: 0 auto;
padding: 0 20px;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: 16px;
line-height: 160%;
}
img {
max-width: 100%;
}
a,
a:active {
color: #606;
text-decoration: none;
}
a:hover {
text-decoration: none;
}
article a {
border-bottom: 1px solid #ddd;
}
article a:hover, article a:active {
border-bottom-color: #222;
}
p, ul, ol, blockquote {
margin-bottom: 1em;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
color: #000;
font-weight: 400;
margin: 0;
}
h1 {
font-weight: 300;
font-size: 48px;
margin: 1em 0 .5em;
}
h1.page-title {
font-size: 48px;
margin: 1em 30px;
line-height: 100%;
}
h2 {
font-size: 24px;
margin: 1.5em 0 .3em;
}
h3 {
font-size: 24px;
margin: 1.2em 0 .3em;
}
h4 {
font-size: 18px;
margin: 1em 0 .2em;
color: #4d4e53;
}
h4.name {
color: #fff;
background: #6d426d;
box-shadow: 0 .25em .5em #d3d3d3;
border-top: 1px solid #d3d3d3;
border-bottom: 1px solid #d3d3d3;
margin: 1.5em 0 0.5em;
padding: .75em 0 .75em 10px;
}
h4.name a {
color: #fc83ff;
}
h4.name a:hover {
border-bottom-color: #fc83ff;
}
h5, .container-overview .subsection-title {
font-size: 120%;
letter-spacing: -0.01em;
margin: 8px 0 3px 0;
}
h6 {
font-size: 100%;
letter-spacing: -0.01em;
margin: 6px 0 3px 0;
font-style: italic;
}
tt, code, kbd, samp {
font-family: Consolas, Monaco, 'Andale Mono', monospace;
background: #f4f4f4;
padding: 1px 5px;
}
.class-description {
font-size: 130%;
line-height: 140%;
margin-bottom: 1em;
margin-top: 1em;
}
.class-description:empty {
margin: 0
}
#main {
float: right;
min-width: 360px;
width: calc(100% - 240px);
}
header {
display: block
}
section {
display: block;
background-color: #fff;
padding: 0 0 0 30px;
}
.variation {
display: none
}
.signature-attributes {
font-size: 60%;
color: #eee;
font-style: italic;
font-weight: lighter;
}
nav {
float: left;
display: block;
width: 250px;
background: #fff;
overflow: auto;
position: fixed;
height: 100%;
}
nav #nav-search{
width: 210px;
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
margin-right: 20px;
margin-top: 20px;
}
nav h3 {
margin-top: 12px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 700;
line-height: 24px;
margin: 15px 0 10px;
padding: 0;
color: #000;
}
nav ul {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
font-size: 100%;
line-height: 17px;
padding: 0;
margin: 0;
list-style-type: none;
}
nav ul a,
nav ul a:active {
font-family: 'Montserrat', sans-serif;
line-height: 18px;
padding: 0;
display: block;
font-size: 12px;
}
nav a:hover,
nav a:active {
color: #606;
}
nav > ul {
padding: 0 10px;
}
nav > ul > li > a {
color: #606;
margin-top: 10px;
}
nav ul ul a {
color: hsl(207, 1%, 60%);
border-left: 1px solid hsl(207, 10%, 86%);
}
nav ul ul a,
nav ul ul a:active {
padding-left: 20px
}
nav h2 {
font-size: 12px;
margin: 0;
padding: 0;
}
nav > h2 > a {
display: block;
margin: 10px 0 -10px;
color: #606 !important;
}
footer {
color: hsl(0, 0%, 28%);
margin-left: 250px;
display: block;
padding: 15px;
font-style: italic;
font-size: 90%;
}
.ancestors {
color: #999
}
.ancestors a {
color: #999 !important;
}
.clear {
clear: both
}
.important {
font-weight: bold;
color: #950B02;
}
.yes-def {
text-indent: -1000px
}
.type-signature {
color: #CA79CA
}
.type-signature:last-child {
color: #eee;
}
.name, .signature {
font-family: Consolas, Monaco, 'Andale Mono', monospace
}
.signature {
color: #fc83ff;
}
.details {
margin-top: 6px;
border-left: 2px solid #DDD;
line-height: 20px;
font-size: 14px;
}
.details dt {
width: auto;
float: left;
padding-left: 10px;
}
.details dd {
margin-left: 70px;
margin-top: 6px;
margin-bottom: 6px;
}
.details ul {
margin: 0
}
.details ul {
list-style-type: none
}
.details pre.prettyprint {
margin: 0
}
.details .object-value {
padding-top: 0
}
.description {
margin-bottom: 1em;
margin-top: 1em;
}
.code-caption {
font-style: italic;
font-size: 107%;
margin: 0;
}
.prettyprint {
font-size: 14px;
overflow: auto;
}
.prettyprint.source {
width: inherit;
line-height: 18px;
display: block;
background-color: #0d152a;
color: #aeaeae;
}
.prettyprint code {
line-height: 18px;
display: block;
background-color: #0d152a;
color: #4D4E53;
}
.prettyprint > code {
padding: 15px;
}
.prettyprint .linenums code {
padding: 0 15px
}
.prettyprint .linenums li:first-of-type code {
padding-top: 15px
}
.prettyprint code span.line {
display: inline-block
}
.prettyprint.linenums {
padding-left: 70px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.prettyprint.linenums ol {
padding-left: 0
}
.prettyprint.linenums li {
border-left: 3px #34446B solid;
}
.prettyprint.linenums li.selected, .prettyprint.linenums li.selected * {
background-color: #34446B;
}
.prettyprint.linenums li * {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
table {
border-spacing: 0;
border: 1px solid #ddd;
border-collapse: collapse;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
width: 100%;
font-size: 14px;
margin: 1em 0;
}
td, th {
margin: 0px;
text-align: left;
vertical-align: top;
padding: 10px;
display: table-cell;
}
thead tr, thead tr {
background-color: #fff;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.params .type {
white-space: nowrap;
}
.params code {
white-space: pre;
}
.params td, .params .name, .props .name, .name code {
color: #4D4E53;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
font-size: 100%;
}
.params td {
border-top: 1px solid #eee
}
.params td.description > p:first-child, .props td.description > p:first-child {
margin-top: 0;
padding-top: 0;
}
.params td.description > p:last-child, .props td.description > p:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
span.param-type, .params td .param-type, .param-type dd {
color: #606;
font-family: Consolas, Monaco, 'Andale Mono', monospace
}
.param-type dt, .param-type dd {
display: inline-block
}
.param-type {
margin: 14px 0;
}
.disabled {
color: #454545
}
/* navicon button */
.navicon-button {
display: none;
position: relative;
padding: 2.0625rem 1.5rem;
transition: 0.25s;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: .8;
}
.navicon-button .navicon:before, .navicon-button .navicon:after {
transition: 0.25s;
}
.navicon-button:hover {
transition: 0.5s;
opacity: 1;
}
.navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after {
transition: 0.25s;
}
.navicon-button:hover .navicon:before {
top: .825rem;
}
.navicon-button:hover .navicon:after {
top: -.825rem;
}
/* navicon */
.navicon {
position: relative;
width: 2.5em;
height: .3125rem;
background: #000;
transition: 0.3s;
border-radius: 2.5rem;
}
.navicon:before, .navicon:after {
display: block;
content: "";
height: .3125rem;
width: 2.5rem;
background: #000;
position: absolute;
z-index: -1;
transition: 0.3s 0.25s;
border-radius: 1rem;
}
.navicon:before {
top: .625rem;
}
.navicon:after {
top: -.625rem;
}
/* open */
.nav-trigger:checked + label:not(.steps) .navicon:before,
.nav-trigger:checked + label:not(.steps) .navicon:after {
top: 0 !important;
}
.nav-trigger:checked + label .navicon:before,
.nav-trigger:checked + label .navicon:after {
transition: 0.5s;
}
/* Minus */
.nav-trigger:checked + label {
-webkit-transform: scale(0.75);
transform: scale(0.75);
}
/* × and + */
.nav-trigger:checked + label.plus .navicon,
.nav-trigger:checked + label.x .navicon {
background: transparent;
}
.nav-trigger:checked + label.plus .navicon:before,
.nav-trigger:checked + label.x .navicon:before {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
background: #FFF;
}
.nav-trigger:checked + label.plus .navicon:after,
.nav-trigger:checked + label.x .navicon:after {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFF;
}
.nav-trigger:checked + label.plus {
-webkit-transform: scale(0.75) rotate(45deg);
transform: scale(0.75) rotate(45deg);
}
.nav-trigger:checked ~ nav {
left: 0 !important;
}
.nav-trigger:checked ~ .overlay {
display: block;
}
.nav-trigger {
position: fixed;
top: 0;
clip: rect(0, 0, 0, 0);
}
.overlay {
display: none;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background: hsla(0, 0%, 0%, 0.5);
z-index: 1;
}
@media only screen and (min-width: 320px) and (max-width: 680px) {
body {
overflow-x: hidden;
}
nav {
background: #FFF;
width: 250px;
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: -250px;
z-index: 3;
padding: 0 10px;
transition: left 0.2s;
}
.navicon-button {
display: inline-block;
position: fixed;
top: 1.5em;
right: 0;
z-index: 2;
}
#main {
width: 100%;
min-width: 360px;
}
#main h1.page-title {
margin: 1em 0;
}
#main section {
padding: 0;
}
footer {
margin-left: 0;
}
}
/** Add a '#' to static members */
[data-type="member"] a::before {
content: '#';
display: inline-block;
margin-left: -14px;
margin-right: 5px;
}
#disqus_thread{
margin-left: 30px;
}

79
docs/styles/prettify.css Normal file
View file

@ -0,0 +1,79 @@
.pln {
color: #ddd;
}
/* string content */
.str {
color: #61ce3c;
}
/* a keyword */
.kwd {
color: #fbde2d;
}
/* a comment */
.com {
color: #aeaeae;
}
/* a type name */
.typ {
color: #8da6ce;
}
/* a literal value */
.lit {
color: #fbde2d;
}
/* punctuation */
.pun {
color: #ddd;
}
/* lisp open bracket */
.opn {
color: #000000;
}
/* lisp close bracket */
.clo {
color: #000000;
}
/* a markup tag name */
.tag {
color: #8da6ce;
}
/* a markup attribute name */
.atn {
color: #fbde2d;
}
/* a markup attribute value */
.atv {
color: #ddd;
}
/* a declaration */
.dec {
color: #EF5050;
}
/* a variable name */
.var {
color: #c82829;
}
/* a function name */
.fun {
color: #4271ae;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums {
margin-top: 0;
margin-bottom: 0;
}

File diff suppressed because one or more lines are too long

3068
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,53 @@
{
"name": "tor-router",
"version": "3.0.0",
"version": "4.0.13",
"main": "src/index.js",
"repository": "git@github.com:znetstar/tor-router.git",
"author": "Zachary Boyd <zachary@zacharyboyd.nyc>",
"license": "MIT",
"description": "A SOCKS proxy for distributing traffic across multiple instances of Tor",
"license": "Apache-2.0",
"description": "A SOCKS, HTTP and DNS proxy for distributing traffic across multiple instances of Tor",
"bin": {
"tor-router": "bin/tor-router"
},
"keywords": [
"tor"
],
"scripts": {
"start": "bin/tor-router -l debug",
"test": "mocha test/test.js"
"start": "bin/tor-router -s -d -j 1",
"test": "mocha -u tdd --exit test/index.js",
"debug": "node --inspect-brk bin/tor-router",
"docs": "grunt docs"
},
"devDependencies": {
"mocha": "^3.2.0",
"request": "^2.79.0",
"socket.io-client": "^1.7.3",
"socks5-http-client": "^1.0.2"
"chai": "^4.1.2",
"docdash": "^1.0.0",
"grunt": "^1.0.3",
"grunt-jsdoc": "^2.3.0",
"jsdoc": "^3.5.5",
"mocha": "^5.2.0",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"requestretry": "^2.0.2",
"socks-proxy-agent": "^4.0.1"
},
"dependencies": {
"async": "^2.1.4",
"commander": "^2.9.0",
"eventemitter2": "^3.0.0",
"@deadcanaries/granax": "^3.2.4",
"arrify": "^2.0.1",
"async-mutex": "^0.1.4",
"bluebird": "^3.5.2",
"del": "^3.0.0",
"eventemitter3": "^3.1.0",
"get-port": "^2.1.0",
"js-weighted-list": "^0.1.1",
"lodash": "^4.17.4",
"native-dns": "https://github.com/znetstar/node-dns.git",
"socket.io": "^1.7.3",
"socksv5": "git+https://github.com/lee-elenbaas/socksv5.git",
"multi-rpc": "^1.5.5",
"nanoid": "^1.2.3",
"native-dns": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb",
"nconf": "^0.10.0",
"shelljs": "^0.8.2",
"socksv5": "git+https://github.com/znetstar/socksv5.git#1480422215cf1464fa06f5aec4a3e7f2117e3403",
"temp": "^0.8.3",
"winston": "^2.3.1"
"winston": "^3.0.0-rc5",
"yargs": "^11.0.0"
}
}

View file

@ -1,56 +1,440 @@
const HTTPServer = require('http').Server;
const TorPool = require('./TorPool');
const { Server, WebSocketTransport, TCPTransport, JSONSerializer } = require("multi-rpc");
const Promise = require('bluebird');
const SOCKSServer = require('./SOCKSServer');
const HTTPServer = require('./HTTPServer');
const DNSServer = require('./DNSServer');
const TorPool = require('./TorPool');
const default_ports = require('./default_ports');
/**
* @typedef ControlServer~InstanceInfo
*
* @property {string} name - Name of the instance.
* @property {string[]} group - Groups the instance belongs to.
* @property {number} dns_port - Port Tor is listening on for DNS Traffic.
* @property {number} socks_port - Port Tor is listening on for SOCKS Traffic.
* @property {number} process_id - Process ID for the Tor process.
* @property {Object} config - Configuration (torrc) set when starting the process.
* @property {number} [weight] - Weight of the instance for weighted load balancing.
*/
/**
* A server which exposes an RPC interface that can control the application.
*/
class ControlServer {
constructor(logger) {
this.server = new HTTPServer();
this.io = require('socket.io')(this.server);
/**
*
* @param {Provider} nconf - Instance of nconf.Provider that will be used to obtain configuration values.
* @param {Logger} [logger] - Winston logger that will be used for logging. If not provided will disable logging.
*/
constructor(nconf, logger) {
/**
* Pool of Tor instances
*
* @type {TorPool}
*/
this.tor_pool = new TorPool(nconf.get('torPath'), (() => nconf.get('torConfig')), nconf.get('parentDataDirectory'), nconf.get('loadBalanceMethod'), nconf.get('granaxOptions'), logger);
/**
* Winston Logger for logging
*
* @type {Logger}
*/
this.logger = logger || require('./winston_silent_logger');
/**
* Nconf Provider for configuration
*
* @type {Provider}
*/
this.nconf = nconf;
this.torPool = new TorPool(null, null, logger);
this.logger = logger;
/**
* RPC Server instance
*
* @type {Server}
*/
let server = this.server = new Server();
this.io.use(this.handleConnection.bind(this));
this.serializer = new JSONSerializer();
this.server.methods.createTorPool = this.createTorPool.bind(this);
this.server.methods.createSocksServer = this.createSOCKSServer.bind(this);
this.server.methods.createDNSServer = this.createDNSServer.bind(this);
this.server.methods.createHTTPServer = this.createHTTPServer.bind(this);
/**
* Returns a list of all instances currently in the pool.
*/
this.server.methods.queryInstances = (() => {
return this.tor_pool.instances.map(ControlServer.instance_info);
}).bind(this);
/**
* Returns information on an instance identified by the {@link TorProcess#instance_name} field.
*/
this.server.methods.queryInstanceByName = ((instance_name) => {
let instance = this.tor_pool.instance_by_name(instance_name);
if (!instance)
throw new Error(`Instance "${instance_name}"" does not exist`);
return ControlServer.instance_info(instance);
}).bind(this);
/**
* Returns information on an instance identified by its index in the pool.
*/
this.server.methods.queryInstanceAt = ((index) => {
if (!this.tor_pool)
throw new Error('No pool created');
let instance = this.tor_pool.instance_at(index);
if (!instance)
throw new Error(`Instance at "${i}"" does not exist`);
return ControlServer.instance_info(this.tor_pool.instance_at(index));
}).bind(this);
/**
* Returns a list of the names of all of the instances in the pool.
*/
this.server.methods.queryInstanceNames = (() => this.tor_pool.instance_names).bind(this);
/**
* Returns a list of the names of all of the current groups.
*/
this.server.methods.queryGroupNames = (() => Array.from(this.tor_pool.group_names)).bind(this);
/**
* Returns a list of the instances that exist in a given group.
*/
this.server.methods.queryInstancesByGroup = ((group) => this.tor_pool.instances_by_group(group).map(ControlServer.instance_info)).bind(this);
/**
* Creates instances from a number, an array of instance definitions or a single instance definition.
* If a number is provided, creates n many instances.
*/
this.server.methods.createInstances = (async (instances_to_create) => {
let instances = await this.tor_pool.create(instances_to_create);
return instances.map(ControlServer.instance_info);
}).bind(this);
/**
* Creates instances from an array of instance definitions or a single instance definition.
*/
this.server.methods.addInstances = (async (defs) => {
let instances = await this.tor_pool.add(defs);
return instances.map(ControlServer.instance_info);
}).bind(this);
/**
* Removes a number of instances from the pool.
*/
this.server.methods.removeInstances = this.tor_pool.remove.bind(this.tor_pool);
/**
* Remove an instance at the index provided from the pool.
*/
this.server.methods.removeInstanceAt = this.tor_pool.remove_at.bind(this.tor_pool);
/**
* Remove an instance from the pool by the {@link TorProcess#instance_name} field.
*/
this.server.methods.removeInstanceByName = this.tor_pool.remove_by_name.bind(this.tor_pool);
/**
* Gets new identities for all instances in the pool.
*/
this.server.methods.newIdentites = this.tor_pool.new_identites.bind(this.tor_pool);
/**
* Get a new identity for the instance at the index provided in the pool.
*/
this.server.methods.newIdentityAt = this.tor_pool.new_identity_at.bind(this.tor_pool);
/**
* Get a new identity for the instance by the {@link TorProcess#instance_name} field.
*/
this.server.methods.newIdentityByName = this.tor_pool.new_identity_by_name.bind(this.tor_pool);
/**
* Gets new identities for all instances in the group.
*/
this.server.methods.newIdentitiesByGroup = this.tor_pool.new_identites_by_group.bind(this.tor_pool);
/**
* Gets the next instance in the pool using the load balance method.
*/
this.server.methods.nextInstance = (() => ControlServer.instance_info(this.tor_pool.next())).bind(this);
/**
* Gets the next instance in the group using the load balance method.
*/
this.server.methods.nextInstanceByGroup = ((group) => {
return ControlServer.instance_info(this.tor_pool.next_by_group(group));
}).bind(this);
/**
* Kills the processes of all instances in the pool.
*/
this.server.methods.closeInstances = this.tor_pool.exit.bind(this.tor_pool);
/**
* Sets a property in the application configuration.
*/
this.server.methods.setConfig = ((key, value) => {
this.nconf.set(key, value);
}).bind(this);
/**
* Gets a property in the application configuration.
*/
this.server.methods.getConfig = ((key) => {
return this.nconf.get(key);
}).bind(this);
/**
* Saves the application configuration to the underlying store (usually a JSON file).
*/
this.server.methods.saveConfig = (async () => {
await new Promise((resolve, reject) => {
this.nconf.save((err) => {
if (err) return reject(err);
resolve();
});
});
}).bind(this);
/**
* Loads the application configuration from the underlying store (usually a JSON file).
*/
this.server.methods.loadConfig = (async () => {
await new Promise((resolve, reject) => {
this.nconf.load((err) => {
if (err) return reject(err);
resolve();
});
});
}).bind(this);
/**
* Sets a configuration property on all instances in the pool.
*/
this.server.methods.setTorConfig = (async (config) => {
await Promise.all(Object.keys(config).map((key) => {
let value = config[key];
return this.tor_pool.set_config_all(key, value);
}));
}).bind(this);
/**
* Sets a configuration property on all instances in a group.
*/
this.server.methods.setTorConfigByGroup = (async (group, config) => {
await Promise.all(Object.keys(config).map((key) => {
let value = config[key];
return this.tor_pool.set_config_by_group(group, key, value);
}));
}).bind(this);
/**
* Retrieves the current load balance method for the pool
*/
this.server.methods.getLoadBalanceMethod = (() => {
return this.tor_pool.load_balance_method;
}).bind(this);
/**
* Sets the current load balance method for the pool
*/
this.server.methods.setLoadBalanceMethod = ((loadBalanceMethod) => {
this.tor_pool.load_balance_method = loadBalanceMethod;
this.nconf.set('loadBalanceMethod', loadBalanceMethod);
}).bind(this);
/**
* Retrieve a configuration property for an instance identified by the {@link TorProcess#instance_name} field.
*/
this.server.methods.getInstanceConfigByName = this.tor_pool.get_config_by_name.bind(this.tor_pool);
/**
* Retrieves a configuration property for an instance by its index in the pool.
*/
this.server.methods.getInstanceConfigAt = this.tor_pool.get_config_at.bind(this.tor_pool);
/**
* Sets a configuration property for an instance identified by the {@link TorProcess#instance_name} field.
*/
this.server.methods.setInstanceConfigByName = this.tor_pool.set_config_by_name.bind(this.tor_pool);
/**
* Sets a configuration property for an instance identified by its index in the pool.
*/
this.server.methods.setInstanceConfigAt = this.tor_pool.set_config_at.bind(this.tor_pool);
/**
* Sends a signal to all instances in the pool
*/
this.server.methods.signalAllInstances = this.tor_pool.signal_all.bind(this.tor_pool);
/**
* Sends a signal to an instance identified by its index in the pool.
*/
this.server.methods.signalInstanceAt = this.tor_pool.signal_at.bind(this.tor_pool);
/**
* Sends a signal to an instance identified by the {@link TorProcess#instance_name} field.
*/
this.server.methods.signalInstanceByName = this.tor_pool.signal_by_name.bind(this.tor_pool);
/**
* Sends a singal to all instances in a group.
*/
this.server.methods.signalInstancesByGroup = this.tor_pool.signal_by_group.bind(this.tor_pool);
/**
* Adds an instance to a group identified by its {@link TorProcess#instance_name} field.
*/
this.server.methods.addInstanceToGroupByName = this.tor_pool.add_instance_to_group_by_name.bind(this.tor_pool);
/**
* Adds an instance to a group identified by its index in the pool.
*/
this.server.methods.addInstanceToGroupAt = this.tor_pool.add_instance_to_group_at.bind(this.tor_pool);
/**
* Removes an instance from a group identified by its {@link TorProcess#instance_name} field.
*/
this.server.methods.removeInstanceFromGroupByName = this.tor_pool.remove_instance_from_group_by_name.bind(this.tor_pool);
/**
* Remove an instance from a group identified by its index in the pool.
*/
this.server.methods.removeInstanceFromGroupAt = this.tor_pool.remove_instance_from_group_at .bind(this.tor_pool);
}
listen() { this.server.listen.apply(this.server, arguments); }
close() { this.server.close.apply(this.server, arguments); }
handleConnection(socket, next) {
socket.on('createTorPool', this.createTorPool.bind(this));
socket.on('createSOCKSServer', this.createSOCKSServer.bind(this));
socket.on('createDNSServer', this.createDNSServer.bind(this));
socket.on('createInstances', (instances, callback) => { this.torPool.create(instances, (error, instances) => {
callback(error)
}); });
socket.on('removeInstances', (instances, callback) => { this.torPool.remove(instances, callback); });
socket.on('newIps', () => { this.torPool.new_ips(); });
socket.on('nextInstance', () => { this.torPool.next(); });
socket.on('closeInstances', () => { this.torPool.exit(); });
next();
/**
* Returns a summary of information on the running instance
* @param {TorProcess} instance
* @static
* @returns {ControlServer~InstanceInfo}
*/
static instance_info(instance) {
return {
name: instance.instance_name,
group: instance.instance_group,
dns_port: instance.dns_port,
socks_port: instance.socks_port,
process_id: instance.process.pid,
config: instance.definition.Config,
weight: instance.definition.weight
};
}
createTorPool(options) {
this.torPool = new TorPool(null, options, this.logger);
return this.torPool;
/**
* Binds the server to a host and port and begins listening for TCP traffic
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async listenTcp(port, hostname) {
this.tcpTransport = new TCPTransport(this.serializer, port, hostname);
this.server.addTransport(this.tcpTransport);
this.logger.info(`[control]: control server listening on tcp://${hostname}:${port}`);
await this.tcpTransport.listen();
}
createSOCKSServer(port) {
this.socksServer = new SOCKSServer(this.torPool, this.logger);
this.socksServer.listen(port || 9050);
this.logger && this.logger.info(`[socks]: Listening on ${port}`);
return this.socksServer;
/**
* Binds the server to a host and port and begins listening for WebSocket traffic
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async listenWs(port, hostname) {
this.wsTransport = new WebSocketTransport(this.serializer, port, hostname);
this.server.addTransport(this.wsTransport);
this.logger.info(`[control]: control server listening on ws://${hostname}:${port}`);
await this.wsTransport.listen();
}
createDNSServer(port) {
this.dnsServer = new DNSServer(this.torPool, this.logger);
this.dnsServer.serve(port || 9053);
this.logger && this.logger.info(`[dns]: Listening on ${port}`);
return this.dnsServer;
/**
* Calls {@link ControlServer#listenTcp} with the same arguments
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async listen(port, hostname) { return await this.listenTcp(port, hostname); }
/**
* Closes the TCP and/or WebSocket servers
*/
close() {
this.server.close();
}
/**
* Creates a new {@link TorPool} instance
* @param {Object} [tor_config] - Default Tor config to be used for the pool of instances.
* @param {string} [load_balance_method] - Load balance method to be used for the pool. Will default to the global configuration if not provided.
*
* @returns {TorPool} - The {@link TorPool} that was created.
*/
createTorPool(tor_config, load_balance_method) {
this.tor_pool = new TorPool(this.nconf.get('torPath'), tor_config, this.nconf.get('parentDataDirectory'), (load_balance_method || this.nconf.get('loadBalanceMethod')), this.nconf.get('granaxOptions'), this.logger);
return this.tor_pool;
}
/**
* Creates an instance of {@link SOCKSServer} and begins listening for traffic
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async createSOCKSServer(port, hostname) {
this.socksServer = new SOCKSServer(this.tor_pool, this.logger, (this.nconf.get('proxyByName') ? { mode: this.nconf.get('proxyByName'), deny_unidentified_users: this.nconf.get('denyUnidentifiedUsers') } : ""));
await this.socksServer.listen(port || default_ports.socks, hostname);
this.logger.info(`[socks]: listening on socks5://${hostname}:${port}`);
}
/**
* Creates an instance of {@link HTTPServer} and begins listening for traffic
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async createHTTPServer(port, hostname) {
this.httpServer = new HTTPServer(this.tor_pool, this.logger, (this.nconf.get('proxyByName') ? { mode: this.nconf.get('proxyByName'), deny_unidentified_users: this.nconf.get('denyUnidentifiedUsers') } : ""));
await this.httpServer.listen(port || default_ports.http, hostname);
this.logger.info(`[http]: listening on http://${hostname}:${port}`);
}
/**
* Creates an instance of {@link DNSServer} and begins listening for traffic
* @param {number} port - Port the server should bind to
* @param {string} [hostname] - Host the server should bind to
* @async
* @returns {Promise}
*/
async createDNSServer(port, hostname) {
this.dnsServer = new DNSServer(this.tor_pool, this.nconf.get('dns:options'), this.nconf.get('dns:timeout'), this.logger);
await this.dnsServer.serve(port || default_ports.dns, hostname);
this.logger.info(`[dns]: listening on dns://${hostname}:${port}`);
}
};
/**
* Module that contains the {@link ControlServer} class.
* @module tor-router/ControlServer
* @see ControlServer
*/
module.exports = ControlServer;

View file

@ -1,44 +1,136 @@
const dns = require('native-dns');
const UDPServer = require('native-dns').UDPServer;
const { UDPServer, Request } = dns;
const Promise = require('bluebird');
/**
* A DNS proxy server that will route requests to instances in the TorPool provided.
* @extends UDPServer
*/
class DNSServer extends UDPServer {
constructor(tor_pool, logger, options, timeout) {
super(options || {});
this.logger = logger;
this.tor_pool = tor_pool;
/**
* Binds the server to a port and IP Address.
*
* @async
* @param {number} port - The port to bind to.
* @param {string} [host="::"] - Address to bind to. Will default to :: or 0.0.0.0 if not specified.
* @returns {Promise}
*
*/
async listen() {
let args = Array.from(arguments);
let inner_func = super.serve;
this.on('request', (req, res) => {
for (let question of req.question) {
let dns_port = (tor_pool.next().dns_port);
let outbound_req = dns.Request({
question,
server: { address: '127.0.0.1', port: dns_port, type: 'udp' },
timeout: this.timeout
});
if (!args[1])
args[1] = null;
outbound_req.on('message', (err, answer) => {
if (!err && answer) {
for (let a of answer.answer){
res.answer.push(a);
this.logger && this.logger.info(`[dns]: ${question.name} type ${dns.consts.QTYPE_TO_NAME[question.type]} → 127.0.0.1:${dns_port}${a.address}`)
}
}
});
return await new Promise((resolve, reject) => {
args.push(() => {
let args = Array.from(arguments);
resolve.apply(args);
});
outbound_req.on('error', (err) => {
});
outbound_req.on('end', () => {
res.send();
});
outbound_req.send();
try {
inner_func.apply(this, args);
} catch (error) {
reject(error);
}
});
}
/**
* Creates an instance of `DNSServer`.
* @param {TorPool} tor_pool - The pool of instances that will be used for requests.
* @param {Object} [dns_options] - Options that will be passed to the parent constructor.
* @param {number} dns_timeout - How long to wait before each outbound DNS request before timing out.
* @param {Logger} [logger] - Winston logger that will be used for logging. If not specified will disable logging.
*/
constructor(tor_pool, dns_options, dns_timeout, logger) {
/**
* Handles an incoming DNS request.
*
* @function handle_request
* @param {Request} req - Incoming DNS request.
* @param {Response} res - Outgoing DNS response.
* @private
*/
const handle_request = (req, res) => {
let connect = (tor_instance) => {
for (let question of req.question) {
let dns_port = (tor_instance.dns_port);
let outbound_req = Request({
question,
server: { address: '127.0.0.1', port: dns_port, type: 'udp' },
timeout: this.dns_timeout
});
outbound_req.on('message', (err, answer) => {
if (!err && answer) {
for (let a of answer.answer){
res.answer.push(a);
}
}
});
outbound_req.on('error', (err) => {
this.logger.error(`[dns]: an error occured while handling the request: ${err.message}`);
});
outbound_req.on('end', () => {
let source = { hostname: req.address.address, port: req.address.port, proto: 'dns' };
/**
* Fires when the proxy has made a connection through an instance.
*
* @event DNSServer#instance-connection
* @param {TorProcess} instance - Instance that has been connected to.
* @param {InstanceConnectionSource} source - Details on the source of the connection.
*/
this.emit('instance_connection', tor_instance, source);
this.logger.verbose(`[dns]: ${source.hostname}:${source.port} → 127.0.0.1:${dns_port}${tor_instance.definition.Name ? ' ('+tor_instance.definition.Name+')' : '' }`);
res.send();
});
outbound_req.send();
};
};
if (this.tor_pool.instances.length) {
connect(this.tor_pool.next());
}
else {
this.logger.debug(`[dns]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`);
this.tor_pool.once('instance_created', connect);
}
};
super(dns_options);
/**
* Winston logger
* @type {Logger}
* @public
*/
this.logger = logger || require('./winston_silent_logger');
/**
* The pool of instances that will be used for requests.
* @type {TorPool}
* @public
*/
this.tor_pool = tor_pool;
/**
* Timeout for each outbound DNS request
* @type {number}
* @public
*/
this.dns_timeout = dns_timeout;
this.on('request', handle_request);
}
};
/**
* Module that contains the {@link DNSSErver} class.
* @module tor-router/DNSServer
* @see DNSServer
*/
module.exports = DNSServer;

352
src/HTTPServer.js Normal file
View file

@ -0,0 +1,352 @@
const http = require('http');
const URL = require('url');
const { Server } = http;
const Promise = require('bluebird');
const socks = require('socksv5');
/**
* Value of the "Proxy-Agent" header that will be sent with each http-connect (https) request
* @constant
* @type {string}
* @default
*/
const TOR_ROUTER_PROXY_AGENT = 'tor-router';
/**
* What will show up when an unauthenticated user attempts to connect when an invalid username
* @constant
* @type {string}
* @default
*/
const REALM = 'Name of instance to route to';
/**
* A HTTP(S) proxy server that will route requests to instances in the TorPool provided.
* @extends Server
*/
class HTTPServer extends Server {
/**
* Binds the server to a port and IP Address.
*
* @async
* @param {number} port - The port to bind to
* @param {string} [host="::"] - Address to bind to. Will default to :: or 0.0.0.0 if not specified.
* @returns {Promise}
*
*/
async listen() {
return await new Promise((resolve, reject) => {
let args = Array.from(arguments);
let inner_func = super.listen;
args.push(() => {
let args = Array.from(arguments);
resolve.apply(args);
});
inner_func.apply(this, args);
});
}
/**
* Handles username authentication for HTTP requests
* @param {ClientRequest} req - Incoming HTTP request
* @param {ClientResponse} res - Outgoing HTTP response
* @private
*/
authenticate_user_http(req, res) {
return this.authenticate_user(req, () => {
res.writeHead(407, { 'Proxy-Authenticate': `Basic realm="${REALM}"` });
res.end();
return false;
})
}
/**
* Handles username authentication for HTTP-Connect requests
* @param {ClientRequest} req - Incoming HTTP request
* @param {Socket} socket - Inbound HTTP-Connect socket
* @private
*/
authenticate_user_connect(req, socket) {
return this.authenticate_user(req, () => {
socket.write(`HTTP/1.1 407 Proxy Authentication Required\r\n'+'Proxy-Authenticate: Basic realm="${REALM}"\r\n` +'\r\n');
socket.end();
return false;
})
}
/**
* Checks the username provided against all groups (for "group" mode) or all instances (for "individual" mode).
* @param {ClientRequest} req - Incoming HTTP request
* @param {Function} deny - Function that when called will deny the connection to the proxy server and prompt the user for credentials (HTTP 407).
* @private
* @throws If the {@link HTTPServer#proxy_by_name} mode is invalid
*/
authenticate_user(req, deny) {
if (!this.proxy_by_name)
return true;
let deny_un = this.proxy_by_name.deny_unidentified_users;
let header = req.headers['authorization'] || req.headers['proxy-authorization'];
if (!header && deny_un) return deny();
else if (!header) return true;
let token = header.split(/\s+/).pop();
if (!token && deny_un) return deny();
else if (!token) return true;
let buf = new Buffer.from(token, 'base64').toString();
if ( !buf && deny_un ) return deny();
else if (!buf) return true;
let username = buf.split(/:/).shift();
if ( !username && deny_un ) return deny();
else if (!username) return true;
let instance;
if (this.proxy_by_name.mode === 'individual')
instance = this.tor_pool.instance_by_name(username);
else if (this.proxy_by_name.mode === 'group') {
if (!this.tor_pool.group_names.has(username)) return deny();
instance = this.tor_pool.next_by_group(username);
}
else
throw Error(`Unknown "proxy_by_name" mode ${this.proxy_by_name.mode}`);
if (!instance) return deny();
req.instance = instance;
return true;
}
/**
* Creates an instance of `HTTPServer`.
* @param {TorPool} tor_pool - The pool of instances that will be used for requests.
* @param {Logger} [logger] - Winston logger that will be used for logging. If not specified will disable logging.
* @param {ProxyByNameConfig} [proxy_by_name] - Enable routing to specific instances or groups of instances using the username field (http://instance-1:@my-server:9050) when connecting.
*/
constructor(tor_pool, logger, proxy_by_name) {
/**
* Handles incoming HTTP Connections.
* @function handle_http_connections
* @param {ClientRequest} - Incoming HTTP request.
* @param {ClientResponse} - Outgoing HTTP response.
* @private
*/
const handle_http_connections = (req, res) => {
if (!this.authenticate_user_http(req, res))
return;
let { instance } = req;
let url = URL.parse(req.url);
url.port = url.port || 80;
let buffer = [];
const onError = (err) => {
this.logger.error("[http-proxy]: an error occured: "+err.message);
res.writeHead(500);
res.end();
}
function onIncomingData(chunk) {
buffer.push(chunk);
}
function preConnectClosed() {
req.finished = true;
}
req.on('data', onIncomingData);
req.on('end', preConnectClosed);
req.on('error', onError);
let connect = (tor_instance) => {
let source = { hostname: req.connection.remoteAddress, port: req.connection.remotePort, proto: 'http', by_name: Boolean(instance) };
let socks_port = tor_instance.socks_port;
const agent = socks.HttpAgent({
proxyHost: '127.0.0.1',
proxyPort: socks_port,
auths: [ socks.auth.None() ],
localDNS: false
});
let proxy_req = http.request({
method: req.method,
hostname: url.hostname,
port: url.port,
path: url.path,
headers: req.headers,
agent
}, (proxy_res) => {
/**
* Fires when the proxy has made a connection through an instance using HTTP or HTTP-Connect.
*
* @event HTTPServer#instance-connection
* @param {TorProcess} instance - Instance that has been connected to.
* @param {InstanceConnectionSource} source - Details on the source of the connection.
*/
this.emit('instance_connection', tor_instance, source);
this.logger.verbose(`[http-proxy]: ${source.hostname}:${source.port} → 127.0.0.1:${socks_port}${tor_instance.definition.Name ? ' ('+tor_instance.definition.Name+')' : '' }${url.hostname}:${url.port}`);
proxy_res.on('data', (chunk) => {
res.write(chunk);
});
proxy_res.on('end', () => {
res.end();
});
res.writeHead(proxy_res.statusCode, proxy_res.headers);
});
proxy_req.on("error", onError);
req.removeListener('data', onIncomingData);
req.on('data', (chunk) => {
proxy_req.write(chunk);
})
req.on('end', () => {
proxy_req.end();
})
while (buffer.length) {
proxy_req.write(buffer.shift());
}
if (req.finished)
proxy_req.end();
};
if (instance) {
if (instance.ready) {
connect(instance);
}
else {
this.logger.debug(`[http-proxy]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`);
instance.once('ready', (() => connect(instance)));
}
}
else if (this.tor_pool.instances.length) {
connect(tor_pool.next());
} else {
this.logger.debug(`[http-proxy]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`);
tor_pool.once('instance_created', connect);
}
}
/**
* Handles incoming HTTP-Connect connections.
* @function handle_connect_connections
* @param {ClientRequest} req - Incoming HTTP Request.
* @param {Socket} inbound_socket - Incoming socket.
* @param {Buffer|string} head - HTTP Request head.
* @private
*/
const handle_connect_connections = (req, inbound_socket, head) => {
if (!this.authenticate_user_connect(req, inbound_socket))
return;
let { instance } = req;
let hostname = req.url.split(':').shift();
let port = Number(req.url.split(':').pop());
let connect = (tor_instance) => {
let source = { hostname: req.connection.remoteAddress, port: req.connection.remotePort, proto: 'http-connect', by_name: Boolean(instance) };
let socks_port = tor_instance.socks_port;
var outbound_socket;
let onClose = (error) => {
inbound_socket && inbound_socket.end();
outbound_socket && outbound_socket.end();
inbound_socket = outbound_socket = void(0);
if (error instanceof Error)
this.logger.error(`[http-connect]: an error occured: ${error.message}`)
};
inbound_socket.on('error', onClose);
inbound_socket.on('close', onClose);
const client = socks.connect({
host: hostname,
port: port,
proxyHost: '127.0.0.1',
proxyPort: socks_port,
localDNS: false,
auths: [ socks.auth.None() ]
}, ($outbound_socket) => {
this.emit('instance_connection', tor_instance, source);
this.logger && this.logger.verbose(`[http-connect]: ${source.hostname}:${source.port} → 127.0.0.1:${socks_port}${tor_instance.definition.Name ? ' ('+tor_instance.definition.Name+')' : '' }${hostname}:${port}`)
outbound_socket = $outbound_socket;
outbound_socket.on('close', onClose);
outbound_socket.on('error', onClose);
inbound_socket.write(`HTTP/1.1 200 Connection Established\r\n'+'Proxy-agent: ${TOR_ROUTER_PROXY_AGENT}\r\n` +'\r\n');
outbound_socket.write(head);
outbound_socket.pipe(inbound_socket);
inbound_socket.pipe(outbound_socket);
});
client.on('error', onClose);
};
if (instance) {
if (instance.ready) {
connect(instance);
}
else {
this.logger.debug(`[http-connect]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`);
instance.once('ready', (() => connect(instance)));
}
}
else if (this.tor_pool.instances.length) {
connect(this.tor_pool.next());
} else {
this.logger.debug(`[http-connect]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`);
this.tor_pool.once('instance_created', connect);
}
}
super(handle_http_connections);
this.on('connect', handle_connect_connections);
/**
* Winston logger.
* @type {Logger}
* @public
*/
this.logger = logger || require('./winston_silent_logger');
/**
* The pool of instances that will be used for requests.
* @type {TorPool}
* @public
*/
this.tor_pool = tor_pool;
/**
* Configuration for "proxy by name" feature.
* @type {ProxyByNameConfig}
* @public
*/
this.proxy_by_name = proxy_by_name;
}
};
/**
* Module that contains the {@link HTTPServer} class.
* @module tor-router/HTTPServer
* @see HTTPServer
*/
module.exports = HTTPServer;

View file

@ -1,20 +1,131 @@
const socks = require('socksv5');
const SOCKS5Server = socks.Server;
const domain = require('domain');
const Promise = require('bluebird');
const { Server } = socks;
class SOCKSServer extends SOCKS5Server{
constructor(tor_pool, logger) {
let handleConnection = (info, accept, deny) => {
let d = domain.create();
/**
* Configuration for the "proxy by name" feature (connecting to specific instances or groups of instances using the username field when connecting).
* @typedef ProxyByNameConfig
*
* @property {boolean} [deny_unidentified_users=false] - Deny unauthenticated (e.g. no username - socks://my-server:9050) users access to the proxy server.
* @property {string} mode - Either "group" for routing to a group of instances or "individual" for routing to individual instances.
*/
/**
* Details on the source of a connection the proxy server.
* @typedef InstanceConnectionSource
* @property {string} hostname - Hostname where the connection was made from.
* @property {number} port - Port where the connection was made from.
* @property {boolean} by_name - Indicates whether the connection was made using a username (made to a specific instance or group of instances).
* @property {string} proto - The protocol of the connection "socks", "http", "http-connect" or "dns"
*/
/**
* A SOCKS5 proxy server that will route requests to instances in the TorPool provided.
* @extends Server
*/
class SOCKSServer extends Server{
/**
* Callback for `authenticate_user`.
* @callback SOCKSServer~authenticate_user_callback
* @param {boolean} allow - Indicates if the connection should be allowed.
* @param {boolean} user - Indicates if the connection should have a session (authentication was successful).
*/
/**
* Binds the server to a port and IP Address.
*
* @async
* @param {number} port - The port to bind to.
* @param {string} [host="::"] - Address to bind to. Will default to :: or 0.0.0.0 if not specified.
* @returns {Promise}
*
*/
async listen() {
return await new Promise((resolve, reject) => {
let args = Array.from(arguments);
let inner_func = super.listen;
args.push(() => {
let args = Array.from(arguments);
resolve.apply(args);
});
inner_func.apply(this, args);
});
}
/**
* Retrieves an instance from the pool or an instance from a group by the name provided.
* @param {string} username - Name of the group or instance to route to.
* @returns {TorProcess}
* @throws If {@link SOCKSServer#proxy_by_name} is set to an invalid value.
* @throws If the name of the instance or group provided is invalid.
* @private
*/
get_instance_pbn(username) {
if (this.proxy_by_name.mode === 'individual')
return this.tor_pool.instance_by_name(username);
else if (this.proxy_by_name.mode === 'group') {
return this.tor_pool.next_by_group(username);
} else
throw Error(`Unknown "proxy_by_name" mode ${this.proxy_by_name.mode}`);
}
/**
* Checks the username provided against all groups (for "group" mode) or all instances (for "individual" mode).
* @param {string} username
* @param {string} password
* @param {SOCKSServer~authenticate_user_callback} callback - Callback for `authenticate_user`.
* @throws If {@link SOCKSServer#proxy_by_name} is invalid.
* @private
*/
authenticate_user(username, password, callback) {
let deny_un = this.proxy_by_name.deny_unidentified_users;
// No username and deny unindentifed then deny
if (!username && deny_un) callback(false);
// Otherwise if there is no username allow
else if (!username) callback(true);
if (this.proxy_by_name.mode === 'individual'){
if (!this.tor_pool.instance_names.includes(username)) return callback(false);
}
else if (this.proxy_by_name.mode === 'group') {
if (!this.tor_pool.group_names.has(username)) return callback(false);
}
else
throw Error(`Unknown "proxy_by_name" mode "${this.proxy_by_name.mode}"`);
// Otherwise allow
callback(true, true);
}
/**
* Creates an instance of `SOCKSServer`.
* @param {TorPool} tor_pool - The pool of instances that will be used for requests
* @param {Logger} [logger] - Winston logger that will be used for logging. If not specified will disable logging.
* @param {ProxyByNameConfig} [proxy_by_name] - Enable routing to specific instances or groups of instances using the username field (socks://instance-1:@my-server:9050) when connecting.
*/
constructor(tor_pool, logger, proxy_by_name) {
/**
* Handles SOCKS5 inbound connections.
*
* @function handle_connections
* @param {object} info - Information about the inbound connection.
* @param {Function} accept - Callback that allows the connection.
* @param {Function} deny - Callback that denies the connection.
* @private
*/
const handle_connections = (info, accept, deny) => {
let inbound_socket = accept(true);
d.add(inbound_socket);
var outbound_socket;
let instance;
if (inbound_socket.user)
instance = this.get_instance_pbn(inbound_socket.user);
let outbound_socket;
let buffer = [];
let socks_port = (tor_pool.next().socks_port);
logger && logger.info(`[socks]: ${info.srcAddr}:${info.srcPort} → 127.0.0.1:${socks_port}${info.dstAddr}:${info.dstPort}`)
let onInboundData = (data) => buffer.push(data)
let onClose = (error) => {
inbound_socket && inbound_socket.end();
outbound_socket && outbound_socket.end();
@ -23,22 +134,19 @@ class SOCKSServer extends SOCKS5Server{
if (error)
this.logger.error(`[socks]: an error occured: ${error.message}`)
d.exit();
};
d.on('error', onClose);
let onInboundData = (data) => buffer.push(data)
if (!inbound_socket) return;
inbound_socket.on('close', onClose);
inbound_socket.on('data', onInboundData);
inbound_socket.on('error', onClose);
d.run(() => {
socks.connect({
let connect = (tor_instance) => {
let source = { hostname: info.srcAddr, port: info.srcPort, proto: 'socks', by_name: Boolean(instance) };
let socks_port = tor_instance.socks_port;
let client = socks.connect({
host: info.dstAddr,
port: info.dstPort,
proxyHost: '127.0.0.1',
@ -46,8 +154,17 @@ class SOCKSServer extends SOCKS5Server{
localDNS: false,
auths: [ socks.auth.None() ]
}, ($outbound_socket) => {
/**
* Fires when the proxy has made a connection through an instance.
*
* @event SOCKSServer#instance-connection
* @param {TorProcess} instance - Instance that has been connected to.
* @param {InstanceConnectionSource} source - Details on the source of the connection.
*/
this.emit('instance_connection', tor_instance, source);
this.logger.verbose(`[socks]: ${source.hostname}:${source.port} → 127.0.0.1:${socks_port}${tor_instance.definition.Name ? ' ('+tor_instance.definition.Name+')' : '' }${info.dstAddr}:${info.dstPort}`)
outbound_socket = $outbound_socket;
d.add(outbound_socket);
outbound_socket && outbound_socket.on('close', onClose);
inbound_socket && inbound_socket.removeListener('data', onInboundData);
@ -64,16 +181,65 @@ class SOCKSServer extends SOCKS5Server{
while (buffer && buffer.length && outbound_socket) {
outbound_socket.write(buffer.shift());
}
})
});
};
});
super(handleConnection);
client.on('error', onClose);
};
if (instance) {
if (instance.ready) {
connect(instance);
}
else {
this.logger.debug(`[socks]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`);
instance.once('ready', (() => connect(instance)));
}
}
else if (this.tor_pool.instances.length) {
connect(this.tor_pool.next());
} else {
this.logger.debug(`[socks]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`);
this.tor_pool.once('instance_created', connect);
}
}
super(handle_connections);
this.logger = logger;
let auth = socks.auth.None();
if (proxy_by_name) {
auth = socks.auth.UserPassword(this.authenticate_user.bind(this));
}
this.useAuth(socks.auth.None());
this.useAuth(auth);
/**
* Winston logger to use.
*
* @type {Logger}
* @public
*/
this.logger = logger || require('./winston_silent_logger');
/**
* Pool of instances use to service requests.
*
* @type {TorPool}
* @public
*/
this.tor_pool = tor_pool;
/**
* Configuration for the "proxy by name" feature.
*
* @type {ProxyByNameConfig}
* @public
*/
this.proxy_by_name = proxy_by_name;
this.logger.debug(`[socks]: connecting to a specific instance by name has ben turned ${proxy_by_name ? 'on' : 'off'}`);
}
};
/**
* Module that contains the {@link SOCKSServer} class.
* @module tor-router/SOCKSServer
* @see SOCKSServer
*/
module.exports = SOCKSServer;

View file

@ -1,4 +1,6 @@
/* From http://stackoverflow.com/questions/1985260/javascript-array-rotate */
/**
* @author Christoph and Marco Bonelli on Stackoverflow <https://bit.ly/2p6gedO>
*/
Array.prototype.rotate = (function() {
// save references to array functions to make lookup faster
var push = Array.prototype.push,
@ -17,68 +19,780 @@ Array.prototype.rotate = (function() {
};
})();
const EventEmitter = require('eventemitter2').EventEmitter2;
const async = require('async');
const TorProcess = require('./TorProcess');
const temp = require('temp');
const path = require('path');
const fs = require('fs');
const { EventEmitter } = require('eventemitter3');
const Promise = require("bluebird");
const _ = require('lodash');
const WeightedList = require('js-weighted-list');
temp.track();
const getPort = require('get-port');
const TorProcess = require('./TorProcess');
const { Mutex } = require('async-mutex');
Promise.promisifyAll(fs);
/**
* Class that represents a pool of Tor processes.
* @extends EventEmitter
*/
class TorPool extends EventEmitter {
constructor(tor_path, config, logger) {
/**
* Creates an instance of `TorPool`.
*
* @param {string} tor_path - Path to the Tor executable.
* @param {Object|Function} [default_config] - Default configuration that will be passed to all Tor instances created. Can be a function. See {@link https://bit.ly/2QrmI3o|Tor Documentation} for all possible options
* @param {string} data_directory - Parent directory for the data directory of each proccess.
* @param {string} load_balance_method - Name of the load balance method to use. See {@link TorPool#load_balance_methods}.
* @param {string} [granax_options] - Object containing options that will be passed to granax for each instance.
* @param {string} [logger] - A winston logger. If not provided no logging will occur.
*
* @throws If "data_directory" is not provided.
*/
constructor(tor_path, default_config, data_directory, load_balance_method, granax_options, logger) {
if (!data_directory)
throw new Error('Invalid "data_directory"');
super();
config = config || {};
this.tor_config = config;
this.tor_path = tor_path || 'tor';
this._instances = [];
this.logger = logger;
this._default_tor_config = default_config;
/**
* Parent directory for the data directory of each proccess.
*
* @type {string}
* @public
*/
this.data_directory = data_directory;
/**
* Name of the load balance method to use.
*
* @type {string}
* @public
*/
this.load_balance_method = load_balance_method;
/**
* Path to the Tor executable.
*
* @type {string}
* @public
*/
this.tor_path = tor_path;
/**
* The winston logger.
*
* @type {Logger}
* @public
*/
this.logger = logger || require('./winston_silent_logger');
/**
* Object containing options that will be passed to granax for each instance.
*
* @type {Logger}
* @public
*/
this.granax_options = granax_options;
}
get instances() { return this._instances.slice(0); }
/**
* Returns a Set containing the names of all of the groups.
*
* @readonly
* @type {Set<string>}
*/
get group_names() {
return new Set(_.flatten(this.instances.map((instance) => instance.instance_group).filter(Boolean)));
}
create_instance(callback) {
let config = _.extend({}, this.tor_config)
let instance = new TorProcess(this.tor_path, config, this.logger);
instance.create((error) => {
if (error) return callback(error);
this._instances.push(instance);
/**
* Returns an array containing all of the instances in a group.
*
* @param {string} group_name - The group to query.
* @returns {string[]}
*
* @throws If the provided group does not exist
*/
instances_by_group(group_name) {
if (!this.group_names.has(group_name))
throw new Error(`Group "${group_name}" doesn't exist`);
let group = this.groups[group_name];
let arr = [];
for (let i = 0; i < group.length; i++) {
arr.push(group[i]);
}
return arr;
}
/**
* Adds an instance to a group. If the group doesn't exist it will be created.
*
* @param {string} group - The group to add the instance to.
* @param {TorProcess} instance - The instance in question.
*/
add_instance_to_group(group, instance) {
instance.definition.Group = _.union(instance.instance_group, [group]);
}
/**
* Adds an instance to a group by the {@link TorProcess#instance_name} property on the instance. If the group doesn't exist it will be created.
*
* @param {string} group - The group to add the instance to.
* @param {string} instance_name - The name of the instance in question.
*
* @throws If an instance with the name provided does not exist
*/
add_instance_to_group_by_name(group, instance_name) {
let instance = this.instance_by_name(instance_name);
if (!instance) throw new Error(`Instance "${instance_name}" not found`);
return this.add_instance_to_group(group, instance);
}
/**
* Adds an instance to a group by the index of the instance in the pool. If the group doesn't exist it will be created.
*
* @param {string} group - The group to add the instance to.
* @param {number} instance_index - The index of the instance in question.
*
* @throws If an instance with the index provided does not exist.
*/
add_instance_to_group_at(group, instance_index) {
let instance = this.instance_at(instance_index);
if (!instance) throw new Error(`Instance at "${instance_index}" not found`);
return this.add_instance_to_group(group, instance);
}
/**
* Removes an instance from a group.
*
* @param {string} group - The group to remove the instance from.
* @param {TorProcess} instance - The instance in question.
*/
remove_instance_from_group(group, instance) {
_.remove(instance.definition.Group, (g) => g === group);
}
/**
* Removes an instance from a group by the {@link TorProcess#instance_name} property on the instance.
*
* @param {string} group - The group to remove the instance from.
* @param {string} instance_name - The name of the instance in question.
*
* @throws If an instance with the name provided does not exist.
*/
remove_instance_from_group_by_name(group, instance_name) {
let instance = this.instance_by_name(instance_name);
if (!instance) throw new Error(`Instance "${instance_name}" not found`);
return this.remove_instance_from_group(group, instance);
}
/**
* Removes an instance from a group by the index of the instance in the pool.
*
* @param {string} group - The group to remove the instance from.
* @param {number} instance_index - The index of the instance in question.
*
* @throws If an instance with the index provided does not exist.
*/
remove_instance_from_group_at(group, instance_index) {
let instance = this.instance_at(instance_index);
if (!instance) throw new Error(`Instance at "${instance_index}" not found`);
return this.remove_instance_from_group(group, instance);
}
/**
* Represents a group of instances. Group is a Proxy with an array as its object. The array is generated by calling {@link TorPool#instances_in_group}.
* When called with an index (e.g. `Group[0]`) will return the instance at that index.
* Helper functions are available as properties.
* @typedef {TorProcess[]} InstanceGroup
*
* @property {Function} add - Adds an instance to the group.
* @property {Function} remove - Removes an instance from the group.
* @property {Function} add_by_name - Adds an instance to the group by the {@link TorProcess#instance_name} property on the instance.
* @property {Function} remove_by_name - Removes an instance from the group by the {@link TorProcess#instance_name} property on the instance.
* @property {Function} remove_at - Removes an instance from the group by the index of the instance in the group.
* @property {number} length - The size of the group of instances
* @property {Function} rotate - Rotates the array of instances
*/
/**
* Represents a collection of groups as an associative array. GroupCollection is a Proxy with a Set as its object. The Set is {@link TorPool#group_names}.
* If a non-existant group is referenced (e.g. `Groups["doesn't exist"]`) it will be created. So `Groups["doesn't exist"].add(my_instance)` will create the group and add the instance to it.
* @typedef {InstanceGroup[]} InstanceGroupCollection
*/
/**
* Represents all groups currently in the pool.
*
* @readonly
* @type {InstanceGroupCollection}
*/
get groups() {
let groupHandler = {
get: (instances, prop) => {
if (!Number.isNaN(Number(prop)))
return instances[prop];
let save_index = () => {
instances = instances.map((instance, index) => {
instance._index = index;
return instance;
});
};
let { group_name } = instances;
if (prop === 'add')
return (instance) => {
this.add_instance_to_group(group_name, instance);
save_index();
};
if (prop === 'add_by_name') {
return (instance_name) => {
this.add_instance_to_group_by_name(group_name, instance_name);
save_index();
};
}
if (prop === 'remove')
return (instance) => {
this.remove_instance_from_group(group_name, instance);
save_index();
};
if (prop === 'remove_by_name') {
return (instance_name) => {
this.remove_instance_from_group_by_name(group_name, instance_name);
save_index();
};
}
if (prop === 'remove_at')
return (instance_index) => {
this.remove_instance_from_group(group_name, instances[instance_index]);
save_index();
};
if (prop === 'length')
return instances.length;
if (prop === 'rotate') {
return (num) => {
instances.rotate(typeof(num) === 'undefined' ? 1 : num);
save_index();
};
}
return void(0);
}
};
let groupsHandler = {
get: (group_names, prop) => {
let instances_in_group = [];
if (group_names.has(prop)) {
instances_in_group = this.instances.filter((instance) => instance.instance_group.indexOf(prop) !== -1);
}
instances_in_group = _.sortBy(instances_in_group, ['_index', 'instance_name']);
instances_in_group.group_name = prop;
return new Proxy(instances_in_group, groupHandler);
}
};
return new Proxy(this.group_names, groupsHandler);
}
/**
* The default configuration that will be passed to each instance. Values from "definition.Config" on each instance will override the default config
*
*/
/** Getter
*
* @type {Object|Function}
*/
get default_tor_config() {
if (typeof(this._default_tor_config) === 'function')
return this._default_tor_config();
else if (this._default_tor_config)
return _.cloneDeep(this._default_tor_config);
else
return {};
}
/**
* Setter
*
* @param {Object|Function} value
*/
set default_tor_config(value) { this._default_tor_config = value; }
/**
* Returns an enumeration of load balance methods as functions
*
* @readonly
* @enum {Function}
* @static
*/
static get load_balance_methods() {
return Object.freeze({
round_robin: function (instances) {
return instances.rotate(1);
},
weighted: function (instances) {
if (!instances._weighted_list) {
instances._weighted_list = new WeightedList(
instances.map((instance) => {
return [ instance.id, instance.definition.Weight, instance ]
})
);
};
let i = instances._weighted_list.peek(instances.length).map((element) => element.data);
i._weighted_list = instances._weighted_list;
return i;
}
});
}
/**
* An array containing all instances in the pool.
*
* @readonly
* @type {TorProcess[]}
*/
get instances() {
return this._instances.slice(0);
}
/**
* An array containing the names of the instances in the pool.
*
* @readonly
* @type {string[]}
*/
get instance_names() {
return this.instances.map((i) => i.instance_name);
}
/**
* Creates an instance then adds it to the pool from the provided definiton.
* Instance will be added (and Promise will resolve) after the instance is fully bootstrapped.
*
* @async
* @param {InstanceDefinition} [instance_definition={}] - Instance definition that will be used to create the instance.
* @returns {Promise<TorProcess>} - The instance that was created.
*
* @throws If an instance with the same {@link InstanceDefinition#Name} already exists.
*/
async create_instance(instance_definition) {
if (!(fs.existsSync(this.data_directory)))
await fs.mkdirAsync(this.data_directory);
instance_definition = instance_definition || {};
if (instance_definition.Name && this.instance_names.indexOf(instance_definition.Name) !== -1)
throw new Error(`Instance named ${instance_definition.Name} already exists`);
this._instances._weighted_list = void(0);
instance_definition.Config = _.extend(_.cloneDeep(this.default_tor_config), (instance_definition.Config || {}));
let instance = new TorProcess(this.tor_path, instance_definition, this.granax_options, this.logger);
instance.definition.Config.DataDirectory = instance.definition.Config.DataDirectory || path.join(this.data_directory, instance.instance_name);
await instance.create();
this._instances.push(instance);
return await new Promise((resolve, reject) => {
instance.once('error', reject);
instance.once('error', callback)
instance.once('ready', () => {
callback && callback(null, instance);
/**
* Fires when an instance has been created.
*
* @event TorPool#instance_created
* @type {TorProcess}
* @param {TorProcess} instance - The instance that was created.
*/
this.emit('instance_created', instance);
resolve(instance);
});
});
}
create(instances, callback) {
if (!Number(instances)) return callback(null, []);
async.map(Array.from(Array(Number(instances))), (nothing, next) => {
this.create_instance(next);
}, (callback || (() => {})));
/**
* Adds one or more instances to the pool from an array of definitions or single definition.
* @param {InstanceDefinition[]|InstanceDefinition} instance_definitions
*
* @async
* @return {Promise<TorProcess[]>}
* @throws If `instance_definitions` is falsy.
*/
async add(instance_definitions) {
if (!instance_definitions)
throw new Error('Invalid "instance_definitions"');
return await Promise.all([].concat(instance_definitions).map((instance_definition) => this.create_instance(instance_definition)));
}
remove(instances, callback) {
let instances_to_remove = this._instances.splice(0, instances);
async.each(instances_to_remove, (instance, next) => {
instance.exit(next);
}, callback);
/**
* Creates one or more instances to the pool from either an array of definitions, a single definition or a number.
* If a number is provided it will create n instances with empty definitions (e.g. `TorPool.create(5)` will create 5 instances).
* @param {InstanceDefinition[]|InstanceDefinition|number} instance_definitions
*
* @async
* @return {Promise<TorProcess[]>}
* @throws If `instances` is falsy.
*/
async create(instances) {
if (!instances)
throw new Error('Invalid "instances"');
if (typeof(instances) === 'number') {
instances = Array.from(Array(instances)).map(() => ({}));
const lock = new Mutex();
instances = await Promise.all(instances.map(async(instance) => {
instance.ports = {};
let release = await lock.acquire();
try {
instance.ports.dns_port = await getPort();
instance.ports.socks_port = await getPort();
instance.ports.control_port = await getPort();
} finally {
release();
}
return instance;
}));
}
return await this.add(instances);
}
/**
* Searches for an instance with the matching {@link TorProcess#instance_name} property.
* @param {string} name - Name of the instance to search for
* @returns {TorProcess} - Matching instance
*/
instance_by_name(name) {
return this._instances.filter((i) => i.instance_name === name)[0];
}
/**
* Returns the instance located at the provided index in the pool.
* Is equivalent to `{@link TorPool#instances}[index]`
* @param {number} index - Index of the instance in the pool
* @returns {TorProcess} - Matching instance
*/
instance_at(index) {
return this._instances[index];
}
/**
* Removes a number of instances from the pool and kills their Tor processes.
* @param {number} instances - Number of instances to remove
* @param {number} [start_at=0] - Index to start removing from
* @async
* @returns {Promise} - Promise will resolve when the processes are dead
*/
async remove(instances, start_at) {
this._instances._weighted_list = void(0);
let instances_to_remove = this._instances.splice((start_at || 0), instances);
await Promise.all(instances_to_remove.map((instance) => instance.exit()));
}
/**
* Removes an instance at the provided index and kills its Tor process.
* @param {number} instance_index - Index of the instance to remove
* @async
* @returns {Promise} - Promise will resolve when the process is dead
*/
async remove_at(instance_index) {
this._instances._weighted_list = void(0);
let instance = this._instances.splice(instance_index, 1)[0];
if (!instance)
throw new Error(`No instance at "${instance_index}"`);
await instance.exit();
}
/**
* Removes an instance whose {@link TorProcess#instance_name} property matches the provided name and kills its Tor process.
* @param {string} instance_name - Name of the instance to remove
* @async
* @returns {Promise} - Promise will resolve when the process is dead
*/
async remove_by_name(instance_name) {
let instance = this.instance_by_name(instance_name);
if (!instance)
throw new Error(`Instance "${name}" not found`);
let instance_index = (this.instances.indexOf(instance));
await this.remove_at(instance_index);
}
/**
* Runs the load balance function ({@link TorPool#load_balance_method}) on the array of instances in the pool and returns the first instance in the array.
*
* @returns {TorProcess} - The first instance in the modified array.
*/
next() {
this._instances = this._instances.rotate(1);
this._instances = TorPool.load_balance_methods[this.load_balance_method](this._instances);
return this.instances[0];
}
exit() {
this.instances.forEach((tor) => tor.exit());
/**
* Rotates the array containing instances in the group provided so that the second element becomes the first element and the first element becomes the last element.
* [1,2,3] -> [2,3,1]
* @todo Load balance methods other than "round_robin" to be used
* @param {string} group - Name of the group
* @returns {TorProcess} - The first element in the modified array
*/
next_by_group(group) {
this.groups[group].rotate(1);
return this.groups[group][0];
}
new_ips() {
this.instances.forEach((tor) => tor.new_ip());
/**
* Kills the Tor processes of all instances in the pool.
*
* @async
* @returns {Promise} - Resolves when all instances have been killed.
*/
async exit() {
await Promise.all(this._instances.map((instance) => instance.exit()));
this._instances = [];
}
/**
* Gets new identities for all instances in the pool.
*
* @async
* @returns {Promise} - Resolves when all instances have new identities.
*/
async new_identites() {
await Promise.all(this.instances.map((instance) => instance.new_identity()));
}
/**
* Gets new identities for all instances in a group.
*
* @async
* @param {string} - Name of the group.
* @returns {Promise} - Resolves when all instances in the group have new identities.
*/
async new_identites_by_group(group) {
await Promise.all(this.instances_by_group(group).map((instance) => instance.new_identity()));
}
/**
* Gets a new identity for the instance at the provided index in the pool.
*
* @async
* @param {number} - Index of the instance in the pool.
* @returns {Promise} - Resolves when the instance has a new identity.
*/
async new_identity_at(index) {
await this.instances[index].new_identity();
}
/**
* Gets a new identity for the instance whose {@link TorProcess.instance_name} matches the provided name.
*
* @async
* @param {string} - Name of the instance.
* @returns {Promise} - Resolves when the instance has a new identity.
*
* @throws When no instance matched the provided name.
*/
async new_identity_by_name(name) {
let instance = this.instance_by_name(name);
if (!instance)
throw new Error(`Instance "${name}" not found`);
await instance.new_identity();
}
/**
* Get a configuration value from the instance whose {@link TorProcess.instance_name} matches the provided name via the control protocol.
*
* @async
* @param {string} name - Name of the instance.
* @param {string} keyword - Name of the configuration property.
*
* @returns {Promise<string[]>} - The configuration property's value.
* @throws When no instance matched the provided name.
*/
async get_config_by_name(name, keyword) {
let instance = this.instance_by_name(name);
if (!instance)
throw new Error(`Instance "${name}" not found`);
return await instance.get_config(keyword);
}
/**
* Set a configuration value for the instance whose {@link TorProcess.instance_name} matches the provided name via the control protocol.
*
* @async
* @param {string} name - Name of the instance.
* @param {string} keyword - Name of the configuration property.
* @param {any} value - Value to set the configuration property to.
*
* @returns {Promise}
* @throws When no instance matched the provided name.
*/
async set_config_by_name(name, keyword, value) {
let instance = this.instance_by_name(name);
if (!instance)
throw new Error(`Instance "${name}" not found`);
return await instance.set_config(keyword, value);
}
/**
* Get a configuration value from the instance at the index in the pool via the control protocol.
*
* @async
* @param {number} index - Index of the instance in the pool.
* @param {string} keyword - Name of the configuration property.
*
* @returns {Promise<string[]>} - The configuration property's value.
* @throws When no instance exists at the provided index.
*/
async get_config_at(index, keyword) {
let instance = this.instances[index];
if (!instance)
throw new Error(`Instance at ${index} not found`);
return await instance.get_config(keyword);
}
/**
* Set a configuration value for the instance at the index in the pool via the control protocol.
*
* @async
* @param {number} index - Index of the instance in the pool.
* @param {string} keyword - Name of the configuration property.
* @param {any} value - Value to set the configuration property to.
*
* @returns {Promise}
* @throws When no instance exists at the provided index.
*/
async set_config_at(index, keyword, value) {
let instance = this.instances[index];
if (!instance)
throw new Error(`Instance at ${index} not found`);
return await instance.set_config(keyword, value);
}
/**
* Set a configuration value for all instances in the provided group via the control protocol.
*
* @async
* @param {string} group - Name of the group.
* @param {string} keyword - Name of the configuration property.
* @param {any} value - Value to set the configuration property to.
*
* @returns {Promise}
* @throws When the provided group does not exist.
*/
async set_config_by_group(group, keyword, value) {
return await Promise.all(this.instances_by_group(group).map((instance) => instance.set_config(keyword, value)));
}
/**
* Set a configuration value for all instances in the pool via the control protocol.
*
* @async
* @param {string} keyword - Name of the configuration property.
* @param {any} value - Value to set the configuration property to.
*
* @returns {Promise}
*/
async set_config_all(keyword, value) {
return await Promise.all(this.instances.map((instance) => instance.set_config(keyword, value)));
}
/**
* Send a signal via the control protocol to all instances in the pool.
*
* @async
* @param {string} signal - The signal to send.
*
* @returns {Promise}
*/
async signal_all(signal) {
await Promise.all(this.instances.map((instance) => instance.signal(signal)));
}
/**
* Send a signal via the control protocol to an instance whose {@link TorProcess#instance_name} property matches the provided name.
*
* @async
* @param {string} name - Name of the instance.
* @param {string} signal - The signal to send.
*
* @returns {Promise}
*/
async signal_by_name(name, signal) {
let instance = this.instance_by_name(name);
if (!instance)
throw new Error(`Instance "${name}" not found`);
await instance.signal(signal);
}
/**
* Send a signal via the control protocol to an instance at the provided index in the pool.
*
* @async
* @param {number} index - Index of the instance in the pool.
* @param {string} signal - The signal to send.
*
* @returns {Promise}
*/
async signal_at(index, signal) {
let instance = this.instances[index];
if (!instance)
throw new Error(`Instance at ${index} not found`);
await instance.signal(signal);
}
/**
* Send a signal via the control protocol to all instances in the provided group.
*
* @async
* @param {string} group - Name of the group.
* @param {string} signal - The signal to send.
*
* @returns {Promise}
* @throws When the provided group does not exist.
*/
async signal_by_group(group, signal) {
await Promise.all(this.instances_by_group(group).map((instance) => instance.signal(signal)));
}
};
/**
* Module that contains the {@link TorPool} class.
* @module tor-router/TorPool
* @see TorPool
*/
module.exports = TorPool;

View file

@ -1,115 +1,432 @@
const spawn = require('child_process').spawn;
const _ = require('lodash');
const temp = require('temp');
const async = require('async');
const fs = require('fs');
const crypto = require('crypto');
const { connect } = require('net');
const os = require('os');
const Promise = require('bluebird');
const _ = require('lodash');
const { EventEmitter } = require('eventemitter3');
const shell = require('shelljs');
const getPort = require('get-port');
const EventEmitter = require('eventemitter2').EventEmitter2;
const del = require('del');
const temp = require('temp');
const { TorController } = require('@deadcanaries/granax');
const nanoid = require("nanoid");
const winston = require('winston');
Promise.promisifyAll(temp);
Promise.promisifyAll(fs);
temp.track();
/**
* @typedef {Object} InstanceDefinition
* @property {string} [Name] - Name of the instance.
* @property {string[]|string} [Group=[]] - Groups the instance belongs to.
* @property {Object} [Config={}] - Configuration that will be passed to Tor. See {@link https://bit.ly/2QrmI3o|Tor Documentation} for all possible options.
* @property {Number} [Weight] - Weight of the instance for "weighted" load balancing.
*/
/**
* Class that represents an individual Tor process.
*
* @extends EventEmitter
*/
class TorProcess extends EventEmitter {
constructor(tor_path, config, logger) {
/**
* Creates an instance of `TorProcess`
*
* @param {string} tor_path - Path to the Tor executable.
* @param {InstanceDefinition} [definition] - Object containing various options for the instance. See {@link InstanceDefinition} for more info.
* @param {Object} [granax_options] - Object containing options that will be passed to granax.
* @param {Logger} [logger] - A winston logger. If not provided no logging will occur.
*/
constructor(tor_path, definition, granax_options, logger) {
super();
this.logger = logger || require('./winston_silent_logger');
this.tor_path = tor_path || 'tor';
this.logger = logger;
definition = definition || {};
definition.Group = definition.Group ? [].concat(definition.Group) : [];
definition.Config = definition.Config || {};
this.tor_config = _.extend({
Log: 'notice stdout',
DataDirectory: temp.mkdirSync(),
NewCircuitPeriod: '10'
}, (config || { }));
this._definition = definition;
this._ports = definition.ports || {};
/**
* Path to the Tor executable.
*
* @type {string}
* @public
*/
this.tor_path = tor_path;
/**
* Object containing options that will be passed to granax.
*
* @type {Object}
* @public
*/
this.granax_options = granax_options;
/**
* The password that will be set for the control protocol.
*
* @type {string}
* @public
*/
this.control_password = crypto.randomBytes(128).toString('base64');
this._id = nanoid(12);
this.tor_config.DataDirectory = this.tor_config.DataDirectory || temp.mkdirSync();
}
exit(callback) {
this.process.once('exit', (code) => {
callback && callback(null, code);
/**
* Kills the Tor process.
*
* @async
* @returns {Promise}
*/
async exit() {
let p = new Promise((resolve, reject) => {
this.once('process_exit', (code) => {
resolve();
});
});
this.process.kill('SIGKILL');
this.process.kill('SIGINT');
await p;
}
new_ip() {
this.process.kill('SIGHUP');
/**
* The unique identifier assigned to each instance.
*
* @readonly
* @type {string}
*/
get id() { return this._id; }
/**
* Groups the instance are currently in.
*
* @readonly
* @type {string[]}
*/
get instance_group() {
return (this.definition && this.definition.Group);
}
/**
* Either the "Name" property of the definition or the {@link TorProcess#id} property.
*
* @readonly
* @type {string}
*/
get instance_name() {
return (this.definition && this.definition.Name) || this.id;
}
/**
* The definition used to create the instance.
*
* @readonly
* @type {string}
*/
get definition() { return this._definition; }
/**
* The configuration passed to Tor. The same value as `definition.Config`.
*
* @readonly
* @type {Object}
*/
get tor_config() { return this.definition.Config; }
/**
* Port Tor is bound to for DNS traffic.
*
* @readonly
* @type {number}
*/
get dns_port() {
return this._dns_port || null;
return this._ports.dns_port;
}
/**
* Port Tor is bound to for SOCKS5 traffic.
*
* @readonly
* @type {number}
*/
get socks_port() {
return this._socks_port || null;
return this._ports.socks_port;
}
create(callback) {
async.auto({
dnsPort: (callback) => getPort().then(port => callback(null, port)),
socksPort: (callback) => getPort().then(port => callback(null, port)),
configPath: ['dnsPort', 'socksPort', (context, callback) => {
let options = {
DNSPort: `127.0.0.1:${context.dnsPort}`,
SocksPort: `127.0.0.1:${context.socksPort}`,
};
let config = _.extend(_.extend({}, this.tor_config), options);
let text = Object.keys(config).map((key) => `${key} ${config[key]}`).join("\n");
temp.open('tor-router', (err, info) => {
if (err) return callback(err);
/**
* Port Tor is bound to for API access.
*
* @readonly
* @type {number}
*/
get control_port() {
return this._ports.control_port;
}
fs.write(info.fd, text);
fs.close(info.fd, (err) => {
callback(err, info.path);
/**
* Instance of granax.TorController connected to the Tor process.
*
* @readonly
* @type {TorController}
*/
get controller() {
return this._controller;
}
/**
* Property identifiyng whether Tor has started.
*
* @readonly
* @type {boolean}
*/
get ready() { return this._ready; }
/* Passthrough to granax */
/**
* Requests a new identity via the control protocol.
*
* @async
*/
async new_identity() {
this.logger.info(`[tor-${this.instance_name}]: requested a new identity`);
await this.controller.cleanCircuitsAsync();
}
/**
* Retrieves a configuration value from the instance via the control protocol.
*
* @async
* @throws Will throw an error if not connected to the control protocol.
* @param {string} keyword - The name of the configuration property to retrieve.
*
* @returns {Promise<string[]>}
*/
async get_config(keyword) {
if (!this.controller)
throw new Error(`Controller is not connected`);
return await this.controller.getConfigAsync(keyword);
}
/**
* Sets a configuration value for the instance via the control protocol.
*
* @async
* @throws Will throw an error if not connected to the control protocol.
* @param {string} keyword - The name of the configuration property to retrieve.
* @param {any} value - Value to set the property to.
*
* @returns {Promise}
*/
async set_config(keyword, value) {
if (!this.controller) {
return new Error(`Controller is not connected`);
}
return await this.controller.setConfigAsync(keyword, value);
}
/**
* Sends a signal via the control tnterface.
*
* @async
* @throws Will throw an error if not connected to the control protocol.
* @param {string} signal - The signal to send.
*
* @returns {Promise}
*/
async signal(signal) {
if (!this.controller) {
throw new Error(`Controller is not connected`);
}
return await this.controller.signal(signal);
}
/**
* Creates the Tor process based on the configuration provided. Promise is resolved when the process has been started.
*
* @async
*
* @returns {Promise<ChildProcess>} - The process that has been created.
*/
async create() {
let dnsPort = this._ports.dns_port || await getPort();
let socksPort = this._ports.socks_port || await getPort();
let controlPort = this._ports.control_port || await getPort();
this.logger.info(`[tor-${this.instance_name}]: DNS PORT = ${dnsPort}`);
this.logger.info(`[tor-${this.instance_name}]: SOCKS PORT = ${socksPort}`);
this.logger.info(`[tor-${this.instance_name}]: CONTROL PORT = ${controlPort}`);
Object.freeze(this._ports);
let options = {
DNSPort: `127.0.0.1:${dnsPort}`,
SocksPort: `127.0.0.1:${socksPort}`,
ControlPort: `127.0.0.1:${controlPort}`,
HashedControlPassword: shell.exec(`${this.tor_path} --quiet --hash-password "${this.control_password}"`, { async: false, silent: true }).stdout.trim()
};
let config = _.extend(_.cloneDeep(this.tor_config), options);
let text = Object.keys(config).map((key) => `${key} ${config[key]}`).join(os.EOL);
let configFile = await temp.openAsync('tor-router');
let configPath = configFile.path;
await fs.writeFileAsync(configPath, text);
let tor = spawn(this.tor_path, ['-f', configPath], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
});
tor.on('close', (async (code) => {
if (this.definition && !this.definition.Name) {
await del(this.tor_config.DataDirectory, { force: true });
}
/**
* An event that fires when the process has closed.
*
* @event TorProcess#process_exit
* @type {number}
* @param {number} code - The exit code from the process.
*/
this.emit('process_exit', code);
}));
tor.stderr.on('data', (data) => {
let error_message = Buffer.from(data).toString('utf8');
this.emit('error', new Error(error_message));
});
this.once('ready', () => {
this._ready = true;
this.logger.info(`[tor-${this.instance_name}]: tor is ready`);
});
this.on('control_listen', (async () => {
try {
let socket = connect(this.control_port);
socket.on('error', ((error) => {
this.logger.error(`[tor-${this.instance_name}]: ${error.stack}`);
this.emit('error', error);
}));
this._controller = new TorController(socket, _.extend({ authOnConnect: false }, this.granax_options));
Promise.promisifyAll(this._controller);
this.controller.on('ready', () => {
this.logger.debug(`[tor-${this.instance_name}]: connected via the tor control protocol`);
this.controller.authenticate(`"${this.control_password}"`, (err) => {
if (err) {
this.logger.error(`[tor-${this.instance_name}]: ${err.stack}`);
this.emit('error', err);
} else {
this.logger.debug(`[tor-${this.instance_name}]: authenticated with tor instance via the control protocol`);
/**
* Is true when the connected via the control protcol
* @type {boolean}
*/
this.control_port_connected = true;
/**
* An event that fires when a connection has been established to the control protocol.
*
* @event TorProcess#controller_ready
*/
this.emit('controller_ready');
}
});
});
}]
}, (error, context) => {
if (error) callback(error);
} catch (error) {
this.logger.error(`[tor-${this.instance_name}]: ${err.stack}`);
this.emit('error', err);
}
}));
this._dns_port = context.dnsPort;
this._socks_port = context.socksPort;
tor.stdout.on('data', (data) => {
let text = Buffer.from(data).toString('utf8');
let msg = text.indexOf('] ') !== -1 ? text.split('] ').pop() : null;
if (text.indexOf('Bootstrapped 100%: Done') !== -1){
this.bootstrapped = true;
/**
* An event that fires when the Tor process is fully bootstrapped (and ready for traffic).
*
* @event TorProcess#ready
*/
this.emit('ready');
}
let tor = spawn(this.tor_path, ['-f', context.configPath], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
shell: '/bin/bash'
});
if (text.indexOf('Opening Control listener on') !== -1) {
this.control_port_listening = true;
/**
* An event that fires when the Tor process has started listening for control interface traffic.
*
* @event TorProcess#control_listen
*/
this.emit('control_listen');
}
tor.stderr.on('data', (data) => {
let error_message = new Buffer(data).toString('utf8');
if (text.indexOf('Opening Socks listener on') !== -1) {
this.socks_port_listening = true;
/**
* An event that fires when the Tor process has started listening for SOCKS5 traffic.
*
* @event TorProcess#socks_listen
*/
this.emit('socks_listen');
}
this.emit('error', new Error(error_message));
});
if (text.indexOf('Opening DNS listener on') !== -1) {
this.dns_port_listening = true;
/**
* An event that fires when the Tor process has started listening for DNS traffic.
*
* @event TorProcess#dns_listen
*/
this.emit('dns_listen');
}
this.once('ready', () => {
this.logger && this.logger.info(`[tor-${tor.pid}]: tor is ready`);
});
if (text.indexOf('[err]') !== -1) {
/**
* An event that fires when the Tor process has written an error to stdout, stderr or when an error occurs connecting via the control protocol.
*
* @event TorProcess#error
* @type {Error}
* @returns {Error}
*/
this.emit('error', new Error(msg));
this.logger.error(`[tor-${this.instance_name}]: ${text}`);
}
tor.stdout.on('data', (data) => {
let text = new Buffer(data).toString('utf8');
let msg = text.split('] ').pop();
if (text.indexOf('Bootstrapped 100%: Done') !== -1){
this.emit('ready');
}
else if (text.indexOf('[notice]') !== -1) {
this.logger.debug(`[tor-${this.instance_name}]: ${msg}`);
}
if (text.indexOf('[err]') !== -1) {
this.emit('error', new Error(msg));
this.logger && this.logger.error(`[tor-${tor.pid}]: ${msg}`);
}
else if (text.indexOf('[notice]') !== -1) {
this.logger && this.logger.debug(`[tor-${tor.pid}]: ${msg}`);
}
else if (text.indexOf('[warn]') !== -1) {
this.logger && this.logger.warn(`[tor-${tor.pid}]: ${msg}`);
}
});
this.process = tor;
callback && callback(null, tor);
else if (text.indexOf('[warn]') !== -1) {
this.logger.warn(`[tor-${this.instance_name}]: ${msg}`);
}
});
this.process = tor;
return tor;
}
};
/**
* Module that contains the {@link TorProcess} class.
* @module tor-router/TorProcess
* @see TorProcess
*/
module.exports = TorProcess;

31
src/default_config.js Normal file
View file

@ -0,0 +1,31 @@
const temp = require('temp');
const path = require('path');
temp.track();
/**
* This module cotains the default configuration for the application.
* @module tor-router/default_config
*/
module.exports = {
"controlHost": 9077,
"websocketControlHost": null,
"parentDataDirectory": temp.mkdirSync(),
"socksHost": null,
"dnsHost": null,
"httpHost": null,
"proxyByName": false,
"denyUnidentifedUsers": false,
"logLevel": "info",
"loadBalanceMethod": "round_robin",
"torConfig": {
"Log": "notice stdout",
"NewCircuitPeriod": "10"
},
"torPath": require('@deadcanaries/granax').tor(require('os').platform()),
"instances": null,
"dns": {
"timeout": 10000,
"options": {}
},
"granaxOptions": null
};

12
src/default_ports.js Normal file
View file

@ -0,0 +1,12 @@
/**
* This module cotains the default ports the application will bind to.
* @module tor-router/default_ports
*/
module.exports = Object.freeze({
socks: 9050,
http: 9080,
dns: 9053,
control: 9077,
controlWs: 9078,
default_host: null
});

View file

@ -1,7 +1,12 @@
/**
* Index module for the project
* @module tor-router
*/
module.exports = {
TorProcess: require('./TorProcess'),
TorPool: require('./TorPool'),
DNSServer: require('./DNSServer'),
SOCKSServer: require('./SOCKSServer'),
HTTPServer: require('./HTTPServer'),
DNSServer: require('./DNSServer'),
ControlServer: require('./ControlServer')
};

266
src/launch.js Normal file
View file

@ -0,0 +1,266 @@
const fs = require('fs');
const { Provider } = require('nconf');
const yargs = require('yargs');
const winston = require('winston')
const Promise = require('bluebird');
const { ControlServer } = require('./');
const default_ports = require('./default_ports');
const package_json = JSON.parse(fs.readFileSync(`${__dirname}/../package.json`, 'utf8'));
/**
* @typedef Host
* @property {string} hostname - The hostname
* @property {number} port - The port
* @private
*/
/**
* Extracts the host and port components from a string
* @param {string} host
* @returns {Host}
* @private
*/
function extractHost (host) {
if (typeof(host) === 'number')
return { hostname: (typeof(default_ports.default_host) === 'string' ? default_ports.default_host : ''), port: host };
else if (typeof(host) === 'string' && host.indexOf(':') !== -1)
return { hostname: host.split(':').shift(), port: Number(host.split(':').pop()) };
else
return null;
}
/**
* Takes an object with a hostname and port returns a formatted string
* @param {Host} host
* @returns {string} - Formatted host (e.g. "0.0.0.0:1234")
*/
function assembleHost(host) {
return `${typeof(host.hostname) === 'string' ? host.hostname : '' }:${host.port}`;
}
/**
* Main function for the application
* @param {Provider} nconf - Instance of `nconf.Provider` used for configuration.
* @param {Logger} [logger] - Winston logger to be used for logging. If not provided will disable logging.
* @async
* @returns {Promise}
*/
async function main(nconf, logger) {
Promise.promisifyAll(nconf);
let instances = nconf.get('instances');
let socks_host = typeof(nconf.get('socksHost')) !== 'boolean' ? extractHost(nconf.get('socksHost')) : nconf.get('socksHost');
let dns_host = typeof(nconf.get('dnsHost')) !== 'boolean' ? extractHost(nconf.get('dnsHost')) : nconf.get('dnsHost');
let http_host = typeof(nconf.get('httpHost')) !== 'boolean' ? extractHost(nconf.get('httpHost')) : nconf.get('httpHost');
let control_host = typeof(nconf.get('controlHost')) !== 'boolean' ? extractHost(nconf.get('controlHost')) : nconf.get('controlHost');
let control_host_ws = typeof(nconf.get('websocketControlHost')) !== 'boolean' ? extractHost(nconf.get('websocketControlHost')) : nconf.get('websocketControlHost');
if (nconf.get('proxyByName') && nconf.get('proxyByName') === true)
nconf.set('proxyByName', 'individual');
if (typeof(control_host) === 'boolean') {
control_host = extractHost(9077);
nconf.set('controlHost', assembleHost(control_host));
}
if (typeof(control_host_ws) === 'boolean') {
control_host_ws = extractHost(9078);
nconf.set('websocketControlPort', assembleHost(control_host_ws));
}
let control = new ControlServer(nconf, logger);
try {
await control.listenTcp(control_host.port, control_host.hostname);
if (control_host_ws) {
control.listenWs(control_host_ws.port, control_host_ws.hostname);
}
if (socks_host) {
if (typeof(socks_host) === 'boolean') {
socks_host = extractHost(default_ports.socks);
nconf.set('socksHost', assembleHost(socks_host));
}
control.createSOCKSServer(socks_host.port, socks_host.hostname);
}
if (http_host) {
if (typeof(http_host) === 'boolean') {
http_host = extractHost(default_ports.http);
nconf.set('httpHost', assembleHost(http_host));
}
control.createHTTPServer(http_host.port, http_host.hostname);
}
if (dns_host) {
if (typeof(dns_host) === 'boolean') {
dns_host = extractHost(default_ports.dns);
nconf.set('dnsPort', assembleHost(dns_host));
}
control.createDNSServer(dns_host.port, dns_host.hostname);
}
if (instances) {
logger.info(`[tor]: starting ${Array.isArray(instances) ? instances.length : instances} tor instance(s)...`);
await control.tor_pool.create(instances);
logger.info('[tor]: tor started');
}
} catch (error) {
logger.error(`[global]: error starting application: ${error.stack}`);
process.exit(1);
}
/**
* Kills all tor processes and exits, logging an error if one occurs.
* @function cleanUp
*
* @param {Error} error - Error or exit code
*/
const cleanUp = (async (error) => {
let thereWasAnExitError = false;
let { handleError } = this;
try {
await control.tor_pool.exit();
} catch (exitError) {
logger.error(`[global]: error closing tor instances: ${exitError.message}`);
thereWasAnExitError = true;
}
if (error instanceof Error) {
logger.error(`[global]: ${error.stack || 'An unknown error occured'}`);
} else {
error = 0;
}
process.exit(Number(Boolean(error || thereWasAnExitError)));
});
process.title = 'tor-router';
process.on('SIGHUP', () => {
control.tor_pool.new_identites();
});
process.on('exit', cleanUp);
process.on('SIGINT', cleanUp);
process.on('SIGTERM', cleanUp);
process.on('uncaughtException', cleanUp.bind({ handleError: true }));
}
/**
* Instance of `nconf.Provider`
* @type {Provider}
*/
let nconf = new Provider();
let argv_config =
yargs
.version(package_json.version)
.usage('Usage: tor-router [arguments]')
.options({
f: {
alias: 'config',
describe: 'Path to a config file to use',
demand: false
},
c: {
alias: 'controlHost',
describe: `Host the control server will bind to, handling TCP connections [default: ${default_ports.default_host}:9077]`,
demand: false
// ,default: 9077
},
w: {
alias: 'websocketControlHost',
describe: 'Host the control server will bind to, handling WebSocket connections. If no hostname is specified will bind to localhost',
demand: false
},
j: {
alias: 'instances',
describe: 'Number of instances using the default config',
demand: false
// ,default: 1
},
s: {
alias: 'socksHost',
describe: 'Host the SOCKS5 Proxy server will bind to. If no hostname is specified will bind to localhost',
demand: false,
// ,default: default_ports.socks
},
d: {
alias: 'dnsHost',
describe: 'Host the DNS Proxy server will bind to. If no hostname is specified will bind to localhost',
demand: false
},
h: {
alias: 'httpHost',
describe: 'Host the HTTP Proxy server will bind to. If no hostname is specified will bind to localhost',
demand: false
},
l: {
alias: 'logLevel',
describe: 'Controls the verbosity of console log output. Default level is "info". Set to "verbose" to see all network traffic logged or "null" to disable logging completely [default: info]',
demand: false
// ,default: "info"
},
p: {
alias: 'parentDataDirectory',
describe: 'Parent directory that will contain the data directories for the instances',
demand: false
},
b: {
alias: "loadBalanceMethod",
describe: 'Method that will be used to sort the instances between each request. Currently supports "round_robin" and "weighted". [default: round_robin]',
demand: false
},
t: {
alias: "torPath",
describe: "Provide the path for the Tor executable that will be used",
demand: false
},
n: {
alias: 'proxyByName',
describe: 'Allow connecting to a specific instance identified by the username field when connecting to a proxy',
demand: false
}
});
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf
.argv(argv_config);
let nconf_config = nconf.get('config');
if (nconf_config) {
if (!require('fs').existsSync(nconf_config)) {
console.error(`[global]: config file "${nconf_config}" does not exist. exiting.`);
process.exit(1);
}
nconf.file(nconf_config);
} else {
nconf.use('memory');
}
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
let logLevel = nconf.get('logLevel');
/**
* Instnace of `winston.Logger`
* @type {Logger}
*/
let logger = winston.createLogger({
level: logLevel,
format: winston.format.simple(),
silent: (logLevel === 'null'),
transports: [ new (winston.transports.Console)({ level: (logLevel !== 'null' ? logLevel : void(0)), silent: (logLevel === 'null') }) ]
});
/**
* Exports the main function for the application, a configured `nconf.Provider` instance and a winston logger
* @module tor-router/launch
*/
module.exports = { main, nconf, logger };

63
src/nconf_load_env.js Normal file
View file

@ -0,0 +1,63 @@
/**
* An array of all valid environment variables
* @constant
* @type {string[]}
*/
const env_whitelist = [
"CONTROL_HOST",
"TOR_PATH",
"INSTANCES",
"SOCKS_HOST",
"DNS_HOST",
"HTTP_HOST",
"LOG_LEVEL",
'PARENT_DATA_DIRECTORIES',
'LOAD_BALANCE_METHOD',
"WEBSOCKET_CONTROL_PORT",
"PROXY_BY_NAME",
"DENY_UNIDENTIFIED_USERS"
];
/**
* Converts a configuration property's name from env variable format to application config format
* `"CONTROL_HOST"` -> `"controlHost"`
* @param {string} env - Environment variable
* @returns {string}
* @private
*/
function env_to_config(env) {
let a = env.toLowerCase().split('_');
i = 1;
while (i < a.length) {
a[i] = a[i][0].toUpperCase() + a[i].substr(1);
i++;
}
return a.join('');
}
/**
* Sets up nconf with the `env` store.
* @param {Provider} nconf - Instance of `nconf.Provider`.
* @returns {Provider} - Same instance of `nconf.Provider`.
*/
function setup_nconf_env(nconf) {
return nconf
.env({
whitelist: env_whitelist.concat(env_whitelist.map(env_to_config)),
parseValues: true,
transform: (obj) => {
if (env_whitelist.includes(obj.key)) {
if (obj.key.indexOf('_') !== -1) {
obj.key = env_to_config(obj.key);
}
}
return obj;
}
});
};
/**
* This module returns a function
* @module tor-router/nconf_load_env
*/
module.exports = setup_nconf_env;

View file

@ -0,0 +1,12 @@
const winston = require('winston');
/**
* Contains a winston `Logger` that is silent (doesn't log anything).
* @module tor-router/winston_silent_logger
*/
module.exports = winston.createLogger({
level: 'info',
format: winston.format.simple(),
silent: true,
transports: [ new (winston.transports.Console)({ silent: true }) ]
});

62
test/ControlServer.js Normal file
View file

@ -0,0 +1,62 @@
const _ = require('lodash');
const { assert } = require('chai');
const { Provider } = require('nconf');
const nconf = new Provider();
const getPort = require('get-port');
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
const { ControlServer, TorPool, HTTPServer, SOCKSServer, DNSServer } = require('../');
let controlServer = new ControlServer(nconf);
let controlPort;
describe('ControlServer', function () {
describe('#listen(port)', function () {
it('should bind to a given port', async function () {
controlPort = await getPort();
await controlServer.listen(controlPort);
});
});
describe('#createTorPool(options)', function () {
it('should create a TorPool with a given configuration', function () {
let torPool = controlServer.createTorPool({ ProtocolWarnings: 1 });
assert.instanceOf(controlServer.tor_pool, TorPool);
assert.equal(1, torPool.default_tor_config.ProtocolWarnings);
});
});
describe('#createSOCKSServer(port)', function () {
it('should create a SOCKS Server', async function () {
let port = await getPort();
controlServer.createSOCKSServer(port);
assert.instanceOf(controlServer.socksServer, SOCKSServer);
});
});
describe('#createDNSServer(port)', function () {
it('should create a DNS Server', async function () {
let port = await getPort();
controlServer.createDNSServer(port);
assert.instanceOf(controlServer.dnsServer, DNSServer);
});
});
describe('#createHTTPServer(port)', function () {
it('should create a HTTP Server', async function () {
let port = await getPort();
controlServer.createHTTPServer(port);
assert.instanceOf(controlServer.httpServer, HTTPServer);
});
});
describe('#close()', function () {
it('should close the RPC Server', function () {
controlServer.close();
});
});
after('shutdown tor pool', async function () {
await controlServer.tor_pool.exit();
});
});

87
test/DNSServer.js Normal file
View file

@ -0,0 +1,87 @@
const { Provider } = require('nconf');
const nconf = new Provider();
const getPort = require('get-port');
const dns = require('native-dns');
const { assert } = require('chai');
const { TorPool, DNSServer, TorProcess } = require('../');
const { WAIT_FOR_CREATE } = require('./constants');
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
let dnsServerTorPool;
let dnsServer;
describe('DNSServer', function () {
dnsServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
dnsServer = new DNSServer(dnsServerTorPool, {}, nconf.get('dns:timeout'));
let dnsPort;
before('start up server', async function (){
this.timeout(WAIT_FOR_CREATE);
await dnsServerTorPool.create(1);
dnsPort = await getPort();
await dnsServer.listen(dnsPort);
});
describe('#handle_dns_request(req, res)', function () {
it('should service a request for example.com', function (done) {
this.timeout(10000);
let req = dns.Request({
question: dns.Question({
name: 'example.com',
type: 'A'
}),
server: { address: '127.0.0.1', port: dnsPort, type: 'udp' },
timeout: 10000,
});
req.on('timeout', function () {
done(new Error('Connection timed out'));
});
req.on('message', function () {
done();
});
req.send();
});
it('should emit the "instance_connection" event', function (done) {
this.timeout(10000);
dnsServer.on('instance_connection', (instance, source) => {
assert.instanceOf(instance, TorProcess);
assert.isObject(source);
done();
});
let req = dns.Request({
question: dns.Question({
name: 'example.com',
type: 'A'
}),
server: { address: '127.0.0.1', port: dnsPort, type: 'udp' },
timeout: 10000,
});
req.on('timeout', function () {
done(new Error('Connection timed out'));
});
req.send();
});
});
after('shutdown server', function () {
dnsServer.close();
});
after('shutdown tor pool', async function () {
await dnsServerTorPool.exit();
});
});

412
test/HTTPServer.js Normal file
View file

@ -0,0 +1,412 @@
const { Provider } = require('nconf');
const nconf = new Provider();
const getPort = require('get-port');
const { assert } = require('chai');
const _ = require('lodash');
const Promise = require('bluebird');
const { TorPool, HTTPServer, TorProcess } = require('../');
const { WAIT_FOR_CREATE, PAGE_LOAD_TIME, RETRY_DELAY, RETRY_STRATEGY, MAX_ATTEMPTS, SHUTDOWN_DELAY, SHUTDOWN_TIMEOUT } = require('./constants');
const request = require('requestretry').defaults({
promiseFactory: ((resolver) => new Promise(resolver)),
maxAttempts: MAX_ATTEMPTS,
retryStrategy: RETRY_STRATEGY,
retryDelay: RETRY_DELAY,
timeout: PAGE_LOAD_TIME
});
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
describe('HTTPServer', function () {
describe('#handle_http_connections(req, res)', function () {
let httpServerTorPool;
let httpServer;
let httpPort;
before('start up server', async function (){
httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
httpServer = new HTTPServer(httpServerTorPool, null, false);
this.timeout(WAIT_FOR_CREATE);
await httpServerTorPool.create(1);
httpPort = await getPort();
await httpServer.listen(httpPort);
});
it('should service a request for example.com', async function () {
this.timeout(PAGE_LOAD_TIME);
await request({
url: 'http://example.com',
proxy: `http://127.0.0.1:${httpPort}`
});
});
it('should emit the "instance_connection" event', function (done) {
this.timeout(PAGE_LOAD_TIME);
let connectionHandler = (instance, source) => {
assert.instanceOf(instance, TorProcess);
assert.isObject(source);
httpServer.removeAllListeners('instance_connection');;
done();
};
httpServer.on('instance_connection', connectionHandler);
request({
url: 'http://example.com',
proxy: `http://127.0.0.1:${httpPort}`
})
.catch((err) => {
})
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
httpServer.close();
await httpServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
describe('#handle_connect_connections(req, inbound_socket, head)', function () {
let httpServerTorPool;
let httpServer;
let httpPort;
before('start up server', async function (){
httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
httpServer = new HTTPServer(httpServerTorPool, null, false);
this.timeout(WAIT_FOR_CREATE);
await httpServerTorPool.create(1);
httpPort = await getPort();
await httpServer.listen(httpPort);
});
it('should service a request for example.com', async function () {
this.timeout(PAGE_LOAD_TIME);
await request({
url: 'https://example.com',
proxy: `http://127.0.0.1:${httpPort}`
});
});
it('should emit the "instance_connection" event', function (done) {
this.timeout(PAGE_LOAD_TIME);
let connectionHandler = (instance, source) => {
assert.instanceOf(instance, TorProcess);
assert.isObject(source);
httpServer.removeAllListeners('instance_connection');;
done();
};
httpServer.on('instance_connection', connectionHandler);
request({
url: 'https://example.com',
proxy: `http://127.0.0.1:${httpPort}`
})
.catch((error) => {
});
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
httpServer.close();
await httpServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
describe('#authenticate_user_http(req, res)', function () {
let httpServerTorPool;
let httpServer;
let httpPort;
let instance_def = {
Name: 'instance-3',
Group: 'foo'
};
before('start up server', async function (){
httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true, mode: 'individual' });
this.timeout(WAIT_FOR_CREATE * 3);
await httpServerTorPool.create(2);
await httpServerTorPool.add(instance_def);
httpPort = await getPort();
await httpServer.listen(httpPort);
});
it(`should service a request for example.com through the instance named ${instance_def.Name}`, function (done) {
this.timeout(PAGE_LOAD_TIME);
let connectionHandler = (instance, source) => {
assert.equal(instance.instance_name, instance_def.Name);
assert.isTrue(source.by_name);
httpServer.removeAllListeners('instance_connection');;
done();
};
httpServer.on('instance_connection', connectionHandler);
request({
url: 'http://example.com',
proxy: `http://${instance_def.Name}:@127.0.0.1:${httpPort}`,
timeout: PAGE_LOAD_TIME
})
.catch(done)
});
it(`four requests made to example.com through the group named "foo" should come from the instances in "foo"`, function (done) {
(async () => {
this.timeout((PAGE_LOAD_TIME * 4) + (WAIT_FOR_CREATE));
await httpServerTorPool.add([
{
Name: 'instance-4',
Group: 'foo'
}
]);
})()
.then(async () => {
httpServer.proxy_by_name.mode = "group";
let names_requested = [];
let connectionHandler = (instance, source) => {
names_requested.push(instance.instance_name);
if (names_requested.length === httpServerTorPool.instances.length) {
names_requested = _.uniq(names_requested).sort();
let names_in_group = httpServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort()
assert.deepEqual(names_requested, names_in_group);
httpServer.removeAllListeners('instance_connection');
done();
}
};
httpServer.on('instance_connection', connectionHandler);
let i = 0;
while (i < httpServerTorPool.instances.length) {
await request({
url: 'http://example.com',
proxy: `http://foo:@127.0.0.1:${httpPort}`
});
i++;
}
})
.then(async () => {
await httpServerTorPool.remove_by_name('instance-4');
httpServer.proxy_by_name.mode = "individual";
})
.catch(done);
});
it(`shouldn't be able to send a request without a username`, async function() {
let f = () => {};
try {
let res = await request({
url: 'http://example.com',
proxy: `http://127.0.0.1:${httpPort}`,
timeout: PAGE_LOAD_TIME
});
if (res.statusCode === 407)
throw new Error('407');
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f, "407", "Did not return 407 status code");
}
});
it(`shouldn't be able to send a request with an incorrect username`, async function() {
let f = () => {};
try {
let res = await request({
url: 'http://example.com',
proxy: `http://blah-blah-blah:@127.0.0.1:${httpPort}`,
timeout: PAGE_LOAD_TIME,
fullResponse: true
});
if (res.statusCode === 407)
throw new Error('407');
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f, "407", "Did not return 407 status code");
}
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
httpServer.close();
await httpServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
describe('#authenticate_user_connect(req, socket)', function () {
let httpServerTorPool;
let httpServer;
let httpPort;
let instance_def = {
Name: 'instance-3',
Group: 'foo'
};
before('start up server', async function (){
httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true, mode: "individual" });
this.timeout(WAIT_FOR_CREATE * 3);
await httpServerTorPool.create(2);
await httpServerTorPool.add(instance_def);
httpPort = await getPort();
await httpServer.listen(httpPort);
});
it(`should service a request for example.com through ${instance_def.Name}`, function (done) {
this.timeout(PAGE_LOAD_TIME);
let req;
let connectionHandler = (instance, source) => {
assert.equal(instance.instance_name, instance_def.Name);
assert.isTrue(source.by_name);
req.cancel();
httpServer.removeAllListeners('instance_connection');
done();
};
httpServer.on('instance_connection', connectionHandler);
req = request({
url: 'https://example.com',
proxy: `http://${instance_def.Name}:@127.0.0.1:${httpPort}`
})
.catch(done);
});
it(`four requests made to example.com through the group named "foo" should come from instances in the "foo" group`, function (done) {
(async () => {
this.timeout((PAGE_LOAD_TIME * 4) + (WAIT_FOR_CREATE));
await httpServerTorPool.add([
{
Name: 'instance-4',
Group: 'foo'
}
]);
httpServer.proxy_by_name.mode = "group";
})()
.then(async () => {
let names_requested = [];
let connectionHandler = (instance, source) => {
names_requested.push(instance.instance_name);
if (names_requested.length === httpServerTorPool.instances.length) {
names_requested = _.uniq(names_requested).sort();
let names_in_group = httpServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort()
assert.deepEqual(names_requested, names_in_group);
httpServer.removeAllListeners('instance_connection');
done();
}
};
httpServer.on('instance_connection', connectionHandler);
let i = 0;
while (i < httpServerTorPool.instances.length) {
await request({
url: 'https://example.com',
proxy: `http://foo:@127.0.0.1:${httpPort}`
});
i++;
}
})
.then(async () => {
await httpServerTorPool.remove_by_name('instance-4');
httpServer.proxy_by_name.mode = "individual";
})
.catch(done);
});
const regular_request = require('request-promise');
it(`shouldn't be able to send a request without a username`, async function() {
let f = () => {};
try {
await regular_request({
url: 'https://example.com',
proxy: `http://127.0.0.1:${httpPort}`,
timeout: PAGE_LOAD_TIME
});
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f, "statusCode=407", "Did not return 407 status code");
}
});
it(`shouldn't be able to send a request with an incorrect username`, async function() {
let f = () => {};
try {
await regular_request({
url: 'https://example.com',
proxy: `http://blah-blah-blah:@127.0.0.1:${httpPort}`,
timeout: PAGE_LOAD_TIME,
retryStrategy: request.RetryStrategies.NetworkError
});
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f, "statusCode=407", "Did not hang up");
}
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
httpServer.close();
await httpServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
});

531
test/RPCInterface.js Normal file
View file

@ -0,0 +1,531 @@
const _ = require('lodash');
const assert = require('chai').assert;
const Promise = require('bluebird');
const { Provider } = require('nconf');
const nconf = new Provider();
const { Client, JSONSerializer, TCPTransport } = require('multi-rpc');
const getPort = require('get-port');
const temp = require('temp');
const fs = require('fs');
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
const { ControlServer } = require('../');
const { WAIT_FOR_CREATE } = require('./constants');
Promise.promisifyAll(fs);
Promise.promisifyAll(temp);
let rpcControlServer = new ControlServer(nconf);
let rpcControlPort;
let rpcClient;
describe('ControlServer - RPC Interface', function () {
before('setup control server', async function () {
rpcControlPort = await getPort();
await rpcControlServer.listen(rpcControlPort);
rpcClient = new Client(new TCPTransport(new JSONSerializer(), rpcControlPort));
});
describe('#createInstances(number_of_instances)', function () {
this.timeout(WAIT_FOR_CREATE*2);
it('should create an instance', async function () {
await rpcClient.invoke('createInstances', [{ Name: 'instance-1', Group: "foo" }]);
});
});
describe('#queryInstanceNames()', function () {
it("should have an instance named \"instance-1\"", async function () {
let instances = await rpcClient.invoke('queryInstanceNames', [ ]);
assert.deepEqual(instances, [ 'instance-1' ]);
});
});
describe('#queryGroupNames()', function () {
it("should have a group named \"foo\"", async function () {
let groups = await rpcClient.invoke('queryGroupNames', [ ]);
assert.deepEqual(groups, [ 'foo' ]);
});
});
describe('#queryInstancesByGroup()', function () {
it("should return an instance named \"instance-1\"", async function () {
let instances = await rpcClient.invoke('queryInstancesByGroup', [ 'foo' ]);
assert.equal(instances.length, 1);
assert.ok(instances[0]);
assert.equal(instances[0].name, 'instance-1');
});
});
describe('#queryInstances()', function () {
this.timeout(3000);
it('should return a list of instances', async function () {
let instances = await rpcClient.invoke('queryInstances', []);
assert.isArray(instances, 'Did not return an array');
assert.isNotEmpty(instances, 'Returned an empty array');
assert.isTrue(instances.every((instance) => ( typeof(instance.name) !== 'undefined' ) && ( instance.name !== null )), 'Objects were not valid');
});
});
describe('#addInstances(definitions)', function () {
this.timeout(WAIT_FOR_CREATE);
it("should add an instance based on a defintion", async function () {
let def = {
Name: 'instance-2',
Group: 'bar'
};
await rpcClient.invoke('addInstances', [ def ]);
});
it("tor pool should now contain and instance that has the same name as the name specified in the defintion", function () {
assert.ok(rpcControlServer.tor_pool.instance_by_name('instance-2'));
});
});
describe('#queryInstanceByName(instance_name)', function () {
this.timeout(3000);
it('should return a single instance by name', async function () {
let instance = await rpcClient.invoke('queryInstanceByName', ['instance-1']);
assert.isOk(instance);
});
});
describe('#queryInstanceAt(index)', function () {
this.timeout(3000);
it('should return a single instance by index', async function () {
let instance = await rpcClient.invoke('queryInstanceAt', [0]);
assert.isOk(instance);
});
});
describe('#addInstanceToGroupByName()', function () {
it(`should add "instance-1" to baz`, async function () {
await rpcClient.invoke('addInstanceToGroupByName', [ 'baz', "instance-1" ]);
});
it('"instance-1" should be added to "baz"', function () {
assert.include(rpcControlServer.tor_pool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
after('remove from group', function () {
rpcControlServer.tor_pool.groups['baz'].remove_by_name('instance-1');
});
});
describe('#addInstanceToGroupAt()', function () {
it(`should add "instance-1" to baz`, async function () {
await rpcClient.invoke('addInstanceToGroupAt', [ 'baz', 0 ]);
});
it('"instance-1" should be added to "baz"', function () {
assert.include(rpcControlServer.tor_pool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
after('remove from group', function () {
rpcControlServer.tor_pool.groups['baz'].remove_by_name('instance-1');
});
});
describe('#removeInstanceFromGroupByName()', function () {
before('add to group', function () {
rpcControlServer.tor_pool.groups['baz'].add_by_name('instance-1');
});
it(`should remove "instance-1" from baz`, async function () {
await rpcClient.invoke('removeInstanceFromGroupByName', [ 'baz', "instance-1" ]);
});
it('"instance-1" should be remove from to "baz"', function () {
assert.notInclude(rpcControlServer.tor_pool.group_names, "baz");
});
});
describe('#removeInstanceFromGroupAt()', function () {
before('add to group', function () {
rpcControlServer.tor_pool.groups['baz'].add_by_name('instance-1');
});
it(`should remove "instance-1" from baz`, async function () {
await rpcClient.invoke('removeInstanceFromGroupAt', [ 'baz', 0 ]);
});
it('"instance-1" should be remove from to "baz"', function () {
assert.notInclude(rpcControlServer.tor_pool.group_names, "baz");
});
});
describe('#newIdentites()', function () {
this.timeout(3000);
it('should request new identities for all instances', async function () {
await rpcClient.invoke('newIdentites', []);
});
});
describe('#newIdentityByName(instance_name)', function () {
this.timeout(3000);
it('should request new identities for all instances', async function () {
await rpcClient.invoke('newIdentityByName', ['instance-1']);
});
});
describe('#newIdentityAt(index)', function () {
this.timeout(3000);
it('should request new identities for all instances', async function () {
await rpcClient.invoke('newIdentityAt', [0]);
});
});
describe('#newIdentitiesByGroup()', function () {
it(`should get new identites for all instances in group`, async function () {
await rpcClient.invoke('newIdentitiesByGroup', [ 'foo' ]);
});
});
describe("#setTorConfig(config_object)", function () {
this.timeout(3000);
it('should set several config variables on all instances', async function () {
await rpcClient.invoke('setTorConfig', [ { TestSocks: 1, ProtocolWarnings: 1 } ]);
});
it('all instances should have the modified variables', async function() {
await Promise.all(rpcControlServer.tor_pool.instances.map(async (instance) => {
let var1 = await instance.get_config('TestSocks');
let var2 = await instance.get_config('ProtocolWarnings');
assert.equal(var1, 1);
assert.equal(var2, 1);
}));
});
after('unset config variables', async function () {
await rpcControlServer.tor_pool.set_config_all('TestSocks', 0);
await rpcControlServer.tor_pool.set_config_all('ProtocolWarnings', 0);
});
});
describe('#setConfig(key, value)', function () {
it('should set the default config of new instances', async function () {
this.timeout(3000);
await rpcClient.invoke('setConfig', [ 'foo', 'bar' ]);
});
it('a new instance should be created with the modified property', function () {
assert.equal(nconf.get('foo'), 'bar');;
});
after('remove instance', function () {
nconf.reset();
});
});
describe('#setTorConfigByGroup()', function () {
it(`should set the config value on all instances`, async function () {
await rpcClient.invoke('setTorConfigByGroup', [ 'foo', { 'ProtocolWarnings': 1 } ]);
});
it('all instances should have the config value set', async function () {
let values = _.flatten(await Promise.all(rpcControlServer.tor_pool.instances_by_group('foo').map((i) => i.get_config('ProtocolWarnings'))));
assert.isTrue(values.every((v) => v === "1"));
});
after('unset config values', async function () {
rpcControlServer.tor_pool.set_config_by_group('foo', 'ProtocolWarnings', 0);
});
});
describe('#getConfig()', function () {
before('set a property', function () {
nconf.set('foo', 'bar');
});
it('should return the property that was set', async function () {
this.timeout(6000);
let value = await rpcClient.invoke('getConfig', [ 'foo' ]);
assert.equal(value, 'bar');
});
after('unset property', function () {
nconf.reset();
});
});
describe('#saveConfig()', function () {
let file_path;
before('set a property and create temp file', async function () {
file_path = temp.path({ suffix: '.json' });
nconf.remove('memory');
nconf.file({ file: file_path });
nconf.set('foo', 'bar');
});
it('should save the config to the the temp file', async function () {
this.timeout(6000);
await rpcClient.invoke('saveConfig', []);
});
it('the temp file should contain the property', async function () {
let tmp_file = await fs.readFileAsync(file_path, 'utf8');
let tmp_json = JSON.parse(tmp_file);
assert.equal(tmp_json.foo, 'bar');
});
after('unset property and delete file', async function () {
nconf.remove('file');
nconf.use('memory');
nconf.reset();
await fs.unlinkAsync(file_path);
});
});
describe('#loadConfig()', function () {
let file;
before('create temp file with property', async function () {
file = await temp.openAsync({ suffix: '.json' });
await fs.writeFileAsync(file.fd, JSON.stringify({ foo: 'bar' }));
nconf.remove('memory');
nconf.file({ file: file.path });
});
it('should load the config from the the temp file', async function () {
this.timeout(6000);
await rpcClient.invoke('loadConfig', []);
});
it("the application's config should contain the property", async function () {
assert.equal(nconf.get('foo'), 'bar');
});
after('unset property and delete file', async function () {
nconf.remove('file');
nconf.use('memory');
nconf.reset();
await fs.unlinkAsync(file.path);
});
});
describe('#getLoadBalanceMethod()', function () {
this.timeout(3000);
before(function () {
rpcControlServer.tor_pool.load_balance_method = 'round_robin';
});
it('should return the current load balance method', async function () {
let lb_method = await rpcClient.invoke('getLoadBalanceMethod', []);
assert.equal(lb_method, 'round_robin');
});
});
describe('#setLoadBalanceMethod(load_balance_method)', function () {
this.timeout(3000);
it('should set the load balance method', async function () {
await rpcClient.invoke('setLoadBalanceMethod', ['weighted']);
});
it('the load balance method should be changed', function () {
assert.equal(rpcControlServer.tor_pool.load_balance_method, 'weighted');
});
after(function () {
rpcControlServer.tor_pool.load_balance_method = 'round_robin';
});
});
describe("#getInstanceConfigByName(instance_name)", function () {
this.timeout(3000);
before('set config property', async function () {
await rpcControlServer.tor_pool.instance_by_name('instance-1').set_config('TestSocks', 1);
});
it('should retrieve the property from the tor instance', async function () {
let values = await rpcClient.invoke('getInstanceConfigByName', ['instance-1', "TestSocks"]);
assert.isNotEmpty(values);
assert.equal(values[0], "1");
});
after('unset config property', async function () {
await rpcControlServer.tor_pool.instance_by_name('instance-1').set_config('TestSocks', 0);
});
});
describe("#getInstanceConfigAt(index)", function () {
this.timeout(3000);
before('set config property', async function () {
await rpcControlServer.tor_pool.instance_at(0).set_config('TestSocks', 1);
});
it('should retrieve the property from the tor instance', async function () {
let values = await rpcClient.invoke('getInstanceConfigAt', [0, "TestSocks"]);
assert.isNotEmpty(values);
assert.equal(values[0], "1");
});
after('unset config property', async function () {
await rpcControlServer.tor_pool.instance_at(0).set_config('TestSocks', 0);
});
});
describe("#setInstanceConfigByName(instance_name)", function () {
this.timeout(3000);
before('set config property', async function () {
await rpcControlServer.tor_pool.instance_by_name('instance-1').set_config('TestSocks', 0);
});
it('should set the property for the tor instance', async function () {
await rpcClient.invoke('setInstanceConfigByName', ['instance-1', 'TestSocks', 1]);
});
it('tor instance should have the modified property', async function () {
let value = await rpcControlServer.tor_pool.instance_by_name('instance-1').get_config('TestSocks');
assert.equal(value, 1);
});
after('unset config property', async function () {
await rpcControlServer.tor_pool.instance_by_name('instance-1').set_config('TestSocks', 0);
});
});
describe("#setInstanceConfigAt(index)", function () {
this.timeout(3000);
before('set config property', async function () {
await rpcControlServer.tor_pool.instance_at(0).set_config('TestSocks', 0);
});
it('should set the property for the tor instance', async function () {
await rpcClient.invoke('setInstanceConfigAt', [0, 'TestSocks', 1]);
});
it('tor instance should have the modified property', async function () {
let value = await rpcControlServer.tor_pool.instance_at(0).get_config('TestSocks');
assert.equal(value, 1);
});
after('unset config property', async function () {
await rpcControlServer.tor_pool.instance_at(0).set_config('TestSocks', 0);
});
});
describe('#signalAllInstances(signal)', function () {
this.timeout(3000);
it('should signal to all interfaces', async function () {
await rpcClient.invoke('signalAllInstances', [ 'DEBUG' ]);
});
});
describe('#signalInstanceAt(signal)', function () {
this.timeout(3000);
it('should signal to all interfaces', async function () {
await rpcClient.invoke('signalInstanceAt', [ 0, 'DEBUG' ]);
});
});
describe('#signalAllInstances(signal)', function () {
this.timeout(3000);
it('should signal to all interfaces', async function () {
await rpcClient.invoke('signalInstanceByName', [ 'instance-1', 'DEBUG' ]);
});
});
describe('#signalInstancesByGroup()', function () {
it(`should get new identites for all instances in group`, async function () {
await rpcClient.invoke('signalInstancesByGroup', [ 'foo', 'DEBUG' ]);
});
});
describe("#nextInstance()", function () {
this.timeout(3000);
let instance_name;
it('should rotate the 0th item in the instances array', async function () {
instance_name = rpcControlServer.tor_pool.instances[0].instance_name;
await rpcClient.invoke('nextInstance', []);
});
it('0th item in the instances array should be different after nextInstance is called', function () {
assert.notEqual(rpcControlServer.tor_pool.instances[0].instance_name, instance_name);
});
});
describe('#nextInstanceByGroup(group)', function () {
before('add "instance-1" to "foo"', function () {
rpcControlServer.tor_pool.add_instance_to_group_by_name('foo', 'instance-2');
});
it('should rotate the instances in group "foo"', async function () {
this.timeout(5000);
let first_instance_name_before = rpcControlServer.tor_pool.groups['foo'][0].instance_name;
await rpcClient.invoke('nextInstanceByGroup', [ 'foo' ]);
let first_instance_name_after = rpcControlServer.tor_pool.groups['foo'][0].instance_name;
assert.notEqual(first_instance_name_after, first_instance_name_before);
});
after('remove "instance-1" from "foo"', function () {
rpcControlServer.tor_pool.remove_instance_from_group_by_name('foo', 'instance-2');
})
});
var instance_num1, instance_num2, i_num;
describe('#removeInstanceAt(index)', function () {
this.timeout(10000);
it("should remove an instance at the position specified", async function () {
instance_num1 = rpcControlServer.tor_pool.instances.length;
await rpcClient.invoke('removeInstanceAt', [0]);
});
it('the tor pool should contain one instance fewer', function () {
assert.equal(rpcControlServer.tor_pool.instances.length, (instance_num1 - 1));
});
});
describe('#removeInstanceByName(instance_name)', function () {
this.timeout(10000);
it("should remove an instance at the position specified", async function () {
instance_num2 = rpcControlServer.tor_pool.instances.length;
await rpcClient.invoke('removeInstanceByName', [ "instance-1" ]);
});
it('the tor pool should contain one instance fewer', function () {
assert.equal(rpcControlServer.tor_pool.instances.length, (instance_num2 - 1));
});
});
describe('#closeInstances()', function () {
this.timeout(10000);
it('should shutdown all instances', async function () {
instance_num = rpcControlServer.tor_pool.instances.length;
await rpcClient.invoke('closeInstances', [ ]);
});
it('no instances should be present in the pool', function () {
assert.equal(rpcControlServer.tor_pool.instances.length, 0);
});
});
after('shutdown tor pool', async function () {
this.timeout(10000);
await rpcControlServer.tor_pool.exit();
});
});

234
test/SOCKSServer.js Normal file
View file

@ -0,0 +1,234 @@
const { Provider } = require('nconf');
const nconf = new Provider();
const getPort = require('get-port');
const { HttpAgent, auth } = require('socksv5');
const { assert } = require('chai');
const _ = require('lodash');
const SocksProxyAgent = require('socks-proxy-agent');
const { TorPool, SOCKSServer, TorProcess } = require('../');
const { WAIT_FOR_CREATE, PAGE_LOAD_TIME, RETRY_DELAY, RETRY_STRATEGY, MAX_ATTEMPTS, SHUTDOWN_DELAY, SHUTDOWN_TIMEOUT } = require('./constants');
const request = require('requestretry').defaults({
promiseFactory: ((resolver) => new Promise(resolver)),
maxAttempts: MAX_ATTEMPTS,
retryStrategy: RETRY_STRATEGY,
retryDelay: RETRY_DELAY,
timeout: PAGE_LOAD_TIME
});
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
describe('SOCKSServer', function () {
describe('#handleConnection(socket)', function () {
let socksPort;
let socksServerTorPool;
let socksServer;
before('start up server', async function (){
socksServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
socksServer = new SOCKSServer(socksServerTorPool, null, false);
this.timeout(WAIT_FOR_CREATE);
await socksServerTorPool.create(1);
socksPort = await getPort();
await socksServer.listen(socksPort);
});
it('should service a request for example.com', async function () {
this.timeout(PAGE_LOAD_TIME);
await request({
url: 'http://example.com',
agent: new HttpAgent({
proxyHost: '127.0.0.1',
proxyPort: socksPort,
localDNS: false,
auths: [ auth.None() ]
})
});
});
it('should emit the "instance_connection" event', function (done) {
this.timeout(PAGE_LOAD_TIME);
let connectionHandler = (instance, source) => {
assert.instanceOf(instance, TorProcess);
assert.isObject(source);
socksServer.removeAllListeners('instance_connection');;
done();
};
socksServer.on('instance_connection', connectionHandler);
request({
url: 'http://example.com',
agent: new HttpAgent({
proxyHost: '127.0.0.1',
proxyPort: socksPort,
localDNS: false,
auths: [ auth.None() ]
})
})
.catch(done)
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
socksServer.close();
await socksServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
describe('#authenticate_user(username, password)', function () {
let socksPort;
let socksServerTorPool;
let socksServer;
let instance_def = {
Name: 'instance-3',
Group: "foo"
};
before('start up server', async function (){
socksServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
socksServer = new SOCKSServer(socksServerTorPool, null, { deny_unidentified_users: true, mode: "individual" });
this.timeout(WAIT_FOR_CREATE * 3);
await socksServerTorPool.create(2);
await socksServerTorPool.add(instance_def);
socksPort = await getPort();
await socksServer.listen(socksPort);
});
it(`should service a request for example.com through ${instance_def.Name}`, function (done) {
this.timeout(PAGE_LOAD_TIME);
let connectionHandler = (instance, source) => {
assert.equal(instance.instance_name, instance_def.Name);
assert.isTrue(source.by_name);
socksServer.removeAllListeners('instance_connection');
done();
};
socksServer.on('instance_connection', connectionHandler);
request({
url: 'http://example.com',
agent: new HttpAgent({
proxyHost: '127.0.0.1',
proxyPort: socksPort,
localDNS: false,
auths: [ auth.UserPassword(instance_def.Name, "doesn't mater") ]
})
})
.catch(done);
});
it(`four requests made to example.com through the group named "foo" should come from the instances in "foo"`, function (done) {
(async () => {
this.timeout((PAGE_LOAD_TIME * 4) + (WAIT_FOR_CREATE));
await socksServerTorPool.add([
{
Name: 'instance-4',
Group: 'foo'
}
]);
})()
.then(async () => {
socksServer.proxy_by_name.mode = "group";
let names_requested = [];
let connectionHandler = (instance, source) => {
names_requested.push(instance.instance_name);
if (names_requested.length === socksServerTorPool.instances.length) {
names_requested = _.uniq(names_requested).sort();
let names_in_group = socksServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort()
assert.deepEqual(names_requested, names_in_group);
socksServer.removeAllListeners('instance_connection');
done();
}
};
socksServer.on('instance_connection', connectionHandler);
let i = 0;
while (i < socksServerTorPool.instances.length) {
await request({
url: 'http://example.com',
agent: new HttpAgent({
proxyHost: '127.0.0.1',
proxyPort: socksPort,
localDNS: false,
auths: [ auth.UserPassword('foo', "doesn't mater") ]
})
});
i++;
}
})
.then(async () => {
await socksServerTorPool.remove_by_name('instance-4');
socksServer.proxy_by_name.mode = "individual";
})
.catch(done);
});
const regular_request = require('request-promise');
it(`shouldn't be able to send a request without a username`, async function() {
let f = () => {};
try {
await regular_request({
url: 'http://example.com',
agent: new SocksProxyAgent(`socks5h://127.0.0.1:${socksPort}`),
timeout: PAGE_LOAD_TIME
});
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f);
}
});
it(`shouldn't be able to send a request with an incorrect username`, async function() {
let f = () => {};
try {
await regular_request({
url: 'http://example.com',
agent: new SocksProxyAgent(`socks5h://blah-blah-blah:@127.0.0.1:${socksPort}`),
timeout: PAGE_LOAD_TIME
});
} catch (error) {
f = () => { throw error };
} finally {
assert.throws(f);
}
});
after('shutdown server and shutdown tor pool', function (done) {
this.timeout(SHUTDOWN_TIMEOUT);
setTimeout(async () => {
socksServer.close();
await socksServerTorPool.exit();
done();
}, SHUTDOWN_DELAY);
});
});
});

859
test/TorPool.js Normal file
View file

@ -0,0 +1,859 @@
const { Provider } = require('nconf');
const nconf = new Provider();
const { assert } = require('chai');
const Promise = require('bluebird');
const _ = require('lodash');
const { TorPool } = require('../');
const { WAIT_FOR_CREATE } = require('./constants');
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
describe('TorPool', function () {
const torPoolFactory = () => new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null);
describe('#default_tor_config', function () {
let tor_pool;
let cfg = { "TestSocks": 1, "Log": "notice stdout", "NewCircuitPeriod": "10" };
before('create tor config based on nconf', function () {
nconf.set('torConfig', cfg);
tor_pool = new TorPool(nconf.get('torPath'), (() => nconf.get('torConfig')), nconf.get('parentDataDirectory'), 'round_robin', null);
});
it('the tor config should have the same value as set in nconf', function () {
assert.deepEqual(nconf.get('torConfig'), tor_pool.default_tor_config);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
before('create tor config based on nconf', function () {
tor_pool = new TorPool(nconf.get('torPath'), cfg, nconf.get('parentDataDirectory'), 'round_robin', null);
});
it('the tor config should have the same value as set', function () {
assert.deepEqual(cfg, tor_pool.default_tor_config);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#create_instance(instance_defintion)', function () {
let instance_defintion = {
Name: 'instance-1',
Config: {
ProtocolWarnings: 1
}
};
let torPool;
before('create tor pool', () => { torPool = torPoolFactory(); })
it('should create one tor instance based on the provided definition', async function () {
this.timeout(WAIT_FOR_CREATE);
await torPool.create_instance(instance_defintion);
});
it('one instance should exist in the instances collection', function () {
assert.equal(1, torPool.instances.length);
});
it('the created instance should have the defintion properties as the input definition', function () {
assert.deepEqual(instance_defintion, torPool.instances[0].definition);
});
it('the created instance should have the same config properties specified in the definiton', async function () {
let value = await torPool.instances[0].get_config('ProtocolWarnings');
assert.equal(value, instance_defintion.Config.ProtocolWarnings);
});
it('should not be able to create an instance with an existing name', async function () {
this.timeout(WAIT_FOR_CREATE * 2);
let fn = () => {}
try {
await torPool.create_instance({ Name: 'foo' });
await torPool.create_instance({ Name: 'foo' });
}
catch (error) {
fn = () => { throw error };
} finally {
assert.throws(fn);
}
});
after('shutdown tor pool', async function () {
await torPool.exit();
});
});
describe('#add(instance_defintions)', function () {
let instance_defintions = [
{ Name: 'instance-1', Group: [], Config: { ProtocolWarnings: 1} },
{ Name: 'instance-2', Group: [], Config: { ProtocolWarnings: 1 } }
];
let torPool;
before('create tor pool', () => { torPool = torPoolFactory(); })
it('should throw if the "instance_defintions" field is falsy', async function () {
let fn = () => {};
try {
await torPool.add();
} catch (error) {
fn = () => { throw error };
}
finally {
assert.throws(fn);
}
});
it('should create instances from several instance definitions', async function () {
this.timeout(WAIT_FOR_CREATE*2);
await torPool.add(_.cloneDeep(instance_defintions))
});
it('2 instances should exist in the pool', function () {
assert.equal(2, torPool.instances.length);
});
it('the created instances should have the same defintion properties as the input definitions', function () {
let live_instance_definitions = torPool.instances.map((instance) => {
let def_clone = _.cloneDeep(instance.definition);
delete def_clone.Config.DataDirectory;
return def_clone;
}).sort((a,b) => (a.Name > b.Name) ? 1 : ((b.Name > a.Name) ? -1 : 0));
assert.deepEqual(instance_defintions, live_instance_definitions);
});
it('the created instances should have the same config properties specified in the definiton', async function () {
this.timeout(10000);
let values = await Promise.all(torPool.instances.map((instance) => instance.get_config('ProtocolWarnings')));
values = _.flatten(values);
assert.isTrue( values.every((value) => value === "1") );
});
after('shutdown tor pool', async function () {
await torPool.exit();
});
});
describe('#create(instances)', function () {
let torPool;
before('create tor pool', () => {
torPool = torPoolFactory();
torPool.default_tor_config = { TestSocks: 1 };
});
it('should throw if the "number_of_instances" field is falsy', async function () {
let fn = () => {};
try {
await torPool.create();
} catch (error) {
fn = () => { throw error; }
}
finally {
assert.throws(fn);
}
});
it('should create 2 instances with the default config', async function () {
this.timeout(WAIT_FOR_CREATE*2);
await torPool.create(2);
});
it('2 instances should exist in the pool', function () {
assert.equal(2, torPool.instances.length);
});
it('the created instances should have the same config properties specified in the default config', async function () {
this.timeout(10000);
let values = await Promise.all(torPool.instances.map((instance) => instance.get_config('TestSocks')));
values = _.flatten(values);
assert.isTrue( values.every((value) => value === "1") );
});
after('shutdown tor pool', async function () {
torPool.default_tor_config = {};
await torPool.exit();
});
});
describe('#instances_by_group(group)', function () {
let tor_pool;
let instances;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instances = (await tor_pool.add([ { Name: 'instance-1', Group: [ "bar", "foo" ] }, { Name: 'instance-2', Group: ["foo", "bar"] } ]));
});
it('should throw if the provided group does not exist', function () {
assert.throws(() => {
tor_pool.instances_by_group('baz');
});
});
it('should return both instances', function () {
assert.deepEqual(tor_pool.instances_by_group('bar'), instances);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#next()', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 3);
await torPool.add([
{
Name: 'instance-1',
Weight: 50,
Group: 'foo'
},
{
Name: 'instance-2',
Weight: 25,
Group: 'bar'
},
{
Name: 'instance-3',
Weight: 2,
Group: 'bar'
}
]);
});
it('result of next should be different if run twice', function () {
let t1 = torPool.next().instance_name;
let t2 = torPool.next().instance_name;
assert.notEqual(t1, t2);
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#next_by_group(group)', function () {
let tor_pool;
before('create tor pool', async function () {
tor_pool = new TorPool(nconf.get('torPath'), (() => nconf.get('torConfig')), nconf.get('parentDataDirectory'), 'round_robin', null);
this.timeout(WAIT_FOR_CREATE * 3);
await tor_pool.add([
{ Group: 'foo' },
{ Group: 'bar' },
{ Group: 'bar' }
]);
});
it('after rotating the first instance in group should be different', function () {
let first_instance_name_before = tor_pool.groups['bar'][0].instance_name;
tor_pool.next_by_group('bar');
let first_instance_name_after = tor_pool.groups['bar'][0].instance_name;
assert.notEqual(first_instance_name_after, first_instance_name_before);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#instance_by_name(instance_name)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add([
{
Name: 'instance-1'
}
]);
});
it('should retrieve instance by name', function () {
assert.ok(torPool.instance_by_name('instance-1'));
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#remove_by_name(instance_name)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add([
{
Name: 'instance-3'
}
]);
});
it('should remove instance by name', async function () {
this.timeout(5000);
await torPool.remove_by_name('instance-3');
assert.notInclude(torPool.instance_names, "instance-3");
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#instance_at(index)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create(1);
});
it('should retrieve an instance by id', function () {
this.timeout(5000);
assert.ok(torPool.instance_at(0));
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#remove_at(index)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE );
await torPool.create(1);
});
it('should remove an instance by id', async function () {
this.timeout(5000);
await torPool.remove_at(0);
assert.notOk(torPool.instances[0]);
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#new_identites()', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 2);
await torPool.create(2);
});
it('should signal to retrieve a new identity to all instances', async function () {
this.timeout(5000);
await torPool.new_identites();
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#new_identites_by_group(group)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 3);
await torPool.add([
{ Name: 'instance-1', Group: 'bar' },
{ Name: 'instance-2', Group: 'foo' }
]);
});
it('should signal to retrieve a new identity to all instances', async function () {
this.timeout(5000);
await torPool.new_identites_by_group('bar');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#new_identity_at(index)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create(1);
});
it('should signal to retrieve a new identity identified by index', async function () {
this.timeout(5000);
await torPool.new_identity_at(0);
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#new_identity_by_name(instance_name)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add({ Name: 'instance-1' });
});
it('should signal to retrieve a new identity identified by name', async function () {
this.timeout(5000);
await torPool.new_identity_by_name('instance-1');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#set_config_all(keyword, value)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 2);
await torPool.create(2);
});
it('should set configuration on all active instances', async function () {
this.timeout(5000);
await torPool.set_config_all('TestSocks', 1);
});
it('all instances should contain the same changed configuration', async function () {
this.timeout(5000);
let values = await Promise.all(torPool.instances.map((instance) => instance.get_config('TestSocks')));
values = _.flatten(values);
assert.isTrue( values.every((value) => value === "1") );
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#set_config_by_group(group, keyword, value)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add([ { Name: 'instance-1', Group: 'foo' }, { Name: 'instance-2', Group: 'bar' } ]);
});
it('should set configuration on all active instances', async function () {
this.timeout(5000);
await torPool.set_config_by_group('bar', 'TestSocks', 1);
});
it('all instances should contain the same changed configuration', async function () {
this.timeout(5000);
let values_from_bar = await Promise.all(torPool.instances_by_group('bar').map((instance) => instance.get_config('TestSocks')));
values_from_bar = _.flatten(values_from_bar);
assert.isTrue( values_from_bar.every((value) => value === "1") );
let values_from_foo = await Promise.all(torPool.instances_by_group('foo').map((instance) => instance.get_config('TestSocks')));
values_from_foo = _.flatten(values_from_foo);
assert.isTrue( values_from_foo.every((value) => value !== "1") );
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#set_config_by_name(name, keyword, value)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add({ Name: 'instance-1' });
});
it('should set a configuration property of an instance identified by name', async function () {
this.timeout(5000);
await torPool.set_config_by_name('instance-1', 'ProtocolWarnings', 1);
});
it('should have the set value', async function () {
this.timeout(5000);
let value = _.flatten(await torPool.get_config_by_name('instance-1', 'ProtocolWarnings'))[0];
assert.equal(value, '1');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#get_config_by_name(name, keyword)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add({ Name: 'instance-1', Config: { ProtocolWarnings: 1 } });
});
it('should get retrieve the configuration of an instance identified by name', async function () {
this.timeout(5000);
let value = _.flatten(await torPool.get_config_by_name('instance-1', 'ProtocolWarnings'))[0];
assert.equal(value, '1');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#set_config_at(index, keyword, value)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create(1);
});
it('should set a configuration property of an instance identified by index', async function () {
this.timeout(5000);
await torPool.set_config_at(0, 'ProtocolWarnings', 1);
});
it('should have the set value', async function () {
this.timeout(5000);
let value = _.flatten(await torPool.get_config_at(0, 'ProtocolWarnings'))[0];
assert.equal(value, '1');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#get_config_at(index, keyword)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.add({ Config: { ProtocolWarnings: 1 } });
});
it('should get retrieve the configuration of an instance identified by name', async function () {
this.timeout(5000);
let value = _.flatten(await torPool.get_config_at(0, 'ProtocolWarnings'))[0];
assert.equal(value, '1');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#signal_all(signal)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create(1);
});
it('should send a signal to all instances', async function () {
this.timeout(5000);
await torPool.signal_all('DEBUG');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#signal_by_name(name, signal)', async function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create({ Name: 'instance-1' });
});
it('should send a signal to an instance identified by name', async function () {
this.timeout(5000);
await torPool.signal_by_name('instance-1', 'DEBUG');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#signal_by_group(group, name, signal)', async function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create([ { "Name": "instance-1", "Group": ["foo"] }, { "Name": "instance-2", "Group": ["foo"] } ]);
});
it('should send a signal to a group of instances', async function () {
this.timeout(5000);
await torPool.signal_by_group('foo', 'DEBUG');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#signal_at(index, signal)', function () {
let torPool;
before('create tor pool', async function () {
torPool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
await torPool.create(1);
});
it('should send a signal to an instance identified by index', async function () {
this.timeout(5000);
await torPool.signal_at(0, 'DEBUG');
});
after('shutdown tor pool', async function () { await torPool.exit(); });
});
describe('#group_names', function () {
let tor_pool;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 2);
await tor_pool.add([
{ Name: 'instance-1', Group: [ "foo", "bar" ] },
{ Name: 'instance-2', Group: "baz" }
]);
});
it('the pool should contain three groups, bar, baz and foo', function () {
assert.deepEqual(tor_pool.group_names, (new Set([ "bar", "baz", "foo" ])));
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#add_instance_to_group(group, instance)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.create(1))[0];
});
it('should add the instance to the group successfully', function () {
tor_pool.add_instance_to_group("foo", instance);
});
it('the instance should be added to the group', function () {
assert.deepEqual(instance.instance_group, ["foo"]);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#add_instance_to_group_by_name(group, instance_name)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.add({ Name: 'instance-1' }))[0];
});
it('should add the instance to the group successfully', function () {
tor_pool.add_instance_to_group_by_name("foo", instance.definition.Name);
});
it('the instance should be added to the group', function () {
assert.deepEqual(instance.instance_group, ["foo"]);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#add_instance_to_group_at(group, instance_index)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.create(1))[0];
});
it('should add the instance to the group successfully', function () {
tor_pool.add_instance_to_group_at("foo", 0);
});
it('the instance should be added to the group', function () {
assert.deepEqual(instance.instance_group, ["foo"]);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#remove_instance_from_group(group, instance)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.add({ Group: "foo" }))[0];
});
it('should remove the instance from the group successfully', function () {
tor_pool.remove_instance_from_group("foo", instance);
});
it('the instance should be no longer be in the group', function () {
assert.notInclude(instance.instance_group, "foo");
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#remove_instance_from_group_by_name(group, instance_name)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.add({ Name: 'instance-1', Group: "foo" }))[0];
});
it('should remove the instance from the group successfully', function () {
tor_pool.remove_instance_from_group_by_name("foo", instance.definition.Name);
});
it('the instance should no longer be in the group', function () {
assert.notInclude(instance.instance_group, "foo");
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#remove_instance_from_group_at(group, instance_index)', function () {
let tor_pool;
let instance;
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE);
instance = (await tor_pool.add({ Group: "foo" }))[0];
});
it('should remove the instance from the group successfully', function () {
tor_pool.remove_instance_from_group_at("foo", 0);
});
it('the instance should no longer be in the group', function () {
assert.notInclude(instance.instance_group, "foo");
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
describe('#groups', function () {
let tor_pool;
let instances;
let get_instance_names = (group_name) => {
let instances = [];
let group = tor_pool.groups[group_name];
for (let i = 0; i < group.length; i++)
instances.push(group[i]);
return instances.map((i) => i.instance_name).sort();
};
before('create tor pool', async function () {
tor_pool = torPoolFactory();
this.timeout(WAIT_FOR_CREATE * 3);
instances = (await tor_pool.add([
{ Group: ["foo", "flob"], Name: 'instance-1' },
{ Group: ["bar", "baz"], Name: 'instance-2' },
{ Group: ["flob"], Name: 'instance-3' }
]));
});
it('should contain three groups, bar, baz and foo', function () {
assert.deepEqual(Array.from(tor_pool.group_names).sort(), [ "bar", "baz", "flob", "foo" ]);
});
it('#[Number] - the 1st element should be "instance-1"', function () {
assert.equal(tor_pool.groups["foo"][0], instances[0]);
});
it('#length() - group "foo" should contain 1 instance', function () {
assert.equal(tor_pool.groups["foo"].length, 1);
});
it('#add() - adding "instance-1" to "baz" should result in "baz" having "instance-1" and "instance-2"', function () {
tor_pool.groups["baz"].add(instances[0]);
assert.deepEqual(get_instance_names("baz"), [ "instance-1", "instance-2" ] );
});
it('#remove() - removing "instance-1" firom "baz" should result in "baz" having just "instance-2"', function () {
tor_pool.groups["baz"].remove(instances[0]);
assert.deepEqual(get_instance_names("baz"), [ "instance-2" ] );
});
it('#add_by_name() - adding "instance-1" to "baz" should result in "baz" having "instance-1" and "instance-2"', function () {
tor_pool.groups["baz"].add_by_name('instance-1');
assert.deepEqual(get_instance_names("baz"), [ "instance-1", "instance-2" ] );
});
it('#remove_by_name() - removing "instance-1" from "baz" should result in "baz" having just "instance-2"', function () {
tor_pool.groups["baz"].remove_by_name('instance-1');
assert.deepEqual(get_instance_names("baz"), [ "instance-2" ] );
});
it('#remove_at() - removing "instance-1" from "baz" should result in "baz" having just "instance-2"', function () {
tor_pool.groups["baz"].add_by_name('instance-1');
tor_pool.groups["baz"].remove_at(0);
assert.deepEqual(get_instance_names("baz"), [ "instance-1" ] );
});
it('#rotate() - the name of the first instance should change', function () {
let first_instance_name_before = tor_pool.groups["flob"][0].instance_name;
tor_pool.groups["flob"].rotate();
let first_instance_name_after = tor_pool.groups["flob"][0].instance_name;
assert.notEqual(first_instance_name_after, first_instance_name_before);
});
after('shutdown tor pool', async function () { await tor_pool.exit(); });
});
});

200
test/TorProcess.js Normal file
View file

@ -0,0 +1,200 @@
const { Provider } = require('nconf');
const nconf = new Provider();
const { assert } = require('chai');
const _ = require('lodash');
const { TorProcess } = require('../');
const { WAIT_FOR_CREATE } = require('./constants');
nconf.use('memory');
require(`${__dirname}/../src/nconf_load_env.js`)(nconf);
nconf.defaults(require(`${__dirname}/../src/default_config.js`));
describe('TorProcess', function () {
const tor_factory = (d) => {
let defaults = { Config: { DataDirectory: nconf.get('parentDataDirectory') } };
d = _.extend(_.cloneDeep(defaults), (d || {}));
return new TorProcess(nconf.get('torPath'), d, null);
}
describe('#instance_name', function () {
let def = { Name: 'foo' };
before('create tor process', function () {
tor = tor_factory(def);
});
it('should have the same name as the definition', function () {
assert.equal(tor.instance_name, def.Name);
});
});
describe('#instance_group', function () {
let def = { Group: ['foo'] };
before('create tor process', function () {
tor = tor_factory(def);
});
it('should have the same group as the definition', function () {
assert.deepEqual(tor.instance_group, def.Group);
});
});
describe('#definition', function () {
let def = { Group: ['foo'], Config: {} };
before('create tor process', function () {
tor = tor_factory(def);
});
it('should be the same as the definition', function () {
let tor_def = _.cloneDeep(tor.definition);
assert.deepEqual(tor.definition, def);
});
});
describe('#exit()', function () {
let tor
before('create tor process', async function () {
tor = tor_factory();
this.timeout(WAIT_FOR_CREATE);
await tor.create();
});
it('should exit cleanly', async function () {
await tor.exit();
});
it('the process should be dead', function () {
assert.isTrue(tor.process.killed);
});
});
describe('#create()', function () {
let tor
before('create tor process', function () {
tor = tor_factory();
});
it('should create the child process', async function () {
this.timeout(5000);
await tor.create();
});
it('should signal when it is listening on the control port', function (done) {
this.timeout(5000);
if (tor.control_port_listening)
return done();
tor.once('control_listen', done);
});
it('should signal when connected to the control port', function (done) {
this.timeout(5000);
if (tor.control_port_connected)
return done();
tor.once('controller_ready', done);
});
it('should signal when it is listening on the socks port', function (done) {
this.timeout(5000);
if (tor.socks_port_listening)
return done();
tor.once('socks_listen', done);
});
it('should signal when it is listening on the dns port', function (done) {
this.timeout(5000);
if (tor.dns_port_listening)
return done();
tor.once('dns_listen', done);
});
it('should signal when bootstrapped', function (done) {
this.timeout(WAIT_FOR_CREATE);
tor.once('error', done);
if (tor.bootstrapped)
return done();
tor.once('ready', done);
});
after('exit tor', async function () {
this.timeout(5000);
await tor.exit();
});
});
describe('#set_config(keyword, value)', function () {
let tor
before('create tor process', function (done) {
tor = tor_factory();
this.timeout(WAIT_FOR_CREATE);
tor.once('controller_ready', done);
tor.create().catch(done);
});
it('should set sample configuration option via the control protocol', async function () {
await tor.set_config('ProtocolWarnings', 1);
});
it('should have the value set', async function () {
let value = (await tor.get_config('ProtocolWarnings'))[0];
assert.equal(value, "1");
});
after('exit tor', async function () {
await tor.exit();
});
});
describe('#get_config(keyword, value)', function () {
let tor
before('create tor process', function (done) {
tor = tor_factory({ Config: { ProtocolWarnings: 1 } });
this.timeout(WAIT_FOR_CREATE);
tor.once('controller_ready', done);
tor.create().catch(done);
});
it('should retrieve sample configuration option via the control protocol', async function () {
let value = await tor.get_config('ProtocolWarnings');
assert.equal(value, 1);
});
after('exit tor', async function () {
await tor.exit();
});
});
describe('#new_identity()', function () {
before('create tor process', function (done) {
tor = tor_factory();
this.timeout(WAIT_FOR_CREATE);
tor.once('controller_ready', done);
tor.create().catch(done);
});
it('should use a new identity', async function () {
await tor.new_identity();
});
after('exit tor', async function () {
await tor.exit();
});
});
describe('#signal()', function () {
let tor;
before('create tor process', function (done) {
tor = tor_factory();
this.timeout(WAIT_FOR_CREATE);
tor.once('controller_ready', done);
tor.create().catch(done);
});
it('should send a signal via the control protocol', async function () {
await tor.signal('DEBUG');
});
after('exit tor', async function () {
await tor.exit();
});
});
});

9
test/constants.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
WAIT_FOR_CREATE: 600000,
PAGE_LOAD_TIME: 75000,
RETRY_DELAY: 5000,
MAX_ATTEMPTS: 15,
RETRY_STRATEGY: require('requestretry').RetryStrategies.HTTPOrNetworkError,
SHUTDOWN_DELAY: 5000,
SHUTDOWN_TIMEOUT: (this.SHUTDOWN_DELAY * 2)
};

9
test/index.js Normal file
View file

@ -0,0 +1,9 @@
describe("TorRouter", function () {
require('./TorProcess');
require('./TorPool');
require('./SOCKSServer');
require('./HTTPServer');
require('./DNSServer');
require('./ControlServer');
require('./RPCInterface');
});

View file

@ -1,309 +0,0 @@
const SocksAgent = require('socks5-http-client/lib/Agent');
const request = require('request');
const async = require('async');
const temp = require('temp');
temp.track();
const TorRouter = require('../');
const getPort = require('get-port');
const dns = require('native-dns');
const io = require('socket.io-client');
const _ = require('lodash');
const get_ip = function (callback) {
request({
url: 'http://monip.org',
agentClass: SocksAgent,
agentOptions: {
socksHost: '127.0.0.1',
socksPort: this.socks_port
}
}, (error, res, body) => {
var ip;
if (body)
ip = body.split('IP : ').pop().split('<').shift();
callback(error || (!body && new Error("Couldn't grab IP")), ip)
});
};
// describe('ControlServer', function () {
// let ports = {};
// var controlServer;
// var client;
// before((done) => {
// async.autoInject({
// dnsPort: (cb) => { getPort().then((port) => { cb(null, port); }) },
// socksPort: (cb) => { getPort().then((port) => { cb(null, port); }) },
// controlPort: (cb) => { getPort().then((port) => { cb(null, port); }) }
// }, (error, context) => {
// _.extend(ports, context);
// done(error);
// });
// });
// controlServer = new TorRouter.ControlServer();
// describe('#listen(port, callback)', () => {
// it('should listen on the control port', (done) => { controlServer.listen(ports.controlPort, done); })
// it('should connect to control server', (done) => {
// client = io.connect(`ws://127.0.0.1:${ports.controlPort}`);
// client.once('connect_error', (err) => {
// console.log(err)
// done(err);
// });
// client.once('connected', () => {
// done();
// })
// });
// });
// describe('#createTorPool(options)', function () {
// it('should create a tor pool', () => {
// client.emit('createTorPool', {});
// });
// });
// describe('#createSOCKSServer(port)', function () {
// it('should create a socks server', () => {
// client.emit('createSOCKSServer', ports.socksPort);
// });
// });
// describe('#createInstances(instances, callback)', function () {
// this.timeout(Infinity);
// it('should create 1 instance', function (done) {
// client.emit('createInstances', 1, (err) => {
// if (err) return done(error);
// done(((controlServer.torPool.instances.length !== 1) && new Error(`It doesn't have 1 instance`)));
// });
// })
// })
// describe('#newIps()', function (done) {
// var oldip;
// this.timeout(Infinity);
// it('should grab the current ip', (done) => {
// get_ip.call({ socks_port: ports.socksPort })((error, ip) => {
// oldip = ip;
// done(error);
// });
// });
// it('should change the ip', (done) => {
// client.emit('newIps');
// setTimeout(() => {
// done();
// }, 1000);
// });
// it('should have a diffrent ip', (done) => {
// get_ip.call({ socks_port: ports.socksPort })((error, ip) => {
// done(((oldip === ip) && new Error("ip hasn't changed")));
// });
// });
// });
// after(() => {
// controlServer.torPool.exit();
// client.close();
// controlServer.close();
// });
// });
describe('TorProcess', function () {
const TOR_DATA_DIR = temp.mkdirSync();
const TorProcess = TorRouter.TorProcess;
describe('#create()', function () {
var tor = new TorProcess('tor', { DataDirectory: TOR_DATA_DIR });
this.timeout(Infinity);
it('should create the process without an error', function (done) {
tor.create(done);
});
it('should signal ready when bootstrapped', function (done) {
tor.once('error', done);
tor.once('ready', done);
});
after('shutdown tor', function () {
tor.exit();
});
});
describe('#new_ip()', function () {
var tor = new TorProcess('tor', { DataDirectory: TOR_DATA_DIR });
var old_ip = null;
this.timeout(Infinity);
before('create a tor instance', function (done) {
tor.once('error', done);
tor.once('ready', done);
tor.create();
});
it('should have an ip address', function (done) {
get_ip.call({ socks_port: tor.socks_port }, (err, ip) => {
if (err) return done(err);
old_ip = ip;
done(err);
});
});
it('should have a new ip address after sending HUP', function (done) {
tor.new_ip();
setTimeout(() => {
get_ip.call({ socks_port: tor.socks_port }, (err, ip) => {
if (err) return done(err);
if (ip === old_ip)
done(new Error(`IP hasn't changed ${old_ip} === ${ip}`));
else
done();
});
}, (10*1000));
});
after('shutdown tor', function () {
tor.exit();
});
});
});
describe('TorPool', function () {
var TorPool = TorRouter.TorPool;
var pool = new TorPool('tor');
this.timeout(Infinity);
describe('#create', function () {
it('should create two instances without any problems', function (done) {
pool.create(2, function (error) {
if (error) return done(error);
done((pool.instances.length !== 2) && new Error('pool does not have two instances'));
});
});
});
describe('#remove', function () {
it('should remove 2 instances from the pool', function (done) {
pool.remove(2, function (error) {
if (error) return done(error);
done((pool.instances.length) && new Error('pool has two instances'));
})
});
})
after(function () {
pool.exit();
});
})
describe('DNSServer', function () {
var TorPool = TorRouter.TorPool;
var DNSServer = TorRouter.DNSServer;
var port;
var pool = new TorPool('tor');
var dns_server = new DNSServer(pool);
describe('#on("request")', function () {
this.timeout(Infinity);
before((done) => {
getPort().then(($port) => {
port = $port;
dns_server.serve(port);
done()
});
})
it('should startup tor', (done) => {
pool.create(2, done);
})
it('should be able to resolve "google.com" ', function (done) {
let req = dns.Request({
question: dns.Question({ name: 'google.com', type: 'A' }),
server: { address: '127.0.0.1', port: port, type: 'udp' },
timeout: 5000
});
req.on('timeout', () => {
done && done(new Error("Request timed out"));
done = null;
});
req.on('message', (e, m) => {
done && done(((!m) || (!m.answer.length)) && new Error('Unable to resolve host'));
done = null;
});
req.send();
});
after(function () {
pool.exit();
dns_server.close();
});
});
});
describe('SOCKSServer', function () {
var TorPool = TorRouter.TorPool;
var SOCKSServer = TorRouter.SOCKSServer;
var pool = new TorPool('tor');
var socks = new SOCKSServer(pool);
this.timeout(Infinity);
var port;
before((done) => {
getPort().then(($port) => {
port = $port;
socks.listen(port, (done));
});
})
describe('#on("connection")', function () {
var ip;
it('should startup tor ', (done) => {
pool.create(2, done);
})
it('should get an ip address', function (done) {
get_ip.call({ socks_port: port }, (err, _ip) => {
ip = _ip;
done(err || (!ip && new Error('could not get an ip')))
});
});
it('should have a different ip', function (done) {
get_ip.call({ socks_port: port }, (err, _ip) => {
done(err || (!ip && new Error('could not get an ip')) || ((_ip === ip) && new Error('IP has not changed')));
});
});
after(() => {
pool.exit();
socks.close();
});
});
});

View file

@ -1,3 +0,0 @@
deb http://deb.torproject.org/torproject.org xenial main
deb-src http://deb.torproject.org/torproject.org xenial main