├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── contrib
├── .gitignore
└── extract_directories.php
├── examples
├── CurlCountry.php
├── CurlWrapper.php
├── TorDNSEL.php
├── common.php
├── dc_GetAllDescriptors-simple.php
├── dc_GetServerDescriptor-simple.php
├── tc_AsyncEvents.php
├── tc_CreateHiddenService.php
├── tc_GetConf.php
├── tc_GetInfo.php
├── tc_NewNym.php
└── tc_SendData.php
├── phpunit.xml
├── src
├── AuthorityStatusDocument.php
├── CircuitStatus.php
├── ControlClient.php
├── DirectoryClient.php
├── Parser.php
├── ProtocolError.php
├── ProtocolReply.php
├── RouterDescriptor.php
├── TorCurlWrapper.php
└── TorDNSEL.php
└── tests
├── ControlClientTest.php
├── ParserTest.php
├── TorDNSELTest.php
└── data
├── dir-status-1
├── status-vote.current.authority-1
└── status-vote.current.consensus-1
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '5.6'
4 | - '7.0'
5 | - '7.1'
6 | - '7.2'
7 | - '7.3'
8 | - '7.4'
9 |
10 | before_script: composer install
11 |
12 | script:
13 | - vendor/bin/phpunit
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Drew Phillips
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of TorUtils nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ## Name:
8 |
9 | **Dapphp\TorUtils** - Classes for interacting with Tor over the control
10 | protocol, querying directory authorities and servers, DNS exit lists, a curl
11 | wrapper that makes it easy to use Tor's SOCKS proxy, and more.
12 |
13 | ## Version:
14 |
15 | **1.15.3**
16 |
17 | ## Author:
18 |
19 | Drew Phillips
20 |
21 | ## Requirements:
22 |
23 | * PHP 5.5 or greater
24 |
25 | ## Description:
26 |
27 | **Dapphp\TorUtils** provides some PHP libraries for working with Tor.
28 | The main functionality focuses on interacting with Tor using the Tor
29 | control protocol and provides many methods to make it easy to send
30 | commands, retrieve directory and node information, and modify Tor's
31 | configuration using the control protocol. A few other utility classes
32 | are provided for robustness.
33 |
34 | The following classes are provided:
35 |
36 | - ControlClient: A class for interacting with Tor's control protocol which
37 | can be used to script your Tor relay or learn information about
38 | other nodes in the Tor network. With this class you can query directory
39 | information through the controller, get and set Tor configuration values,
40 | fetch information with the GETINFO command, subscribe to events, and send
41 | raw commands to the controller and get back parsed responses.
42 |
43 | - DirectoryClient: A class for querying information directly from Tor
44 | directory authorities (or any other Tor directory server). This class
45 | can be used to fetch information about active nodes in the network by
46 | nickname or fingerprint or retrieve a list of all active nodes to get
47 | information such as IP address, contact info, exit policies, certs,
48 | uptime, flags and more.
49 |
50 | - TorDNSEL: A simple interface for querying an address against the Tor
51 | DNS exit lists to see if an IP address belongs to a Tor exit node.
52 |
53 | - TorCurlWrapper: A wrapper for cURL that ensures HTTP requests are proxied
54 | through Tor using SOCKS5 with DNS resolution over Tor (if supported). It also
55 | turns cURL errors into an Exception and parses responses into headers and body
56 | parts.
57 |
58 | ## Basic Example:
59 |
60 | This library provides a lot of different functionality (see examples directory)
61 | and a wide range of possibility but a common use case is sending a signal to
62 | the Tor controller to change IP addresses. This shows how to do just that:
63 |
64 | connect(); // connect to 127.0.0.1:9051
74 | $tc->authenticate('password'); // can also use cookie or empty auth
75 | $tc->signal(ControlClient::SIGNAL_NEWNYM);
76 | echo "Signal sent - IP changed successfully!\n";
77 | } catch (\Exception $ex) {
78 | echo "Signal failed: " . $ex->getMessage() . "\n";
79 | }
80 |
81 |
82 | ## Examples:
83 |
84 | The source package comes with several examples of interacting with the
85 | Tor control protocol and directory authorities. See the `examples/`
86 | directory in the source package.
87 |
88 | Currently, the following examples are provided:
89 |
90 | - dc_GetAllDescriptors-simple.php: Uses the DirectoryClient class to query a
91 | directory authority for a list of all currently known descriptors and prints
92 | basic information on each descriptor.
93 |
94 | - dc_GetServerDescriptor-simple.php: Uses the DirectoryClient to fetch info
95 | about a single descriptor and prints the information.
96 |
97 | - tc_AsyncEvents.php: Uses the ControlClient to subscribe to some events which
98 | the controller will send to a registered callback as they are generated.
99 |
100 | - tc_GetConf.php: Uses the ControlClient to interact with the controller to
101 | fetch and set Tor configuration options.
102 |
103 | - tc_GetInfo.php: Uses ControlClient to talk to the Tor controller to get
104 | various pieces of information about the controller and routers on the network.
105 |
106 | - tc_NewNym.php: Uses ControlClient to send the "NEWNYM" signal to the
107 | controller to change IP addresses (as shown above).
108 |
109 | - tc_CreateHiddenService.php: Tells the controller to create a new Onion
110 | ("Hidden") Service. This example shows how to programatically add a new hidden
111 | service, delete it, and re-create it with the private key that was generated.
112 | The private key can be securely stored to restart the service at a later time
113 | using the same onion address.
114 |
115 | - tc_SendData.php: Shows how to use ControlClient to send arbitrary commands
116 | and read the response from the controller. Replies are returned as
117 | ProtocolReply objects which give easy access to the status of the reply (e.g.
118 | success/fail) and provides methods to access individual reply lines or
119 | iterate over each line and process the data.
120 |
121 | - TorDNSEL.php: An example of using the Tor DNS Exit Lists to check if a remote
122 | IP address connecting to a specific IP:Port combination is a Tor exit router.
123 |
124 | - CurlWrapper.php: Shows how to use the cURL wrapper class to make HTTP requests
125 | through the Tor socks proxy.
126 |
127 | - CurlCountry.php: Shows how to use Exit Nodes from a specific country with the
128 | cURL wrapper.
129 |
130 | ## TODO:
131 |
132 | The following commands are not directly implemented by ControlClient and would
133 | need to be implemented or the implementation could communicate directly with
134 | the controller using the provided functions to issue commands:
135 |
136 | - RESETCONF
137 | - SAVECONF
138 | - MAPADDRESS
139 | - EXTENDCIRCUIT
140 | - SETCIRCUITPURPOSE
141 | - ATTACHSTREAM
142 | - POSTDESCRIPTOR
143 | - REDIRECTSTREAM
144 | - CLOSESTREAM
145 | - CLOSECIRCUIT
146 | - USEFEATURE
147 | - LOADCONF
148 | - TAKEOWNERSHIP
149 | - DROPGUARDS
150 | - HSFETCH
151 | - HSPOST
152 |
153 | ## Copyright:
154 |
155 | Copyright (c) 2022 Drew Phillips
156 | All rights reserved.
157 |
158 | Redistribution and use in source and binary forms, with or without
159 | modification, are permitted provided that the following conditions are met:
160 |
161 | - Redistributions of source code must retain the above copyright notice,
162 | this list of conditions and the following disclaimer.
163 | - Redistributions in binary form must reproduce the above copyright notice,
164 | this list of conditions and the following disclaimer in the documentation
165 | and/or other materials provided with the distribution.
166 |
167 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
168 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
169 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
170 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
171 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
172 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
173 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
174 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
175 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
176 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
177 | POSSIBILITY OF SUCH DAMAGE.
178 |
179 | ## Donate:
180 |
181 | BTC: 1dJyNBaKBqRXVVMw5uVPyz7M3tMrh3gU2
182 | ETH: 0x51A1057D485da13fB9C37C8ed3C5B3BA59e950D1
183 | Flattr: [OpenInternet](https://flattr.com/@drew010/domain/openinternet.io)
184 |
185 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "dapphp/torutils",
3 | "description" : "Classes for interacting with Tor over the control protocol, querying directory authorities and servers, DNS exit lists, a curl wrapper that makes it easy to use Tor's SOCKS proxy, and more.",
4 | "license" : "BSD-3-Clause",
5 | "require" : {
6 | "php" : ">=5.5"
7 | },
8 | "authors" : [ {
9 | "name" : "Drew Phillips",
10 | "email" : "drew@drew-phillips.com",
11 | "homepage" : "http://drew-phillips.com"
12 | } ],
13 | "keywords" : [ "tor", "tor client", "anonynimity", "onion", "curl" ],
14 | "autoload" : {
15 | "psr-4" : {
16 | "Dapphp\\TorUtils\\" : "src/"
17 | }
18 | },
19 | "type" : "library",
20 | "homepage" : "https://github.com/dapphp/TorUtils",
21 | "require-dev": {
22 | "phpunit/phpunit": "^5.7"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/contrib/.gitignore:
--------------------------------------------------------------------------------
1 | auth_dirs.inc
2 | fallback_dirs.inc
3 |
--------------------------------------------------------------------------------
/contrib/extract_directories.php:
--------------------------------------------------------------------------------
1 | '" . $match[4] . "', // " . $match[1] . "\n";
34 | }
35 | }
36 | echo "\n";
37 | } else {
38 | echo "$file does not exist or is not readable; skipping authorities.\n";
39 | }
40 |
41 | $file = __DIR__ . '/fallback_dirs.inc';
42 |
43 | if (is_readable($file)) {
44 | $fallbacks = file_get_contents($file);
45 |
46 | if (preg_match_all('/"(\d+\.\d+\.\d+\.\d+:\d+) orport=(\d+) id=([\w\d]+).*?nickname=([^\s]+)/is', $fallbacks, $matches)) {
47 | printf("Exporting %d fallback directories\n", sizeof($matches[0]));
48 | for ($i = 0; $i < sizeof($matches[0]); ++$i) {
49 | printf(" '%s' => '%s', // %s\n", $matches[3][$i], $matches[1][$i], $matches[4][$i]);
50 | //echo " '" . $matches[3][$i] . "' => '" . $matches[1][$i] . "', // \n";
51 | }
52 | }
53 | echo "\n";
54 | } else {
55 | echo "$file does not exist or is not readable; skipping fallback directories.\n";
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/examples/CurlCountry.php:
--------------------------------------------------------------------------------
1 | connect(); // connect
13 | $tc->authenticate('password'); // authenticate
14 |
15 | foreach($countries as $country) {
16 | $country = '{' . $country . '}'; // e.g. {US}
17 |
18 | $tc->setConf(array('ExitNodes' => $country)); // set config to use exit node from country
19 |
20 | // get new curl wrapped through Tor SOCKS5 proxy
21 | $curl = new Dapphp\TorUtils\TorCurlWrapper();
22 | $curl->setopt(CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:41.0) Gecko/20100101 Firefox 41.0');
23 |
24 | // make request - should go through exit node from specified country
25 | if ($curl->httpGet('http://whatismycountry.com')) {
26 | echo $curl->getResponseBody();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/CurlWrapper.php:
--------------------------------------------------------------------------------
1 | setopt(CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:41.0) Gecko/20100101 Firefox/41.0');
14 | $torcurl->setopt(CURLOPT_TIMEOUT, 15);
15 | $torcurl->setopt(CURLOPT_HTTPHEADER,
16 | array(
17 | 'Accept-Language: en-US,en;q=0.5',
18 | 'DNT: 1',
19 | 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
20 | )
21 | );
22 |
23 | // uncomment the follow lines to show verbose output from curl
24 | //$torcurl->setopt(CURLOPT_VERBOSE, true);
25 | //$torcurl->setopt(CURLOPT_STDERR, fopen('php://output', 'w'));
26 |
27 | try {
28 | $torcurl->httpGet('https://check.torproject.org/');
29 |
30 | echo sprintf("Request to %s returned HTTP %d.
\n\n",
31 | $torcurl->getInfo()['url'], $torcurl->getHttpStatusCode());
32 |
33 | // show response headers in textarea
34 | echo "Response Headers:
\n
\n\n";
42 |
43 | // show response body in textarea
44 | echo "Response Body: (Content-Type: " . $torcurl->getInfo()['content_type'] . ")"
45 | ."
\n
\n";
48 |
49 | //print_r($torcurl->getInfo());
50 |
51 | // Example post:
52 | /*
53 | $torcurl->httpPost(
54 | 'http://example.com/form',
55 | http_build_query([ 'name' => 'Your Name', 'email' => 'Your Email', 'message' => 'Hello!' ])
56 | );
57 | // OR (sample file upload using CURLFile [PHP >= 5.5])
58 | $torcurl->httpPost(
59 | 'http://example.com/upload',
60 | [
61 | 'action' => 'upload',
62 | 'name' => 'Your Name',
63 | 'file1' => new CURLFile('/path/to/img.jpg', 'image/jpeg', 'file1'),
64 | 'file2' => new CURLFile('/path/to/img2.jpg', 'image/jpeg', 'file2'),
65 | 'submit' => 'Submit',
66 | ]
67 | );
68 | */
69 |
70 | } catch (\Exception $ex) {
71 | echo sprintf("Request to %s failed with error %d: %s\n",
72 | $torcurl->getInfo()['url'],
73 | $ex->getCode(),
74 | $ex->getMessage());
75 |
76 | // Inspect $torcurl->getInfo() for more details.
77 | // The request can fail for a number of reasons including but not limited
78 | // to: a broken Tor circuit, bad exit, network errors within Tor, etc.
79 | }
80 |
--------------------------------------------------------------------------------
/examples/TorDNSEL.php:
--------------------------------------------------------------------------------
1 | getMessage());
17 | }
18 | */
19 |
20 | // Test lookups
21 | // First array index is the remote IP (client/potential exit relay)
22 | // second is the DNS server to use for the query (consider using your local caching resolver!)
23 | $lookups = array(
24 | array('195.176.3.20', 'check-01.torproject.org'), /* DigiGesTor4e3 */
25 | array('185.220.103.4', '1.1.1.1'), /* CalyxInstitute16 */
26 | array('185.220.103.4', '9.9.9.9'), /* CalyxInstitute16 */
27 | array('185.220.101.220', 'check-01.torproject.org'), /* niftyguard */
28 | array('89.34.27.59', 'check-01.torproject.org'), /* Hydra2 */
29 | array('104.215.148.63', 'check-01.torproject.org'), /* not a relay */
30 | array('208.111.35.21', '10.11.12.13'), // should time out
31 | );
32 |
33 | foreach($lookups as $lookup) {
34 | list($remoteIP, $server) = $lookup;
35 |
36 | try {
37 | echo "[o] Checking $remoteIP using server $server...\n";
38 |
39 | // send DNS request to Tor DNS exit list service
40 | // returns true if $remoteIP is a Tor exit relay
41 | $isTor = TorDNSEL::isTor($remoteIP, $server);
42 |
43 | if ($isTor) {
44 | echo "[+] Tor exit relay: *YES*\n";
45 | } else {
46 | echo "[-] Tor exit relay: No\n";
47 | echo "[-] Fingerprint(s): N/A\n";
48 | }
49 |
50 | if ($isTor) {
51 | $fingerprints = TorDNSEL::getFingerprints($remoteIP, $server);
52 |
53 | if (!empty($fingerprints)) {
54 | echo sprintf(
55 | "[+] Fingerprint(s): %s\n",
56 | join(', ', $fingerprints)
57 | );
58 | } else { /* Service should return a fingerprint if address is an exit relay */ }
59 | }
60 |
61 | echo "\n";
62 |
63 | } catch (\Exception $ex) {
64 | echo sprintf("[!] Query failed: %s\n",
65 | $ex->getMessage());
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/examples/common.php:
--------------------------------------------------------------------------------
1 | 31536000,
10 | 'days' => 86400,
11 | 'hours' => 3600,
12 | 'minutes' => 60,
13 | 'seconds' => 1
14 | );
15 |
16 | $return = array();
17 |
18 | foreach($units as $unit => $secs) {
19 | $num = intval($seconds / $secs);
20 |
21 | if ($num > 0) {
22 | $return[$unit] = $num;
23 | }
24 | $seconds %= $secs;
25 | }
26 |
27 | if ($array) {
28 | return $return;
29 | } else {
30 | $s = '';
31 | foreach($return as $unit => $value) {
32 | $s .= "$value $unit, ";
33 | }
34 | $s = substr($s, 0, -2);
35 | return $s;
36 | }
37 | }
38 |
39 | /*
40 | Original author: http://jeffreysambells.com/2012/10/25/human-readable-filesize-php
41 | */
42 | function humanFilesize($bytes, $decimals = 2) {
43 | $size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
44 | $factor = floor((strlen($bytes) - 1) / 3);
45 | return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$size[$factor];
46 | }
47 |
--------------------------------------------------------------------------------
/examples/dc_GetAllDescriptors-simple.php:
--------------------------------------------------------------------------------
1 | getAllServerDescriptors();
10 |
11 | echo sprintf("We know about %d descriptors.\n\n", sizeof($descriptors));
12 |
13 | /** @var \Dapphp\TorUtils\RouterDescriptor $descriptor */
14 | foreach($descriptors as $descriptor) {
15 | echo sprintf("%-19s %s %16s:%s\n", $descriptor->nickname, $descriptor->fingerprint, $descriptor->ip_address, $descriptor->or_port);
16 |
17 | echo sprintf("Running: %s\n", $descriptor->platform);
18 | echo sprintf("Uptime: %s\n", uptimeToString($descriptor->getCurrentUptime(), false));
19 | echo sprintf("Contact: %s\n", $descriptor->contact);
20 | echo sprintf("Bandwidth (avg / burst / observed): %d / %d / %d\n", $descriptor->bandwidth_average, $descriptor->bandwidth_burst, $descriptor->bandwidth_observed);
21 |
22 | if (sizeof($descriptor->or_address) > 0)
23 | echo sprintf("OR Address: %68s\n", implode(', ', $descriptor->or_address));
24 |
25 | echo sprintf(
26 | "Exit Policy:\n accept: %s\n reject: %s\n",
27 | isset($descriptor->exit_policy4['accept']) ? implode(' ', $descriptor->exit_policy4['accept']) : '',
28 | implode(' ', $descriptor->exit_policy4['reject'])
29 | );
30 |
31 | echo str_pad('', 80, '-') . "\n";
32 | }
33 |
--------------------------------------------------------------------------------
/examples/dc_GetServerDescriptor-simple.php:
--------------------------------------------------------------------------------
1 | setPreferredServer('1.2.3.4:80'); // Optional server to always use first for directory lookups
9 |
10 | try {
11 | $descriptor = $client->getServerDescriptor('81C55D403A82BF6E7C3FBDBD41D102B7088900D9');
12 | } catch (\Exception $ex) {
13 | echo "Request to directory failed: " . $ex->getMessage() . "\n";
14 | exit;
15 | }
16 |
17 | echo sprintf("%-19s %40s\n", $descriptor->nickname, $descriptor->fingerprint);
18 | echo sprintf("Running %s\n", $descriptor->platform);
19 | echo sprintf("Online for %s\n", uptimeToString($descriptor->getCurrentUptime(), false));
20 | echo sprintf("OR Address: %s:%s", $descriptor->ip_address, $descriptor->or_port);
21 |
22 | if ($descriptor->or_address) {
23 | foreach ($descriptor->or_address as $address) {
24 | echo ", $address";
25 | }
26 | }
27 | echo "\n";
28 |
29 | echo sprintf("Exit Policy:\n Accept:\n %s\n Reject:\n %s\n",
30 | implode("\n ", $descriptor->exit_policy4['accept']),
31 | implode("\n ", $descriptor->exit_policy4['reject'])
32 | );
33 |
--------------------------------------------------------------------------------
/examples/tc_AsyncEvents.php:
--------------------------------------------------------------------------------
1 | setDebug(true);
11 |
12 | try {
13 | $tc->connect(); // connect to 127.0.0.1:9051
14 | $tc->authenticate();
15 | } catch (\Exception $ex) {
16 | echo "Failed to create Tor control connection: " . $ex->getMessage() . "\n";
17 | exit;
18 | }
19 |
20 | // register anonymous function as the event handler for async events
21 | $tc->setAsyncEventHandler(function($event, $data) {
22 | // depending on the $event - data may be an array or ProtocolReply object
23 | // for NS and NEWCONSENSUS events, $data is an array of RouterDescriptor objects keyed by fingerprint
24 |
25 | switch($event) {
26 | case 'ADDRMAP':
27 | echo sprintf(
28 | "Resolved %s. Error: %s. Address: %s\n",
29 | $data['ADDRESS'],
30 | (isset($data['error']) ? 'YES' : 'No'),
31 | $data['NEWADDRESS']
32 | );
33 | break;
34 |
35 | case 'INFO':
36 | case 'NOTICE':
37 | case 'WARN':
38 | case 'DEBUG':
39 | foreach($data->getReplyLines() as $replyLine) {
40 | echo sprintf("LOG: %s\n", $replyLine);
41 | }
42 | break;
43 |
44 | case 'CIRC':
45 | foreach($data as $circuit) {
46 | echo $circuit;
47 | }
48 | break;
49 |
50 | case 'SIGNAL':
51 | $signal = $data[0];
52 | echo $signal . "\n";
53 | break;
54 |
55 | default:
56 | echo "Got event $event\n";
57 | var_dump($data);
58 | }
59 | });
60 |
61 | // tell controller to notify of these events; could also pass events as an array
62 | $tc->setEvents('ADDRMAP NS NEWCONSENSUS SIGNAL CONF_CHANGED STATUS_GENERAL CIRC INFO NOTICE WARN');
63 |
64 | // enable debug output and logging to file so we can see events received
65 | // $tc->setDebug(1)->setDebugOutputFile(fopen('/tmp/tor.txt', 'w+'));
66 |
67 | while (true) {
68 | // when reading a reply, if an async event is received then the callback given
69 | // to setAsyncEventHandler will be called
70 | // after the event is processed the client will then re-attempt to read
71 | // reply back from the controller, if there is one. Otherwise readReply blocks
72 | // until data is available
73 |
74 | $read = $tc->getInfoTrafficRead();
75 | $writ = $tc->getInfoTrafficWritten();
76 |
77 | echo "Traffic = $read / $writ \r";
78 | sleep(1);
79 |
80 | // $reply = $tc->readReply(); // blocks until reply received
81 | // unless you are ONLY reading events from the controller, don't call
82 | // readReply() without sending a command first, otherwise it could block
83 | // the script until an event is received.
84 | // When an asyncEventHandler function is supplied, events will always be
85 | // received and dispatched between other command data and responses but if
86 | // events are infrequently received, don't call readReply without having sent
87 | // a command first.
88 |
89 | }
90 |
91 | $tc->quit();
92 |
93 |
--------------------------------------------------------------------------------
/examples/tc_CreateHiddenService.php:
--------------------------------------------------------------------------------
1 | setDebug(true);
40 |
41 | try {
42 | $tc->connect(); // connect to 127.0.0.1:9051
43 | $tc->authenticate();
44 | } catch (\Exception $ex) {
45 | echo "Failed to create Tor control connection: " . $ex->getMessage() . "\n";
46 | exit(1);
47 | }
48 |
49 | // the types of hidden services keys we can create
50 | $keyTypes = [
51 | ControlClient::ONION_KEYTYPE_CURVE25519 => 'Hidden Service v3 (ED25519-V3)',
52 | //ControlClient::ONION_KEYTYPE_RSA1024 => 'Hidden Service v2 (RSA1024)' // Obselete
53 | ];
54 |
55 | foreach($keyTypes as $keyType => $keyDesc) {
56 | // Try to create a newer hidden service v3 ed25519 key first, then fall back to older RSA1024 keys.
57 | // Release 0.3.2.9 introduced hidden service v3 protocol, but the control port did not support
58 | // adding them with ADD_ONION until a later version (0.3.3.x-stable?). Older clients will not support this method.
59 |
60 | try {
61 | echo "Attempting to create hidden service using $keyDesc key type: ";
62 |
63 | // define options for service creation
64 | $options = array(
65 | 'KeyType' => ControlClient::ONION_KEYTYPE_NEW, // default
66 | 'KeyBlob' => $keyType, // create a NEW HS key of this type
67 | 'Flags' => 0, // option flags for service creation
68 | 'Target' => HIDDEN_SERVICE_TARGET, // local port the hidden service forwards to
69 | );
70 |
71 | // Note: acceptable flags are a bitwise combination of:
72 | ControlClient::ONION_FLAG_DETACH | ControlClient::ONION_FLAG_DISCARDPK | ControlClient::ONION_FLAG_BASICAUTH | ControlClient::ONION_FLAG_NONANON;
73 |
74 | // try to add the hidden service (throws ProtocolError if creation failed)
75 | $service = $tc->addHiddenService(HIDDEN_SERVICE_PORT, $options);
76 |
77 | echo "Hidden service created!\n\n" .
78 | "Address = {$service['ServiceID']}.onion\n" .
79 | "Key = {$service['PrivateKey']}\n\n";
80 |
81 | break;
82 |
83 | } catch (ProtocolError $pe) {
84 | echo "Failed to create hidden service: " . $pe->getMessage() . "\n";
85 | if ($keyType == ControlClient::ONION_KEYTYPE_RSA1024) {
86 | // failed to create an ed25519-v3 key *and* an older RSA1024 key :(
87 | exit(1);
88 | }
89 | } catch (Exception $ex) {
90 | echo "Error: " . $ex->getMessage() . "\n";
91 | exit(1);
92 | }
93 | }
94 |
95 | echo "Press [Enter] to delete the service and re-create it by suppling the private key...";
96 | fread(STDIN, 1);
97 | echo "\n";
98 |
99 | // delete the hidden service by supplying its onion address
100 | $tc->delHiddenService($service['ServiceID']);
101 |
102 | echo "Hidden service deleted. Sleeping for 10 seconds before re-creating.\n";
103 | for ($i = 10; $i > 0; --$i) {
104 | echo "$i \r";
105 | sleep(1);
106 | }
107 | echo " \n\n";
108 |
109 | echo "Re-creating hidden service...\n";
110 |
111 | // Re-create the service using whichever keytype was used to create the
112 | // service initially. ed25519 for newer Tor versions, RSA1024 for older.
113 | $keyParts = explode(':', $service['PrivateKey'], 2);
114 | $service['KeyType'] = $keyParts[0];
115 | $service['PrivateKey'] = $keyParts[1];
116 |
117 | // options for re-creating hidden service at a later date
118 | $options = array(
119 | 'KeyType' => $service['KeyType'],
120 | 'KeyBlob' => $service['PrivateKey'],
121 | 'Target' => HIDDEN_SERVICE_TARGET,
122 | 'Flags' => ControlClient::ONION_FLAG_DISCARDPK,
123 | );
124 |
125 | try {
126 | // re-create the hidden service
127 | $service = $tc->addHiddenService(HIDDEN_SERVICE_PORT, $options);
128 |
129 | echo "Hidden service running at {$service['ServiceID']}.onion:" . HIDDEN_SERVICE_PORT . "\n\n";
130 | } catch (ProtocolError $pe) {
131 | echo "Failed to create service! " . $pe->getMessage() . "\n";
132 | exit(1);
133 | }
134 |
135 | echo "Press Control-C to terminate.\n";
136 |
137 | // Run indefinitely, leaving the hidden service accessible until the Tor
138 | // control client disconnects.
139 |
140 | for (;;) {
141 | sleep(10);
142 | }
143 |
--------------------------------------------------------------------------------
/examples/tc_GetConf.php:
--------------------------------------------------------------------------------
1 | connect(); // connect to 127.0.0.1:9051
12 | $tc->authenticate();
13 | } catch (\Exception $ex) {
14 | echo "Failed to create Tor control connection: " . $ex->getMessage() . "\n";
15 | exit;
16 | }
17 |
18 | // Get configuration values for 4 Tor options
19 | try {
20 | $config = $tc->getConf('BandwidthRate Nickname SocksPort ORPort');
21 | // $config is array where key is the option and value is the current setting
22 |
23 | foreach($config as $keyword => $value) {
24 | echo "Config value {$keyword} = {$value}\n";
25 | }
26 | } catch (ProtocolError $pe) {
27 | echo 'GETCONF failed: ' . $pe->getMessage();
28 | }
29 |
30 | echo "\n";
31 |
32 | // Get configuration values with non-existent values
33 | // GETCONF fails if any unknown options are present
34 | try {
35 | $config = $tc->getConf('ORPort NonExistentConfigValue DirPort AnotherFakeValue');
36 | } catch (ProtocolError $pe) {
37 | echo 'GETCONF failed: ' . $pe->getMessage();
38 | }
39 |
40 | echo "\n\n";
41 |
42 | // Read config values into array
43 | $config = $tc->getConf('Log CookieAuthentication');
44 | var_dump($config);
45 |
46 | //$config['Log'] = 'notice stderr';
47 | //$config['Log'] = 'notice file /var/log/tor/tor.log';
48 |
49 | // SETCONF using previously fetched config values
50 | $tc->setConf($config);
51 |
52 | // SETCONF with non-existent option
53 | // SETCONF fails and nothing is set if any unknown options are present
54 | try {
55 | // add non-existent config value to array
56 | $config['IDontExist'] = 'some string value';
57 | $tc->setConf($config);
58 | } catch (\Exception $ex) {
59 | echo $ex->getMessage() . "\n";
60 | }
61 |
62 | $tc->quit();
63 |
--------------------------------------------------------------------------------
/examples/tc_GetInfo.php:
--------------------------------------------------------------------------------
1 | controller communication
12 | //$tc->setDebug(true);
13 |
14 | try {
15 | $tc->connect(); // connect to 127.0.0.1:9051
16 | $tc->authenticate();
17 | } catch (\Exception $ex) {
18 | echo "Failed to create Tor control connection: " . $ex->getMessage() . "\n";
19 | exit;
20 | }
21 |
22 | // ask controller for tor version
23 | $ver = $tc->getVersion();
24 | $rec = $tc->getInfoStatusVersionCurrent();
25 | $cur = $tc->getInfoStatusVersionRecommended();
26 |
27 | echo "*** Connected to controller***\n*** Controller is running Tor $ver ($rec) ***\n";
28 | echo "Current recommended versions are: " . implode(', ', $cur) . "\n";
29 | echo "\n";
30 |
31 | try {
32 | // get tor node's external ip, if known.
33 | // If Tor could not determine IP, an exception is thrown
34 | $address = $tc->getInfoAddress();
35 | } catch (ProtocolError $pex) {
36 | $address = 'Unknown';
37 | }
38 |
39 | try {
40 | // get router fingerprint (if any) - clients will not have a fingerprint
41 | $fingerprint = $tc->getInfoFingerprint();
42 | } catch (ProtocolError $pex) {
43 | $fingerprint = $pex->getMessage();
44 | }
45 |
46 | echo sprintf("*** Controller IP Address: %s / Fingerprint: %s ***\n", $address, $fingerprint);
47 |
48 | try {
49 | $uptime = $tc->getInfo(ControlClient::GETINFO_UPTIME);
50 | } catch (ProtocolError $pex) {
51 | $uptime = 'Uptime not supported by this version of Tor';
52 | }
53 |
54 | echo sprintf(" Uptime: %s\n", $uptime);
55 |
56 | // ask controller how many bytes Tor has transferred
57 | $read = $tc->getInfoTrafficRead();
58 | $writ = $tc->getInfoTrafficWritten();
59 |
60 | echo sprintf("*** Tor traffic (read / written): %s / %s ***\n", humanFilesize($read), humanFilesize($writ));
61 |
62 | echo "\n";
63 |
64 | $descriptor = null;
65 | $relay = 'InMemoryOfJohnKerr'; // example relay for script
66 |
67 | try {
68 | echo "Fetching relay info for $relay...\n\n";
69 |
70 | // Fetch info for this descriptor from controller.
71 | // Modern clients don't download full descriptors by default so use getInfoMicroDescriptor.
72 | // To fetch full info, set the Tor option FetchUselessDescriptors to 1 and call $tc->getInfoDescriptor() instead.
73 | // When using getInfoDescriptor(), there's no need to use the DirectoryClient below.
74 | // Microdescriptors include the nickname, onion key, ntor onion key, family, accept/reject rules, and the ed25519 id key
75 | $descriptor = $tc->getInfoMicroDescriptor($relay);
76 |
77 | // If descriptor found, query directory info to get flags.
78 | // Directory info is a reduced set of data including consensus data like
79 | // the consensus weight, relay flags (e.g. Exit, Guard, HSDir etc), the IP
80 | // and accept/reject list
81 | $dirinfo = $tc->getInfoDirectoryStatus($relay);
82 |
83 | // combine the two RouterDescriptor objects from getInfoDescriptor and getInfoDirectoryStatus
84 | // into one object
85 | $descriptor->combine($dirinfo);
86 |
87 | // Unless FetchUselessDescriptors (see above) is enabled, uptime, bandwidth, contact info, and version can
88 | // only be fetched from the directory.
89 | // If FetchUselessDescriptors is enabled, this is not needed when calling getInfoDescriptor() instead of getInfoMicroDescriptor().
90 | $dc = new DirectoryClient();
91 | $dirinfo = $dc->getServerDescriptor($descriptor->fingerprint); // populates uptime, bandwidth, contact info, version
92 |
93 | $descriptor->combine($dirinfo);
94 |
95 | echo "== Descriptor Info ==\n" .
96 | "Nickname : {$descriptor->nickname}\n" .
97 | "Fingerprint : {$descriptor->fingerprint}\n" .
98 | "Running : {$descriptor->platform}\n" .
99 | "Uptime : " . uptimeToString($descriptor->getCurrentUptime(), false) . "\n" .
100 | "OR Address(es): " . $descriptor->ip_address . ':' . $descriptor->or_port;
101 |
102 | if (sizeof($descriptor->or_address) > 0) {
103 | echo ', ' . implode(', ', $descriptor->or_address);
104 | }
105 | echo "\n" .
106 | "Contact : {$descriptor->contact}\n" .
107 | "BW (observed) : " . number_format($descriptor->bandwidth_observed) . " B/s\n" .
108 | "BW (average) : " . number_format($descriptor->bandwidth_average) . " B/s\n" .
109 | "Flags : " . implode(' ', $descriptor->flags) . "\n\n";
110 | } catch (ProtocolError $pe) {
111 | // doesn't necessarily mean the node doesn't exist
112 | // the controller may not have updated directory info yet
113 | echo $pe->getMessage() . "\n\n"; // Unrecognized key "desc/name/MilesPrower
114 | }
115 |
116 | try {
117 | echo "CIRCUITS\n";
118 | $circuits = $tc->getInfoCircuitStatus();
119 |
120 | if (sizeof($circuits) > 0) {
121 | foreach($circuits as $circuit) {
122 | /** @var $circuit \Dapphp\TorUtils\CircuitStatus */
123 |
124 | echo $circuit; // __toString
125 | }
126 | } else {
127 | echo "No active circuits established\n";
128 | }
129 | } catch (\Exception $ex) {
130 | echo "Failed to get circuit status: " . $ex->getMessage() . "\n";
131 | }
132 | echo "\n";
133 |
134 | try {
135 | echo "Sending heartbeat signal to controller...";
136 |
137 | $tc->signal(ControlClient::SIGNAL_HEARTBEAT);
138 | // watch tor.log file for heartbeat message
139 |
140 | echo "OK";
141 | } catch (ProtocolError $pe) {
142 | echo $pe->getMessage();
143 | }
144 |
145 | echo "\n\n";
146 |
147 | if ($descriptor) {
148 | try {
149 | $descriptor->country = $tc->getInfoIpToCountry($descriptor->ip_address);
150 | } catch (ProtocolError $pe) {
151 | echo "Failed to get IP country for relay at {$descriptor->ip_address}: " . $pe->getMessage() . "\n\n";
152 | }
153 |
154 | echo "Dumping raw RouterDescriptor object:\n";
155 |
156 | print_r($descriptor);
157 | }
158 |
159 | echo "Closing connection to controller\n";
160 | $tc->quit();
161 |
--------------------------------------------------------------------------------
/examples/tc_NewNym.php:
--------------------------------------------------------------------------------
1 | connect('127.0.0.1', 9051); // connect to controller at 127.0.0.1:9051
15 | $tc->authenticate('password'); // authenticate using hashedcontrolpassword "password"
16 | $tc->signal(ControlClient::SIGNAL_NEWNYM); // send signal to change IP
17 |
18 | echo "Signal sent - IP changed successfully!\n";
19 | } catch (\Exception $ex) {
20 | echo "Signal failed: " . $ex->getMessage() . "\n";
21 | }
22 |
--------------------------------------------------------------------------------
/examples/tc_SendData.php:
--------------------------------------------------------------------------------
1 | controller communication
11 | //$tc->setDebug(true);
12 |
13 | try {
14 | $tc->connect(); // connect to 127.0.0.1:9051
15 | $tc->authenticate();
16 | } catch (\Exception $ex) {
17 | echo "Failed to create Tor control connection: " . $ex->getMessage() . "\n";
18 | exit;
19 | }
20 |
21 | try {
22 | // send arbitrary command; use GETINFO command with 'entry-guards' parameter
23 | $tc->sendData('GETINFO entry-guards');
24 |
25 | // read and parse controller response into a ProtocolReply object
26 | $reply = $tc->readReply();
27 |
28 | // show the status code of the command, and output the raw response
29 | printf("Reply status: %d\n", $reply->getStatusCode());
30 | echo $reply . "\n\n"; // invokes __toString() to return the server reply
31 |
32 | // get an array of response lines
33 | $lines = $reply->getReplyLines();
34 |
35 | echo "Entry Guard(s):\n";
36 |
37 | for ($i = 1; $i < sizeof($lines); ++$i) {
38 | // iterate over each line skipping the first line which was the status
39 | // match the fingerprint, nickname, and router status of the entry guards
40 | if (preg_match('/\$?([\w\d]{40})(~|=)([\w\d]{1,19}) ([\w-]+)/', $lines[$i], $match)) {
41 | echo " Nickname = '{$match[3]}' / Fingerprint = '{$match[1]}' / Status = '{$match[4]}'\n";
42 | } else {
43 | echo " {$lines[$i]}\n";
44 | }
45 | }
46 |
47 | echo "\n";
48 | } catch (ProtocolError $pe) {
49 | echo sprintf(
50 | "Command failed: Controller reponse %s: %s\n",
51 | $pe->getStatusCode(),
52 | $pe->getMessage()
53 | );
54 | }
55 |
56 | // send unrecognized command - check whether reply was successful
57 | $tc->sendData('FAKE_COMMAND data data data');
58 |
59 | // read the reply
60 | $reply = $tc->readReply();
61 |
62 | // isPositiveReply returns true if the command returned a successful response.
63 | if (false == $reply->isPositiveReply()) {
64 | // show the status code and reply from the controller
65 | echo "Command failed: " . $reply->getStatusCode() . ' ' . $reply[0] . "\n";
66 |
67 | // yields: Command failed: 510 Unrecognized command "FAKE_COMMAND"
68 | }
69 |
70 | echo "\n";
71 |
72 | $tc->quit();
73 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 | tests
13 |
14 |
15 |
16 |
17 | src
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/AuthorityStatusDocument.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | /**
42 | * AuthorityStatusDocument class. This class models a Tor circuit.
43 | *
44 | */
45 | class AuthorityStatusDocument
46 | {
47 | /** @var int A document format version. For this code, the latest version known is "3". */
48 | public $statusVersion = 0;
49 |
50 | /** @var string "vote" or "consensus", depending on the type of the document */
51 | public $voteStatus = '';
52 |
53 | /** @var int[] A list of supported methods for generating consensuses from votes. Does not occur in consensuses. */
54 | public $consensusMethods = [];
55 |
56 | /** @var string The consensus method; does not occur in votes */
57 | public $consensusMethod = '';
58 |
59 | /** @var string|null The publication time for this status document (if a vote) */
60 | public $published = null;
61 |
62 | /** @var string The start of the Interval for this vote. Before this time, the consensus document produced from
63 | * this vote is not officially in use.
64 | */
65 | public $validAfter = '';
66 |
67 | /** @var string The time at which the next consensus should be produced; before this time, there is no point in
68 | * downloading another consensus, since there won't be a new one.
69 | */
70 | public $freshUntil = '';
71 |
72 | /** @var string The end of the Interval for this vote. After this time, all clients should try to find a more
73 | * recent consensus.
74 | */
75 | public $validUntil = '';
76 |
77 | /** @var int The number of seconds allowed to collect votes from all authorities */
78 | public $voteDelaySeconds = 0;
79 |
80 | /** @var int The number of seconds allowed to collect signatures from all authorities */
81 | public $distDelaySeconds = 0;
82 |
83 | /** @var string[] A list of recommended Tor versions for client usage. The versions are given as defined by
84 | * version-spec.txt. If absent, no opinion is held about client versions.
85 | */
86 | public $clientVersions = [];
87 |
88 | /** @var string[] A list of recommended Tor versions for relay usage. The versions are given as defined by
89 | * version-spec.txt. If absent, no opinion is held about server versions.
90 | */
91 | public $serverVersions = [];
92 |
93 | /** @var string[] A space-separated list of all of the flags that this document might contain. */
94 | public $knownFlags = [];
95 |
96 | /** @var array A list of the internal performance thresholds that the directory authority had at the moment it was
97 | * forming a vote.
98 | */
99 | public $flagThresholds = [];
100 |
101 | /** @var string[] */
102 | public $recommendedClientProtocols = [];
103 |
104 | /** @var string[] */
105 | public $recommendedRelayProtocols = [];
106 |
107 | /** @var string[] */
108 | public $requiredClientProtocols = [];
109 |
110 | /** @var string[] */
111 | public $requiredRelayProtocols = [];
112 |
113 | /** @var array The parameters list, if present, contains a space-separated list of case-sensitive key-value pairs.
114 | * See param-spec.txt for a list of parameters and their meanings.
115 | */
116 | public $params = [];
117 |
118 | /** @var string The shared random value that was generated during the second-to-last shared randomness protocol run,
119 | * encoded in base64.
120 | */
121 | public $sharedRandPreviousValue = '';
122 |
123 | /** @var string The shared random value that was generated during the latest shared
124 | randomness protocol run, encoded in base64. */
125 | public $sharedRandCurrentValue = '';
126 |
127 | /** @var array */
128 | public $authorities = [];
129 |
130 | /** @var RouterDescriptor[] A list of relays along with their information and status according to the document. */
131 | public $descriptors = [];
132 |
133 | /** @var array List of optional weights to apply to router bandwidths during path selection. Appears at most once
134 | * for a consensus. Does not appear in votes. */
135 | public $bandwidthWeights = [];
136 |
137 | /** @var array his is a signature of the status document, with the initial item "network-status-version", and the
138 | * signature item "directory-signature", using the signing key. Only one entry for a vote, and at least one for a
139 | * consensus.
140 | */
141 | public $directorySignatures = [];
142 |
143 | }
--------------------------------------------------------------------------------
/src/CircuitStatus.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | /**
42 | * CircuitStatus class. This class models a Tor circuit.
43 | *
44 | */
45 | class CircuitStatus
46 | {
47 | public $id;
48 |
49 | public $status;
50 |
51 | public $path = array();
52 |
53 | public $buildFlags = array();
54 |
55 | public $purpose;
56 |
57 | public $hsState;
58 |
59 | public $rendQuery;
60 |
61 | public $created;
62 |
63 | public $reason;
64 |
65 | public $remoteReason;
66 |
67 | public $socksUsername;
68 |
69 | public $socksPassword;
70 |
71 | public function __toString()
72 | {
73 | $type = array('Guard', 'Middle', 'Exit');
74 | $path = '';
75 | if (sizeof($this->path) > 0) {
76 | $i = 1;
77 |
78 | foreach($this->path as $p) {
79 | $what = (isset($type[$i - 1]) ? $type[$i - 1] : '');
80 | $path .= sprintf(" %s %-19s", $p[0], $p[1]);
81 |
82 | if (!in_array('ONEHOP_TUNNEL', $this->buildFlags) && sizeof($this->path) == 3) {
83 | $path .= " $i / $what";
84 | }
85 |
86 | $path .= "\n";
87 | $i++;
88 | }
89 | }
90 |
91 | return sprintf(
92 | "Purpose: %-8s Flags: %s Circuit ID: %d %s %s\n" .
93 | "%s\n",
94 | $this->purpose, implode(' ', $this->buildFlags), $this->id, $this->status, $this->getAge(), $path
95 | );
96 | }
97 |
98 | protected function getAge()
99 | {
100 | if ($this->created) {
101 | $dt = new \DateTime($this->created, new \DateTimeZone('UTC'));
102 | $now = new \DateTime(null, new \DateTimeZone('UTC'));
103 | $int = $dt->diff($now);
104 | return $int->format('%hh%im');
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/DirectoryClient.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | require_once 'Parser.php';
42 | require_once 'ProtocolReply.php';
43 |
44 | use Dapphp\TorUtils\Parser;
45 | use Dapphp\TorUtils\ProtocolReply;
46 |
47 | /**
48 | * Class for getting router info from Tor directory authorities
49 | *
50 | */
51 | class DirectoryClient
52 | {
53 | /**
54 | * @var array $directoryAuthorities List of directory authorities https://gitweb.torproject.org/tor.git/tree/src/app/config/auth_dirs.inc
55 | */
56 | protected $directoryAuthorities = array(
57 | '9695DFC35FFEB861329B9F1AB04C46397020CE31' => '128.31.0.39:9131', // moria1
58 | '847B1F850344D7876491A54892F904934E4EB85D' => '86.59.21.38:80', // tor26
59 | '7EA6EAD6FD83083C538F44038BBFA077587DD755' => '45.66.33.45:80', // dizum
60 | 'BA44A889E64B93FAA2B114E02C2A279A8555C533' => '66.111.2.131:9030', // Serge
61 | 'F2044413DAC2E02E3D6BCF4735A19BCA1DE97281' => '131.188.40.189:80', // gabelmoo
62 | '7BE683E65D48141321C5ED92F075C55364AC7123' => '193.23.244.244:80', // dannenberg
63 | 'BD6A829255CB08E66FBE7D3748363586E46B3810' => '171.25.193.9:443', // maatuska
64 | '74A910646BCEEFBCD2E874FC1DC997430F968145' => '199.58.81.140:80', // longclaw
65 | '24E2F139121D4394C54B5BCC368B3B411857C413' => '204.13.164.118:80', // bastet
66 | );
67 |
68 | /**
69 | * @var array (deprecated) array of directory fallbacks
70 | */
71 | protected $directoryFallbacks = array();
72 |
73 | protected $preferredServer;
74 |
75 | protected $connectTimeout = 5;
76 | protected $readTimeout = 30;
77 | protected $userAgent = 'dapphp/TorUtils 1.1.13';
78 |
79 | protected $parser;
80 | protected $serverList;
81 |
82 | /**
83 | * DirectoryClient constructor
84 | */
85 | public function __construct()
86 | {
87 | $this->serverList = $this->directoryAuthorities;
88 | shuffle($this->serverList);
89 |
90 | $this->parser = new Parser();
91 | }
92 |
93 | /**
94 | * Set the preferred directory server to use for lookups. This server will always be used
95 | * first. If the preferred server times out or fails, the lookup will proceed using a random
96 | * server from the list of directory authorities and fallbacks.
97 | *
98 | * @param string $server The directory server to connect to (e.g. 1.2.3.4:80)
99 | * @return self
100 | */
101 | public function setPreferredServer($server)
102 | {
103 | $this->preferredServer = $server;
104 |
105 | return $this;
106 | }
107 |
108 | public function setServerList($list)
109 | {
110 | $this->serverList = $list;
111 |
112 | return $this;
113 | }
114 |
115 | /**
116 | * Set the connection timeout period (in seconds). Attempts to connect to
117 | * directories that take longer than this will time out and try the next host.
118 | *
119 | * @param number $timeout The connection timeout in seconds
120 | * @throws \InvalidArgumentException If timeout is non-numeric or less than 1
121 | * @return self
122 | */
123 | public function setConnectTimeout($timeout)
124 | {
125 | if (!preg_match('/^\d+$/', $timeout) || (int)$timeout < 1) {
126 | throw new \InvalidArgumentException('Timeout must be a positive integer');
127 | }
128 |
129 | $this->connectTimeout = (int)$timeout;
130 |
131 | return $this;
132 | }
133 |
134 | /**
135 | * Set the read timeout in seconds (default = 30). Directory requests
136 | * that fail to receive any data after this many seconds will time out
137 | * and try the next host.
138 | *
139 | * @param number $timeout The read timeout in seconds
140 | * @throws \InvalidArgumentException If timeout is non-numeric or less than 1
141 | * @return self
142 | */
143 | public function setReadTimeout($timeout)
144 | {
145 | if (!preg_match('/^\d+$/', $timeout) || (int)$timeout < 1) {
146 | throw new \InvalidArgumentException('Timeout must be a positive integer');
147 | }
148 |
149 | $this->readTimeout = (int)$timeout;
150 |
151 | return $this;
152 | }
153 |
154 | public function getReadTimeout()
155 | {
156 | return $this->readTimeout;
157 | }
158 |
159 | /**
160 | * Get the list of Tor directory authority servers
161 | *
162 | * @return array Array of directory authorities, keyed by fingerprint (value may be a string [ip address] or array of IP addresses)
163 | */
164 | public function getDirectoryAuthorities()
165 | {
166 | return $this->directoryAuthorities;
167 | }
168 |
169 | /**
170 | * Get the list of Tor directory authority servers
171 | *
172 | * @return array Array of directory fallbacks, keyed by fingerprint (value may be a string [ip address] or array of IP addresses)
173 | */
174 | public function getDirectoryFallbacks()
175 | {
176 | return $this->directoryFallbacks;
177 | }
178 |
179 | /**
180 | * Fetch a list of all known router descriptors on the Tor network
181 | *
182 | * @return array Array of RouterDescriptor objects
183 | * @throws \Exception If directory requests failed
184 | */
185 | public function getAllServerDescriptors()
186 | {
187 | $reply = $this->request(
188 | sprintf('/tor/server/all%s', (function_exists('gzuncompress') ? '.z' : ''))
189 | );
190 |
191 | return $this->parser->parseDirectoryStatus($reply);
192 | }
193 |
194 | /**
195 | * Fetch directory information about a router
196 | * @param string|array $fingerprint router fingerprint or array of fingerprints to get information about
197 | * @return mixed Array of RouterDescriptor objects, or a single RouterDescriptor object
198 | * @throws \Exception
199 | */
200 | public function getServerDescriptor($fingerprint)
201 | {
202 | if (is_array($fingerprint)) {
203 | $fp = implode('+', $fingerprint);
204 | } else {
205 | $fp = $fingerprint;
206 | }
207 |
208 | $uri = sprintf('/tor/server/fp/%s%s', $fp, (function_exists('gzuncompress') ? '.z' : ''));
209 |
210 | $reply = $this->request($uri);
211 |
212 | $descriptors = $this->parser->parseDirectoryStatus($reply);
213 |
214 | if (sizeof($descriptors) == 1) {
215 | return array_shift($descriptors);
216 | } else {
217 | return $descriptors;
218 | }
219 | }
220 |
221 | public function statusVoteCurrentAuthority($address = null)
222 | {
223 | $uri = '/tor/status-vote/current/authority.z';
224 |
225 | $reply = $this->request($uri, $address);
226 |
227 | return $this->parser->parseVoteConsensusStatusDocument($reply);
228 | }
229 |
230 | public function statusVoteCurrentConsensus($address = null)
231 | {
232 | $uri = '/tor/status-vote/current/consensus.z';
233 |
234 | $reply = $this->request($uri, $address);
235 |
236 | return $this->parser->parseVoteConsensusStatusDocument($reply);
237 | }
238 |
239 | /**
240 | * Make an HTTP GET request to a directory server and return the response
241 | *
242 | * @param string $uri The URI to fetch (e.g. /tor/server/all.z)
243 | * @param string|null $directoryServer The host:port or ip:port of the directory server to use, or null to use
244 | * random selections from the default list
245 | * @return \Dapphp\TorUtils\ProtocolReply If no error occurs, a ProtocolReply object is returned. The first line may
246 | * be the HTTP status line. Implementations must tolerate the first reply line being an HTTP response code.
247 | * @throws \Exception If the request to the directory failed (e.g. 404 Not Found, Connection Timed Out)
248 | */
249 | public function get($uri, $directoryServer = null)
250 | {
251 | return $this->request($uri, $directoryServer);
252 | }
253 |
254 | /**
255 | * Pick a random dir authority to query and perform the HTTP request for directory info
256 | *
257 | * @param string $uri Uri to request
258 | * @throws \Exception No authorities responded
259 | * @return \Dapphp\TorUtils\ProtocolReply The reply from the directory authority
260 | */
261 | private function request($uri, $directoryServer = null)
262 | {
263 | reset($this->serverList);
264 | $used = false;
265 |
266 | do {
267 | // pick a server from the list, it is randomized in __construct
268 | if ($directoryServer && !$used) {
269 | $server = $directoryServer;
270 | $used = true;
271 | } elseif ($this->preferredServer && !$used) {
272 | $server = $this->preferredServer;
273 | $used = true;
274 | } else {
275 | $server = $this->getNextServer();
276 | }
277 |
278 | if ($server === false) {
279 | throw new \Exception('No more directory servers available to query');
280 | }
281 |
282 | list($host, $port) = @explode(':', $server);
283 | if (!$port) $port = 80;
284 |
285 | $fp = fsockopen($host, $port, $errno, $errstr, $this->connectTimeout);
286 | if (!$fp) continue;
287 |
288 | $request = $this->getHttpRequest('GET', $host, $uri);
289 |
290 | $written = fwrite($fp, $request);
291 |
292 | if ($written === false) {
293 | trigger_error("Failed to write directory request to $server", E_USER_NOTICE);
294 | continue;
295 | } elseif (strlen($request) != $written) {
296 | trigger_error("Request to $server failed; could not write all data", E_USER_NOTICE);
297 | continue;
298 | }
299 |
300 | $response = '';
301 |
302 | stream_set_blocking($fp, 0);
303 |
304 | $read = array($fp);
305 | $write = null;
306 | $except = null;
307 | $err = false;
308 |
309 | while (!feof($fp)) {
310 | $changed = stream_select($read, $write, $except, $this->readTimeout);
311 |
312 | if ($changed === false) {
313 | trigger_error("stream_select() returned error while reading data from $server", E_USER_NOTICE);
314 | $err = true;
315 | break;
316 | } elseif ($changed < 1) {
317 | trigger_error("Failed to read all data from $server within timeout", E_USER_NOTICE);
318 | $err = true;
319 | break;
320 | } else {
321 | $data = fgets($fp);
322 |
323 | if ($data === false) {
324 | trigger_error("Directory read failed while talking to $server", E_USER_NOTICE);
325 | $err = true;
326 | break;
327 | } else {
328 | $response .= $data;
329 | }
330 | }
331 | }
332 |
333 | fclose($fp);
334 |
335 | if ($err) {
336 | continue;
337 | }
338 |
339 | list($headers, $body) = explode("\r\n\r\n", $response, 2);
340 | $headers = $this->parseHttpResponseHeaders($headers);
341 |
342 | if ($headers['status_code'] == '503') {
343 | trigger_error("Directory $server returned 503 {$headers['message']}", E_USER_NOTICE);
344 | continue;
345 | } elseif ($headers['status_code'] == '504') {
346 | // observed this from various fallback dirs. This code is not defined in dir-spec.txt
347 | trigger_error("Directory $server returned 504 {$headers['message']}", E_USER_NOTICE);
348 | continue;
349 | }
350 |
351 | if ($headers['status_code'] !== '200') {
352 | throw new \Exception(
353 | sprintf(
354 | 'Directory %s returned a negative response code to request. %s %s',
355 | $server,
356 | $headers['status_code'],
357 | $headers['message']
358 | )
359 | );
360 | }
361 |
362 | $encoding = (isset($headers['headers']['content-encoding'])) ? $headers['headers']['content-encoding'] : null;
363 |
364 | if ($encoding == 'deflate') {
365 | if (!function_exists('gzuncompress')) {
366 | throw new \Exception('Directory response was gzip compressed but PHP does not have zlib support enabled');
367 | }
368 |
369 | $body = gzuncompress($body);
370 | if ($body === false) {
371 | throw new \Exception('Failed to inflate response data');
372 | }
373 | } elseif ($encoding == 'identity') {
374 | // nothing to do
375 | } else {
376 | throw new \Exception('Directory sent response in an unknown encoding: ' . $encoding);
377 | }
378 |
379 | break;
380 | } while (true);
381 |
382 | $reply = new ProtocolReply();
383 | $reply->appendReplyLine(
384 | sprintf('%s %s', $headers['status_code'], $headers['message'])
385 | );
386 | $reply->appendReplyLines(explode("\n", $body));
387 |
388 | return $reply;
389 | }
390 |
391 | /**
392 | * Construct an http request for talking to a directory server
393 | *
394 | * @param string $method GET|POST
395 | * @param string $host IP/hostname to query
396 | * @param string $uri The request URI
397 | * @return string Completed HTTP request
398 | */
399 | private function getHttpRequest($method, $host, $uri)
400 | {
401 | return sprintf(
402 | "%s %s HTTP/1.0\r\n" .
403 | "Host: %s\r\n" .
404 | "Connection: close\r\n" .
405 | "User-Agent: %s\r\n" .
406 | "\r\n",
407 | $method, $uri, $host, $this->userAgent
408 | );
409 | }
410 |
411 | /**
412 | * Parse HTTP response headers from the directory reply
413 | *
414 | * @param string $headers String of http response headers
415 | * @throws \Exception Response was not a valid http response
416 | * @return array Array with http status_code, message, and lines of headers
417 | */
418 | private function parseHttpResponseHeaders($headers)
419 | {
420 | $lines = explode("\r\n", $headers);
421 | $response = array_shift($lines);
422 | $header = array();
423 |
424 | if (!preg_match('/^HTTP\/\d\.\d (\d{3}) (.*)$/i', $response, $match)) {
425 | throw new \Exception('Directory server sent a malformed HTTP response');
426 | }
427 |
428 | $code = $match[1];
429 | $message = $match[2];
430 |
431 | foreach($lines as $line) {
432 | if (strpos($line, ':') === false) {
433 | throw new \Exception('Directory server sent an HTTP response line missing the ":" separator');
434 | }
435 | list($name, $value) = explode(':', $line, 2);
436 | $header[strtolower($name)] = trim($value);
437 | }
438 |
439 | return array(
440 | 'status_code' => $code,
441 | 'message' => $message,
442 | 'headers' => $header,
443 | );
444 | }
445 |
446 | /**
447 | * Get the next directory authority from the list to query
448 | *
449 | * @return string IP:Port of directory
450 | */
451 | private function getNextServer()
452 | {
453 | $server = current($this->serverList);
454 | next($this->serverList);
455 | return $server;
456 | }
457 | }
458 |
--------------------------------------------------------------------------------
/src/Parser.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | require_once 'RouterDescriptor.php';
42 | require_once 'ProtocolReply.php';
43 | require_once 'ProtocolError.php';
44 |
45 | use Dapphp\TorUtils\RouterDescriptor;
46 | use Dapphp\TorUtils\ProtocolReply;
47 | use Dapphp\TorUtils\ProtocolError;
48 |
49 | /**
50 | * Class for parsing replies from the control connection or directories.
51 | *
52 | * Typically, implementors will not need to use this class as it is used by
53 | * the ControlClient and DirectoryClient to parse responses.
54 | *
55 | */
56 | class Parser
57 | {
58 | private $descriptorReplyLines = array(
59 | 'router' => '_parseRouter',
60 | 'platform' => '_parsePlatform',
61 | 'published' => '_parsePublished',
62 | 'fingerprint' => '_parseFingerprint',
63 | 'hibernating' => '_parseHibernating',
64 | 'uptime' => '_parseUptime',
65 | 'overload-general' => '_parseOverloadGeneral',
66 | 'onion-key' => '_parseOnionKey',
67 | 'ntor-onion-key' => '_parseNtorOnionKey',
68 | 'signing-key' => '_parseSigningKey',
69 | 'accept' => '_parseAccept',
70 | 'reject' => '_parseReject',
71 | 'ipv6-policy' => '_parseIPv6Policy',
72 | 'router-signature' => '_parseRouterSignature',
73 | 'contact' => '_parseContact',
74 | 'family' => '_parseFamily',
75 | 'caches-extra-info' => '_parseCachesExtraInfo',
76 | 'extra-info-digest' => '_parseExtraInfoDigest',
77 | 'hidden-service-dir' => '_parseHiddenServiceDir',
78 | 'bandwidth' => '_parseBandwidth',
79 | 'protocols' => '_parseProtocols',
80 | 'allow-single-hop-exits'
81 | => '_parseAllowSingleHopExits',
82 | 'or-address' => '_parseORAddress',
83 | 'master-key-ed25519' => '_parseMasterKeyEd25519',
84 | 'router-sig-ed25519' => '_parseRouterSigEd25519',
85 | 'identity-ed25519' => '_parseIdentityEd25519',
86 | 'onion-key-crosscert'
87 | => '_parseOnionKeyCrosscert',
88 | 'ntor-onion-key-crosscert'
89 | => '_parseNtorOnionKeyCrosscert',
90 | 'tunnelled-dir-server'
91 | => '_parseTunnelledDirServer',
92 | 'proto' => '_parseProtoVersions',
93 | 'a' => '_parseALine',
94 | 'p' => '_parseAccept',
95 | 'p6' => '_parseIPv6Policy',
96 | 'id' => '_parseIdLine',
97 | );
98 |
99 | /**
100 | * Parse directory status reply (v3 directory style)
101 | *
102 | * @param ProtocolReply $reply The reply to parse
103 | * @return array Array of \Dapphp\TorUtils\RouterDescriptor objects
104 | */
105 | public function parseVoteConsensusStatusDocument(ProtocolReply $reply)
106 | {
107 | $doc = new AuthorityStatusDocument();
108 | $descriptor = null;
109 | $authority = null;
110 |
111 | $line = $reply->shift();
112 | if (in_array($line, [ '.', '250 OK', '200 OK', '' ])) {
113 | $line = $reply->shift();
114 | }
115 |
116 | if (empty($line)) {
117 | throw new \Exception('Reply was empty');
118 | }
119 |
120 | $parts = array_map('trim', explode(' ', $line));
121 |
122 | if ($parts[0] !== 'network-status-version') {
123 | throw new \Exception('Reply did not begin with network-status-version, got "' . $line . '".');
124 | }
125 |
126 | $doc->statusVersion = (int)$parts[1];
127 |
128 | foreach($reply as $line) {
129 | $parts = explode(' ', $line, 2);
130 | $keyword = $parts[0];
131 | $extra = isset($parts[1]) ? $parts[1] : null;
132 |
133 | switch ($keyword) {
134 | case 'vote-status':
135 | $doc->voteStatus = $extra;
136 | break;
137 |
138 | case 'consensus-methods':
139 | $doc->consensusMethods = explode(' ', $extra);
140 | break;
141 |
142 | case 'consensus-method':
143 | $doc->consensusMethod = (int)$extra;
144 | break;
145 |
146 | case 'published':
147 | $doc->published = $extra;
148 | break;
149 |
150 | case 'valid-after':
151 | $doc->validAfter = $extra;
152 | break;
153 |
154 | case 'fresh-until':
155 | $doc->freshUntil = $extra;
156 | break;
157 |
158 | case 'valid-until':
159 | $doc->validUntil = $extra;
160 | break;
161 |
162 | case 'voting-delay':
163 | $extra = explode(' ', $extra);
164 | $doc->voteDelaySeconds = (int)$extra[0];
165 | $doc->distDelaySeconds = (int)$extra[1];
166 | break;
167 |
168 | case 'client-versions':
169 | $doc->clientVersions = array_map('trim', explode(',', $extra));
170 | break;
171 |
172 | case 'server-versions':
173 | $doc->serverVersions = array_map('trim', explode(',', $extra));
174 | break;
175 |
176 | case 'known-flags':
177 | $doc->knownFlags = array_map('trim', explode(' ', $extra));
178 | break;
179 |
180 | case 'flag-thresholds':
181 | $doc->flagThresholds = $this->parseDelimitedData($extra);
182 | break;
183 |
184 | case 'recommended-client-protocols':
185 | $doc->recommendedClientProtocols = $this->parseDelimitedData($extra);
186 | break;
187 |
188 | case 'recommended-relay-protocols':
189 | $doc->recommendedRelayProtocols = $this->parseDelimitedData($extra);
190 | break;
191 |
192 | case 'required-client-protocols':
193 | $doc->requiredClientProtocols = $this->parseDelimitedData($extra);
194 | break;
195 |
196 | case 'required-relay-protocols':
197 | $doc->requiredRelayProtocols = $this->parseDelimitedData($extra);
198 | break;
199 |
200 | case 'params':
201 | $doc->params = $this->parseDelimitedData($extra);
202 | break;
203 |
204 | case 'shared-rand-current-value':
205 | list($numReveals, $value) = explode(' ', $extra);
206 | $doc->sharedRandCurrentValue = $value;
207 | break;
208 |
209 | case 'shared-rand-previous-value':
210 | list($numReveals, $value) = explode(' ', $extra);
211 | $doc->sharedRandPreviousValue = $value;
212 | break;
213 |
214 | case 'dir-source':
215 | if (!empty($authority)) {
216 | $doc->authorities[] = $authority;
217 | }
218 |
219 | list($nickname, $identity, $hostname, $ip, $dirPort, $orPort) = explode(' ', $extra);
220 | $authority = [
221 | 'nickname' => $nickname,
222 | 'fingerprint' => $identity,
223 | 'hostname' => $hostname,
224 | 'ip_address' => $ip,
225 | 'dir_port' => $dirPort,
226 | 'or_port' => $orPort,
227 | ];
228 | break;
229 |
230 | case 'contact':
231 | $authority['contact'] = $extra;
232 | break;
233 |
234 | case 'vote-digest':
235 | $authority['vote-digest'] = $extra;
236 | break;
237 |
238 | case 'shared-rand-participate':
239 | $authority['shared-rand-participate'] = true;
240 | break;
241 |
242 | case 'shared-rand-commit':
243 | if (!isset($authority['shared-rand-commit'])) {
244 | // If a vote contains multiple commits from the same authority, the receiver MUST only consider
245 | // the first commit listed.
246 | $parts = explode(' ', $extra);
247 | $authority['shared-rand-commit'] = [
248 | 'version' => $parts[0],
249 | 'algname' => $parts[1],
250 | 'identity' => $parts[2],
251 | 'commit' => $parts[3],
252 | 'reveal' => isset($parts[4]) ? $parts[4] : null,
253 | ];
254 | }
255 | break;
256 |
257 | // authority key certificates
258 | case 'dir-key-certificate-version':
259 | case 'fingerprint':
260 | case 'dir-key-published':
261 | case 'dir-key-expires':
262 | $authority[$keyword] = $extra;
263 | break;
264 |
265 | case 'dir-identity-key':
266 | case 'dir-signing-key':
267 | $authority[$keyword] = $this->_parseRsaKey($reply);
268 | break;
269 |
270 | case 'dir-key-crosscert':
271 | // TODO: Implementations MUST verify that the signature is a correct signature of the hash of the identity key using the signing key.
272 | $authority[$keyword] = $this->_parseBlockData($reply, '-----BEGIN ID SIGNATURE-----', '-----END ID SIGNATURE-----');
273 | break;
274 |
275 | case 'dir-key-certification':
276 | $authority[$keyword] = $this->_parseBlockData($reply, '-----BEGIN SIGNATURE-----', '-----END SIGNATURE-----');
277 | break;
278 |
279 | case 'r':
280 | if (!empty($authority)) {
281 | $doc->authorities[] = $authority;
282 | $authority = null;
283 | }
284 | if (isset($descriptor) && $descriptor) {
285 | $doc->descriptors[] = $descriptor;
286 | }
287 |
288 | $descriptor = new RouterDescriptor();
289 | $descriptor->methods = [];
290 | $descriptor->setArray($this->_parseRLine($line));
291 | break;
292 |
293 | case 'a':
294 | $descriptor->setArray($this->_parseALine($line));
295 | break;
296 |
297 | case 's':
298 | $descriptor->setArray($this->_parseSLine($line));
299 | break;
300 |
301 | case 'v':
302 | $descriptor->setArray($this->_parsePlatform($extra));
303 | break;
304 |
305 | case 'pr':
306 | $descriptor->setArray($this->_parseProtoVersions($extra));
307 | break;
308 |
309 | case 'w':
310 | $descriptor->setArray($this->_parseWLine($line));
311 | break;
312 |
313 | case 'p':
314 | $descriptor->setArray($this->_parsePLine($line));
315 | break;
316 |
317 | case 'm':
318 | list ($methods, $digest) = explode(' ', $extra);
319 | $methods = array_map('trim', explode(',', $methods));
320 | $digest = $this->parseDelimitedData($digest);
321 | foreach($methods as $method) {
322 | $descriptor->methods[$method][array_keys($digest)[0]] = array_values($digest)[0];
323 | }
324 | break;
325 |
326 | case 'id':
327 | $parts = explode(' ', $extra);
328 | $descriptor->ed25519_identity = $parts[1];
329 | break;
330 |
331 | case 'stats':
332 | $descriptor->stats = $this->parseDelimitedData($extra);
333 | break;
334 |
335 | case 'directory-footer':
336 | if (isset($descriptor) && $descriptor)
337 | $doc->descriptors[] = $descriptor;
338 | break;
339 |
340 | case 'bandwidth-weights':
341 | $doc->bandwidthWeights = array_map('intval', $this->parseDelimitedData($extra));
342 | break;
343 |
344 | case 'directory-signature':
345 | $parts = explode(' ', $extra);
346 | $alg = 'sha1';
347 | if (count($parts) == 3) {
348 | $alg = array_shift($parts);
349 | }
350 | $identity = $parts[0];
351 | $digest = $parts[1];
352 | $signature = $this->_parseBlockData(
353 | $reply,
354 | '-----BEGIN SIGNATURE-----',
355 | '-----END SIGNATURE-----'
356 | );
357 |
358 | $doc->directorySignatures[] = [
359 | 'algorithm' => $alg,
360 | 'identity' => $identity,
361 | 'digest' => $digest,
362 | 'signature' => $signature,
363 | ];
364 |
365 | break;
366 |
367 | default:
368 | break;
369 |
370 | }
371 | }
372 |
373 | return $doc;
374 | }
375 |
376 | /**
377 | * Parse directory status reply (v3 directory style)
378 | *
379 | * @param ProtocolReply $reply The reply to parse
380 | * @return array Array of \Dapphp\TorUtils\RouterDescriptor objects
381 | */
382 | public function parseRouterStatus(ProtocolReply $reply)
383 | {
384 | $descriptors = array();
385 | $descriptor = null;
386 |
387 | foreach($reply->getReplyLines() as $line) {
388 | if ($line == '.' || $line == '250 OK') {
389 | continue;
390 | }
391 |
392 | switch($line[0][0]) {
393 | case 'r':
394 | if ($descriptor != null)
395 | $descriptors[$descriptor->fingerprint] = $descriptor;
396 |
397 | $descriptor = new RouterDescriptor();
398 | $descriptor->setArray($this->_parseRLine($line));
399 | break;
400 |
401 | case 'a':
402 | $descriptor->setArray($this->_parseALine($line));
403 | break;
404 |
405 | case 's':
406 | $descriptor->setArray($this->_parseSLine($line));
407 | break;
408 |
409 | case 'v':
410 | $descriptor->setArray($this->_parsePlatform($line));
411 | break;
412 |
413 | case 'w':
414 | $descriptor->setArray($this->_parseWLine($line));
415 | break;
416 |
417 | case 'p':
418 | $descriptor->setArray($this->_parsePLine($line));
419 | break;
420 |
421 | default:
422 | //var_dump("UNKNOWN ROUTER STATUS LINE {$line[0][0]}: ", $line);
423 | }
424 | }
425 |
426 | $descriptors[$descriptor->fingerprint] = $descriptor;
427 |
428 | return $descriptors;
429 | }
430 |
431 | /**
432 | * Parse a router descriptor or microdescriptor
433 | *
434 | * @param ProtocolReply $reply The reply to parse
435 | * @return array Array of \Dapphp\TorUtils\RouterDescriptor objects
436 | */
437 | public function parseDirectoryStatus(ProtocolReply $reply)
438 | {
439 | $descriptors = array();
440 | $descriptor = new RouterDescriptor();
441 | $mds = false;
442 |
443 | if (strpos($reply[0], 'onion-key') === 0 || strpos($reply[1], 'onion-key') === 0) {
444 | $mds = true; // parsing full microdescriptor list
445 | }
446 |
447 | foreach($reply as $line) {
448 | if (preg_match('/^200 OK/i', $line)) continue; // for DirectoryClient HTTP responses
449 | if (trim($line) == '') continue;
450 |
451 | $opt = false;
452 |
453 | if (substr($line, 0, 4) == 'opt ') {
454 | $opt = true;
455 | $line = substr($line, 4);
456 | }
457 |
458 | $values = explode(' ', $line, 2);
459 | if (sizeof($values) < 2) {
460 | $values[1] = null;
461 | }
462 | list ($keyword, $value) = $values;
463 |
464 | if ($keyword == 'router' || ($keyword == 'onion-key' && $mds)) {
465 | if ($descriptor && $descriptor->fingerprint) {
466 | $descriptors[$descriptor->fingerprint] = $descriptor;
467 | } elseif ($descriptor && $mds) {
468 | $descriptors[] = $descriptor;
469 | }
470 |
471 | $descriptor = new RouterDescriptor();
472 | }
473 |
474 | if (array_key_exists($keyword, $this->descriptorReplyLines)) {
475 | $descriptor->setArray(
476 | call_user_func(
477 | array($this, $this->descriptorReplyLines[$keyword]), $value, $reply
478 | )
479 | );
480 | } else {
481 | if (!$opt) {
482 | trigger_error('No callback found for keyword ' . $keyword, E_USER_NOTICE);
483 | }
484 | }
485 | }
486 |
487 | if ($descriptor->fingerprint) {
488 | $descriptors[$descriptor->fingerprint] = $descriptor;
489 | } else {
490 | $descriptors[] = $descriptor;
491 | }
492 |
493 | return $descriptors;
494 | }
495 |
496 | public function parseAddrMap($line)
497 | {
498 | if (strpos($line, 'ADDRMAP') !== 0) {
499 | throw new \Exception('Data passed to parseAddrMap must begin with ADDRMAP');
500 | }
501 |
502 | if (!preg_match('/^ADDRMAP ([^\s]+) ([^\s]+) (?:(NEVER|"[^"]+"))( .*)?$/', $line, $match)) {
503 | throw new ProtocolError("Invalid ADDRMAP line '{$line}'");
504 | }
505 |
506 | $map = [
507 | 'ADDRESS' => $match[1],
508 | 'NEWADDRESS' => $match[2],
509 | 'EXPIRY' => str_replace('"', '', $match[3]),
510 | ];
511 |
512 | $map = array_merge($map, $this->parseKeywordArguments($match[4]));
513 |
514 | return $map;
515 | }
516 |
517 | /**
518 | * Parase a circuit status (CIRC) response
519 | *
520 | * @param string $line A circuit status line (with or without /^CIRC/)
521 | * @throws ProtocolError If status line or value is malformed
522 | * @return \Dapphp\TorUtils\CircuitStatus
523 | */
524 | public function parseCircuitStatusLine($line)
525 | {
526 | require_once __DIR__ . '/CircuitStatus.php';
527 |
528 | $c = new CircuitStatus();
529 |
530 | if (preg_match('/^\s*CIRC /', $line)) {
531 | $line = preg_replace('/^\s*CIRC\s*/', '', $line);
532 | }
533 |
534 | $parts = explode(' ', $line, 3);
535 |
536 | if (sizeof($parts) < 3) {
537 | throw new ProtocolError('Error parsing circuit status, expected at least 3 parts but got ' . sizeof($parts));
538 | }
539 |
540 | $c->id = $parts[0];
541 | $c->status = $parts[1];
542 | $line = $parts[2];
543 |
544 | if (!in_array($c->status, array('LAUNCHED', 'BUILT', 'EXTENDED', 'FAILED', 'CLOSED'))) {
545 | throw new ProtocolError("Unknown circuit status '{$c->status}'");
546 | }
547 |
548 | if ($line[0] == '$') {
549 | list ($temp, $line) = explode(' ', $line, 2);
550 | $temp = explode(',', $temp);
551 |
552 | foreach($temp as $hop) {
553 | $fpnick = explode('~', $hop);
554 | // TODO: check size
555 | $c->path[] = array($fpnick[0], $fpnick[1]);
556 | }
557 | }
558 |
559 | for ($i = 0; $i < 9; ++$i) {
560 | if (trim($line) == '') break;
561 | $parts = explode(' ', $line, 2);
562 |
563 | if (sizeof($parts) < 1) break;
564 |
565 | $what = $parts[0];
566 |
567 | if (sizeof($parts) == 2) {
568 | $line = $parts[1];
569 | } else {
570 | $line = '';
571 | }
572 |
573 | $parts = explode('=', $what, 2);
574 |
575 | if (sizeof($parts) < 2) {
576 | throw new ProtocolError("Expecting KEY=VALUE; got $what");
577 | }
578 |
579 | $key = $parts[0];
580 | $val = $parts[1];
581 |
582 | switch($key) {
583 | case 'BUILD_FLAGS':
584 | $c->buildFlags = explode(',', $val);
585 | break;
586 |
587 | case 'PURPOSE':
588 | $c->purpose = $val;
589 | break;
590 |
591 | case 'HS_STATE':
592 | $c->hsState = $val;
593 | break;
594 |
595 | case 'REND_QUERY':
596 | $c->rendQuery = $val;
597 | break;
598 |
599 | case 'TIME_CREATED':
600 | $c->created = $val;
601 | break;
602 |
603 | case 'REASON':
604 | $c->reason = $val;
605 | break;
606 |
607 | case 'REMOTE_REASON':
608 | $c->remoteReason = $val;
609 | break;
610 |
611 | case 'SOCKS_USERNAME':
612 | $c->socksUsername = $val;
613 | break;
614 |
615 | case 'SOCKS_PASSWORD':
616 | $c->socksPassword = $val;
617 | break;
618 | }
619 | }
620 |
621 | return $c;
622 | }
623 |
624 | private function _parseRouter($line)
625 | {
626 | $values = explode(' ', $line);
627 |
628 | if (sizeof($values) < 5) {
629 | throw new ProtocolError('Error parsing router line. Expected 5 values, got ' . sizeof($values));
630 | }
631 |
632 | return array(
633 | 'nickname' => $values[0],
634 | 'ip_address' => $values[1],
635 | 'or_port' => $values[2],
636 | /* socksport - deprecated */
637 | 'dir_port' => $values[4],
638 | );
639 | }
640 |
641 | private function _parsePlatform($line)
642 | {
643 | return array('platform' => $line);
644 | }
645 |
646 | private function _parsePublished($line)
647 | {
648 | $values = explode(' ', $line);
649 |
650 | if (sizeof($values) != 2) {
651 | throw new ProtocolError('Error parsing published line. Expected 2 values, got ' . sizeof($values));
652 | }
653 |
654 | $date = $values[0];
655 | $time = $values[1];
656 |
657 | // TODO: validate
658 |
659 | return array(
660 | 'published' => $line,
661 | );
662 | }
663 |
664 | private function _parseFingerprint($line)
665 | {
666 | return array(
667 | 'fingerprint' => str_replace(' ', '', $line),
668 | );
669 | }
670 |
671 | private function _parseHibernating($line)
672 | {
673 | return array(
674 | 'hibernating' => $line,
675 | );
676 | }
677 |
678 | private function _parseUptime($line)
679 | {
680 | if (!preg_match('/^\d+$/', $line)) {
681 | throw new ProtocolError('Invalid uptime, expected numeric value');
682 | }
683 |
684 | return array(
685 | 'uptime' => $line,
686 | );
687 | }
688 |
689 | private function _parseOverloadGeneral($line)
690 | {
691 | return array(
692 | 'overload_general' => true,
693 | );
694 | }
695 |
696 | private function _parseOnionKey($line, ProtocolReply $reply)
697 | {
698 | $key = $this->_parseRsaKey($reply);
699 |
700 | return array(
701 | 'onion_key' => $key,
702 | );
703 | }
704 |
705 | private function _parseNtorOnionKey($line)
706 | {
707 | $len = strlen($line) % 4;
708 | $line = str_pad($line, strlen($line) + $len, '=');
709 |
710 | if (base64_decode($line) === false) {
711 | throw new ProtocolError('ntor-onion-key did not contain valid base64 encoded data');
712 | }
713 |
714 | return array(
715 | 'ntor_onion_key' => $line,
716 | );
717 | }
718 |
719 | private function _parseSigningKey($line, ProtocolReply $reply)
720 | {
721 | $key = $this->_parseRsaKey($reply);
722 |
723 | return array(
724 | 'signing_key' => $key,
725 | );
726 | }
727 |
728 | private function _parseAccept($line)
729 | {
730 | $exit = $line;
731 |
732 | return array(
733 | 'exit_policy4' => array('accept' => $exit),
734 | );
735 | }
736 |
737 | private function _parseReject($line)
738 | {
739 | $exit = $line;
740 |
741 | return array(
742 | 'exit_policy4' => array('reject' => $exit),
743 | );
744 | }
745 |
746 | private function _parseIPv6Policy($line)
747 | {
748 | list($policy, $portlist) = explode(' ', $line);
749 | $ports = explode(',', $portlist);
750 | $p = array($policy => $ports);
751 |
752 | if (isset($p['reject'])) {
753 | $p['accept'] = array('*:*');
754 | } else {
755 | $p['reject'] = array('*:*');
756 | }
757 |
758 | return array(
759 | 'exit_policy6' => $p,
760 | );
761 | }
762 |
763 | private function _parseRouterSignature($line, ProtocolReply $reply)
764 | {
765 | $key = $this->_parseBlockData($reply, '-----BEGIN SIGNATURE-----', '-----END SIGNATURE-----');
766 |
767 | return array(
768 | 'router_signature' => $key,
769 | );
770 | }
771 |
772 | private function _parseContact($line)
773 | {
774 | return array('contact' => $line);
775 | }
776 |
777 | private function _parseFamily($line)
778 | {
779 | return array(
780 | 'family' => explode(' ', $line),
781 | );
782 | }
783 |
784 | private function _parseCachesExtraInfo($line)
785 | {
786 | // presence of this field indicates the server caches extra info
787 | return array('caches_extra_info' => true);
788 | }
789 |
790 | private function _parseExtraInfoDigest($line)
791 | {
792 | return array(
793 | 'extra_info_digest' => $line,
794 | );
795 | }
796 |
797 | private function _parseHiddenServiceDir($line)
798 | {
799 | if (empty($line) || ($line && trim($line) == '')) {
800 | $line = '2';
801 | }
802 |
803 | return array(
804 | 'hidden_service_dir' => $line,
805 | );
806 | }
807 |
808 | private function _parseBandwidth($line)
809 | {
810 | $values = explode(' ', $line);
811 |
812 | if (sizeof($values) < 3) {
813 | throw new ProtocolError('Error parsing bandwidth line. Expected 3 values, got ' . sizeof($values));
814 | }
815 |
816 | return array(
817 | 'bandwidth_average' => $values[0],
818 | 'bandwidth_burst' => $values[1],
819 | 'bandwidth_observed' => $values[2],
820 | );
821 | }
822 |
823 | private function _parseProtocols($line)
824 | {
825 | return array(
826 | 'protocols' => $line,
827 | );
828 | }
829 |
830 | private function _parseProtoVersions($line)
831 | {
832 | $protos = [];
833 | $entries = explode(' ', $line);
834 |
835 | // this line looks something like:
836 | // proto Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2
837 | // but could include a value like "Something=3,5-6"
838 |
839 | foreach($entries as $entry) {
840 | list($keyword, $values) = explode('=', $entry);
841 | $protos[$keyword] = [];
842 |
843 | $values = explode(',', $values);
844 | foreach($values as $value) {
845 | if (strpos($value, '-') !== false) {
846 | $value = explode('-', $value);
847 | $value[0] = (int)$value[0];
848 | $value[1] = (int)$value[1];
849 |
850 | if ($value[0] < $value[1]) {
851 | $protos[$keyword] = array_merge($protos[$keyword], range($value[0], $value[1]));
852 | }
853 | } else {
854 | $protos[$keyword][] = $value;
855 | }
856 | }
857 | }
858 |
859 | return array(
860 | 'proto' => $protos,
861 | );
862 | }
863 |
864 | private function _parseAllowSingleHopExits($line)
865 | {
866 | // presence of this line indicates the router allows single hop exits
867 | return array('allow_single_hop_exits' => true);
868 | }
869 |
870 | private function _parseORAddress($line)
871 | {
872 | return array('or_address' => $line);
873 | }
874 |
875 | private function _parseMasterKeyEd25519($line)
876 | {
877 | return array('ed25519_key' => $line);
878 | }
879 |
880 | private function _parseRouterSigEd25519($line)
881 | {
882 | return array('ed25519_sig' => $line);
883 | }
884 |
885 | private function _parseIdentityEd25519($line, ProtocolReply $reply)
886 | {
887 | $cert = $this->_parseBlockData($reply, '-----BEGIN ED25519 CERT-----', '-----END ED25519 CERT-----');
888 |
889 | return array(
890 | 'ed25519_identity' => $cert,
891 | );
892 | }
893 |
894 | private function _parseOnionKeyCrosscert($line, ProtocolReply $reply)
895 | {
896 | $cert = $this->_parseBlockData($reply, '-----BEGIN CROSSCERT-----', '-----END CROSSCERT-----');
897 |
898 | return array(
899 | 'onion_key_crosscert' => $cert,
900 | );
901 | }
902 |
903 | public function _parseNtorOnionKeyCrosscert($line, ProtocolReply $reply)
904 | {
905 | $signbit = $line;
906 | $cert = $this->_parseBlockData($reply, '-----BEGIN ED25519 CERT-----', '-----END ED25519 CERT-----');
907 |
908 | return array(
909 | 'ntor_onion_key_crosscert_signbit' => $signbit,
910 | 'ntor_onion_key_crosscert' => $cert,
911 | );
912 | }
913 |
914 | public function _parseTunnelledDirServer($line)
915 | {
916 | return array('tunnelled_dir_server' => true);
917 | }
918 |
919 | public function _parseIdLine($line)
920 | {
921 | $ret = array();
922 |
923 | list($keytype, $value) = explode(' ', $line, 2);
924 |
925 | if ($keytype == 'rsa1024') {
926 | /* base64 encoded fingerprint - implementations should ignore
927 | bin2hex(base64_decode($value)) == fingerprint */
928 | } elseif ($keytype == 'ed25519') {
929 | $ret['ed25519_key'] = $value;
930 | } else { /* unknown key type - ignore */ }
931 |
932 | return $ret;
933 | }
934 |
935 | private function _parseRsaKey(ProtocolReply $reply)
936 | {
937 | return $this->_parseBlockData($reply, '-----BEGIN RSA PUBLIC KEY-----', '-----END RSA PUBLIC KEY-----');
938 | }
939 |
940 | private function _parseRLine($line)
941 | {
942 | $values = explode(' ', $line);
943 |
944 | return array(
945 | 'nickname' => $values[1],
946 | 'fingerprint' => substr(self::base64ToHexString($values[2]), 0, 40),
947 | 'digest' => substr(self::base64ToHexString($values[3]), 0, 40),
948 | 'published' => $values[4] . ' ' . $values[5],
949 | 'ip_address' => $values[6],
950 | 'or_port' => $values[7],
951 | 'dir_port' => $values[8],
952 | );
953 | }
954 |
955 | private function _parseALine($line)
956 | {
957 | if (strpos($line, ' ') !== false) {
958 | $values = explode(' ', $line, 2);
959 | $line = $values[1];
960 | }
961 |
962 | if (preg_match('/\[([^]]+)]+:(\d+)/', $line, $match)) {
963 | $ip = $match[1];
964 | $port = $match[2];
965 | } else {
966 | list($ip, $port) = explode(':', $line);
967 | }
968 |
969 | return array(
970 | 'or_port' => $port,
971 | 'ipv6_address' => $ip,
972 | );
973 | }
974 |
975 | private function _parseSLine($line)
976 | {
977 | $values = explode(' ', $line);
978 | array_shift($values);
979 |
980 | return array(
981 | 'flags' => $values,
982 | );
983 | }
984 |
985 | private function _parseWLine($line)
986 | {
987 | $bandwidth = $this->_parseDelimitedData($line, 'w');
988 |
989 | if (!isset($bandwidth['Bandwidth'])) {
990 | throw new ProtocolError("Bandwidth value not present in 'w' line");
991 | }
992 |
993 | return array(
994 | 'bandwidth' => $bandwidth['Bandwidth'],
995 | 'bandwidth_measured' => (isset($bandwidth['Measured']) ? $bandwidth['Measured'] : null),
996 | 'bandwidth_unmeasured' => (isset($bandwidth['Unmeasured']) ? $bandwidth['Unmeasured'] : null),
997 | );
998 | }
999 |
1000 | private function _parsePLine($line)
1001 | {
1002 | $values = explode(' ', $line);
1003 |
1004 | return array(
1005 | 'exit_policy4' => array($values[1] => $values[2]),
1006 | );
1007 | }
1008 |
1009 | public function parseProtocolInfo($reply)
1010 | {
1011 | /*
1012 | 250-PROTOCOLINFO 1
1013 | 250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/var/run/tor/control.authcookie"
1014 | 250-VERSION Tor="0.2.4.24"
1015 | 250 OK
1016 | */
1017 | $methods = $cookiefile = $version = null;
1018 | $info = $reply[0];
1019 | $auth = $reply[1];
1020 | $version = $reply[2];
1021 |
1022 | $pInfo = array_map('trim', explode(' ', $info, 2));
1023 | if (sizeof($pInfo) != 2 || $pInfo[0] != 'PROTOCOLINFO') {
1024 | throw new ProtocolError(sprintf('Unexpected PROTOCOLINFO response; got "%s"', $info));
1025 | } elseif (!preg_match('/^\d$/', $pInfo[1])) {
1026 | throw new ProtocolError(sprintf('Invalid PROTOCOLINFO version. Expected 1*DIGIT; got "%s"', $pInfo[1]));
1027 | }
1028 |
1029 | $authInfo = array_map('trim', explode(' ', $auth, 2));
1030 | if (sizeof($authInfo) != 2 || $authInfo[0] != 'AUTH') {
1031 | throw new ProtocolError(sprintf('Expected AUTH line; got "%s"', $auth));
1032 | }
1033 |
1034 | $values = $this->_parseDelimitedData($authInfo[1]);
1035 |
1036 | if (!isset($values['METHODS']) || empty($values['METHODS'])) {
1037 | throw new ProtocolError('PROTOCOLINFO reply did not contain any authentication methods');
1038 | }
1039 |
1040 | $methods = $values['METHODS'];
1041 |
1042 | if (isset($values['COOKIEFILE'])) {
1043 | $cookiefile = $values['COOKIEFILE'];
1044 | }
1045 |
1046 | $versionInfo = array_map('trim', explode(' ', $version, 2));
1047 | if (sizeof($versionInfo) != 2 || $versionInfo[0] != 'VERSION') {
1048 | throw new ProtocolError(sprintf('Expected VERSION line; got "%s"', $version));
1049 | }
1050 |
1051 | $version = $this->_parseDelimitedData($versionInfo[1]);
1052 | if (!isset($version['Tor'])) {
1053 | throw new ProtocolError('PROTOCOLINFO VERSION line did not match expected format');
1054 | }
1055 |
1056 | $version = $version['Tor'];
1057 |
1058 | return array(
1059 | 'methods' => explode(',', $methods),
1060 | 'cookiefile' => $cookiefile,
1061 | 'version' => $version,
1062 | );
1063 | }
1064 |
1065 | private function _parseBlockData(ProtocolReply $reply, $startDelimiter, $endDelimter)
1066 | {
1067 | $reply->next();
1068 |
1069 | $line = $reply->current();
1070 |
1071 | if ($line != $startDelimiter) {
1072 | throw new ProtocolError('Expected line beginning with "' . $startDelimiter . '", got ' . $line);
1073 | }
1074 |
1075 | $data = $line;
1076 |
1077 | do {
1078 | $reply->next();
1079 | if (!$reply->valid()) {
1080 | throw new \Exception('Reached end of reply without matching end delimiter "' . $endDelimter . '"');
1081 | }
1082 | $line = $reply->current();
1083 | $data .= "\n" . $line;
1084 | } while ($reply->valid() && $line != $endDelimter);
1085 |
1086 | return $data;
1087 | }
1088 |
1089 | public function parseKeywordArguments($input)
1090 | {
1091 | $eventData = [];
1092 | $offset = 0;
1093 |
1094 | do {
1095 | if ($input[$offset] == ' ') {
1096 | $offset++;
1097 | continue;
1098 | }
1099 |
1100 | $value = null;
1101 | $temp = substr($input, $offset);
1102 | $keyword = $this->parseAlpha($temp);
1103 |
1104 | $offset += strlen($keyword);
1105 |
1106 | if ($input[$offset] != '=') {
1107 | throw new \InvalidArgumentException(
1108 | sprintf('Expected "=" at offset %d, got %s', $offset, $input[$offset])
1109 | );
1110 | }
1111 |
1112 | $offset++;
1113 |
1114 | $temp = substr($input, $offset);
1115 |
1116 | if (0 === strlen($temp)) {
1117 | // empty value, end of line
1118 | $value = '';
1119 | } elseif ($input[$offset] == ' ') {
1120 | // empty value, more keywords remain
1121 | $value = '';
1122 | $offset += 1;
1123 | } elseif ($input[$offset] == '"') {
1124 | $value = $this->parseQuotedString($temp);
1125 | $offset += strlen($value) + 3;
1126 | } else {
1127 | $value = $this->parseNonSpDquote($temp);
1128 | $offset += strlen($value) + 1;
1129 | }
1130 |
1131 | $eventData[$keyword] = $value;
1132 |
1133 | } while ($offset < strlen($input));
1134 |
1135 | return $eventData;
1136 | }
1137 |
1138 | public function parseAlpha($input)
1139 | {
1140 | if (preg_match('/([a-zA-Z_]{1,})/', $input, $match)) {
1141 | return $match[1];
1142 | } else {
1143 | throw new \InvalidArgumentException("Illegal keyword format");
1144 | }
1145 | }
1146 |
1147 | public function parseQuotedString($input)
1148 | {
1149 | $len = strlen($input);
1150 | $val = '';
1151 | $terminated = false;
1152 |
1153 | for ($i = 1; $i < $len; ++$i) {
1154 | $c = $input[$i];
1155 |
1156 | if ($c == '"') {
1157 | if (strlen($val) > 1 && $val[strlen($val)-1] != '\\') {
1158 | $terminated = true;
1159 | break;
1160 | }
1161 | }
1162 |
1163 | if (preg_match('/[\x01-\x08\x0b\x0c\x0e-\x7f]/', $c)) {
1164 | $val .= $c;
1165 | }
1166 | }
1167 |
1168 | if (!$terminated) {
1169 | throw new \InvalidArgumentException("Unterminated quote string encountered");
1170 | }
1171 |
1172 | return $val;
1173 | }
1174 |
1175 | public function parseNonSpDquote($input)
1176 | {
1177 | if (preg_match('/^([\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]+)(?:\s|$)/', $input, $match)) {
1178 | return $match[1];
1179 | } else {
1180 | throw new \InvalidArgumentException("Illegal keyword argument string encountered: $input");
1181 | }
1182 | }
1183 |
1184 | public function parseDelimitedData($data, $prefix = null, $delimiter = '=', $boundary = ' ')
1185 | {
1186 | return $this->_parseDelimitedData($data, $prefix, $delimiter, $boundary);
1187 | }
1188 |
1189 | private function _parseDelimitedData($data, $prefix = null, $delimiter = '=', $boundary = ' ')
1190 | {
1191 | $return = [];
1192 |
1193 | if ($prefix && is_string($prefix)) {
1194 | $data = preg_replace('/^' . preg_quote($prefix) . ' /', '', $data);
1195 | }
1196 |
1197 | $eof = true;
1198 | $item = '';
1199 | $value = '';
1200 | $state = 'i';
1201 | $quoted = false;
1202 | $length = strlen($data);
1203 |
1204 | for ($p = 0; $p < $length; ++$p) {
1205 | $c = $data[$p];
1206 | $eof = $p + 1 >= $length;
1207 |
1208 | switch ($state) {
1209 | case 'i':
1210 | if ($c == $delimiter) {
1211 | $state = 'd';
1212 | } else {
1213 | $item .= $c;
1214 | }
1215 | break;
1216 |
1217 | case 'd':
1218 | if ($c == '"') {
1219 | $quoted = true;
1220 | $state = 'dr';
1221 | } else {
1222 | $value .= $c;
1223 | $quoted = false;
1224 | $state = 'dr';
1225 | }
1226 | break;
1227 |
1228 | /** @noinspection PhpMissingBreakStatementInspection */
1229 | case 'dr':
1230 | if ((!$quoted && $c == $boundary) || ($quoted && $c == '"')) {
1231 | $state = 'n'; // fall through to next case
1232 | } else {
1233 | $value .= $c;
1234 | break;
1235 | }
1236 |
1237 | case 'n':
1238 | $return[$item] = $value;
1239 | $item = $value = '';
1240 | $state = 'i';
1241 | $quoted = false;
1242 | break;
1243 | }
1244 | }
1245 |
1246 | if ($eof) {
1247 | if ($quoted) {
1248 | throw new \Exception("EOF encountering while parsing quoted value in delimited data");
1249 | }
1250 |
1251 | $return[$item] = $value;
1252 | }
1253 |
1254 | return $return;
1255 | }
1256 |
1257 | public static function base64ToHexString($base64)
1258 | {
1259 | $padLength = strlen($base64) % 4;
1260 | $base64 .= str_pad($base64, $padLength, '=');
1261 | $identity = base64_decode($base64);
1262 |
1263 | return strtoupper(bin2hex($identity));
1264 | }
1265 | }
1266 |
--------------------------------------------------------------------------------
/src/ProtocolError.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | /**
42 | * Tor ProtocolReply object.
43 | *
44 | * This object represents a reply from the Tor control protocol or directory
45 | * server. The ProtocolReply holds the status code of the reply and gives
46 | * access to individual lines of data from the response.
47 | *
48 | */
49 | class ProtocolReply implements \Iterator, \ArrayAccess, \Countable
50 | {
51 | private $statusCode;
52 | private $command;
53 | private $dataReply = false;
54 | private $position = 0;
55 | private $lines = array();
56 | private $dirty = true;
57 | private $keys = array();
58 |
59 | /**
60 | * ProtocolReply constructor.
61 | *
62 | * @param ?string $command The command for which the reply will be read
63 | * Certain command responses reply with the command that was sent. Giving
64 | * the command is not necessary, but will remove it from the first line of
65 | * the reply *if* the command name was present in the reply and matched
66 | * what was given.
67 | */
68 | public function __construct($command = null)
69 | {
70 | $this->command = $command;
71 | }
72 |
73 | /**
74 | * Get the name of the command set in the constructor.
75 | *
76 | * Note: this method will not return the actual name of the command in the
77 | * reply, it is only set if a $command was passed to the constructor.
78 | *
79 | * @return string Name of the command being parsed.
80 | */
81 | public function getCommand()
82 | {
83 | return $this->command;
84 | }
85 |
86 | /**
87 | * Gets the status code of the reply (if set)
88 | *
89 | * @return int Response status code.
90 | */
91 | public function getStatusCode()
92 | {
93 | return $this->statusCode;
94 | }
95 |
96 | /**
97 | * Returns a string representation of the reply
98 | *
99 | * @return string The reply from the controller
100 | */
101 | public function __toString()
102 | {
103 | return implode("\n", $this->lines);
104 | }
105 |
106 | /**
107 | * Get the reply as an array of lines
108 | *
109 | * @return array Array of response lines
110 | */
111 | public function getReplyLines()
112 | {
113 | return $this->lines;
114 | }
115 |
116 | /**
117 | * Append a line to the reply and process it. Typically this function
118 | * should not be called as it is only used by the classes for building
119 | * the intial reply object
120 | *
121 | * @param string $line A line of data from the reply to append
122 | */
123 | public function appendReplyLine($line)
124 | {
125 | $this->dirty = true;
126 | $status = null;
127 | $first = sizeof($this->lines) == 0;
128 | $line = rtrim($line, "\r\n");
129 | $command = !empty($this->command) ? $this->command : '';
130 |
131 | if (preg_match('/^(\d{3})-' . preg_quote($command, '/') . '=(.*)$/', $line, $match)) {
132 | // ###-COMMAND=data reply...
133 | $status = $match[1];
134 |
135 | if (strlen(trim($match[2])) > 0) {
136 | $this->lines[]= $match[2];
137 | }
138 | } elseif ($first && preg_match('/^(\d{3})\+' . preg_quote($command, '/') . '=$/', $line, $match)) {
139 | // ###+COMMAND=
140 | $status = $match[1];
141 | $this->dataReply = true;
142 | } elseif (preg_match('/^650[+\-]/', $line)) {
143 | $status = 650;
144 | $this->lines[] = substr($line, 4);
145 | } elseif (preg_match('/^(\d{3})-(.*)$/', $line, $match)) {
146 | // ###-DATA RESPONSE
147 | // or
148 | // ###-Key=Value response
149 | $status = $match[1];
150 | $this->lines[] = $match[2];
151 | } elseif (
152 | !$this->dataReply && (
153 | preg_match('/^(25\d)\s*(.*)$/', $line, $match)
154 | ||
155 | preg_match('/^([456][015]\d)\s*(.*)$/', $line, $match)
156 | )
157 | ) {
158 | // ### STATUS
159 | // https://gitweb.torproject.org/torspec.git/tree/control-spec.txt - Section 4. Replies
160 | // Positive completion replies begin with 25x
161 | if (!$this->statusCode) {
162 | $status = $match[1];
163 | }
164 | $this->lines[] = $match[2];
165 | } else {
166 | // other data from multi-line reply
167 | $this->lines[] = $line;
168 | }
169 |
170 | if ($status != null && $first) $this->statusCode = $status;
171 | }
172 |
173 | /**
174 | * Append multiple lines of data to the reply. Typically this should not
175 | * be used as it is used by the classes constructing replies.
176 | *
177 | * @param array $lines Array of response lines to append
178 | */
179 | public function appendReplyLines(array $lines)
180 | {
181 | $this->lines = array_merge($this->lines, $lines);
182 | $this->dirty = true;
183 | }
184 |
185 | /**
186 | * Tell if the status code of this reply indicates success or not
187 | *
188 | * @return boolean true if reply indicates success, false otherwise
189 | */
190 | public function isPositiveReply()
191 | {
192 | if (strlen($this->statusCode) > 0) {
193 | return substr($this->statusCode, 0, 1) === '2'; // reply begins with 2xy
194 | } else {
195 | return false;
196 | }
197 | }
198 |
199 | public function shift()
200 | {
201 | $this->dirty = true;
202 | return array_shift($this->lines);
203 | }
204 |
205 | /**
206 | * (non-PHPdoc)
207 | * @see Iterator::rewind()
208 | */
209 | public function rewind()
210 | {
211 | $this->position = 0;
212 | }
213 |
214 | /**
215 | * (non-PHPdoc)
216 | * @see Iterator::current()
217 | */
218 | public function current()
219 | {
220 | $key = $this->key();
221 | return $this->lines[$key];
222 | }
223 |
224 | /**
225 | * (non-PHPdoc)
226 | * @see Iterator::key()
227 | */
228 | public function key()
229 | {
230 | if ($this->dirty) {
231 | $this->keys = array_keys($this->lines);
232 | $this->dirty = false;
233 | }
234 | if (isset($this->keys[$this->position])) {
235 | return $this->keys[$this->position];
236 | } else {
237 | return null;
238 | }
239 | }
240 |
241 | /**
242 | * (non-PHPdoc)
243 | * @see Iterator::next()
244 | */
245 | public function next()
246 | {
247 | ++$this->position;
248 | }
249 |
250 | /**
251 | * (non-PHPdoc)
252 | * @see Iterator::valid()
253 | */
254 | public function valid()
255 | {
256 | return ($this->key() !== null);
257 | }
258 |
259 | /**
260 | * (non-PHPdoc)
261 | * @param $offset
262 | * @return bool
263 | * @see ArrayAccess::offsetExists()
264 | */
265 | public function offsetExists($offset)
266 | {
267 | return isset($this->lines[$offset]);
268 | }
269 |
270 | /**
271 | * (non-PHPdoc)
272 | * @param $offset
273 | * @return mixed|null
274 | * @see ArrayAccess::offsetGet()
275 | */
276 | public function offsetGet($offset)
277 | {
278 | return isset($this->lines[$offset]) ? $this->lines[$offset] : null;
279 | }
280 |
281 | /**
282 | * (non-PHPdoc)
283 | * @param $offset
284 | * @param $value
285 | * @see ArrayAccess::offsetSet()
286 | */
287 | public function offsetSet($offset, $value)
288 | {
289 | if (is_null($offset)) {
290 | $this->lines[] = $value;
291 | $this->dirty = true;
292 | } else {
293 | $this->lines[$offset] = $value;
294 | }
295 | }
296 |
297 | /**
298 | * (non-PHPdoc)
299 | * @param $offset
300 | * @see ArrayAccess::offsetUnset()
301 | */
302 | public function offsetUnset($offset)
303 | {
304 | unset($this->lines[$offset]);
305 | $this->dirty = true;
306 | }
307 |
308 | public function count()
309 | {
310 | return count($this->lines);
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/RouterDescriptor.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | /**
42 | * RouterDescriptor class. This class holds all the data relating to a Tor
43 | * node on the network such as nickname, fingerprint, IP address etc.
44 | *
45 | */
46 | class RouterDescriptor
47 | {
48 | /** @var string The OR's nickname */
49 | public $nickname;
50 |
51 | /** @var string Hash of its identity key, encoded in base64, with trailing equals sign(s) removed */
52 | public $fingerprint;
53 |
54 | /** @var string Hash of its most recent descriptor as signed, encoded in base64 */
55 | public $digest;
56 |
57 | /** @var string Publication time of its most recent descriptor as YYYY-MM-DD HH:MM:SS, in UTC */
58 | public $published;
59 |
60 | /** @var string OR's current IP address */
61 | public $ip_address;
62 |
63 | /** @var string OR's current IPv6 address (if using IPv6) */
64 | public $ipv6_address;
65 |
66 | /** @var int OR's current port */
67 | public $or_port;
68 |
69 | /** @var int OR's current directory port, or 0 for none */
70 | public $dir_port;
71 |
72 | /** @var array Additional IP addresses of the OR */
73 | public $or_address = array();
74 |
75 | /** @var string The version of the Tor protocol that this relay is running */
76 | public $platform;
77 |
78 | /** @var string Contact info for the OR as given by the operator */
79 | public $contact;
80 |
81 | /** @var array Array of relay nicknames or hex digests run by an operator */
82 | public $family;
83 |
84 | /** @var int OR uptime in seconds at the time of publication */
85 | public $uptime;
86 |
87 | /** @var bool resent only if the router allows single-hop circuits to make exit connections. Most Tor relays do not support this */
88 | public $allow_single_hop_exits = false;
89 |
90 | /** @var bool Present only if this router is a directory cache that provides extra-info documents */
91 | public $caches_extra_info = false;
92 |
93 | /** @var string a public key in PEM format. This key is used to encrypt CREATE cells for this OR */
94 | public $onion_key;
95 |
96 | /** @var string base-64-encoded-key. A public key used for the ntor circuit extended handshake */
97 | public $ntor_onion_key;
98 |
99 | /** @var string a public key in PEM format. The OR's long-term identity key. It MUST be 1024 bits. */
100 | public $signing_key;
101 |
102 | /** @var string The "SIGNATURE" object contains a signature of the PKCS1-padded hash of the entire server descriptor */
103 | public $router_signature;
104 |
105 | /** @var string Ed25519 master key */
106 | public $ed25519_key;
107 |
108 | /** @var string Ed25519 router signature */
109 | public $ed25519_sig;
110 |
111 | /** @var string Ed25519 identity key in PEM format */
112 | public $ed25519_identity;
113 |
114 | /** @var string RSA signature of sha1 hash of identity key & ed25519 identity key */
115 | public $onion_key_crosscert;
116 |
117 | /** @var string Ed25519 certificate */
118 | public $ntor_onion_key_crosscert;
119 |
120 | /** @var string sign bit of the ntor_onion_key_crosscert */
121 | public $ntor_onion_key_crosscert_signbit;
122 |
123 | /** @var string space-separated sequences of numbers, to indicate which protocols the server supports. As of 30 Mar 2008, specified protocols are "Link 1 2 Circuit 1" */
124 | public $protocols;
125 |
126 | /** @var array Array of protocols (Cons, Desc, DirCache, HSDir, HSIntro,
127 | * HSRend, Link, LinkAuth, Microdesc) and versions supported by a relay.
128 | * Each protocol key contains an array with each invididual version of
129 | * that protocol supported. */
130 | public $proto = array();
131 |
132 | /** @var string a hex-encoded digest of the router's extra-info document, as signed in the router's extra-info */
133 | public $extra_info_digest;
134 |
135 | /** @var bool Present only if this router stores and serves hidden service descriptors. */
136 | public $hidden_service_dir;
137 |
138 | /** @var int An estimate of the bandwidth of this relay, in an arbitrary unit (currently kilobytes per second) */
139 | public $bandwidth;
140 |
141 | /** @var int indicates a measured bandwidth currently produced by measuring stream capacities */
142 | public $bandwidth_measured;
143 |
144 | /** @var int From consensus when bandwidth value is not based on a threshold of 3 or more measurements for this relay */
145 | public $bandwidth_unmeasured;
146 |
147 |
148 | /** @var int volume per second that the OR is willing to sustain over long periods */
149 | public $bandwidth_average;
150 |
151 | /** @var int volume that the OR is willing to sustain in very short intervals */
152 | public $bandwidth_burst;
153 |
154 | /** @var int Estimate of the capacity this relay can handle */
155 | public $bandwidth_observed;
156 |
157 | /** @var array Node status flags (e.g. Exit, Fast, Guard, Running, Stable, Valid) */
158 | public $flags = array();
159 |
160 | /** @var bool Proposal 237 - This relay accepts tunnelled directory requests */
161 | public $tunnelled_directory_server = false;
162 |
163 | /** @var array IPv4 exit policy $exit_policy4['reject'] = array() and $exit_policy4['accept'] = array() */
164 | public $exit_policy4 = array();
165 |
166 | /** @var array IPv6 exit policy $exit_policy6['reject'] = array() and $exit_policy6['accept'] = array() */
167 | public $exit_policy6 = array();
168 |
169 | /** @var string 2 letter country code of the relay IP address */
170 | public $country = null;
171 |
172 | /**
173 | * Set one or more descriptor values from an array
174 | *
175 | * @param array $values Array of key=>value properties to set
176 | * @return self
177 | */
178 | public function setArray(array $values)
179 | {
180 | foreach ($values as $key => $value) {
181 | if ($key === 'exit_policy4' || $key === 'exit_policy6') {
182 | if (!is_array($this->$key))
183 | $this->$key = array();
184 |
185 | if (!isset($this->{$key}['accept']))
186 | $this->{$key}['accept'] = array();
187 |
188 | if (!isset($this->{$key}['reject']))
189 | $this->{$key}['reject'] = array();
190 |
191 | if (isset($value['accept'])) {
192 | if (is_array($value['accept'])) {
193 | $this->{$key}['accept'] = array_merge($this->{$key}['accept'], $value['accept']);
194 | } else {
195 | array_push($this->{$key}['accept'], $value['accept']);
196 | }
197 | }
198 | if (isset($value['reject'])) {
199 | if (is_array($value['reject'])) {
200 | $this->{$key}['reject'] = array_merge($this->{$key}['reject'], $value['reject']);
201 | } else {
202 | array_push($this->{$key}['reject'], $value['reject']);
203 | }
204 | }
205 | } else if ($key === 'or_address') {
206 | array_push($this->{$key}, $value);
207 | } else if (property_exists($this, $key)) {
208 | $this->$key = $value;
209 | }
210 | }
211 |
212 | return $this;
213 | }
214 |
215 | /**
216 | * Get the properties of this descriptor as an array
217 | *
218 | * @return array Array of descriptor information
219 | */
220 | public function getArray()
221 | {
222 | $return = array();
223 |
224 | foreach ($this as $key => $value) {
225 | $return[$key] = $value;
226 | }
227 |
228 | return $return;
229 | }
230 |
231 | /**
232 | * Combine information from a second descriptor with this one.
233 | * Information from the second descriptor not present in $this is added.
234 | *
235 | * @param RouterDescriptor $descriptor The descriptor information to merge
236 | * @return RouterDescriptor $this
237 | */
238 | public function combine(RouterDescriptor $descriptor)
239 | {
240 | foreach($this as $prop => $val) {
241 | if (empty($val) && !empty($descriptor->$prop)) {
242 | $this->$prop = $descriptor->$prop;
243 | }
244 | }
245 |
246 | return $this;
247 | }
248 |
249 | /**
250 | * Return the current calculated uptime of the node based on when the
251 | * descriptor was published and the current time
252 | *
253 | * @param bool $asArray
254 | * @return int|int[]|NULL
255 | */
256 | public function getCurrentUptime($asArray = false)
257 | {
258 | if (isset($this->published) && isset($this->uptime)) {
259 | $uptime = $this->uptime + time() - strtotime($this->published . ' GMT');
260 |
261 | if ((bool)$asArray === false) {
262 | return $uptime;
263 | } else {
264 | $units = array(
265 | 'days' => 86400,
266 | 'hours' => 3600,
267 | 'minutes' => 60,
268 | 'seconds' => 1
269 | );
270 |
271 | foreach($units as $unit => $secs) {
272 | $num = intval($uptime / $secs);
273 |
274 | if ($num > 0) {
275 | $units[$unit] = $num;
276 | } else {
277 | $units[$unit] = 0;
278 | }
279 | $uptime %= $secs;
280 | }
281 |
282 | return $units;
283 | }
284 | } else {
285 | return null;
286 | }
287 | }
288 |
289 | public function __toString()
290 | {
291 | $str = '';
292 |
293 | $str .= sprintf("Nickname: %s Fingerprint: %s\n", $this->nickname, $this->fingerprint);
294 | if (!empty($this->uptime)) {
295 | $uptime = $this->getCurrentUptime(true);
296 | $u = '';
297 | $u .= ($uptime['days'] > 0) ? "{$uptime['days']}d " : '';
298 | $u .= ($uptime['hours'] > 0) ? "{$uptime['hours']}h " : '';
299 | $u .= ($uptime['minutes'] > 0) ? "{$uptime['minutes']}m " : '';
300 | $u .= ($uptime['seconds'] > 0) ? "{$uptime['seconds']}s" : '';
301 | $str .= sprintf("Uptime: %s\n", trim($u));
302 | }
303 | if (!empty($this->flags)) {
304 | $str .= sprintf("Flags: %s\n", implode(' ', $this->flags));
305 | }
306 | if (!empty($this->bandwidth)) {
307 | $str .= sprintf("Weight: %d\n", $this->bandwidth);
308 | }
309 | if ($this->bandwidth_observed > 0) {
310 | $str .= sprintf("Bandwidth: %s MB/s\n", number_format($this->bandwidth_observed / 1000000.0, 2));
311 | }
312 | $str .= sprintf("Platform: %s\n", $this->platform);
313 | $str .= sprintf("Contact: %s\n", $this->contact);
314 | $str .= sprintf("IP Addr: %s\n", $this->ip_address);
315 | if (!empty($this->country)) {
316 | $str .= sprintf("Country: %s\n", strtoupper($this->country));
317 | }
318 | $str .= sprintf("OR Port: %d Dir Port: %d\n", $this->or_port, $this->dir_port);
319 | $str .= sprintf("Exit Policy:\n %s\n %s\n", 'accept ' . (str_replace('accept ', '', implode(' ', $this->exit_policy4['accept']))),
320 | 'reject ' . (str_replace('reject ', '', implode(' ', $this->exit_policy4['reject']))));
321 |
322 | return $str;
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/src/TorCurlWrapper.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 | namespace Dapphp\TorUtils;
40 |
41 | /**
42 | * Tor cURL wrapper class
43 | *
44 | * A class to wrap cURL requests through Tor using SOCKS5
45 | *
46 | * @version 1.1
47 | * @author Drew Phillips
48 | *
49 | */
50 | class TorCurlWrapper
51 | {
52 | private $_ch;
53 | private $_info;
54 | private $statusLine;
55 | private $_responseHeaders;
56 | private $_responseBody;
57 | private $_socksHost;
58 | private $_socksPort;
59 |
60 | /**
61 | * TorCurlWrapper constructor.
62 | *
63 | * Creates a new TorCurlWrapper and initializes a cURL handle with Tor
64 | * as the SOCKS proxy. Defaults to 127.0.0.1:9050
65 | *
66 | * By default, TorCurlWrapper will have cURL track cookies across requests but does not save them.
67 | * Override this behavior by calling TorCurlWrapper::setopt(CURLOPT_COOKIEJAR|CURLOPT_COOKIEFILE, $value)
68 | *
69 | * Other default behavior:
70 | * - Enables CURLOPT_AUTOREFERER by default
71 | * - Enables CURLOPT_FOLLOWLOCATION by default
72 | * - Tells cURL to send Accept-Encoding headers with supported encodings (e.g. gzip, deflate)
73 | *
74 | * Intelligently tries to get cURL to resolve DNS names through Tor, or
75 | * emits a warning if DNS resolution over Tor is not supported.
76 | *
77 | * Example:
78 | *
79 | * $torcurl = new Dapphp\TorUtils\TorCurlWrapper('127.0.0.1:9050');
80 | * // OR
81 | * $torcurl = new Dapphp\TorUtils\TorCurlWrapper('127.0.0.1', 9050);
82 | *
83 | * $torcurl->setopt(CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/20100101 Firefox/38.0');
84 | *
85 | * try {
86 | * $torcurl->httpGet('https://check.torproject.org/');
87 | *
88 | * $http_status = $torcurl->getHttpStatusCode();
89 | * print_r($torcurl->getResponseBody());
90 | * } catch (\Exception $ex) {
91 | * echo "Request failed. Curl error " . $ex->getCode() . ": " . $ex->getMessage();
92 | * }
93 | *
94 | */
95 | public function __construct($proxy = '127.0.0.1', $port = 9050)
96 | {
97 | if (!extension_loaded('curl')) {
98 | throw new \Exception('curl extension is not loaded');
99 | }
100 |
101 | $ch = curl_init();
102 |
103 | if (strpos($proxy, ':') !== false) {
104 | list($proxy, $port) = explode(':', $proxy, 2);
105 | $port = intval($port);
106 | }
107 |
108 | $this->_socksHost = $proxy;
109 | $this->_socksPort = $port;
110 |
111 | curl_setopt($ch, CURLOPT_PROXY, $proxy);
112 | curl_setopt($ch, CURLOPT_PROXYPORT, $port);
113 |
114 | if (defined('CURLPROXY_SOCKS5_HOSTNAME')) {
115 | // PHP >= 5.5.23
116 | curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
117 | } else {
118 | $curl_version = curl_version();
119 | if (version_compare($curl_version['version'], '7.18.0') < 0) {
120 | // curl version too low to even use socks5-hostname
121 | trigger_error(
122 | 'cURL SOCKS5_HOSTNAME not supported. ' .
123 | 'DNS names will *NOT* be resolved using Tor!',
124 | E_USER_WARNING
125 | );
126 | curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
127 | } else {
128 | // curl supports socks5-hostname, PHP does not know about it yet
129 | // tested to work on PHP 5.5.9 with libcurl >= 7.18.0
130 | // php doesn't care what the proxytype is if it doesn't know about it
131 | curl_setopt($ch, CURLOPT_PROXYTYPE, 7);
132 | }
133 | }
134 |
135 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
136 | curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
137 | curl_setopt($ch, CURLOPT_ENCODING, '');
138 |
139 | // enable cookie handling for the duration of the handle
140 | curl_setopt($ch, CURLOPT_COOKIEFILE, '');
141 |
142 | $this->_ch = $ch;
143 | }
144 |
145 | /**
146 | * Destructor. Closes cURL handle and frees resources.
147 | */
148 | public function __destruct()
149 | {
150 | curl_close($this->_ch);
151 | }
152 |
153 | /**
154 | * Wrapper to curl_setopt on the underlying cURL handle.
155 | *
156 | * @param int $option The CURLOPT_XXX option to set
157 | * @param mixed $value The value to be set on option
158 | * @throws \Exception Throws exception if an attempt is made to change the proxy address or type
159 | * @return bool Returns TRUE on success or FALSE on failure
160 | */
161 | public function setopt($option, $value)
162 | {
163 | if ($option == CURLOPT_PROXY) {
164 | throw new \Exception('Cannot set CURLOPT_PROXY - use constructor instead');
165 | } else if ($option == CURLOPT_PROXYTYPE) {
166 | throw new \Exception('Cannot set CURLOPT_PROXYTYPE - SOCKS5_HOSTNAME is required');
167 | }
168 |
169 | return curl_setopt($this->_ch, $option, $value);
170 | }
171 |
172 | /**
173 | * Execute an HTTP GET request to $url using the underlying cURL handle
174 | *
175 | * @param string $url Optional URL to fetch, can also use TorCurlWrapper::setopt(CURLOPT_URL, $url)
176 | * @throws \Exception Throws exception if curl_exec() fails
177 | * @return boolean Returns TRUE if the request was successful (does not necessarily indicate a 200 OK response from the server)
178 | */
179 | public function httpGet($url = null)
180 | {
181 | if (!is_null($url)) {
182 | $this->setopt(CURLOPT_URL, $url);
183 | }
184 |
185 | $this->setopt(CURLOPT_HTTPGET, 1);
186 |
187 | return $this->_execute();
188 | }
189 |
190 | /**
191 | * Execute an HTTP POST request to $url using the underlying cURL handle, passing $params as the POST data
192 | *
193 | * @param string $url Optional URL to fetch, can also use TorCurlWrapper::setopt(CURLOPT_URL, $url)
194 | * @param mixed $params Passed directly to curl_setopt as CURLOPT_POSTFIELDS.
195 | * Be aware of the implications of passing an array vs. a string, or uploading files (CURLFile vs @).
196 | * @throws \Exception Throws exception if curl_exec() fails
197 | * @return boolean Returns TRUE if the request was successful (does not necessarily indicate a 200 OK response from the server)
198 | */
199 | public function httpPost($url = null, $params = null)
200 | {
201 | if (!is_null($url)) {
202 | $this->setopt(CURLOPT_URL, $url);
203 | }
204 |
205 | $this->setopt(CURLOPT_POST, 1);
206 | $this->setopt(CURLOPT_POSTFIELDS, $params);
207 |
208 | return $this->_execute();
209 | }
210 |
211 | /**
212 | * Close the underlying cURL handle and frees the resource.
213 | *
214 | * The TorCurlWrapper constructor is called again on the object to re-initialize it with a fresh cURL handle.
215 | *
216 | * This causes cURL to close any connections, and reset any session cookies it may have been tracking
217 | */
218 | public function close()
219 | {
220 | curl_close($this->_ch);
221 | $this->__construct($this->_socksHost, $this->_socksPort);
222 | }
223 |
224 | /**
225 | * Get the HTTP status line from the last response.
226 | *
227 | * @return string The HTTP status line from the last response
228 | */
229 | public function getHttpStatusLine()
230 | {
231 | return $this->statusLine;
232 | }
233 |
234 | /**
235 | * Gets the response headers from the previous request
236 | *
237 | * @param string|null $header The header (case-insensitive) to get, or an array of headers if null
238 | * @return string|array|null Returns an array of headers from the previous request, or the value of a single header if $header was passed and exists, null if no headers or $header is was not set
239 | */
240 | public function getResponseHeaders($header = null)
241 | {
242 | $return = null;
243 |
244 | if ($header === null) {
245 | $return = $this->_responseHeaders;
246 | } else {
247 | foreach($this->_responseHeaders as $name => $val) {
248 | if (strtolower($name) == strtolower($header)) {
249 | return $val;
250 | }
251 | }
252 | }
253 |
254 | return $return;
255 | }
256 |
257 | /**
258 | * Gets the response body of the previous request
259 | *
260 | * @return string|null The contents of the last response, or null if no previous response. Could return an empty string for empty responses.
261 | */
262 | public function getResponseBody()
263 | {
264 | return $this->_responseBody;
265 | }
266 |
267 | /**
268 | * Gets the HTTP status code from the previous request
269 | *
270 | * @return int|null The response code of the previous request, or 0 if last response failed or null if not set
271 | */
272 | public function getHttpStatusCode()
273 | {
274 | if (isset($this->_info['http_code'])) {
275 | return $this->_info['http_code'];
276 | } else {
277 | return null;
278 | }
279 | }
280 |
281 | /**
282 | * Returns the data from curl_getinfo() for the last request
283 | *
284 | * @return array The info from the last cURL request
285 | */
286 | public function getInfo()
287 | {
288 | return $this->_info;
289 | }
290 |
291 | /**
292 | * Execute a request on the cURL handle
293 | *
294 | * @throws \Exception Throws exception if curl_exec() fails
295 | * @return boolean returns TRUE on success
296 | */
297 | private function _execute()
298 | {
299 | $this->_responseHeaders = null;
300 | $this->_responseBody = null;
301 |
302 | curl_setopt($this->_ch, CURLOPT_HEADER, 1);
303 | curl_setopt($this->_ch, CURLOPT_RETURNTRANSFER, 1);
304 |
305 | $response = curl_exec($this->_ch);
306 |
307 | $this->_info = curl_getinfo($this->_ch);
308 |
309 | if ($response === false) {
310 | throw new \Exception(curl_error($this->_ch), curl_errno($this->_ch));
311 | } else {
312 | for ($i = 0; $i < intval($this->_info['redirect_count']); ++$i) {
313 | // remove any headers from previous redirected responses
314 | list( , $response) = explode("\r\n\r\n", $response, 2);
315 | }
316 |
317 | list($headers, $this->_responseBody) = explode("\r\n\r\n", $response, 2);
318 | list($this->statusLine, $this->_responseHeaders) = $this->_parseHeaders($headers);
319 |
320 | return true;
321 | }
322 | }
323 |
324 | /**
325 | * Parse response headers into an array keyed by header name
326 | *
327 | * @param string $headers String of HTTP response headers
328 | * @return array Returns array with first element being the HTTP status line and then an array of headers
329 | */
330 | private function _parseHeaders($headers)
331 | {
332 | $parsed = array();
333 | $headers = explode("\r\n", $headers);
334 | $status_line = array_shift($headers); // remove http response code
335 |
336 | foreach($headers as $header) {
337 | list($name, $value) = explode(':', $header, 2);
338 | $parsed[$name] = ltrim($value);
339 | }
340 |
341 | return [ $status_line, $parsed ];
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/src/TorDNSEL.php:
--------------------------------------------------------------------------------
1 |
36 | *
37 | */
38 |
39 |
40 | namespace Dapphp\TorUtils;
41 |
42 | /**
43 | * Tor DNS Exit List Checker
44 | *
45 | * This class contains a simple DNS client meant only to know enough about the DNS protocol in order to perform
46 | * simple DNSEL queries specific to the Tor Project's Exit List Service. TCP is not supported. Use for anything other
47 | * than this purpose at your own risk.
48 | *
49 | * Other options include PHP's dns_get_record() function, but this lacks any facilities for getting error information.
50 | * Using Pear Net_DNS2 is an alternative option.
51 | *
52 | * @package Dapphp\TorUtils
53 | */
54 | class TorDNSEL
55 | {
56 | private $dnsRequestTimeout = 5;
57 |
58 | /** @var string[] Default list of DNS servers used for Tor DNSEL checks */
59 | private static $dnsServers = [
60 | 'check-01.torproject.org',
61 | ];
62 |
63 | /**
64 | * Perform a DNS lookup of an IP-port combination to the public Tor DNS
65 | * exit list service.
66 | *
67 | * This function determines if the remote IP address is a Tor exit node
68 | * that permits connections to the specified IP:Port combination.
69 | *
70 | * @deprecated 1.1.14 Will be removed in future releases and replaced by a simpler interface
71 | *
72 | * @param string $ip No longer used. IP address (dotted quad) of the local server
73 | * @param string $port No longer used. Numeric port the remote client is connecting to (e.g. 80, 443, 53)
74 | * @param string $remoteIp IP address of the client (potential Tor exit relay) to check
75 | * @param string $dnsServer The DNS server to query (by default queries check-01.torproject.org)
76 | * @return boolean true if the $remoteIp is a Tor exit relay
77 | */
78 | public static function IpPort($ip, $port, $remoteIp, $dnsServer = 'check-01.torproject.org')
79 | {
80 | return static::isTor($remoteIp, $dnsServer);
81 | }
82 |
83 | /**
84 | * Check if a remote IP address is a Tor exit relay by querying the address against Tor DNS Exit List service.
85 | * This check sends a DNS "A" query to the $dnsServer (or the default if none is provided) and checks the answer to
86 | * determine if the visitor is coming from a Tor exit or not.
87 | *
88 | * @param string $remoteAddr The remote IP address, (e.g. $_SERVER['REMOTE_ADDR']) (IPv4 or IPv6) - Note: IPv6 is not currently supported by TorDNSEL
89 | * @param string|null $dnsServer The DNS resolver to query against (if null it will use check-01.torproject.or)
90 | * Consider using a local, caching resolver for DNSEL queries!
91 | * @return bool true if the visitor's IP address is a Tor exit relay, false if it is not
92 | * @throws \Exception
93 | */
94 | public static function isTor($remoteAddr, $dnsServer = null)
95 | {
96 | $response = self::query($remoteAddr, 1 /* A */, $dnsServer);
97 |
98 | if (!empty($response['answers']) && $response['answers'][0]['TYPE'] == 1) {
99 | return '127.0.0.2' == $response['answers'][0]['data'];
100 | }
101 |
102 | return false;
103 | }
104 |
105 | /**
106 | * Query the Tor DNS Exit List service for a list of relay fingerprints that belong to the supplied IP address.
107 | * If $remoteAddr is not a Tor exit relay, an empty array is returned.
108 | *
109 | * @param string $remoteAddr The Tor exit relay IP address (IPv4 or IPv6) - Note: IPv6 is not currently supported by TorDNSEL
110 | * @param string|null $dnsServer The DNS resolver to query against (if null it will use check-01.torproject.or)
111 | * Consider using a local, caching resolver for DNSEL queries!
112 | * @return array An array of Tor relay fingerprints, if the IP address is a Tor exit relay
113 | * @throws \Exception If there is a network error or the DNS query fails
114 | */
115 | public static function getFingerprints($remoteAddr, $dnsServer = null)
116 | {
117 | $response = self::query($remoteAddr, 16 /* TXT */, $dnsServer);
118 | $fingerprints = [];
119 |
120 | foreach($response['answers'] as $answer) {
121 | if ($answer['TYPE'] == 16) {
122 | $fingerprints[] = $answer['data'];
123 | }
124 | }
125 |
126 | return $fingerprints;
127 | }
128 |
129 | /**
130 | * Query something
131 | *
132 | * @param string $remoteAddr
133 | * @param int $queryType
134 | * @param string|null $dnsServer
135 | * @return array
136 | * @throws \Exception
137 | */
138 | protected static function query($remoteAddr, $queryType, $dnsServer = null)
139 | {
140 | $dnsel = new self();
141 |
142 | if (empty($dnsServer)) {
143 | $servers = self::$dnsServers;
144 | shuffle($servers);
145 | $dnsServer = $servers[0];
146 | }
147 |
148 | return $dnsel->dnsLookup($dnsel->getTorDNSELName($remoteAddr), $queryType, $dnsServer);
149 | }
150 |
151 | public function __construct() {}
152 |
153 | /**
154 | * Construct a hostname in the format of {remote_ip}.dnsel.torproject.org
155 | *
156 | * @param string $remoteIp The client IP address which may or may not be a Tor exit relay
157 | * @return string
158 | */
159 | public function getTorDNSELName($remoteIp)
160 | {
161 | if (strpos($remoteIp, ':') !== false) {
162 | $addr = $this->expandIPv6Address($remoteIp);
163 | } elseif (strpos($remoteIp, '.') !== false) {
164 | $addr = implode('.', array_reverse(explode('.', $remoteIp)));
165 | } else {
166 | throw new \InvalidArgumentException('Invalid IPv6/IPv6 address');
167 | }
168 |
169 | return sprintf(
170 | '%s.%s',
171 | $addr,
172 | 'dnsel.torproject.org'
173 | );
174 | }
175 |
176 | /**
177 | * Perform a DNS lookup to the Tor DNS exit list service and determine
178 | * if the remote connection could be a Tor exit node.
179 | *
180 | * @param string $host hostname in the designated tordnsel format
181 | * @param int $type The query type
182 | * @param string $dnsServer IP/host of the DNS server to use for querying
183 | * @throws \Exception DNS failures, socket failures
184 | * @return array
185 | */
186 | private function dnsLookup($host, $type, $dnsServer)
187 | {
188 | $query = $this->generateDNSQuery($host, $type);
189 | $data = $this->performDNSLookup($query, $dnsServer);
190 |
191 | if (empty($data)) {
192 | throw new \Exception('DNS request timed out');
193 | }
194 |
195 | $response = $this->parseDnsResponse($data);
196 | $this->assertPositiveResponse($response);
197 |
198 | if (substr($query, 0, 2) != substr($data, 0, 2)) {
199 | // query transaction ID does not match
200 | throw new \Exception('DNS answer packet transaction ID mismatch');
201 | }
202 |
203 | return $response;
204 | }
205 |
206 | /**
207 | *
208 | * @param array $response
209 | * @return bool
210 | * @throws \Exception
211 | */
212 | public function assertPositiveResponse(array $response)
213 | {
214 | switch($response['header']['RCODE']) {
215 | case 0:
216 | return true;
217 |
218 | case 1:
219 | throw new \Exception('The name server was unable to interpret the query.');
220 |
221 | case 2:
222 | throw new \Exception('Server failure - The name server was unable to process this query due to a problem with the name server.');
223 |
224 | case 3:
225 | // nxdomain
226 | return false;
227 |
228 | case 4:
229 | throw new \Exception('Not Implemented - The name server does not support the requested kind of query.');
230 |
231 | case 5:
232 | throw new \Exception('Refused - The name server refuses to perform the specified operation for policy reasons.');
233 |
234 | default:
235 | throw new \Exception("Bad RCODE in DNS response. RCODE = '{$response['RCODE']}'");
236 | }
237 | }
238 |
239 | /**
240 | * Generate a DNS query to send to the DNS server. This generates a
241 | * simple DNS "A" query for the given hostname.
242 | *
243 | * @param string $host Hostname used in the query
244 | * @param int $queryType
245 | * @return string
246 | */
247 | private function generateDNSQuery($host, $queryType)
248 | {
249 | $id = mt_rand(1, 0x7fff);
250 | $req = pack('n6',
251 | $id, // Request ID
252 | 0x100, // standard query
253 | 1, // # of questions
254 | 0, // answer RRs
255 | 0, // authority RRs
256 | 0 // additional RRs
257 | );
258 |
259 | foreach (explode('.', $host) as $bit) {
260 | // split name levels into bits
261 | $l = strlen($bit);
262 | // append query with length of segment, and the domain bit
263 | $req .= chr($l) . $bit;
264 | }
265 |
266 | // null pad the name to indicate end of record
267 | $req .= "\0";
268 |
269 | $req .= pack('n2',
270 | $queryType,
271 | 1 // class IN
272 | );
273 |
274 | return $req;
275 | }
276 |
277 | /**
278 | * Send UDP packet containing DNS request to the DNS server
279 | *
280 | * @param string $query DNS query
281 | * @param string $dns_server Server to query
282 | * @param int $port Port number of the DNS server
283 | * @throws \Exception Failed to send UDP packet
284 | * @return string DNS response or empty string if request timed out
285 | */
286 | private function performDNSLookup($query, $dns_server, $port = 53)
287 | {
288 | $fp = fsockopen('udp://' . $dns_server, $port, $errno, $errstr);
289 |
290 | if (!$fp) {
291 | throw new \Exception("Failed to send DNS request. Error {$errno}: {$errstr}");
292 | }
293 |
294 | fwrite($fp, $query);
295 |
296 | socket_set_timeout($fp, $this->dnsRequestTimeout);
297 | if (($resp = fread($fp, 8192)) !== false) {
298 | return $resp;
299 | } else {
300 | return '';
301 | }
302 | }
303 |
304 | /**
305 | * Parses the DNS response
306 | *
307 | * @param string $data DNS response
308 | * @throws \Exception Failed to parse response (malformed)
309 | * @return array Array with parsed response
310 | */
311 | private function parseDnsResponse($data)
312 | {
313 | $p = 0;
314 | $header = [];
315 | $rsize = strlen($data);
316 |
317 | if ($rsize < 12) {
318 | throw new \Exception('DNS lookup failed. Response is less than 12 octets');
319 | }
320 |
321 | // read back transaction ID
322 | $id = unpack('n', substr($data, $p, 2));
323 | $p += 2;
324 | $header['ID'] = $id[1];
325 |
326 | // read query flags
327 | $flags = unpack('n', substr($data, $p, 2));
328 | $flags = $flags[1];
329 | $p += 2;
330 |
331 | // read flag bits
332 | $header['QR'] = ($flags >> 15);
333 | $header['Opcode'] = ($flags >> 11) & 0x0f;
334 | $header['AA'] = ($flags >> 10) & 1;
335 | $header['TC'] = ($flags >> 9) & 1;
336 | $header['RD'] = ($flags >> 8) & 1;
337 | $header['RA'] = ($flags >> 7) & 1;
338 | $header['RCODE'] = ($flags & 0x0f);
339 |
340 | if ($header['QR'] != 1) {
341 | throw new \Exception('DNS response QR flag is not set to response (1)');
342 | }
343 |
344 | // read count fields
345 | $counts = unpack('n4', substr($data, $p, 8));
346 | $p += 8;
347 |
348 | $header['QDCOUNT'] = $counts[1];
349 | $header['ANCOUNT'] = $counts[2];
350 | $header['NSCOUNT'] = $counts[3];
351 | $header['ARCOUNT'] = $counts[4];
352 |
353 | $records = array();
354 | $records['questions'] = array();
355 | $records['answers'] = array();
356 | $records['authority'] = array();
357 | $records['additional'] = array();
358 |
359 | for ($i = 0; $i < $header['QDCOUNT']; ++$i) {
360 | $records['questions'][] = $this->readDNSQuestion($data, $p);
361 | }
362 |
363 | for ($i = 0; $i < $header['ANCOUNT']; ++$i) {
364 | $records['answers'][] = $this->readDNSRR($data, $p);
365 | }
366 |
367 | for ($i = 0; $i < $header['NSCOUNT']; ++$i) {
368 | $records['authority'][] = $this->readDNSRR($data, $p);
369 | }
370 |
371 | for ($i = 0; $i < $header['ARCOUNT']; ++$i) {
372 | $records['additional'][] = $this->readDNSRR($data, $p);
373 | }
374 |
375 | return array(
376 | 'header' => $header,
377 | 'questions' => $records['questions'],
378 | 'answers' => $records['answers'],
379 | 'authority' => $records['authority'],
380 | 'additional' => $records['additional'],
381 | );
382 | }
383 |
384 | /**
385 | * Read a DNS name from a response
386 | *
387 | * @param string $data The DNS response packet
388 | * @param int $offset Starting offset of $data to begin reading
389 | * @return string The DNS name in the packet
390 | */
391 | private function readDNSName($data, &$offset)
392 | {
393 | $name = array();
394 |
395 | do {
396 | $len = substr($data, $offset, 1);
397 | $offset += 1;
398 |
399 | if ($len == "\0") {
400 | // null terminator
401 | break;
402 | } else if ($len == "\xC0") {
403 | // pointer or sequence of names ending in pointer
404 | $off = unpack('n', substr($data, $offset - 1, 2));
405 | $offset += 1;
406 | $noff = $off[1] & 0x3fff;
407 | $name[] = $this->readDNSName($data, $noff);
408 | break;
409 | } else {
410 | // name segment precended by the length of the segment
411 | $len = unpack('C', $len);
412 | $name[] = substr($data, $offset, $len[1]);
413 | $offset += $len[1];
414 | }
415 | } while (true);
416 |
417 | return implode('.', $name);
418 | }
419 |
420 | /**
421 | * Read a DNS question section
422 | *
423 | * @param string $data The DNS response packet
424 | * @param int $offset Starting offset of $data to begin reading
425 | * @return array Array with question information
426 | */
427 | private function readDNSQuestion($data, &$offset)
428 | {
429 | $question = [];
430 | $name = $this->readDNSName($data, $offset);
431 |
432 | $type = unpack('n', substr($data, $offset, 2));
433 | $offset += 2;
434 | $class = unpack('n', substr($data, $offset, 2));
435 | $offset += 2;
436 |
437 | $question['name'] = $name;
438 | $question['type'] = $type[1];
439 | $question['class'] = $class[1];
440 |
441 | return $question;
442 | }
443 |
444 | /**
445 | * Read a DNS resource record
446 | *
447 | * @param string $data The DNS response packet
448 | * @param int $offset Starting offset of $data to begin reading
449 | * @return array Array with RR information
450 | */
451 | private function readDNSRR($data, &$offset)
452 | {
453 | $rr = array();
454 |
455 | $rr['name'] = $this->readDNSName($data, $offset);
456 |
457 | $fields = unpack('nTYPE/nCLASS/NTTL/nRDLENGTH', substr($data, $offset, 10));
458 | $offset += 10;
459 |
460 | $rdata = substr($data, $offset, $fields['RDLENGTH']);
461 | $offset += $fields['RDLENGTH'];
462 |
463 | $rr['TYPE'] = $fields['TYPE'];
464 | $rr['CLASS'] = $fields['CLASS'];
465 | $rr['TTL'] = $fields['TTL'];
466 | $rr['SIZE'] = $fields['RDLENGTH'];
467 | $rr['RDATA'] = $rdata;
468 |
469 | switch($rr['TYPE']) {
470 | /*
471 | A 1 a host address
472 | NS 2 an authoritative name server
473 | MD 3 a mail destination (Obsolete - use MX)
474 | MF 4 a mail forwarder (Obsolete - use MX)
475 | CNAME 5 the canonical name for an alias
476 | SOA 6 marks the start of a zone of authority
477 | MB 7 a mailbox domain name (EXPERIMENTAL)
478 | MG 8 a mail group member (EXPERIMENTAL)
479 | MR 9 a mail rename domain name (EXPERIMENTAL)
480 | NULL 10 a null RR (EXPERIMENTAL)
481 | WKS 11 a well known service description
482 | PTR 12 a domain name pointer
483 | HINFO 13 host information
484 | MINFO 14 mailbox or mail list information
485 | MX 15 mail exchange
486 | TXT 16 text strings
487 | */
488 | case 1: // A
489 | $addr = unpack('Naddr', $rr['RDATA']);
490 | $rr['data'] = long2ip($addr['addr']);
491 | break;
492 |
493 | case 2: // NS
494 | $temp = $offset - $fields['RDLENGTH'];
495 | $rr['data'] = $this->readDNSName($data, $temp);
496 | break;
497 |
498 | case 16: // TXT
499 | $txtLength = ord(substr($rr['RDATA'], 0, 1));
500 | $rr['data'] = substr($rr['RDATA'], 1, $txtLength);
501 |
502 | break;
503 | }
504 |
505 | return $rr;
506 | }
507 |
508 | public function expandIPv6Address($address)
509 | {
510 | if (empty($address)) {
511 | throw new \InvalidArgumentException('IPv6 address is empty');
512 | }
513 |
514 | $address = strtolower((string)$address);
515 | /** @noinspection RegExpRedundantEscape */
516 | $address = preg_replace('/^\[|\]$/', '', $address);
517 |
518 | if (
519 | substr_count($address, ':') < 2
520 | ||
521 | preg_match('/[^a-f0-9:]/', $address)
522 | ) {
523 | throw new \InvalidArgumentException('Invalid IPv6 address');
524 | }
525 |
526 | $hextets = explode(':', $address);
527 | $fill = null;
528 |
529 | for ($i = 0; $i < sizeof($hextets); ++$i) {
530 | if ($hextets[$i] == '') {
531 | if (!is_null($fill)) {
532 | $hextets[$i] = '0000';
533 | } else {
534 | $fill = $i;
535 | }
536 | } else {
537 | $hextets[$i] = str_pad($hextets[$i], 4, '0', STR_PAD_LEFT);
538 | }
539 | }
540 |
541 | if (!is_null($fill)) {
542 | array_splice($hextets, $fill, 1, '0000');
543 | while (8 > sizeof($hextets)) {
544 | array_splice($hextets, $fill, 0, '0000');
545 | }
546 | }
547 |
548 | $expanded = join('', $hextets);
549 |
550 | return join('.', array_reverse(preg_split('//', $expanded, -1, PREG_SPLIT_NO_EMPTY)));
551 | }
552 | }
553 |
--------------------------------------------------------------------------------
/tests/ControlClientTest.php:
--------------------------------------------------------------------------------
1 | recvData = $data;
13 | }
14 |
15 | return $tc;
16 | }
17 |
18 | public function testSingleLinePositiveReply()
19 | {
20 | $response = array("250 OK\r\n");
21 | $tc = $this->getMockControlClient($response);
22 | $reply = $tc->readReply();
23 |
24 | $this->assertEquals(250, $reply->getStatusCode());
25 | }
26 |
27 | public function testReadMultiReply()
28 | {
29 | $cmd = 'testing';
30 | $response = array(
31 | "250+{$cmd}=\r\n",
32 | "this is some data for a reply line\r\n",
33 | "this is some data for another line\r\n",
34 | "and finally the last line of reply\r\n",
35 | ".\r\n",
36 | "250 OK\r\n",
37 | );
38 |
39 | $tc = $this->getMockControlClient($response);
40 | $reply = $tc->readReply($cmd);
41 |
42 | $this->assertEquals(250, $reply->getStatusCode());
43 | $this->assertEquals(rtrim($response[1], "\r\n"), $reply[0]);
44 | $this->assertEquals(rtrim($response[2], "\r\n"), $reply[1]);
45 | $this->assertEquals(rtrim($response[3], "\r\n"), $reply[2]);
46 | }
47 |
48 | public function testReadMultiReply2()
49 | {
50 | $cmd = 'option/value';
51 | $response = array(
52 | "250-{$cmd}=answer\r\n",
53 | "250 OK\r\n",
54 | );
55 |
56 | $tc = $this->getMockControlClient($response);
57 | $reply = $tc->readReply($cmd);
58 |
59 | $this->assertEquals(250, $reply->getStatusCode());
60 | $this->assertEquals('answer', $reply[0]);
61 | $this->assertEquals(1, sizeof($reply->getReplyLines()));
62 | }
63 |
64 | public function testReadLongMultiReply()
65 | {
66 | $cmd = 'getinfo/testing';
67 | $response = array(
68 | "250+{$cmd}=\r\n",
69 | );
70 |
71 | for ($i = 0; $i < 1000; ++$i) {
72 | $response[] = str_repeat('x', 80) . "\r\n";
73 | }
74 |
75 | $response[] = ".\r\n";
76 | $response[] = "250 OK\r\n";
77 |
78 | $tc = $this->getMockControlClient($response);
79 | $reply = $tc->readReply($cmd);
80 |
81 | $this->assertEquals(250, $reply->getStatusCode());
82 | $this->assertEquals(1000, sizeof($reply->getReplyLines()));
83 | $this->assertEquals(str_repeat('x', 80), $reply[500]);
84 | }
85 |
86 | public function testParseReplyWithAsyncEvent()
87 | {
88 | $cmd = 'GETCONF SOCKSPORT ORPORT';
89 | $response = array(
90 | "650 CIRC 212 EXTENDED \$844AE9CAD04325E955E2BE1521563B79FE7094B7~Smeerboel BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2016-12-22T06:11:06.611813\r\n",
91 | "250-SOCKSPORT=9050\r\n",
92 | "250 ORPORT=0\r\n",
93 | );
94 |
95 | $GLOBALS['async_event'] = '';
96 | $GLOBALS['async_data'] = '';
97 | $values = [];
98 |
99 | $tc = $this->getMockControlClient($response);
100 |
101 | $tc->setAsyncEventHandler(function($event, $data) {
102 | $GLOBALS['async_event'] = $event;
103 | $GLOBALS['async_data'] = $data;
104 | });
105 |
106 | $reply = $tc->readReply($cmd);
107 |
108 | // the async event data comes first, but it should be read and handled
109 | // by the async event handler, and then the response to the issued
110 | // GETCONF command should be handled properly and returned in $reply
111 |
112 | foreach($reply->getReplyLines() as $line) {
113 | $values = array_merge($values, $tc->getParser()->parseKeywordArguments($line));
114 | }
115 |
116 | $this->assertEquals(250, $reply->getStatusCode());
117 | $this->assertEquals('9050', $values['SOCKSPORT']);
118 | $this->assertEquals('0', $values['ORPORT']);
119 |
120 | $this->assertEquals('CIRC', $GLOBALS['async_event']);
121 | $this->assertInstanceOf(Dapphp\TorUtils\CircuitStatus::class, $GLOBALS['async_data'][0]);
122 | $this->assertEquals('EXTENDED', $GLOBALS['async_data'][0]->status);
123 | $this->assertEquals('212', $GLOBALS['async_data'][0]->id);
124 | $this->assertEquals('GENERAL', $GLOBALS['async_data'][0]->purpose);
125 |
126 | }
127 |
128 | public function testAuthentication()
129 | {
130 | $response = array(
131 | "250-PROTOCOLINFO 1\r\n",
132 | "250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/var/run/tor/control.authcookie\"\r\n",
133 | "250-VERSION Tor=\"0.2.9.8\"\r\n",
134 | "250 OK\r\n",
135 | "250 OK\r\n", // authenticate reply
136 | );
137 |
138 | $tc = $this->getMockControlClient($response);
139 |
140 | $tc->authenticate("password");
141 |
142 | $this->addToAssertionCount(1);
143 | }
144 |
145 | public function testAuthentication2()
146 | {
147 | $response = array(
148 | "250-PROTOCOLINFO 1\r\n",
149 | "250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/Users/nosx/Library/Application Support/TorBrowser-Data/Tor/control_auth_cookie\"\r\n",
150 | "250-VERSION Tor=\"0.4.3.6\"\r\n",
151 | "250 OK\r\n",
152 | "250 OK\r\n", // authenticate reply
153 | );
154 |
155 | $tc = $this->getMockControlClient($response);
156 |
157 | $tc->authenticate("password");
158 |
159 | $this->addToAssertionCount(1);
160 | }
161 |
162 |
163 |
164 | public function testFailedAuthentication()
165 | {
166 | $response = array(
167 | "250-PROTOCOLINFO 1\r\n",
168 | "250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/var/run/tor/control.authcookie\"\r\n",
169 | "250-VERSION Tor=\"0.2.9.8\"\r\n",
170 | "250 OK\r\n",
171 | "515 Authentication failed: Password did not match HashedControlPassword *or* authentication cookie.\r\n",
172 | );
173 |
174 | $this->expectException(\Dapphp\TorUtils\ProtocolError::class);
175 | $this->expectExceptionCode(515);
176 | $this->expectExceptionMessage('Authentication failed: Password did not match HashedControlPassword *or* authentication cookie.');
177 |
178 | $tc = $this->getMockControlClient($response);
179 | $tc->authenticate("this is most definitely the wrong password ;)");
180 | }
181 | }
182 |
183 | /**
184 | * Weak way to mock a ControlClient such that when a test invokes readReply to
185 | * read from the control socket, we can override _recvData to return lines of
186 | * test data rather than reading from a socket.
187 | *
188 | * To mock reads, set recvData to a numerically keyed array where each index
189 | * is a line of data from the controller.
190 | */
191 | class ControlClientMock extends ControlClient
192 | {
193 | public $recvData = [];
194 |
195 | public function recvData()
196 | {
197 | $v = current($this->recvData);
198 | next($this->recvData);
199 | return $v;
200 | }
201 |
202 | public function sendData($data)
203 | {
204 | $data = $data . "\r\n";
205 | return strlen($data);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/tests/TorDNSELTest.php:
--------------------------------------------------------------------------------
1 | getTorDNSELName($remoteaddr);
13 |
14 | $this->assertEquals('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.dnsel.torproject.org', $name);
15 |
16 | $remoteaddr = '[2001:db8:85a3:8d3:1319:8a2e:370:7348]';
17 | $name = $dnsel->getTorDNSELName($remoteaddr);
18 |
19 | $this->assertEquals('8.4.3.7.0.7.3.0.e.2.a.8.9.1.3.1.3.d.8.0.3.a.5.8.8.b.d.0.1.0.0.2.dnsel.torproject.org', $name);
20 |
21 | $remoteaddr = '1.2.3.4';
22 | $name = $dnsel->getTorDNSELName($remoteaddr);
23 |
24 | $this->assertEquals('4.3.2.1.dnsel.torproject.org', $name);
25 |
26 | $remoteaddr = '29.58.116.203';
27 | $name = $dnsel->getTorDNSELName($remoteaddr);
28 |
29 | $this->assertEquals('203.116.58.29.dnsel.torproject.org', $name);
30 | }
31 |
32 | /**
33 | * @dataProvider expandIPv6AddressDataProvider
34 | */
35 | public function testExpandIPv6Address($address, $expected)
36 | {
37 | $dnsel = new TorDNSEL();
38 | $result = $dnsel->expandIPv6Address($address);
39 |
40 | $this->assertEquals($expected, $result);
41 | }
42 |
43 | public function expandIPv6AddressDataProvider()
44 | {
45 | return [
46 | [ '::', '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0', ],
47 | [ '::1', '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0', ],
48 |
49 | [ '2001:db8::1', '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
50 | [ '2001:0db8::0001', '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
51 | [ '2001:db8:0:0:0:0:2:1', '1.0.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
52 | [ '2001:db8::2:1', '1.0.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
53 | [ '2001:db8::1:0:0:1', '1.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
54 | [ '2001:0db8:0000:0000:0001:0000:0000:0001',
55 | '1.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2', ],
56 |
57 | [ '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
58 | '8.4.3.7.0.7.3.0.e.2.a.8.9.1.3.1.3.d.8.0.3.a.5.8.8.b.d.0.1.0.0.2', ],
59 |
60 | [ 'fe80::1ff:fe23:4567:890a', 'a.0.9.8.7.6.5.4.3.2.e.f.f.f.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f', ],
61 | [ 'fdda:5cc1:23:4::1f', 'f.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.4.0.0.0.3.2.0.0.1.c.c.5.a.d.d.f', ],
62 | [ '2001:b011:4006:170c::11', '1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.0.7.1.6.0.0.4.1.1.0.b.1.0.0.2', ],
63 | [ '2620:6e:a001:705:face:b00c:15:bad',
64 | 'd.a.b.0.5.1.0.0.c.0.0.b.e.c.a.f.5.0.7.0.1.0.0.a.e.6.0.0.0.2.6.2', ],
65 | [ '2620:7:6001::101', '1.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.6.7.0.0.0.0.2.6.2', ],
66 | [ '[2a0a:3840:1337:125:0:b9c1:7d9b:1337]',
67 | '7.3.3.1.b.9.d.7.1.c.9.b.0.0.0.0.5.2.1.0.7.3.3.1.0.4.8.3.a.0.a.2', ],
68 | ];
69 | }
70 |
71 | /**
72 | * @dataProvider invalidIPv6AddressesDataProvider
73 | */
74 | public function testInvalidIPv6Addresses($address)
75 | {
76 | $dnsel = new TorDNSEL();
77 |
78 | $this->expectException(\InvalidArgumentException::class);
79 |
80 | $dnsel->expandIPv6Address($address);
81 | }
82 |
83 | public function invalidIPv6AddressesDataProvider()
84 | {
85 | return [
86 | [ '', ],
87 | [ ':', ],
88 | [ '[:]', ],
89 | [ '1.2.3.4', ],
90 | [ '2620:7:6001::101z', ],
91 | ];
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/data/dir-status-1:
--------------------------------------------------------------------------------
1 | router MilesPrower 62.210.129.246 443 0 80
2 | identity-ed25519
3 | -----BEGIN ED25519 CERT-----
4 | AQQABkpoAf+DJAKIzwhjVPYx9mb6gDqoQlSwXy7XShUUODjX8t6DAQAgBADlB5lo
5 | C28eXudPNJKSSRmInedLnauMUdHbZu6daqGyOr3J7A2Vqah4nOwhP3UIjI7Iyb+s
6 | 9JBg0x8J/t73itfaEJt+vK+MNwroUsHaVKqGnqTyhB9B84548MF55CI/EAw=
7 | -----END ED25519 CERT-----
8 | master-key-ed25519 5QeZaAtvHl7nTzSSkkkZiJ3nS52rjFHR22bunWqhsjo
9 | platform Tor 0.2.9.8 on Linux
10 | proto Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2
11 | published 2016-12-25 04:18:42
12 | fingerprint 79E1 69B2 5E4C 7CE9 9584 F6ED 06F3 7947 8F23 E2B8
13 | uptime 460860
14 | bandwidth 19660800 26214400 22719552
15 | extra-info-digest 2435753908F3A7C24CCABD0EA6D336C7F4B36932 j5q4h6/dTpPIyYuM2+ABiQi6gPkMpAPgu3MrUZAAEF0
16 | onion-key
17 | -----BEGIN RSA PUBLIC KEY-----
18 | MIGJAoGBALipQFp0vo/wxvUBrDbw5SamdFhLHlA9GepSauOnZA5nY1FKOW1pjygp
19 | DZeU1RAUqj7ST4xD6CH8IA1mAs95GIp9LrwuYunbw7NeuxELhCXwK1WphqoAw97t
20 | 4S/sbl72lZ2b29eHxbPoNFPO+2jQbqCPcCM3kGbqWsNef4WZP5ydAgMBAAE=
21 | -----END RSA PUBLIC KEY-----
22 | signing-key
23 | -----BEGIN RSA PUBLIC KEY-----
24 | MIGJAoGBAMX8r0RFoXr7etUrzJqOAQx1hAeqLwMyMU3xbkBBlLXn+8wTcOvyOAKP
25 | 3+MP3pQZyI/+oK2D0N3PLQk4CyrigF8AjR91QVAHcVxC1DOouA54seki5JoTWbZ2
26 | z45XOAsoekZM1K0Mb2LF6Z+7gjdtdl5D5Cdp5THpcekJDqhjuBo3AgMBAAE=
27 | -----END RSA PUBLIC KEY-----
28 | onion-key-crosscert
29 | -----BEGIN CROSSCERT-----
30 | o39cQgODkckCXMWaUFjtK2txEsJHQF+xtmY58HiZUfh+IMyDNS4ulfvNCKnqEgkb
31 | YkeavB+buvmiPSTFreYVuIXCwzcRhlPoI7aOFpuSwnLQBf78GqK/6gigS/FCRCgp
32 | MnEOw5XSii/mE6eiyDYFZprEDuG+CZ5xpcaSKJdLPDA=
33 | -----END CROSSCERT-----
34 | ntor-onion-key-crosscert 0
35 | -----BEGIN ED25519 CERT-----
36 | AQoABkltAeUHmWgLbx5e5080kpJJGYid50udq4xR0dtm7p1qobI6ADH2H9oTEUVO
37 | LOw4UBPbuOBIIfhk9oMBjAPMafZGhZ97jLml49L2nv7FXPH2LqS14Rc4fYZMAO5z
38 | p7YdmuNWrQY=
39 | -----END ED25519 CERT-----
40 | family $383B7179FEE38D6773D4327F4B5856798BD85202 $BC924D50078666A0208F9D75F29CA73645FB604D $BE953C95C98D207742A66DDC05B0A476FF2225C9 $FE32CAC855ABC707ED7FEDAF720046FE914EB491
41 | hidden-service-dir
42 | contact sysop[at]openinternet.io BTC: 1N1s2BmWqbRH4Af5jjrNkm8XChnsRmPgA5
43 | ntor-onion-key 0dmEb3q00YCLRBwacEbHtq7QashHn5ikuG8eoJ8Iwio=
44 | reject 0.0.0.0/8:*
45 | reject 169.254.0.0/16:*
46 | reject 127.0.0.0/8:*
47 | reject 192.168.0.0/16:*
48 | reject 10.0.0.0/8:*
49 | reject 172.16.0.0/12:*
50 | reject 62.210.129.246:*
51 | accept *:43
52 | accept *:53
53 | accept *:80
54 | accept *:443
55 | accept *:6660-6669
56 | accept *:6679
57 | accept *:6697
58 | accept *:8008
59 | accept *:8080
60 | accept *:8332-8333
61 | accept *:8888
62 | accept *:11371
63 | accept *:19294
64 | reject *:*
65 | tunnelled-dir-server
66 | router-sig-ed25519 oe+otEbietfKZxmdWDpOZ8Xhg78xjSoRLl6izosiwONLuNCSuHdY/dOEdpRakzSTk7pwYQyWfxgcybsyAziDAQ
67 | router-signature
68 | -----BEGIN SIGNATURE-----
69 | MzkuT987jIcEbdI2U1X5UeN7/sEHCx0Ci981bULK8P1pBaxotbJg0CaeDA2bqpYW
70 | HGFnvCRbXs2ChKiZgOEFhUbpCAQhbiOO5mmRLhQUlwlvyh+/9PlTn/cddQwtZkcA
71 | F43fc8318iP8ypVmvbHCZeryJPZLeGdMHzxTET9RWeo=
72 | -----END SIGNATURE-----
73 | router OpenInternetDotIO 208.113.166.5 443 0 80
74 | identity-ed25519
75 | -----BEGIN ED25519 CERT-----
76 | AQQABkuQAbKMCo8awjBiyM4JWUYWtmvi6Cg1Zzffp4MXxyDtPBmAAQAgBABlpjAY
77 | UyZiq/1hHdpSxBxBaLLE4XFaceravwCoEfDpMH2GK5kUYHozkNxHyHdGrS3cFRUl
78 | pwBuHOcRPs+vodqTMyKdAAtlystrB8+n0HN2zmyIRVHMxyY8+zLdsO0pogE=
79 | -----END ED25519 CERT-----
80 | master-key-ed25519 ZaYwGFMmYqv9YR3aUsQcQWiyxOFxWnHq2r8AqBHw6TA
81 | platform Tor 0.2.9.8 on Linux
82 | proto Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2
83 | published 2016-12-25 08:00:32
84 | fingerprint E2EC 4A6D 3E00 2866 C2A4 9207 109F 7281 2F9D 2E62
85 | uptime 28859
86 | bandwidth 19660800 26214400 697305
87 | extra-info-digest 9C1FA959CD66521B70A8E4CA559B1933F0AE921F uPcGZnTCmVguXR6iJHO/sxgAIdMYjObZyomlU5b8hQY
88 | onion-key
89 | -----BEGIN RSA PUBLIC KEY-----
90 | MIGJAoGBAMOeZHEh+rWA6Z/9CB2zi6NixxXl9SLDwYHPJOe71mtElcUXuVqBUTfg
91 | 393ywiC3n9/cLMPGTYuVb/dJo845GwgqlePphzUrHU7yiEG07GiVa8aV8J/ScoJB
92 | kcqFM8hX4Bg04wfeEDVwL9qH6kr/p8UJ5cY6CbgcMj1d8I3GA/xNAgMBAAE=
93 | -----END RSA PUBLIC KEY-----
94 | signing-key
95 | -----BEGIN RSA PUBLIC KEY-----
96 | MIGJAoGBALrtNxJ5y3TTToqJdmajzosf+nX4tONSQ9F4jEtbg9qEN88G57X3WdFd
97 | N30o0XfR1HsZ0sBiDCPOGjBzHY9/q5Y6iHOh0uocY9Jg+OH1m3oPupOfkUOcCaKe
98 | k3EWeR4o01mv+m+Z3gJXWN4SF4tz0gKz3hgRMVA8YTH7aZPP9bmdAgMBAAE=
99 | -----END RSA PUBLIC KEY-----
100 | onion-key-crosscert
101 | -----BEGIN CROSSCERT-----
102 | uwkh/WYZ6CaCCHZew5+7RmwvkxJI3+Lt1Vyv4z/xi3uecGlrhw7OP6hrG/9BKvRo
103 | UzN01Ev1/BWhcXuFqb3C/h/hna4sbhzC/XpBGL28/LtWuBQO0lbWp+xtpRMrggRt
104 | duvGJBR7eoV5lkPpu37Btxnb8KDd8JeC27bl0SZVkVs=
105 | -----END CROSSCERT-----
106 | ntor-onion-key-crosscert 0
107 | -----BEGIN ED25519 CERT-----
108 | AQoABklxAWWmMBhTJmKr/WEd2lLEHEFossThcVpx6tq/AKgR8OkwAIrjVqnSM8Uh
109 | gFbXmowxmMxETtlC9QKCQFphBd0iEWNGO3d++5wK3SisIj8Z96JwtcMnW/eT2AtL
110 | DYeX/IxYQAA=
111 | -----END ED25519 CERT-----
112 | family $383B7179FEE38D6773D4327F4B5856798BD85202 $BC924D50078666A0208F9D75F29CA73645FB604D $BE953C95C98D207742A66DDC05B0A476FF2225C9 $FE32CAC855ABC707ED7FEDAF720046FE914EB491
113 | hidden-service-dir
114 | contact sysop[at]openinternet.io BTC: 1HYR9K2zvqTLx3nMhYJNnAGeQzMLxrfZbT
115 | ntor-onion-key c1CpCkTTM/qnMq+W/OgA6xrXcgLW2S/A1ooENA8WsXg=
116 | reject 0.0.0.0/8:*
117 | reject 169.254.0.0/16:*
118 | reject 127.0.0.0/8:*
119 | reject 192.168.0.0/16:*
120 | reject 10.0.0.0/8:*
121 | reject 172.16.0.0/12:*
122 | reject 208.113.166.5:*
123 | accept *:43
124 | accept *:53
125 | accept *:80
126 | accept *:443
127 | accept *:6660-6669
128 | accept *:6679
129 | accept *:6697
130 | accept *:8008
131 | accept *:8080
132 | accept *:8332-8333
133 | accept *:8888
134 | accept *:11371
135 | accept *:19294
136 | reject *:*
137 | tunnelled-dir-server
138 | router-sig-ed25519 1REeM71XSNVVALIC3egQ/loiA/GCmELexapyumNq8SmN42CDDb4hYj8IG3i1q3kM74hgHI2fl/SVW2oqYcMzBA
139 | router-signature
140 | -----BEGIN SIGNATURE-----
141 | NrumpuEA8G3J/ERfcsgF/h7Id/qxXbGxRFrdV5HBQbb+Q+4buntOWLHllQNJsmEv
142 | htUWkMyAdCvgqN7i+NqDO+YdqoKz1HAvBAApc4Qu6Cwdekn6jJjG4tXycooKDsWr
143 | G2ghL2xsYqXN4alStDjJxk+FsmG5qBgMZg/SKE+bMlM=
144 | -----END SIGNATURE-----
145 | router freedominsteadofnsa 136.243.102.134 9001 0 0
146 | identity-ed25519
147 | -----BEGIN ED25519 CERT-----
148 | AQQABkrmAZo7Mju4R2yXCW7IkJA8V/eQjSN7tUkAfoe+vp+tfnF/AQAgBADN+YeK
149 | 4abchXhvtBt2UBFNWoe6KfVLs+6TRzE46Gcyf+EW1AN5a2DvVYiPFMaZxlL6g6ZA
150 | nhmxRAMa3tLiUgnS5Pje/MbCxacLzOu/IAhiHC02mABn3puRh1mqrAwFKQQ=
151 | -----END ED25519 CERT-----
152 | master-key-ed25519 zfmHiuGm3IV4b7QbdlARTVqHuin1S7Puk0cxOOhnMn8
153 | platform Tor 0.2.7.6 on Linux
154 | protocols Link 1 2 Circuit 1
155 | published 2016-12-25 03:37:50
156 | fingerprint 9880 63DF 0FBD 3DB7 3A8F E1F5 8207 12B9 5D24 8C78
157 | uptime 15683100
158 | bandwidth 550000 1250000 674776
159 | extra-info-digest 83A0901FA2D67C12F222CC59540044C82BD27780 0g7aooa9WHEGfqfzcDBOV5MW/7g9FCBGxZpMYsWJ3QY
160 | onion-key
161 | -----BEGIN RSA PUBLIC KEY-----
162 | MIGJAoGBAPOkQB6IHcNN5nMGs4Y5EoxSb5SFhB0zn7pfN1XAMoWsVehutP45+alQ
163 | AJOjdN+/AGRYOuodDHYE4afTmoIXox+ojS8jCS4UuE/tkpevISMXVxRklJg0mKZT
164 | +QAvqkxthbfsfQnWIa/A8sqm9mCg3fFANmk4pMugiIAyxIouUanhAgMBAAE=
165 | -----END RSA PUBLIC KEY-----
166 | signing-key
167 | -----BEGIN RSA PUBLIC KEY-----
168 | MIGJAoGBAOE88fRvAKN+vPA+Dtkq4Cpdk+ssG4gq7gtyar7qXHC+ukn+xA6e1m0p
169 | ydVElSgSbOGMQrVTqWnGkXbf/Qe+9nu/CjclBRqAU2txIu/UEfBl5FPRdCgz6WWE
170 | NIKNMFG1nS6Ax9yPvYl6nct4tsYMEFOhPXi0trybl/KfPtoU4QnHAgMBAAE=
171 | -----END RSA PUBLIC KEY-----
172 | onion-key-crosscert
173 | -----BEGIN CROSSCERT-----
174 | EBV5Lwnnit96jdWp/0fGzuji0Cvz4icTlslaR+HbR77sTVpXL4FHWfKnksdvLi0+
175 | J13E1eM6cfganVeLqMPKVNLxaIPNC9dGmulqaTn1DPGdok0CvQZ/Ws7ij/oVcRMI
176 | 9EfDFXtnkA53+vP0NtN7/p2j355AgsT7wumWchMhw6g=
177 | -----END CROSSCERT-----
178 | ntor-onion-key-crosscert 0
179 | -----BEGIN ED25519 CERT-----
180 | AQoABklsAc35h4rhptyFeG+0G3ZQEU1ah7op9Uuz7pNHMTjoZzJ/ANXyfnnNZgZk
181 | BraUyNM4nZmvQ3SiGZ18abdj9f7HXLS44wmzZE/bwZP73UjA3ugmUyHXrBi1sR6P
182 | ulw0KVjGtws=
183 | -----END ED25519 CERT-----
184 | hidden-service-dir
185 | ntor-onion-key +dnBNPxQ+ovac5yMxaTCMX66hVzr9Waw/lnRYjr5/EA=
186 | reject 0.0.0.0/8:*
187 | reject 169.254.0.0/16:*
188 | reject 127.0.0.0/8:*
189 | reject 192.168.0.0/16:*
190 | reject 10.0.0.0/8:*
191 | reject 172.16.0.0/12:*
192 | reject 136.243.102.134:*
193 | accept *:53
194 | accept *:1194
195 | reject *:*
196 | router-sig-ed25519 6X8OuMRv8ieaAYrPMcH7WaDIE/kpKgkG/9rmqk4QK5Cnq3nerPwTGz7fW5kOaQdU324m6PZ1+hU4Af5x3QgEBw
197 | router-signature
198 | -----BEGIN SIGNATURE-----
199 | FemsovP4PcJVbjghtwY51IDtqms2PTbVxqNBM5gnPe1CD7h1i2UTPe4V49aXs9uw
200 | eABnOXAIgnhjwgEewqYVl6tLqU20P34RJ5z+11yxz2xaUREJwTgAuQS1lP6rjS8a
201 | nNvrXQksryIw5JiXE2ggECuj6boBxcgHiLnYsPjGAUw=
202 | -----END SIGNATURE-----
203 | router FreeLauriLove 178.62.66.18 9001 0 9030
204 | platform Tor 0.2.4.27 on Linux
205 | protocols Link 1 2 Circuit 1
206 | published 2016-12-25 09:11:24
207 | fingerprint 8096 EA61 F733 C303 0351 4019 44F5 4F25 4185 098C
208 | uptime 376492
209 | bandwidth 1073741824 1073741824 13298235
210 | extra-info-digest F68A8E125732C8EFF93B640CFCCD3F4E0001D245
211 | onion-key
212 | -----BEGIN RSA PUBLIC KEY-----
213 | MIGJAoGBAJ7dnYMII0RqtcFS+FSVE42yZCYPfkp5f4Haoe+WaMG45lEc8o18WrVg
214 | ncGp+RN4UjY4zMb4wj85Af4T+yPQy3AruHXvJXyQVrJG1fVBMPE8Eu1cElOO8TBQ
215 | fA0a0ulN6eDFZHlelXSjvOS3WFssIb49UrO90NwvT5NW0lZz28+fAgMBAAE=
216 | -----END RSA PUBLIC KEY-----
217 | signing-key
218 | -----BEGIN RSA PUBLIC KEY-----
219 | MIGJAoGBAL/ZLEl8kjMM+PzriratNY9anZSPq++6GugQF8afLe1e66JXvSLL0SIN
220 | CvyahU/+AFE33lzzGywIMEmJKlbLZV1JbHxbAVQ0T1FNg/ppU93cF6npNRYs1R5w
221 | aTb6my6YVVrltCO4yasNbqTEd158DzfJdDEg2hHiMLgPj8f5UuPrAgMBAAE=
222 | -----END RSA PUBLIC KEY-----
223 | family $4B37E840A7F26F85BA363927D6C6F2769EB4C918 $73067CD4ADD8A294BDA913DF45B63190A52B5F9F $C50A2620C520D5751392DC1FD3CB1A26CA255ED0 Cyberia
224 | hidden-service-dir
225 | contact email@redacted - 1MAdEpv9UP7FRUigtDP39c6c9nPf64eSjw
226 | ntor-onion-key BUDBZIeQ4LJBpIi+99rY7XsAbAT0RsXNGdaCGInVaH8=
227 | reject *:*
228 | router-signature
229 | -----BEGIN SIGNATURE-----
230 | IH4/7VY0qcOynDwac2DspRqtuP28Pyp271ksJjwpSrzURWQ5o0PrkfMUAEPLFhOj
231 | af2UiwRCDissqCFgZ+G8UU9EXNY/Xqyou2Txu+J7VMpKm2iYxuVsRcVz6YyRGOxJ
232 | BtfuB08P+oopuJcJi/AtnqW/sjlsx16Y4MPfmgCPgds=
233 | -----END SIGNATURE-----
234 | router IPredator 197.231.221.211 9001 0 9030
235 | identity-ed25519
236 | -----BEGIN ED25519 CERT-----
237 | AQQABklxAZv9QJ8v921eVF6Cl7oMDIJJsk+0WKGjsjcvPrhPBGX2AQAgBABeXNMz
238 | egah1A71N79N131lOMDuxTj0uJunJl7Gizv/hvDT7q0LtNP914HQizTQjRl9skc0
239 | gJ6S+77ad68eTLAwD7Cp4xEznIjXtnxduhR1+LzXUy03GHeBHFN5eMxJvA4=
240 | -----END ED25519 CERT-----
241 | master-key-ed25519 XlzTM3oGodQO9Te/Tdd9ZTjA7sU49LibpyZexos7/4Y
242 | platform Tor 0.3.0.1-alpha on Linux
243 | proto Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
244 | published 2016-12-25 11:01:38
245 | fingerprint BC63 0CBB B518 BE7E 9F4E 0971 2AB0 269E 9DC7 D626
246 | uptime 194525
247 | bandwidth 268435456 402653184 95030633
248 | extra-info-digest BC2FB1C74513ED1F8EFF2526D8A789BD7A3857E8 6QAr7ErEImvT6XTqFyGrf1XigO7DvDK/6oj4Fcaw6WM
249 | onion-key
250 | -----BEGIN RSA PUBLIC KEY-----
251 | MIGJAoGBANoCeAvwYgdo6FLLtj7vN9/NQmOVpocwfXRgbK2eRxV2QfO91uNpTl3V
252 | lpA5KzW2ICSFrzr1VjsQ3h6IbmVanVdJ5tq2dzUwDZW47tqvQ0jRYN+AuQ94ylpu
253 | W07TCZb3XTFDikLOf/lZ+Rv9yLeEO7UxdK9vL0HmUgFtib0ZkNn3AgMBAAE=
254 | -----END RSA PUBLIC KEY-----
255 | signing-key
256 | -----BEGIN RSA PUBLIC KEY-----
257 | MIGJAoGBAM/+LB3FL6nwSQJ7NoBiC6Gt43bE2lr/wCNS5JB/4Gkux7pOjLF3Ae2d
258 | hYrhgVx9Zk8kLWhJ9Rn0rY1qZ7QUbDbykpjH5Rjm8o5CtzD1aYL0csmWvGCUvIWo
259 | wAvMZktSBaGdU42wNDlBHRbn/dZns1+xrL3I2jV65EuT3D07UbgRAgMBAAE=
260 | -----END RSA PUBLIC KEY-----
261 | onion-key-crosscert
262 | -----BEGIN CROSSCERT-----
263 | rwag0ZcFW34PBJ78zmocv/6F1Xzr50fQb7vqJitMV1BOi37U6sYcyKvS27INXjnW
264 | 7nuedSaQCywg2RRf1f8+Lqaz2/mQDHf+WbVyG7J92sO+u295P5D1O8ge9zTg61gN
265 | J1rwxs3z+82dL7ak9WXNzEgYrrYCAajq5bkXSYjdaAg=
266 | -----END CROSSCERT-----
267 | ntor-onion-key-crosscert 1
268 | -----BEGIN ED25519 CERT-----
269 | AQoABkl0AV5c0zN6BqHUDvU3v03XfWU4wO7FOPS4m6cmXsaLO/+GACOC8J2QX9dc
270 | vdbWOnT5/yAYt3IzzVSjNI625sNRtjK58Y6ZKPmgW2BsqwSZS5z1gfjA5KvoHk9i
271 | jOFT8+MHcwI=
272 | -----END ED25519 CERT-----
273 | hidden-service-dir
274 | contact tor@ipredator.se - 1Q3mjKbZwZFEigC8edUZ8ywX4QD7kxFzNC
275 | ntor-onion-key p2SSQYN9XDYaqF6k/PJ8BU0y1nV6TI4eTyK9LjF9uFg=
276 | reject 0.0.0.0/8:*
277 | reject 169.254.0.0/16:*
278 | reject 127.0.0.0/8:*
279 | reject 192.168.0.0/16:*
280 | reject 10.0.0.0/8:*
281 | reject 172.16.0.0/12:*
282 | reject 197.231.221.211:*
283 | reject *:109
284 | reject *:110
285 | reject *:143
286 | reject *:25
287 | reject *:119
288 | reject *:135-139
289 | reject *:445
290 | reject *:563
291 | reject *:1214
292 | reject *:4661-4666
293 | reject *:6346-6429
294 | reject *:6699
295 | reject *:6881-6999
296 | accept *:*
297 | ipv6-policy reject 25,109-110,119,135-139,143,445,563,1214,4661-4666,6346-6429,6699,6881-6999
298 | tunnelled-dir-server
299 | router-sig-ed25519 xrS6JUgL1KmqrYMbDzp/eLAwGW7bJTJe2S11th66SSjZYoIUd63zr7dA8km00eEZrY9Lg7lnX/ml45sambCECQ
300 | router-signature
301 | -----BEGIN SIGNATURE-----
302 | pzUSv+rE59u4AC2XfSWFkJ0bzjeq2hxqQUhRZ7pILUm6/nV4hzjNhVB8lPzroBKf
303 | lPG1WKgMey1j417O0Jrwp285xYDK4Z0BUpuxtu84vBmSmlrnfm81+Vjpj95ESUsZ
304 | GQ+iO0/jXVtQWvyX5kAPyHmPtUOvNhMDt+naNbYq+VM=
305 | -----END SIGNATURE-----
306 |
307 |
308 |
309 | router pp14relay 212.47.246.18 443 0 80
310 | identity-ed25519
311 | -----BEGIN ED25519 CERT-----
312 | AQQABkmzAU2SQFG9lwjVz5p6x6220c2lvwjlgpbMAm+vkDZRJW86AQAgBABwe3qs
313 | xCngjgM94vxUXW7fLaqSAqfvdk8F7+kpsYFYxVMKpWW8wJZ65XhzKdfOSMwdDVRn
314 | Lve7jlohxKGJIw+8kbmfDEvJsKpU7SubrT7Ne+8gaCNdAes3xo76Xzx5ZQg=
315 | -----END ED25519 CERT-----
316 | master-key-ed25519 cHt6rMQp4I4DPeL8VF1u3y2qkgKn73ZPBe/pKbGBWMU
317 | platform Tor 0.2.8.6 on Linux
318 | protocols Link 1 2 Circuit 1
319 | published 2016-12-25 05:23:58
320 | fingerprint BE95 3C95 C98D 2077 42A6 6DDC 05B0 A476 FF22 25C9
321 | uptime 10575016
322 | bandwidth 20480000 25600000 10856489
323 | extra-info-digest F5396C2BB9D73726468E49044C76F0FF0BC83E6E 9g9bJZ7p5ORZmHppkiN0eAFRCUmJ1oT0mY54uL6ov4I
324 | onion-key
325 | -----BEGIN RSA PUBLIC KEY-----
326 | MIGJAoGBANHTJ8IbS3fXzw6QdFNmCmr0Kt5tLUZAo3b/40DhiF6xvotfIQbSl0kC
327 | SeLvopDOStRC5m6W6I8gHsBIXX83aCwap1iWT+KWQKARCy9pINHFaZ2waf+/xDUL
328 | 6OVwLHh/9UtknvFCtJ2arZaUcAGpVo0LekpT8KYrvT7eHDM2NYpDAgMBAAE=
329 | -----END RSA PUBLIC KEY-----
330 | signing-key
331 | -----BEGIN RSA PUBLIC KEY-----
332 | MIGJAoGBANUXrccYX0LzzuhTMV+JVLhEfMy4r9rPUz0PPSmZBD4+Q2MvYaVio8iG
333 | YrfOkbns2ICweirC3F4urASwZAfrIq/nn/POU3ZRbVf+GYeJNRQK8LNnBjLwHris
334 | YqBPJG3v0xhK9BqqYlDsAj1cxzdC5TaoqSSLBiQRbA8WA3DM92y7AgMBAAE=
335 | -----END RSA PUBLIC KEY-----
336 | onion-key-crosscert
337 | -----BEGIN CROSSCERT-----
338 | QNi/PxW1pZ9YisdoDLZckVzgke5kxfIpYiO/ZaDvxexVmNtpZ+ZaVoSJpj2uadbz
339 | ntLE11MefMA82aKpzRO9BXijvjo++Ps6oBPdkf8jRKgRUCmamnK4t9+qaNZTCvTe
340 | mQWdAARKGO9ns8t+kuLqB+v4hs6B7AowIOglnNF0AQI=
341 | -----END CROSSCERT-----
342 | ntor-onion-key-crosscert 1
343 | -----BEGIN ED25519 CERT-----
344 | AQoABkluAXB7eqzEKeCOAz3i/FRdbt8tqpICp+92TwXv6SmxgVjFAKBZ6BkVCJ7I
345 | 4Lks3i8JV0NSJsFW2y4aX4WlLfdOrzWN1Ixb+Ku5T/Xf4Mc7rjO5paf2XYEavwVN
346 | iJxbTNfuEwM=
347 | -----END ED25519 CERT-----
348 | family $383B7179FEE38D6773D4327F4B5856798BD85202 $79E169B25E4C7CE99584F6ED06F379478F23E2B8 $BC924D50078666A0208F9D75F29CA73645FB604D $DD116ACB4D775A04D7D0A7D8C6E41DDC7FA5F8BC $FE32CAC855ABC707ED7FEDAF720046FE914EB491
349 | hidden-service-dir
350 | contact 0x6E7B19EE BTC: 14Z2dRe2RBS8jcpYHExVQzkcsDMxuPJWEr
351 | ntor-onion-key cqLoInsBOmaak2N/POzo0r5nY0lONaQ6AFP5ZxraNFQ=
352 | reject *:*
353 | tunnelled-dir-server
354 | router-sig-ed25519 yd1P/PHHhJDVGpZ4YjLdNAn8KE3wbp5R1hQqRTVsCIStGPxWFytwBa0Uhs15gcn5WoIGq9ju7wpPNOgj/JDFDA
355 | router-signature
356 | -----BEGIN SIGNATURE-----
357 | QP8QPC/IFwil1VnJRG1iGSELGaULFbLR0Cn1Ea2KErbyU9QOPd/PFF+lM1Ts5vH4
358 | y4C1LJtI1U96+qIYcmj+zIC95workDPDw7PdAs6mOgLq4oVwBIo941I14XfCD0Q2
359 | 7EuYxSIX/fVyuXUjUrxCHgoylHzcX9/6x/uN1d4kMPA=
360 | -----END SIGNATURE-----
--------------------------------------------------------------------------------