1 # Copyright 2012-2020, Damian Johnson and The Tor Project
2 # See LICENSE for licensing information
5 Parses replies from the control socket.
11 convert - translates a ControlMessage into a particular response subclass
13 ControlMessage - Message that's read from the control socket.
14 |- SingleLineResponse - Simple tor response only including a single line of information.
16 |- from_str - provides a ControlMessage for the given string
17 |- is_ok - response had a 250 status
18 |- content - provides the parsed message content
19 +- raw_content - unparsed socket data
21 ControlLine - String subclass with methods for parsing controller responses.
22 |- remainder - provides the unparsed content
23 |- is_empty - checks if the remaining content is empty
24 |- is_next_quoted - checks if the next entry is a quoted value
25 |- is_next_mapping - checks if the next entry is a KEY=VALUE mapping
26 |- peek_key - provides the key of the next entry
27 |- pop - removes and returns the next entry
28 +- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
39 import stem
.util
.str_tools
41 from typing
import Any
, Iterator
, List
, Optional
, Sequence
, Tuple
, Union
57 KEY_ARG
= re
.compile('^(\\S+)=')
60 def convert(response_type
: str, message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> None:
62 Converts a :class:`~stem.response.ControlMessage` into a particular kind of
63 tor response. This does an in-place conversion of the message from being a
64 :class:`~stem.response.ControlMessage` to a subclass for its response type.
65 Recognized types include...
67 =========================== =====
69 =========================== =====
70 **ADD_ONION** :class:`stem.response.add_onion.AddOnionResponse`
71 **AUTHCHALLENGE** :class:`stem.response.authchallenge.AuthChallengeResponse`
72 **EVENT** :class:`stem.response.events.Event` subclass
73 **GETCONF** :class:`stem.response.getconf.GetConfResponse`
74 **GETINFO** :class:`stem.response.getinfo.GetInfoResponse`
75 **MAPADDRESS** :class:`stem.response.mapaddress.MapAddressResponse`
76 **ONION_CLIENT_AUTH_VIEW** :class:`stem.response.onion_client_auth.OnionClientAuthViewResponse`
77 **PROTOCOLINFO** :class:`stem.response.protocolinfo.ProtocolInfoResponse`
78 **SINGLELINE** :class:`stem.response.SingleLineResponse`
79 =========================== =====
81 :param response_type: type of tor response to convert to
82 :param message: message to be converted
83 :param kwargs: optional keyword arguments to be passed to the parser method
86 * :class:`stem.ProtocolError` the message isn't a proper response of
88 * :class:`stem.InvalidArguments` the arguments given as input are
89 invalid, this is can only be raised if the response_type is: **GETINFO**,
91 * :class:`stem.InvalidRequest` the arguments given as input are
92 invalid, this is can only be raised if the response_type is:
94 * :class:`stem.OperationFailed` if the action the event represents failed,
95 this is can only be raised if the response_type is: **MAPADDRESS**
96 * **TypeError** if argument isn't a :class:`~stem.response.ControlMessage`
97 or response_type isn't supported
100 import stem
.response
.add_onion
101 import stem
.response
.authchallenge
102 import stem
.response
.events
103 import stem
.response
.getinfo
104 import stem
.response
.getconf
105 import stem
.response
.mapaddress
106 import stem
.response
.onion_client_auth
107 import stem
.response
.protocolinfo
109 if not isinstance(message
, ControlMessage
):
110 raise TypeError('Only able to convert stem.response.ControlMessage instances')
113 'ADD_ONION': stem
.response
.add_onion
.AddOnionResponse
,
114 'AUTHCHALLENGE': stem
.response
.authchallenge
.AuthChallengeResponse
,
115 'EVENT': stem
.response
.events
.Event
,
116 'GETCONF': stem
.response
.getconf
.GetConfResponse
,
117 'GETINFO': stem
.response
.getinfo
.GetInfoResponse
,
118 'MAPADDRESS': stem
.response
.mapaddress
.MapAddressResponse
,
119 'ONION_CLIENT_AUTH_VIEW': stem
.response
.onion_client_auth
.OnionClientAuthViewResponse
,
120 'PROTOCOLINFO': stem
.response
.protocolinfo
.ProtocolInfoResponse
,
121 'SINGLELINE': SingleLineResponse
,
125 response_class
= response_types
[response_type
]
127 raise TypeError('Unsupported response type: %s' % response_type
)
129 message
.__class
__ = response_class
130 message
._parse
_message
(**kwargs
) # type: ignore
133 # TODO: These aliases are for type hint compatability. We should refactor how
134 # message conversion is performed to avoid this headache.
136 def _convert_to_single_line(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.SingleLineResponse':
137 stem
.response
.convert('SINGLELINE', message
)
138 return message
# type: ignore
141 def _convert_to_event(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.events.Event':
142 stem
.response
.convert('EVENT', message
)
143 return message
# type: ignore
146 def _convert_to_getinfo(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.getinfo.GetInfoResponse':
147 stem
.response
.convert('GETINFO', message
)
148 return message
# type: ignore
151 def _convert_to_getconf(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.getconf.GetConfResponse':
152 stem
.response
.convert('GETCONF', message
)
153 return message
# type: ignore
156 def _convert_to_add_onion(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.add_onion.AddOnionResponse':
157 stem
.response
.convert('ADD_ONION', message
)
158 return message
# type: ignore
161 def _convert_to_onion_client_auth_view(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.onion_client_auth.OnionClientAuthViewResponse':
162 stem
.response
.convert('ONION_CLIENT_AUTH_VIEW', message
)
163 return message
# type: ignore
166 def _convert_to_mapaddress(message
: 'stem.response.ControlMessage', **kwargs
: Any
) -> 'stem.response.mapaddress.MapAddressResponse':
167 stem
.response
.convert('MAPADDRESS', message
)
168 return message
# type: ignore
171 class ControlMessage(object):
173 Message from the control socket. This is iterable and can be stringified for
174 individual message components stripped of protocol formatting. Messages are
177 :var int arrived_at: unix timestamp for when the message arrived
179 .. versionchanged:: 1.7.0
180 Implemented equality and hashing.
182 .. versionchanged:: 1.8.0
183 Moved **arrived_at** from the Event class up to this base ControlMessage.
187 def from_str(content
: Union
[str, bytes
], msg_type
: Optional
[str] = None, normalize
: bool = False, **kwargs
: Any
) -> 'stem.response.ControlMessage':
189 Provides a ControlMessage for the given content.
191 .. versionadded:: 1.1.0
193 .. versionchanged:: 1.6.0
194 Added the normalize argument.
196 :param content: message to construct the message from
197 :param msg_type: type of tor reply to parse the content as
198 :param normalize: ensures expected carriage return and ending newline
200 :param kwargs: optional keyword arguments to be passed to the parser method
202 :returns: stem.response.ControlMessage instance
205 if isinstance(content
, str):
206 content
= stem
.util
.str_tools
._to
_bytes
(content
)
209 if not content
.endswith(b
'\n'):
212 content
= re
.sub(b
'([\r]?)\n', b
'\r\n', content
)
214 msg
= stem
.socket
.recv_message_from_bytes_io(io
.BytesIO(stem
.util
.str_tools
._to
_bytes
(content
)), arrived_at
= kwargs
.pop('arrived_at', None))
216 if msg_type
is not None:
217 convert(msg_type
, msg
, **kwargs
)
221 def __init__(self
, parsed_content
: Sequence
[Tuple
[str, str, bytes
]], raw_content
: bytes
, arrived_at
: Optional
[float] = None) -> None:
222 if not parsed_content
:
223 raise ValueError("ControlMessages can't be empty")
225 # TODO: Change arrived_at to a float (can't yet because it causes Event
226 # equality checks to fail - events include arrived_at within their hash
227 # whereas ControlMessages don't).
229 self
.arrived_at
= int(arrived_at
if arrived_at
else time
.time())
231 self
._parsed
_content
= parsed_content
232 self
._raw
_content
= raw_content
233 self
._str
= None # type: Optional[str]
234 self
._hash
= stem
.util
._hash
_attr
(self
, '_raw_content')
236 def is_ok(self
) -> bool:
238 Checks if any of our lines have a 2xx response.
240 :returns: **True** if any lines have a 2xx response code, **False** otherwise
243 for code
, _
, _
in self
._parsed
_content
:
244 if code
.isdigit() and (200 <= int(code
) < 300):
249 # TODO: drop this alias when we provide better type support
251 def _content_bytes(self
) -> List
[Tuple
[str, str, bytes
]]:
252 return self
.content(get_bytes
= True) # type: ignore
254 def content(self
, get_bytes
: bool = False) -> List
[Tuple
[str, str, str]]:
256 Provides the parsed message content. These are entries of the form...
260 (status_code, divider, content)
263 Three character code for the type of response (defined in section 4 of
267 Single character to indicate if this is mid-reply, data, or an end to the
268 message (defined in section 2.3 of the control-spec).
271 The following content is the actual payload of the line.
273 For data entries the content is the full multi-line payload with newline
274 linebreaks and leading periods unescaped.
276 The **status_code** and **divider** are both strings (**bytes** in python
277 2.x and **unicode** in python 3.x). The **content** however is **bytes** if
278 **get_bytes** is **True**.
280 .. versionchanged:: 1.1.0
281 Added the get_bytes argument.
283 :param get_bytes: provides **bytes** for the **content** rather than a **str**
285 :returns: **list** of (str, str, str) tuples for the components of this message
289 return [(code
, div
, stem
.util
.str_tools
._to
_unicode
(content
)) for (code
, div
, content
) in self
._parsed
_content
]
291 return list(self
._parsed
_content
) # type: ignore
293 def raw_content(self
, get_bytes
: bool = False) -> Union
[str, bytes
]:
295 Provides the unparsed content read from the control socket.
297 .. versionchanged:: 1.1.0
298 Added the get_bytes argument.
300 :param get_bytes: if **True** then this provides **bytes** rather than a **str**
302 :returns: **str** of the socket data used to generate this message
306 return stem
.util
.str_tools
._to
_unicode
(self
._raw
_content
)
308 return self
._raw
_content
310 def _parse_message(self
) -> None:
311 raise NotImplementedError('Implemented by subclasses')
313 def __str__(self
) -> str:
315 Content of the message, stripped of status code and divider protocol
319 if self
._str
is None:
320 self
._str
= '\n'.join(list(self
))
324 def __iter__(self
) -> Iterator
['stem.response.ControlLine']:
326 Provides :class:`~stem.response.ControlLine` instances for the content of
327 the message. This is stripped of status codes and dividers, for instance...
332 desc/id/* -- Router descriptors by ID.
333 desc/name/* -- Router descriptors by nickname.
337 Would provide two entries...
342 desc/id/* -- Router descriptors by ID.
343 desc/name/* -- Router descriptors by nickname."
347 for _
, _
, content
in self
._parsed
_content
:
348 yield ControlLine(stem
.util
.str_tools
._to
_unicode
(content
))
350 def __len__(self
) -> int:
352 :returns: number of ControlLines
355 return len(self
._parsed
_content
)
357 def __getitem__(self
, index
: int) -> 'stem.response.ControlLine':
359 :returns: :class:`~stem.response.ControlLine` at the index
362 content
= self
._parsed
_content
[index
][2]
363 content
= stem
.util
.str_tools
._to
_unicode
(content
)
365 return ControlLine(content
)
367 def __hash__(self
) -> int:
370 def __eq__(self
, other
: Any
) -> bool:
371 return hash(self
) == hash(other
) if isinstance(other
, ControlMessage
) else False
373 def __ne__(self
, other
: Any
) -> bool:
374 return not self
== other
377 class ControlLine(str):
379 String subclass that represents a line of controller output. This behaves as
380 a normal string with additional methods for parsing and popping entries from
381 a space delimited series of elements like a stack.
383 None of these additional methods effect ourselves as a string (which is still
384 immutable). All methods are thread safe.
387 def __new__(self
, value
: str) -> 'stem.response.ControlLine':
388 return str.__new
__(self
, value
) # type: ignore
390 def __init__(self
, value
: str) -> None:
391 self
._remainder
= value
392 self
._remainder
_lock
= threading
.RLock()
394 def remainder(self
) -> str:
396 Provides our unparsed content. This is an empty string after we've popped
399 :returns: **str** of the unparsed content
402 return self
._remainder
404 def is_empty(self
) -> bool:
406 Checks if we have further content to pop or not.
408 :returns: **True** if we have additional content, **False** otherwise
411 return self
._remainder
== ''
413 def is_next_quoted(self
, escaped
: bool = False) -> bool:
415 Checks if our next entry is a quoted value or not.
417 :param escaped: unescapes the string
419 :returns: **True** if the next entry can be parsed as a quoted value, **False** otherwise
422 start_quote
, end_quote
= _get_quote_indices(self
._remainder
, escaped
)
423 return start_quote
== 0 and end_quote
!= -1
425 def is_next_mapping(self
, key
: Optional
[str] = None, quoted
: bool = False, escaped
: bool = False) -> bool:
427 Checks if our next entry is a KEY=VALUE mapping or not.
429 :param key: checks that the key matches this value, skipping the check if **None**
430 :param quoted: checks that the mapping is to a quoted value
431 :param escaped: unescapes the string
433 :returns: **True** if the next entry can be parsed as a key=value mapping,
437 remainder
= self
._remainder
# temp copy to avoid locking
438 key_match
= KEY_ARG
.match(remainder
)
441 if key
and key
!= key_match
.groups()[0]:
445 # checks that we have a quoted value and that it comes after the 'key='
446 start_quote
, end_quote
= _get_quote_indices(remainder
, escaped
)
447 return start_quote
== key_match
.end() and end_quote
!= -1
449 return True # we just needed to check for the key
451 return False # doesn't start with a key
453 def peek_key(self
) -> str:
455 Provides the key of the next entry, providing **None** if it isn't a
458 :returns: **str** with the next entry's key
461 remainder
= self
._remainder
462 key_match
= KEY_ARG
.match(remainder
)
465 return key_match
.groups()[0]
469 def pop(self
, quoted
: bool = False, escaped
: bool = False) -> str:
471 Parses the next space separated entry, removing it and the space from our
472 remaining content. Examples...
476 >>> line = ControlLine("\\"We're all mad here.\\" says the grinning cat.")
477 >>> print line.pop(True)
478 "We're all mad here."
481 >>> print line.remainder()
484 >>> line = ControlLine("\\"this has a \\\\\\" and \\\\\\\\ in it\\" foo=bar more_data")
485 >>> print line.pop(True, True)
486 "this has a \\" and \\\\ in it"
488 :param quoted: parses the next entry as a quoted value, removing the quotes
489 :param escaped: unescapes the string
491 :returns: **str** of the next space separated entry
494 * **ValueError** if quoted is True without the value being quoted
495 * **IndexError** if we don't have any remaining content left to parse
498 with self
._remainder
_lock
:
499 next_entry
, remainder
= _parse_entry(self
._remainder
, quoted
, escaped
, False)
500 self
._remainder
= remainder
501 return next_entry
# type: ignore
503 # TODO: drop this alias when we provide better type support
505 def _pop_mapping_bytes(self
, quoted
: bool = False, escaped
: bool = False) -> Tuple
[str, bytes
]:
506 return self
.pop_mapping(quoted
, escaped
, get_bytes
= True) # type: ignore
508 def pop_mapping(self
, quoted
: bool = False, escaped
: bool = False, get_bytes
: bool = False) -> Tuple
[str, str]:
510 Parses the next space separated entry as a KEY=VALUE mapping, removing it
511 and the space from our remaining content.
513 .. versionchanged:: 1.6.0
514 Added the get_bytes argument.
516 :param quoted: parses the value as being quoted, removing the quotes
517 :param escaped: unescapes the string
518 :param get_bytes: provides **bytes** for the **value** rather than a **str**
520 :returns: **tuple** of the form (key, value)
522 :raises: **ValueError** if this isn't a KEY=VALUE mapping or if quoted is
523 **True** without the value being quoted
524 :raises: **IndexError** if there's nothing to parse from the line
527 with self
._remainder
_lock
:
529 raise IndexError('no remaining content to parse')
531 key_match
= KEY_ARG
.match(self
._remainder
)
534 raise ValueError("the next entry isn't a KEY=VALUE mapping: " + self
._remainder
)
537 key
= key_match
.groups()[0]
538 remainder
= self
._remainder
[key_match
.end():]
540 next_entry
, remainder
= _parse_entry(remainder
, quoted
, escaped
, get_bytes
)
541 self
._remainder
= remainder
542 return (key
, next_entry
) # type: ignore
545 def _parse_entry(line
: str, quoted
: bool, escaped
: bool, get_bytes
: bool) -> Tuple
[Union
[str, bytes
], str]:
547 Parses the next entry from the given space separated content.
549 :param line: content to be parsed
550 :param quoted: parses the next entry as a quoted value, removing the quotes
551 :param escaped: unescapes the string
552 :param get_bytes: provides **bytes** for the entry rather than a **str**
554 :returns: **tuple** of the form (entry, remainder)
557 * **ValueError** if quoted is True without the next value being quoted
558 * **IndexError** if there's nothing to parse from the line
562 raise IndexError('no remaining content to parse')
564 next_entry
, remainder
= '', line
567 # validate and parse the quoted value
568 start_quote
, end_quote
= _get_quote_indices(remainder
, escaped
)
570 if start_quote
!= 0 or end_quote
== -1:
571 raise ValueError("the next entry isn't a quoted value: " + line
)
573 next_entry
, remainder
= remainder
[1:end_quote
], remainder
[end_quote
+ 1:]
575 # non-quoted value, just need to check if there's more data afterward
577 next_entry
, remainder
= remainder
.split(' ', 1)
579 next_entry
, remainder
= remainder
, ''
582 # Tor does escaping in its 'esc_for_log' function of 'common/util.c'. It's
583 # hard to tell what controller functions use this in practice, but direct
586 # * 'COOKIEFILE' field of PROTOCOLINFO responses
587 # * logged messages about bugs
588 # * the 'getinfo_helper_listeners' function of control.c
590 # Ideally we'd use "next_entry.decode('string_escape')" but it was removed
591 # in python 3.x and 'unicode_escape' isn't quite the same...
593 # https://stackoverflow.com/questions/14820429/how-do-i-decodestring-escape-in-python3
595 next_entry
= codecs
.escape_decode(next_entry
)[0] # type: ignore
598 next_entry
= stem
.util
.str_tools
._to
_unicode
(next_entry
) # normalize back to str
601 return (stem
.util
.str_tools
._to
_bytes
(next_entry
), remainder
.lstrip())
603 return (next_entry
, remainder
.lstrip())
606 def _get_quote_indices(line
: str, escaped
: bool) -> Tuple
[int, int]:
608 Provides the indices of the next two quotes in the given content.
610 :param line: content to be parsed
611 :param escaped: unescapes the string
613 :returns: **tuple** of two ints, indices being -1 if a quote doesn't exist
616 indices
, quote_index
= [], -1
619 quote_index
= line
.find('"', quote_index
+ 1)
621 # if we have escapes then we need to skip any r'\"' entries
623 # skip check if index is -1 (no match) or 0 (first character)
624 while quote_index
>= 1 and line
[quote_index
- 1] == '\\':
625 quote_index
= line
.find('"', quote_index
+ 1)
627 indices
.append(quote_index
)
629 return tuple(indices
) # type: ignore
632 class SingleLineResponse(ControlMessage
):
634 Reply to a request that performs an action rather than querying data. These
635 requests only contain a single line, which is 'OK' if successful, and a
636 description of the problem if not.
638 :var str code: status code for our line
639 :var str message: content of the line
642 def is_ok(self
, strict
: bool = False) -> bool:
644 Checks if the response code is "250". If strict is **True** then this
645 checks if the response is "250 OK"
647 :param strict: checks for a "250 OK" message if **True**
650 * If strict is **False**: **True** if the response code is "250", **False** otherwise
651 * If strict is **True**: **True** if the response is "250 OK", **False** otherwise
655 return self
.content()[0] == ('250', ' ', 'OK')
657 return self
.content()[0][0] == '250'
659 def _parse_message(self
) -> None:
660 content
= self
.content()
663 raise stem
.ProtocolError('Received multi-line response')
664 elif len(content
) == 0:
665 raise stem
.ProtocolError('Received empty response')
667 code
, _
, msg
= content
[0]
669 self
.code
= stem
.util
.str_tools
._to
_unicode
(code
)
670 self
.message
= stem
.util
.str_tools
._to
_unicode
(msg
)