├── .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 | Build Status 3 | Total Downloads 4 | Latest Stable Version 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----- --------------------------------------------------------------------------------