Replace tpo git repository URL by gitlab
[stem.git] / stem / response / protocolinfo.py
blobca3a62034016e333c3e249bb7c3896f71fbf533d
1 # Copyright 2012-2020, Damian Johnson and The Tor Project
2 # See LICENSE for licensing information
4 import sys
6 import stem.response
7 import stem.socket
8 import stem.version
9 import stem.util.str_tools
11 from stem.util import log
12 from typing import Tuple
15 class ProtocolInfoResponse(stem.response.ControlMessage):
16 """
17 Version one PROTOCOLINFO query response.
19 The protocol_version is the only mandatory data for a valid PROTOCOLINFO
20 response, so all other values are None if undefined or empty if a collection.
22 :var int protocol_version: protocol version of the response
23 :var stem.version.Version tor_version: version of the tor process
24 :var tuple auth_methods: :data:`stem.connection.AuthMethod` types that tor will accept
25 :var tuple unknown_auth_methods: strings of unrecognized auth methods
26 :var str cookie_path: path of tor's authentication cookie
27 """
29 def _parse_message(self) -> None:
30 # Example:
31 # 250-PROTOCOLINFO 1
32 # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
33 # 250-VERSION Tor="0.2.1.30"
34 # 250 OK
36 from stem.connection import AuthMethod
38 self.protocol_version = None
39 self.tor_version = None
40 self.auth_methods = () # type: Tuple[stem.connection.AuthMethod, ...]
41 self.unknown_auth_methods = () # type: Tuple[str, ...]
42 self.cookie_path = None
44 auth_methods, unknown_auth_methods = [], []
45 remaining_lines = list(self)
47 if not self.is_ok() or not remaining_lines.pop() == 'OK':
48 raise stem.ProtocolError("PROTOCOLINFO response didn't have an OK status:\n%s" % self)
50 # sanity check that we're a PROTOCOLINFO response
51 if not remaining_lines[0].startswith('PROTOCOLINFO'):
52 raise stem.ProtocolError('Message is not a PROTOCOLINFO response:\n%s' % self)
54 while remaining_lines:
55 line = remaining_lines.pop(0)
56 line_type = line.pop()
58 if line_type == 'PROTOCOLINFO':
59 # Line format:
60 # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
61 # PIVERSION = 1*DIGIT
63 if line.is_empty():
64 raise stem.ProtocolError("PROTOCOLINFO response's initial line is missing the protocol version: %s" % line)
66 try:
67 self.protocol_version = int(line.pop())
68 except ValueError:
69 raise stem.ProtocolError('PROTOCOLINFO response version is non-numeric: %s' % line)
71 # The piversion really should be '1' but, according to the spec, tor
72 # does not necessarily need to provide the PROTOCOLINFO version that we
73 # requested. Log if it's something we aren't expecting but still make
74 # an effort to parse like a v1 response.
76 if self.protocol_version != 1:
77 log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
78 elif line_type == 'AUTH':
79 # Line format:
80 # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
81 # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
82 # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
83 # AuthCookieFile = QuotedString
85 # parse AuthMethod mapping
86 if not line.is_next_mapping('METHODS'):
87 raise stem.ProtocolError("PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line)
89 for method in line.pop_mapping()[1].split(','):
90 if method == 'NULL':
91 auth_methods.append(AuthMethod.NONE)
92 elif method == 'HASHEDPASSWORD':
93 auth_methods.append(AuthMethod.PASSWORD)
94 elif method == 'COOKIE':
95 auth_methods.append(AuthMethod.COOKIE)
96 elif method == 'SAFECOOKIE':
97 auth_methods.append(AuthMethod.SAFECOOKIE)
98 else:
99 unknown_auth_methods.append(method)
100 message_id = 'stem.response.protocolinfo.unknown_auth_%s' % method
101 log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
103 # our auth_methods should have a single AuthMethod.UNKNOWN entry if
104 # any unknown authentication methods exist
105 if AuthMethod.UNKNOWN not in auth_methods:
106 auth_methods.append(AuthMethod.UNKNOWN)
108 # parse optional COOKIEFILE mapping (quoted and can have escapes)
110 if line.is_next_mapping('COOKIEFILE', True, True):
111 path = line._pop_mapping_bytes(True, True)[1]
113 # fall back if our filesystem encoding doesn't work
115 for encoding in [sys.getfilesystemencoding(), 'utf-8', 'latin-1']:
116 try:
117 self.cookie_path = path.decode(encoding)
118 break
119 except ValueError:
120 pass
122 if self.cookie_path is None:
123 raise stem.ProtocolError("Cookie path '%s' mismatches our filesystem encoding (%s)" % (repr(path), sys.getfilesystemencoding()))
124 elif line_type == 'VERSION':
125 # Line format:
126 # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
127 # TorVersion = QuotedString
129 if not line.is_next_mapping('Tor', True):
130 raise stem.ProtocolError("PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line)
132 try:
133 self.tor_version = stem.version.Version(line.pop_mapping(True)[1])
134 except ValueError as exc:
135 raise stem.ProtocolError(exc)
136 else:
137 log.debug("Unrecognized PROTOCOLINFO line type '%s', ignoring it: %s" % (line_type, line))
139 self.auth_methods = tuple(auth_methods)
140 self.unknown_auth_methods = tuple(unknown_auth_methods)