1# Copyright 2011-2019, Damian Johnson and The Tor Project 2# See LICENSE for licensing information 3 4""" 5Tor versioning information and requirements for its features. These can be 6easily parsed and compared, for instance... 7 8:: 9 10 >>> from stem.version import get_system_tor_version, Requirement 11 >>> my_version = get_system_tor_version() 12 >>> print(my_version) 13 0.2.1.30 14 >>> my_version >= Requirement.TORRC_CONTROL_SOCKET 15 True 16 17**Module Overview:** 18 19:: 20 21 get_system_tor_version - gets the version of our system's tor installation 22 23 Version - Tor versioning information 24 25.. data:: Requirement (enum) 26 27 Enumerations for the version requirements of features. 28 29 .. deprecated:: 1.6.0 30 Requirement entries belonging to tor versions which have been obsolete for 31 at least six months will be removed when we break backward compatibility 32 in the 2.x stem release. 33 34 ===================================== =========== 35 Requirement Description 36 ===================================== =========== 37 **AUTH_SAFECOOKIE** SAFECOOKIE authentication method 38 **DESCRIPTOR_COMPRESSION** `Expanded compression support for ZSTD and LZMA <https://gitweb.torproject.org/torspec.git/commit/?id=1cb56afdc1e55e303e3e6b69e90d983ee217d93f>`_ 39 **DORMANT_MODE** **DORMANT** and **ACTIVE** :data:`~stem.Signal` 40 **DROPGUARDS** DROPGUARDS requests 41 **EVENT_AUTHDIR_NEWDESCS** AUTHDIR_NEWDESC events 42 **EVENT_BUILDTIMEOUT_SET** BUILDTIMEOUT_SET events 43 **EVENT_CIRC_MINOR** CIRC_MINOR events 44 **EVENT_CLIENTS_SEEN** CLIENTS_SEEN events 45 **EVENT_CONF_CHANGED** CONF_CHANGED events 46 **EVENT_DESCCHANGED** DESCCHANGED events 47 **EVENT_GUARD** GUARD events 48 **EVENT_HS_DESC_CONTENT** HS_DESC_CONTENT events 49 **EVENT_NETWORK_LIVENESS** NETWORK_LIVENESS events 50 **EVENT_NEWCONSENSUS** NEWCONSENSUS events 51 **EVENT_NS** NS events 52 **EVENT_SIGNAL** SIGNAL events 53 **EVENT_STATUS** STATUS_GENERAL, STATUS_CLIENT, and STATUS_SERVER events 54 **EVENT_STREAM_BW** STREAM_BW events 55 **EVENT_TRANSPORT_LAUNCHED** TRANSPORT_LAUNCHED events 56 **EVENT_CONN_BW** CONN_BW events 57 **EVENT_CIRC_BW** CIRC_BW events 58 **EVENT_CELL_STATS** CELL_STATS events 59 **EVENT_TB_EMPTY** TB_EMPTY events 60 **EVENT_HS_DESC** HS_DESC events 61 **EXTENDCIRCUIT_PATH_OPTIONAL** EXTENDCIRCUIT queries can omit the path if the circuit is zero 62 **FEATURE_EXTENDED_EVENTS** 'EXTENDED_EVENTS' optional feature 63 **FEATURE_VERBOSE_NAMES** 'VERBOSE_NAMES' optional feature 64 **GETINFO_CONFIG_TEXT** 'GETINFO config-text' query 65 **GETINFO_GEOIP_AVAILABLE** 'GETINFO ip-to-country/ipv4-available' query and its ipv6 counterpart 66 **GETINFO_MICRODESCRIPTORS** 'GETINFO md/all' query 67 **GETINFO_UPTIME** 'GETINFO uptime' query 68 **HIDDEN_SERVICE_V3** Support for v3 hidden services 69 **HSFETCH** HSFETCH requests 70 **HSFETCH_V3** HSFETCH for version 3 hidden services 71 **HSPOST** HSPOST requests 72 **ADD_ONION** ADD_ONION and DEL_ONION requests 73 **ADD_ONION_BASIC_AUTH** ADD_ONION supports basic authentication 74 **ADD_ONION_NON_ANONYMOUS** ADD_ONION supports non-anonymous mode 75 **ADD_ONION_MAX_STREAMS** ADD_ONION support for MaxStreamsCloseCircuit 76 **LOADCONF** LOADCONF requests 77 **MICRODESCRIPTOR_IS_DEFAULT** Tor gets microdescriptors by default rather than server descriptors 78 **SAVECONF_FORCE** Added the 'FORCE' flag to SAVECONF 79 **TAKEOWNERSHIP** TAKEOWNERSHIP requests 80 **TORRC_CONTROL_SOCKET** 'ControlSocket <path>' config option 81 **TORRC_PORT_FORWARDING** 'PortForwarding' config option 82 **TORRC_DISABLE_DEBUGGER_ATTACHMENT** 'DisableDebuggerAttachment' config option 83 **TORRC_VIA_STDIN** Allow torrc options via 'tor -f -' (:trac:`13865`) 84 ===================================== =========== 85""" 86 87import os 88import re 89 90import stem.prereq 91import stem.util 92import stem.util.enum 93import stem.util.system 94 95if stem.prereq._is_lru_cache_available(): 96 from functools import lru_cache 97else: 98 from stem.util.lru_cache import lru_cache 99 100# cache for the get_system_tor_version function 101VERSION_CACHE = {} 102 103VERSION_PATTERN = re.compile(r'^([0-9]+)\.([0-9]+)\.([0-9]+)(\.[0-9]+)?(-\S*)?(( \(\S*\))*)$') 104 105 106def get_system_tor_version(tor_cmd = 'tor'): 107 """ 108 Queries tor for its version. This is os dependent, only working on linux, 109 osx, and bsd. 110 111 :param str tor_cmd: command used to run tor 112 113 :returns: :class:`~stem.version.Version` provided by the tor command 114 115 :raises: **IOError** if unable to query or parse the version 116 """ 117 118 if tor_cmd not in VERSION_CACHE: 119 version_cmd = '%s --version' % tor_cmd 120 121 try: 122 version_output = stem.util.system.call(version_cmd) 123 except OSError as exc: 124 # make the error message nicer if this is due to tor being unavialable 125 126 if 'No such file or directory' in str(exc): 127 if os.path.isabs(tor_cmd): 128 exc = "Unable to check tor's version. '%s' doesn't exist." % tor_cmd 129 else: 130 exc = "Unable to run '%s'. Maybe tor isn't in your PATH?" % version_cmd 131 132 raise IOError(exc) 133 134 for line in version_output: 135 # output example: 136 # Oct 21 07:19:27.438 [notice] Tor v0.2.1.30. This is experimental software. Do not rely on it for strong anonymity. (Running on Linux i686) 137 # Tor version 0.2.1.30. 138 139 if line.startswith('Tor version ') and line.endswith('.'): 140 try: 141 version_str = line[12:-1] 142 VERSION_CACHE[tor_cmd] = Version(version_str) 143 break 144 except ValueError as exc: 145 raise IOError(exc) 146 147 if tor_cmd not in VERSION_CACHE: 148 raise IOError("'%s' didn't provide a parseable version:\n\n%s" % (version_cmd, '\n'.join(version_output))) 149 150 return VERSION_CACHE[tor_cmd] 151 152 153@lru_cache() 154def _get_version(version_str): 155 return Version(version_str) 156 157 158class Version(object): 159 """ 160 Comparable tor version. These are constructed from strings that conform to 161 the 'new' style in the `tor version-spec 162 <https://gitweb.torproject.org/torspec.git/tree/version-spec.txt>`_, 163 such as "0.1.4" or "0.2.2.23-alpha (git-7dcd105be34a4f44)". 164 165 .. versionchanged:: 1.6.0 166 Added all_extra parameter. 167 168 :var int major: major version 169 :var int minor: minor version 170 :var int micro: micro version 171 :var int patch: patch level (**None** if undefined) 172 :var str status: status tag such as 'alpha' or 'beta-dev' (**None** if undefined) 173 :var str extra: first extra information without its parentheses such as 174 'git-8be6058d8f31e578' (**None** if undefined) 175 :var list all_extra: all extra information entries, without their parentheses 176 :var str git_commit: git commit id (**None** if it wasn't provided) 177 178 :param str version_str: version to be parsed 179 180 :raises: **ValueError** if input isn't a valid tor version 181 """ 182 183 def __init__(self, version_str): 184 self.version_str = version_str 185 version_parts = VERSION_PATTERN.match(version_str) 186 187 if version_parts: 188 major, minor, micro, patch, status, extra_str, _ = version_parts.groups() 189 190 # The patch and status matches are optional (may be None) and have an extra 191 # proceeding period or dash if they exist. Stripping those off. 192 193 if patch: 194 patch = int(patch[1:]) 195 196 if status: 197 status = status[1:] 198 199 self.major = int(major) 200 self.minor = int(minor) 201 self.micro = int(micro) 202 self.patch = patch 203 self.status = status 204 self.all_extra = [entry[1:-1] for entry in extra_str.strip().split()] if extra_str else [] 205 self.extra = self.all_extra[0] if self.all_extra else None 206 self.git_commit = None 207 208 for extra in self.all_extra: 209 if extra and re.match('^git-[0-9a-f]{16}$', extra): 210 self.git_commit = extra[4:] 211 break 212 else: 213 raise ValueError("'%s' isn't a properly formatted tor version" % version_str) 214 215 def __str__(self): 216 """ 217 Provides the string used to construct the version. 218 """ 219 220 return self.version_str 221 222 def _compare(self, other, method): 223 """ 224 Compares version ordering according to the spec. 225 """ 226 227 if not isinstance(other, Version): 228 return False 229 230 for attr in ('major', 'minor', 'micro', 'patch'): 231 my_version = getattr(self, attr) 232 other_version = getattr(other, attr) 233 234 if my_version is None: 235 my_version = 0 236 237 if other_version is None: 238 other_version = 0 239 240 if my_version != other_version: 241 return method(my_version, other_version) 242 243 # According to the version spec... 244 # 245 # If we *do* encounter two versions that differ only by status tag, we 246 # compare them lexically as ASCII byte strings. 247 248 my_status = self.status if self.status else '' 249 other_status = other.status if other.status else '' 250 251 return method(my_status, other_status) 252 253 def __hash__(self): 254 return stem.util._hash_attr(self, 'major', 'minor', 'micro', 'patch', 'status', cache = True) 255 256 def __eq__(self, other): 257 return self._compare(other, lambda s, o: s == o) 258 259 def __ne__(self, other): 260 return not self == other 261 262 def __gt__(self, other): 263 """ 264 Checks if this version meets the requirements for a given feature. We can 265 be compared to either a :class:`~stem.version.Version` or 266 :class:`~stem.version._VersionRequirements`. 267 """ 268 269 if isinstance(other, _VersionRequirements): 270 for rule in other.rules: 271 if rule(self): 272 return True 273 274 return False 275 276 return self._compare(other, lambda s, o: s > o) 277 278 def __ge__(self, other): 279 if isinstance(other, _VersionRequirements): 280 for rule in other.rules: 281 if rule(self): 282 return True 283 284 return False 285 286 return self._compare(other, lambda s, o: s >= o) 287 288 289class _VersionRequirements(object): 290 """ 291 Series of version constraints that can be compared to. For instance, this 292 allows for comparisons like 'if I'm greater than version X in the 0.2.2 293 series, or greater than version Y in the 0.2.3 series'. 294 295 This is a logical 'or' of the series of rules. 296 """ 297 298 def __init__(self): 299 self.rules = [] 300 301 def greater_than(self, version, inclusive = True): 302 """ 303 Adds a constraint that we're greater than the given version. 304 305 :param stem.version.Version version: version we're checking against 306 :param bool inclusive: if comparison is inclusive or not 307 """ 308 309 if inclusive: 310 self.rules.append(lambda v: version <= v) 311 else: 312 self.rules.append(lambda v: version < v) 313 314 def less_than(self, version, inclusive = True): 315 """ 316 Adds a constraint that we're less than the given version. 317 318 :param stem.version.Version version: version we're checking against 319 :param bool inclusive: if comparison is inclusive or not 320 """ 321 322 if inclusive: 323 self.rules.append(lambda v: version >= v) 324 else: 325 self.rules.append(lambda v: version > v) 326 327 def in_range(self, from_version, to_version, from_inclusive = True, to_inclusive = False): 328 """ 329 Adds constraint that we're within the range from one version to another. 330 331 :param stem.version.Version from_version: beginning of the comparison range 332 :param stem.version.Version to_version: end of the comparison range 333 :param bool from_inclusive: if comparison is inclusive with the starting version 334 :param bool to_inclusive: if comparison is inclusive with the ending version 335 """ 336 337 def new_rule(v): 338 if from_inclusive and to_inclusive: 339 return from_version <= v <= to_version 340 elif from_inclusive: 341 return from_version <= v < to_version 342 else: 343 return from_version < v < to_version 344 345 self.rules.append(new_rule) 346 347 348safecookie_req = _VersionRequirements() 349safecookie_req.in_range(Version('0.2.2.36'), Version('0.2.3.0')) 350safecookie_req.greater_than(Version('0.2.3.13')) 351 352Requirement = stem.util.enum.Enum( 353 ('AUTH_SAFECOOKIE', safecookie_req), 354 ('DESCRIPTOR_COMPRESSION', Version('0.3.1.1-alpha')), 355 ('DORMANT_MODE', Version('0.4.0.1-alpha')), 356 ('DROPGUARDS', Version('0.2.5.1-alpha')), 357 ('EVENT_AUTHDIR_NEWDESCS', Version('0.1.1.10-alpha')), 358 ('EVENT_BUILDTIMEOUT_SET', Version('0.2.2.7-alpha')), 359 ('EVENT_CIRC_MINOR', Version('0.2.3.11-alpha')), 360 ('EVENT_CLIENTS_SEEN', Version('0.2.1.10-alpha')), 361 ('EVENT_CONF_CHANGED', Version('0.2.3.3-alpha')), 362 ('EVENT_DESCCHANGED', Version('0.1.2.2-alpha')), 363 ('EVENT_GUARD', Version('0.1.2.5-alpha')), 364 ('EVENT_HS_DESC_CONTENT', Version('0.2.7.1-alpha')), 365 ('EVENT_NS', Version('0.1.2.3-alpha')), 366 ('EVENT_NETWORK_LIVENESS', Version('0.2.7.2-alpha')), 367 ('EVENT_NEWCONSENSUS', Version('0.2.1.13-alpha')), 368 ('EVENT_SIGNAL', Version('0.2.3.1-alpha')), 369 ('EVENT_STATUS', Version('0.1.2.3-alpha')), 370 ('EVENT_STREAM_BW', Version('0.1.2.8-beta')), 371 ('EVENT_TRANSPORT_LAUNCHED', Version('0.2.5.0-alpha')), 372 ('EVENT_CONN_BW', Version('0.2.5.2-alpha')), 373 ('EVENT_CIRC_BW', Version('0.2.5.2-alpha')), 374 ('EVENT_CELL_STATS', Version('0.2.5.2-alpha')), 375 ('EVENT_TB_EMPTY', Version('0.2.5.2-alpha')), 376 ('EVENT_HS_DESC', Version('0.2.5.2-alpha')), 377 ('EXTENDCIRCUIT_PATH_OPTIONAL', Version('0.2.2.9')), 378 ('FEATURE_EXTENDED_EVENTS', Version('0.2.2.1-alpha')), 379 ('FEATURE_VERBOSE_NAMES', Version('0.2.2.1-alpha')), 380 ('GETINFO_CONFIG_TEXT', Version('0.2.2.7-alpha')), 381 ('GETINFO_GEOIP_AVAILABLE', Version('0.3.2.1-alpha')), 382 ('GETINFO_MICRODESCRIPTORS', Version('0.3.5.1-alpha')), 383 ('GETINFO_UPTIME', Version('0.3.5.1-alpha')), 384 ('HIDDEN_SERVICE_V3', Version('0.3.3.1-alpha')), 385 ('HSFETCH', Version('0.2.7.1-alpha')), 386 ('HSFETCH_V3', Version('0.4.1.1-alpha')), 387 ('HSPOST', Version('0.2.7.1-alpha')), 388 ('ADD_ONION', Version('0.2.7.1-alpha')), 389 ('ADD_ONION_BASIC_AUTH', Version('0.2.9.1-alpha')), 390 ('ADD_ONION_NON_ANONYMOUS', Version('0.2.9.3-alpha')), 391 ('ADD_ONION_MAX_STREAMS', Version('0.2.7.2-alpha')), 392 ('LOADCONF', Version('0.2.1.1')), 393 ('MICRODESCRIPTOR_IS_DEFAULT', Version('0.2.3.3')), 394 ('SAVECONF_FORCE', Version('0.3.1.1-alpha')), 395 ('TAKEOWNERSHIP', Version('0.2.2.28-beta')), 396 ('TORRC_CONTROL_SOCKET', Version('0.2.0.30')), 397 ('TORRC_PORT_FORWARDING', Version('0.2.3.1-alpha')), 398 ('TORRC_DISABLE_DEBUGGER_ATTACHMENT', Version('0.2.3.9')), 399 ('TORRC_VIA_STDIN', Version('0.2.6.3-alpha')), 400) 401