2018-12-15 00:08:54 +00:00
# Copyright 2011-2018, Damian Johnson and The Tor Project
2015-11-23 21:13:53 +00:00
# See LICENSE for licensing information
"""
Module for interacting with the Tor control socket . The
: class : ` ~ stem . control . Controller ` is a wrapper around a
: class : ` ~ stem . socket . ControlSocket ` , retaining many of its methods ( connect ,
close , is_alive , etc ) in addition to providing its own for working with the
socket at a higher level .
Stem has ` several ways < . . / faq . html #how-do-i-connect-to-tor>`_ of getting a
: class : ` ~ stem . control . Controller ` , but the most flexible are
: func : ` ~ stem . control . Controller . from_port ` and
: func : ` ~ stem . control . Controller . from_socket_file ` . These static
: class : ` ~ stem . control . Controller ` methods give you an * * unauthenticated * *
Controller you can then authenticate yourself using its
: func : ` ~ stem . control . Controller . authenticate ` method . For example . . .
: :
import getpass
import sys
import stem
import stem . connection
from stem . control import Controller
if __name__ == ' __main__ ' :
try :
controller = Controller . from_port ( )
except stem . SocketError as exc :
print ( " Unable to connect to tor on port 9051: %s " % exc )
sys . exit ( 1 )
try :
controller . authenticate ( )
except stem . connection . MissingPassword :
pw = getpass . getpass ( " Controller password: " )
try :
controller . authenticate ( password = pw )
except stem . connection . PasswordAuthFailed :
print ( " Unable to authenticate, password is incorrect " )
sys . exit ( 1 )
except stem . connection . AuthenticationFailure as exc :
print ( " Unable to authenticate: %s " % exc )
sys . exit ( 1 )
print ( " Tor is running version %s " % controller . get_version ( ) )
controller . close ( )
If you ' re fine with allowing your script to raise exceptions then this can be more nicely done as...
: :
from stem . control import Controller
if __name__ == ' __main__ ' :
with Controller . from_port ( ) as controller :
controller . authenticate ( )
print ( " Tor is running version %s " % controller . get_version ( ) )
* * Module Overview : * *
: :
2018-12-15 00:08:54 +00:00
event_description - brief description of a tor event type
2015-11-23 21:13:53 +00:00
Controller - General controller class intended for direct use
| | - from_port - Provides a Controller based on a port connection .
| + - from_socket_file - Provides a Controller based on a socket file connection .
|
| - authenticate - authenticates this controller with tor
2018-12-15 00:08:54 +00:00
| - reconnect - reconnects and authenticates to socket
2015-11-23 21:13:53 +00:00
|
| - get_info - issues a GETINFO query for a parameter
| - get_version - provides our tor version
| - get_exit_policy - provides our exit policy
| - get_ports - provides the local ports where tor is listening for connections
| - get_listeners - provides the addresses and ports where tor is listening for connections
| - get_accounting_stats - provides stats related to relaying limits
| - get_protocolinfo - information about the controller interface
| - get_user - provides the user tor is running as
| - get_pid - provides the pid of our tor process
2018-12-15 00:08:54 +00:00
| - is_user_traffic_allowed - checks if we send or receive direct user traffic
2015-11-23 21:13:53 +00:00
|
| - get_microdescriptor - querying the microdescriptor for a relay
| - get_microdescriptors - provides all currently available microdescriptors
| - get_server_descriptor - querying the server descriptor for a relay
| - get_server_descriptors - provides all currently available server descriptors
| - get_network_status - querying the router status entry for a relay
2018-12-15 00:08:54 +00:00
| - get_network_statuses - provides all presently available router status entries
2015-11-23 21:13:53 +00:00
| - get_hidden_service_descriptor - queries the given hidden service descriptor
|
| - get_conf - gets the value of a configuration option
| - get_conf_map - gets the values of multiple configuration options
2018-12-15 00:08:54 +00:00
| - is_set - determines if an option differs from its default
2015-11-23 21:13:53 +00:00
| - set_conf - sets the value of a configuration option
| - reset_conf - reverts configuration options to their default values
| - set_options - sets or resets the values of multiple configuration options
|
| - get_hidden_service_conf - provides our hidden service configuration
| - set_hidden_service_conf - sets our hidden service configuration
| - create_hidden_service - creates a new hidden service or adds a new port
| - remove_hidden_service - removes a hidden service or drops a port
|
| - list_ephemeral_hidden_services - list ephemeral hidden serivces
| - create_ephemeral_hidden_service - create a new ephemeral hidden service
| - remove_ephemeral_hidden_service - removes an ephemeral hidden service
|
| - add_event_listener - attaches an event listener to be notified of tor events
| - remove_event_listener - removes a listener so it isn ' t notified of further events
|
| - is_caching_enabled - true if the controller has enabled caching
| - set_caching - enables or disables caching
| - clear_cache - clears any cached results
|
| - load_conf - loads configuration information as if it was in the torrc
| - save_conf - saves configuration information to the torrc
|
| - is_feature_enabled - checks if a given controller feature is enabled
| - enable_feature - enables a controller feature that has been disabled by default
|
| - get_circuit - provides an active circuit
| - get_circuits - provides a list of active circuits
| - new_circuit - create new circuits
| - extend_circuit - create new circuits and extend existing ones
| - repurpose_circuit - change a circuit ' s purpose
| - close_circuit - close a circuit
|
| - get_streams - provides a list of active streams
| - attach_stream - attach a stream to a circuit
| - close_stream - close a stream
|
| - signal - sends a signal to the tor client
| - is_newnym_available - true if tor would currently accept a NEWNYM signal
| - get_newnym_wait - seconds until tor would accept a NEWNYM signal
| - get_effective_rate - provides our effective relaying rate limit
| - is_geoip_unavailable - true if we ' ve discovered our geoip db to be unavailable
| - map_address - maps one address to another such that connections to the original are replaced with the other
+ - drop_guards - drops our set of guard relays and picks a new set
BaseController - Base controller class asynchronous message handling
| - msg - communicates with the tor process
| - is_alive - reports if our connection to tor is open or closed
| - is_localhost - returns if the connection is for the local system or not
| - connection_time - time when we last connected or disconnected
| - is_authenticated - checks if we ' re authenticated to tor
| - connect - connects or reconnects to tor
| - close - shuts down our connection to the tor process
| - get_socket - provides the socket used for control communication
| - get_latest_heartbeat - timestamp for when we last heard from tor
| - add_status_listener - notifies a callback of changes in our status
2018-12-15 00:08:54 +00:00
+ - remove_status_listener - prevents further notification of status changes
2015-11-23 21:13:53 +00:00
. . data : : State ( enum )
Enumeration for states that a controller can have .
== == == == == == == == == == =
State Description
== == == == == == == == == == =
* * INIT * * new control connection
* * RESET * * received a reset / sighup signal
* * CLOSED * * control connection closed
== == == == == == == == == == =
. . data : : EventType ( enum )
Known types of events that the
: func : ` ~ stem . control . Controller . add_event_listener ` method of the
: class : ` ~ stem . control . Controller ` can listen for .
The most frequently listened for event types tend to be the logging events
( * * DEBUG * * , * * INFO * * , * * NOTICE * * , * * WARN * * , and * * ERR * * ) , bandwidth usage
( * * BW * * ) , and circuit or stream changes ( * * CIRC * * and * * STREAM * * ) .
Enums are mapped to : class : ` ~ stem . response . events . Event ` subclasses as
follows . . .
2018-12-15 00:08:54 +00:00
. . deprecated : : 1.6 .0
Tor dropped EventType . AUTHDIR_NEWDESCS as of version 0.3 .2 .1 .
( : spec : ` 6e887 ba ` )
2015-11-23 21:13:53 +00:00
== == == == == == == == == == == = == == == == == =
EventType Event Class
== == == == == == == == == == == = == == == == == =
* * ADDRMAP * * : class : ` stem . response . events . AddrMapEvent `
* * AUTHDIR_NEWDESCS * * : class : ` stem . response . events . AuthDirNewDescEvent `
* * BUILDTIMEOUT_SET * * : class : ` stem . response . events . BuildTimeoutSetEvent `
* * BW * * : class : ` stem . response . events . BandwidthEvent `
* * CELL_STATS * * : class : ` stem . response . events . CellStatsEvent `
* * CIRC * * : class : ` stem . response . events . CircuitEvent `
* * CIRC_BW * * : class : ` stem . response . events . CircuitBandwidthEvent `
* * CIRC_MINOR * * : class : ` stem . response . events . CircMinorEvent `
* * CLIENTS_SEEN * * : class : ` stem . response . events . ClientsSeenEvent `
* * CONF_CHANGED * * : class : ` stem . response . events . ConfChangedEvent `
* * CONN_BW * * : class : ` stem . response . events . ConnectionBandwidthEvent `
* * DEBUG * * : class : ` stem . response . events . LogEvent `
* * DESCCHANGED * * : class : ` stem . response . events . DescChangedEvent `
* * ERR * * : class : ` stem . response . events . LogEvent `
* * GUARD * * : class : ` stem . response . events . GuardEvent `
* * HS_DESC * * : class : ` stem . response . events . HSDescEvent `
* * HS_DESC_CONTENT * * : class : ` stem . response . events . HSDescContentEvent `
* * INFO * * : class : ` stem . response . events . LogEvent `
2018-12-15 00:08:54 +00:00
* * NETWORK_LIVENESS * * : class : ` stem . response . events . NetworkLivenessEvent `
2015-11-23 21:13:53 +00:00
* * NEWCONSENSUS * * : class : ` stem . response . events . NewConsensusEvent `
* * NEWDESC * * : class : ` stem . response . events . NewDescEvent `
* * NOTICE * * : class : ` stem . response . events . LogEvent `
* * NS * * : class : ` stem . response . events . NetworkStatusEvent `
* * ORCONN * * : class : ` stem . response . events . ORConnEvent `
* * SIGNAL * * : class : ` stem . response . events . SignalEvent `
* * STATUS_CLIENT * * : class : ` stem . response . events . StatusEvent `
* * STATUS_GENERAL * * : class : ` stem . response . events . StatusEvent `
* * STATUS_SERVER * * : class : ` stem . response . events . StatusEvent `
* * STREAM * * : class : ` stem . response . events . StreamEvent `
* * STREAM_BW * * : class : ` stem . response . events . StreamBwEvent `
* * TB_EMPTY * * : class : ` stem . response . events . TokenBucketEmptyEvent `
* * TRANSPORT_LAUNCHED * * : class : ` stem . response . events . TransportLaunchedEvent `
* * WARN * * : class : ` stem . response . events . LogEvent `
== == == == == == == == == == == = == == == == == =
. . data : : Listener ( enum )
Purposes for inbound connections that Tor handles .
== == == == == == = == == == == == =
Listener Description
== == == == == == = == == == == == =
* * OR * * traffic we ' re relaying as a member of the network (torrc ' s * * ORPort * * and * * ORListenAddress * * )
* * DIR * * mirroring for tor descriptor content ( torrc ' s **DirPort** and **DirListenAddress**)
* * SOCKS * * client traffic we ' re sending over Tor (torrc ' s * * SocksPort * * and * * SocksListenAddress * * )
* * TRANS * * transparent proxy handling ( torrc ' s **TransPort** and **TransListenAddress**)
* * NATD * * forwarding for ipfw NATD connections ( torrc ' s **NatdPort** and **NatdListenAddress**)
* * DNS * * DNS lookups for our traffic ( torrc ' s **DNSPort** and **DNSListenAddress**)
* * CONTROL * * controller applications ( torrc ' s **ControlPort** and **ControlListenAddress**)
== == == == == == = == == == == == =
"""
import calendar
import collections
import functools
import inspect
import io
import os
import threading
import time
try :
# Added in 2.7
from collections import OrderedDict
except ImportError :
from stem . util . ordereddict import OrderedDict
try :
2018-12-15 00:08:54 +00:00
# Added in 3.x
2015-11-23 21:13:53 +00:00
import queue
except ImportError :
import Queue as queue
import stem . descriptor . microdescriptor
import stem . descriptor . reader
import stem . descriptor . router_status_entry
import stem . descriptor . server_descriptor
import stem . exit_policy
import stem . response
import stem . response . events
import stem . socket
2018-12-15 00:08:54 +00:00
import stem . util
import stem . util . conf
2015-11-23 21:13:53 +00:00
import stem . util . connection
import stem . util . enum
import stem . util . str_tools
import stem . util . system
import stem . util . tor_tools
import stem . version
2018-12-15 00:08:54 +00:00
from stem import UNDEFINED , CircStatus , Signal
2015-11-23 21:13:53 +00:00
from stem . util import log
2018-12-15 00:08:54 +00:00
# When closing the controller we attempt to finish processing enqueued events,
# but if it takes longer than this we terminate.
EVENTS_LISTENING_TIMEOUT = 0.1
MALFORMED_EVENTS = ' MALFORMED_EVENTS '
2015-11-23 21:13:53 +00:00
# state changes a control socket can have
State = stem . util . enum . Enum ( ' INIT ' , ' RESET ' , ' CLOSED ' )
EventType = stem . util . enum . UppercaseEnum (
' ADDRMAP ' ,
' AUTHDIR_NEWDESCS ' ,
' BUILDTIMEOUT_SET ' ,
' BW ' ,
' CELL_STATS ' ,
' CIRC ' ,
' CIRC_BW ' ,
' CIRC_MINOR ' ,
' CONF_CHANGED ' ,
' CONN_BW ' ,
' CLIENTS_SEEN ' ,
' DEBUG ' ,
' DESCCHANGED ' ,
' ERR ' ,
' GUARD ' ,
' HS_DESC ' ,
' HS_DESC_CONTENT ' ,
' INFO ' ,
2018-12-15 00:08:54 +00:00
' NETWORK_LIVENESS ' ,
2015-11-23 21:13:53 +00:00
' NEWCONSENSUS ' ,
' NEWDESC ' ,
' NOTICE ' ,
' NS ' ,
' ORCONN ' ,
' SIGNAL ' ,
' STATUS_CLIENT ' ,
' STATUS_GENERAL ' ,
' STATUS_SERVER ' ,
' STREAM ' ,
' STREAM_BW ' ,
' TB_EMPTY ' ,
' TRANSPORT_LAUNCHED ' ,
' WARN ' ,
)
Listener = stem . util . enum . UppercaseEnum (
' OR ' ,
' DIR ' ,
' SOCKS ' ,
' TRANS ' ,
' NATD ' ,
' DNS ' ,
' CONTROL ' ,
)
2018-12-15 00:08:54 +00:00
# torrc options that cannot be changed once tor's running
IMMUTABLE_CONFIG_OPTIONS = set ( map ( stem . util . str_tools . _to_unicode , map ( str . lower , (
' AccelDir ' ,
' AccelName ' ,
' DataDirectory ' ,
' DisableAllSwap ' ,
' DisableDebuggerAttachment ' ,
' HardwareAccel ' ,
' HiddenServiceNonAnonymousMode ' ,
' HiddenServiceSingleHopMode ' ,
' KeepBindCapabilities ' ,
' PidFile ' ,
' RunAsDaemon ' ,
' Sandbox ' ,
' SyslogIdentityTag ' ,
' TokenBucketRefillInterval ' ,
' User ' ,
) ) ) )
LOG_CACHE_FETCHES = True # provide trace level logging for cache hits
2015-11-23 21:13:53 +00:00
# Configuration options that are fetched by a special key. The keys are
# lowercase to make case insensitive lookups easier.
MAPPED_CONFIG_KEYS = {
' hiddenservicedir ' : ' HiddenServiceOptions ' ,
' hiddenserviceport ' : ' HiddenServiceOptions ' ,
' hiddenserviceversion ' : ' HiddenServiceOptions ' ,
' hiddenserviceauthorizeclient ' : ' HiddenServiceOptions ' ,
' hiddenserviceoptions ' : ' HiddenServiceOptions ' ,
}
# unchangeable GETINFO parameters
CACHEABLE_GETINFO_PARAMS = (
2018-12-15 00:08:54 +00:00
' address ' ,
2015-11-23 21:13:53 +00:00
' version ' ,
' config-file ' ,
' exit-policy/default ' ,
' fingerprint ' ,
' config/names ' ,
' config/defaults ' ,
' info/names ' ,
' events/names ' ,
' features/names ' ,
' process/descriptor-limit ' ,
2018-12-15 00:08:54 +00:00
' status/version/current ' ,
)
CACHEABLE_GETINFO_PARAMS_UNTIL_SETCONF = (
' accounting/enabled ' ,
2015-11-23 21:13:53 +00:00
)
# GETCONF parameters we shouldn't cache. This includes hidden service
# perameters due to the funky way they're set and retrieved (for instance,
# 'SETCONF HiddenServiceDir' effects 'GETCONF HiddenServiceOptions').
UNCACHEABLE_GETCONF_PARAMS = (
' hiddenserviceoptions ' ,
' hiddenservicedir ' ,
' hiddenserviceport ' ,
' hiddenserviceversion ' ,
' hiddenserviceauthorizeclient ' ,
)
SERVER_DESCRIPTORS_UNSUPPORTED = " Tor is currently not configured to retrieve \
server descriptors . As of Tor version 0.2 .3 .25 it downloads microdescriptors \
instead unless you set ' UseMicrodescriptors 0 ' in your torrc . "
2018-12-15 00:08:54 +00:00
EVENT_DESCRIPTIONS = None
class AccountingStats ( collections . namedtuple ( ' AccountingStats ' , [ ' retrieved ' , ' status ' , ' interval_end ' , ' time_until_reset ' , ' read_bytes ' , ' read_bytes_left ' , ' read_limit ' , ' written_bytes ' , ' write_bytes_left ' , ' write_limit ' ] ) ) :
"""
Accounting information , determining the limits where our relay suspends
itself .
: var float retrieved : unix timestamp for when this was fetched
: var str status : hibernation status of ' awake ' , ' soft ' , or ' hard '
: var datetime interval_end : time when our limits reset
: var int time_until_reset : seconds until our limits reset
: var int read_bytes : number of bytes we ' ve read relaying
: var int read_bytes_left : number of bytes we can read until we suspend
: var int read_limit : reading threshold where we suspend
: var int written_bytes : number of bytes we ' ve written relaying
: var int write_bytes_left : number of bytes we can write until we suspend
: var int write_limit : writing threshold where we suspend
"""
class UserTrafficAllowed ( collections . namedtuple ( ' UserTrafficAllowed ' , [ ' inbound ' , ' outbound ' ] ) ) :
"""
Indicates if we ' re likely to be servicing direct user traffic or not.
: var bool inbound : if * * True * * we ' re likely providing guard or bridge connnections
: var bool outbound : if * * True * * we ' re likely providng exit connections
"""
class CreateHiddenServiceOutput ( collections . namedtuple ( ' CreateHiddenServiceOutput ' , [ ' path ' , ' hostname ' , ' hostname_for_client ' , ' config ' ] ) ) :
"""
Attributes of a hidden service we ' ve created.
Both the * * hostnames * * and * * hostname_for_client * * attributes can only be
provided if we ' re able to read the hidden service directory. If the method
was called with * * client_names * * then we may provide the
* * hostname_for_client * * , and otherwise can provide the * * hostnames * * .
: var str path : hidden service directory
: var str hostname : content of the hostname file if available
: var dict hostname_for_client : mapping of client names to their onion address
if available
: var dict config : tor ' s new hidden service configuration
"""
2015-11-23 21:13:53 +00:00
def with_default ( yields = False ) :
"""
Provides a decorator to support having a default value . This should be
treated as private .
"""
def decorator ( func ) :
def get_default ( func , args , kwargs ) :
arg_names = inspect . getargspec ( func ) . args [ 1 : ] # drop 'self'
default_position = arg_names . index ( ' default ' ) if ' default ' in arg_names else None
if default_position is not None and default_position < len ( args ) :
return args [ default_position ]
else :
return kwargs . get ( ' default ' , UNDEFINED )
if not yields :
@functools.wraps ( func )
def wrapped ( self , * args , * * kwargs ) :
try :
return func ( self , * args , * * kwargs )
2018-12-15 00:08:54 +00:00
except :
2015-11-23 21:13:53 +00:00
default = get_default ( func , args , kwargs )
if default == UNDEFINED :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
else :
return default
else :
@functools.wraps ( func )
def wrapped ( self , * args , * * kwargs ) :
try :
for val in func ( self , * args , * * kwargs ) :
yield val
2018-12-15 00:08:54 +00:00
except :
2015-11-23 21:13:53 +00:00
default = get_default ( func , args , kwargs )
if default == UNDEFINED :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
else :
if default is not None :
for val in default :
yield val
return wrapped
return decorator
2018-12-15 00:08:54 +00:00
def event_description ( event ) :
"""
Provides a description for Tor events .
: param str event : the event for which a description is needed
: returns : * * str * * The event description or * * None * * if this is an event name
we don ' t have a description for
"""
global EVENT_DESCRIPTIONS
if EVENT_DESCRIPTIONS is None :
config = stem . util . conf . Config ( )
config_path = os . path . join ( os . path . dirname ( __file__ ) , ' settings.cfg ' )
try :
config . load ( config_path )
EVENT_DESCRIPTIONS = dict ( [ ( key . lower ( ) [ 18 : ] , config . get_value ( key ) ) for key in config . keys ( ) if key . startswith ( ' event.description. ' ) ] )
except Exception as exc :
log . warn ( " BUG: stem failed to load its internal manual information from ' %s ' : %s " % ( config_path , exc ) )
return None
return EVENT_DESCRIPTIONS . get ( event . lower ( ) )
2015-11-23 21:13:53 +00:00
class BaseController ( object ) :
"""
Controller for the tor process . This is a minimal base class for other
controllers , providing basic process communication and event listing . Don ' t
use this directly - subclasses like the : class : ` ~ stem . control . Controller `
provide higher level functionality .
It ' s highly suggested that you don ' t interact directly with the
: class : ` ~ stem . socket . ControlSocket ` that we ' re constructed from - use our
wrapper methods instead .
If the * * control_socket * * is already authenticated to Tor then the caller
should provide the * * is_authenticated * * flag . Otherwise , we will treat the
socket as though it hasn ' t yet been authenticated.
"""
def __init__ ( self , control_socket , is_authenticated = False ) :
self . _socket = control_socket
self . _msg_lock = threading . RLock ( )
self . _status_listeners = [ ] # tuples of the form (callback, spawn_thread)
self . _status_listeners_lock = threading . RLock ( )
# queues where incoming messages are directed
self . _reply_queue = queue . Queue ( )
self . _event_queue = queue . Queue ( )
# thread to continually pull from the control socket
self . _reader_thread = None
# thread to pull from the _event_queue and call handle_event
self . _event_notice = threading . Event ( )
self . _event_thread = None
# saves our socket's prior _connect() and _close() methods so they can be
# called along with ours
self . _socket_connect = self . _socket . _connect
self . _socket_close = self . _socket . _close
self . _socket . _connect = self . _connect
self . _socket . _close = self . _close
self . _last_heartbeat = 0.0 # timestamp for when we last heard from tor
self . _is_authenticated = False
self . _state_change_threads = [ ] # threads we've spawned to notify of state changes
if self . _socket . is_alive ( ) :
self . _launch_threads ( )
if is_authenticated :
self . _post_authentication ( )
def msg ( self , message ) :
"""
Sends a message to our control socket and provides back its reply .
: param str message : message to be formatted and sent to tor
: returns : : class : ` ~ stem . response . ControlMessage ` with the response
: raises :
* : class : ` stem . ProtocolError ` the content from the socket is
malformed
* : class : ` stem . SocketError ` if a problem arises in using the
socket
* : class : ` stem . SocketClosed ` if the socket is shut down
"""
with self . _msg_lock :
# If our _reply_queue isn't empty then one of a few things happened...
#
# - Our connection was closed and probably re-restablished. This was
# in reply to pulling for an asynchronous event and getting this is
# expected - ignore it.
#
# - Pulling for asynchronous events produced an error. If this was a
# ProtocolError then it's a tor bug, and if a non-closure SocketError
# then it was probably a socket glitch. Deserves an INFO level log
# message.
#
# - This is a leftover response for a msg() call. We can't tell who an
# exception was earmarked for, so we only know that this was the case
# if it's a ControlMessage.
#
# This is the most concerning situation since it indicates that one of
# our callers didn't get their reply. However, this is still a
# perfectly viable use case. For instance...
#
# 1. We send a request.
# 2. The reader thread encounters an exception, for instance a socket
# error. We enqueue the exception.
# 3. The reader thread receives the reply.
# 4. We raise the socket error, and have an undelivered message.
#
# Thankfully this only seems to arise in edge cases around rapidly
# closing/reconnecting the socket.
while not self . _reply_queue . empty ( ) :
try :
response = self . _reply_queue . get_nowait ( )
if isinstance ( response , stem . SocketClosed ) :
pass # this is fine
elif isinstance ( response , stem . ProtocolError ) :
log . info ( ' Tor provided a malformed message ( %s ) ' % response )
elif isinstance ( response , stem . ControllerError ) :
log . info ( ' Socket experienced a problem ( %s ) ' % response )
elif isinstance ( response , stem . response . ControlMessage ) :
log . info ( ' Failed to deliver a response: %s ' % response )
except queue . Empty :
# the empty() method is documented to not be fully reliable so this
# isn't entirely surprising
break
try :
self . _socket . send ( message )
response = self . _reply_queue . get ( )
# If the message we received back had an exception then re-raise it to the
# caller. Otherwise return the response.
if isinstance ( response , stem . ControllerError ) :
raise response
else :
return response
2018-12-15 00:08:54 +00:00
except stem . SocketClosed :
2015-11-23 21:13:53 +00:00
# If the recv() thread caused the SocketClosed then we could still be
# in the process of closing. Calling close() here so that we can
# provide an assurance to the caller that when we raise a SocketClosed
# exception we are shut down afterward for realz.
self . close ( )
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
def is_alive ( self ) :
"""
Checks if our socket is currently connected . This is a pass - through for our
2018-12-15 00:08:54 +00:00
socket ' s :func:`~stem.socket.BaseSocket.is_alive` method.
2015-11-23 21:13:53 +00:00
: returns : * * bool * * that ' s **True** if our socket is connected and **False** otherwise
"""
return self . _socket . is_alive ( )
def is_localhost ( self ) :
"""
Returns if the connection is for the local system or not .
. . versionadded : : 1.3 .0
: returns : * * bool * * that ' s **True** if the connection is for the local host and **False** otherwise
"""
return self . _socket . is_localhost ( )
def connection_time ( self ) :
"""
Provides the unix timestamp for when our socket was either connected or
disconnected . That is to say , the time we connected if we ' re currently
connected and the time we disconnected if we ' re not connected.
. . versionadded : : 1.3 .0
: returns : * * float * * for when we last connected or disconnected , zero if
we ' ve never connected
"""
return self . _socket . connection_time ( )
def is_authenticated ( self ) :
"""
Checks if our socket is both connected and authenticated .
: returns : * * bool * * that ' s **True** if our socket is authenticated to tor
and * * False * * otherwise
"""
2018-12-15 00:08:54 +00:00
return self . _is_authenticated if self . is_alive ( ) else False
2015-11-23 21:13:53 +00:00
def connect ( self ) :
"""
Reconnects our control socket . This is a pass - through for our socket ' s
: func : ` ~ stem . socket . ControlSocket . connect ` method .
: raises : : class : ` stem . SocketError ` if unable to make a socket
"""
self . _socket . connect ( )
def close ( self ) :
"""
Closes our socket connection . This is a pass - through for our socket ' s
2018-12-15 00:08:54 +00:00
: func : ` ~ stem . socket . BaseSocket . close ` method .
2015-11-23 21:13:53 +00:00
"""
self . _socket . close ( )
# Join on any outstanding state change listeners. Closing is a state change
# of its own, so if we have any listeners it's quite likely there's some
# work in progress.
#
# It's important that we do this outside of our locks so those daemons have
# access to us. This is why we're doing this here rather than _close().
for t in self . _state_change_threads :
if t . is_alive ( ) and threading . current_thread ( ) != t :
t . join ( )
def get_socket ( self ) :
"""
Provides the socket used to speak with the tor process . Communicating with
the socket directly isn ' t advised since it may confuse this controller.
: returns : : class : ` ~ stem . socket . ControlSocket ` we ' re communicating with
"""
return self . _socket
def get_latest_heartbeat ( self ) :
"""
Provides the unix timestamp for when we last heard from tor . This is zero
if we ' ve never received a message.
: returns : float for the unix timestamp of when we last heard from tor
"""
return self . _last_heartbeat
def add_status_listener ( self , callback , spawn = True ) :
"""
Notifies a given function when the state of our socket changes . Functions
are expected to be of the form . . .
: :
my_function ( controller , state , timestamp )
The state is a value from the : data : ` stem . control . State ` enum . Functions
* * must * * allow for new values . The timestamp is a float for the unix time
when the change occurred .
This class only provides * * State . INIT * * and * * State . CLOSED * * notifications .
Subclasses may provide others .
If spawn is * * True * * then the callback is notified via a new daemon thread .
If * * False * * then the notice is under our locks , within the thread where
the change occurred . In general this isn ' t advised, especially if your
callback could block for a while . If still outstanding these threads are
joined on as part of closing this controller .
: param function callback : function to be notified when our state changes
: param bool spawn : calls function via a new thread if * * True * * , otherwise
it ' s part of the connect/close method call
"""
with self . _status_listeners_lock :
self . _status_listeners . append ( ( callback , spawn ) )
def remove_status_listener ( self , callback ) :
"""
Stops listener from being notified of further events .
: param function callback : function to be removed from our listeners
: returns : * * bool * * that ' s **True** if we removed one or more occurrences of
the callback , * * False * * otherwise
"""
with self . _status_listeners_lock :
new_listeners , is_changed = [ ] , False
for listener , spawn in self . _status_listeners :
if listener != callback :
new_listeners . append ( ( listener , spawn ) )
else :
is_changed = True
self . _status_listeners = new_listeners
return is_changed
def __enter__ ( self ) :
return self
def __exit__ ( self , exit_type , value , traceback ) :
self . close ( )
def _handle_event ( self , event_message ) :
"""
Callback to be overwritten by subclasses for event listening . This is
notified whenever we receive an event from the control socket .
: param stem . response . ControlMessage event_message : message received from
the control socket
"""
pass
def _connect ( self ) :
self . _launch_threads ( )
self . _notify_status_listeners ( State . INIT )
self . _socket_connect ( )
self . _is_authenticated = False
def _close ( self ) :
# Our is_alive() state is now false. Our reader thread should already be
# awake from recv() raising a closure exception. Wake up the event thread
# too so it can end.
self . _event_notice . set ( )
self . _is_authenticated = False
# joins on our threads if it's safe to do so
for t in ( self . _reader_thread , self . _event_thread ) :
if t and t . is_alive ( ) and threading . current_thread ( ) != t :
t . join ( )
self . _notify_status_listeners ( State . CLOSED )
self . _socket_close ( )
def _post_authentication ( self ) :
# actions to be taken after we have a newly authenticated connection
self . _is_authenticated = True
def _notify_status_listeners ( self , state ) :
"""
Informs our status listeners that a state change occurred .
: param stem . control . State state : state change that has occurred
"""
# Any changes to our is_alive() state happen under the send lock, so we
# need to have it to ensure it doesn't change beneath us.
with self . _socket . _get_send_lock ( ) :
with self . _status_listeners_lock :
# States imply that our socket is either alive or not, which may not
# hold true when multiple events occur in quick succession. For
# instance, a sighup could cause two events (State.RESET for the sighup
# and State.CLOSE if it causes tor to crash). However, there's no
# guarantee of the order in which they occur, and it would be bad if
# listeners got the State.RESET last, implying that we were alive.
expect_alive = None
if state in ( State . INIT , State . RESET ) :
expect_alive = True
elif state == State . CLOSED :
expect_alive = False
change_timestamp = time . time ( )
if expect_alive is not None and expect_alive != self . is_alive ( ) :
return
self . _state_change_threads = list ( filter ( lambda t : t . is_alive ( ) , self . _state_change_threads ) )
for listener , spawn in self . _status_listeners :
if spawn :
args = ( self , state , change_timestamp )
2018-12-15 00:08:54 +00:00
notice_thread = threading . Thread ( target = listener , args = args , name = ' %s notification ' % state )
2015-11-23 21:13:53 +00:00
notice_thread . setDaemon ( True )
notice_thread . start ( )
self . _state_change_threads . append ( notice_thread )
else :
listener ( self , state , change_timestamp )
def _launch_threads ( self ) :
"""
Initializes daemon threads . Threads can ' t be reused so we need to recreate
them if we ' re restarted.
"""
# In theory concurrent calls could result in multiple start() calls on a
# single thread, which would cause an unexpected exception. Best be safe.
with self . _socket . _get_send_lock ( ) :
if not self . _reader_thread or not self . _reader_thread . is_alive ( ) :
2018-12-15 00:08:54 +00:00
self . _reader_thread = threading . Thread ( target = self . _reader_loop , name = ' Tor listener ' )
2015-11-23 21:13:53 +00:00
self . _reader_thread . setDaemon ( True )
self . _reader_thread . start ( )
if not self . _event_thread or not self . _event_thread . is_alive ( ) :
2018-12-15 00:08:54 +00:00
self . _event_thread = threading . Thread ( target = self . _event_loop , name = ' Event notifier ' )
2015-11-23 21:13:53 +00:00
self . _event_thread . setDaemon ( True )
self . _event_thread . start ( )
def _reader_loop ( self ) :
"""
Continually pulls from the control socket , directing the messages into
queues based on their type . Controller messages come in two varieties . . .
* Responses to messages we ' ve sent (GETINFO, SETCONF, etc).
* Asynchronous events , identified by a status code of 650.
"""
while self . is_alive ( ) :
try :
control_message = self . _socket . recv ( )
self . _last_heartbeat = time . time ( )
if control_message . content ( ) [ - 1 ] [ 0 ] == ' 650 ' :
# asynchronous message, adds to the event queue and wakes up its handler
self . _event_queue . put ( control_message )
self . _event_notice . set ( )
else :
# response to a msg() call
self . _reply_queue . put ( control_message )
except stem . ControllerError as exc :
# Assume that all exceptions belong to the reader. This isn't always
# true, but the msg() call can do a better job of sorting it out.
#
# Be aware that the msg() method relies on this to unblock callers.
self . _reply_queue . put ( exc )
def _event_loop ( self ) :
"""
Continually pulls messages from the _event_queue and sends them to our
handle_event callback . This is done via its own thread so subclasses with a
lengthy handle_event implementation don ' t block further reading from the
socket .
"""
2018-12-15 00:08:54 +00:00
socket_closed_at = None
2015-11-23 21:13:53 +00:00
while True :
try :
event_message = self . _event_queue . get_nowait ( )
self . _handle_event ( event_message )
2018-12-15 00:08:54 +00:00
# Attempt to finish processing enqueued events when our controller closes
if not self . is_alive ( ) :
if not socket_closed_at :
socket_closed_at = time . time ( )
elif time . time ( ) - socket_closed_at > EVENTS_LISTENING_TIMEOUT :
break
2015-11-23 21:13:53 +00:00
except queue . Empty :
if not self . is_alive ( ) :
break
self . _event_notice . wait ( )
self . _event_notice . clear ( )
class Controller ( BaseController ) :
"""
2018-12-15 00:08:54 +00:00
Connection with Tor ' s control socket. This is built on top of the
2015-11-23 21:13:53 +00:00
BaseController and provides a more user friendly API for library users .
"""
@staticmethod
2018-12-15 00:08:54 +00:00
def from_port ( address = ' 127.0.0.1 ' , port = ' default ' ) :
2015-11-23 21:13:53 +00:00
"""
Constructs a : class : ` ~ stem . socket . ControlPort ` based Controller .
2018-12-15 00:08:54 +00:00
If the * * port * * is * * ' default ' * * then this checks on both 9051 ( default
for relays ) and 9151 ( default for the Tor Browser ) . This default may change
in the future .
. . versionchanged : : 1.5 .0
Use both port 9051 and 9151 by default .
2015-11-23 21:13:53 +00:00
: param str address : ip address of the controller
: param int port : port number of the controller
: returns : : class : ` ~ stem . control . Controller ` attached to the given port
: raises : : class : ` stem . SocketError ` if we ' re unable to establish a connection
"""
2018-12-15 00:08:54 +00:00
import stem . connection
2015-11-23 21:13:53 +00:00
if not stem . util . connection . is_valid_ipv4_address ( address ) :
raise ValueError ( ' Invalid IP address: %s ' % address )
2018-12-15 00:08:54 +00:00
elif port != ' default ' and not stem . util . connection . is_valid_port ( port ) :
2015-11-23 21:13:53 +00:00
raise ValueError ( ' Invalid port: %s ' % port )
2018-12-15 00:08:54 +00:00
if port == ' default ' :
control_port = stem . connection . _connection_for_default_port ( address )
else :
control_port = stem . socket . ControlPort ( address , port )
2015-11-23 21:13:53 +00:00
return Controller ( control_port )
@staticmethod
def from_socket_file ( path = ' /var/run/tor/control ' ) :
"""
Constructs a : class : ` ~ stem . socket . ControlSocketFile ` based Controller .
: param str path : path where the control socket is located
: returns : : class : ` ~ stem . control . Controller ` attached to the given socket file
: raises : : class : ` stem . SocketError ` if we ' re unable to establish a connection
"""
control_socket = stem . socket . ControlSocketFile ( path )
return Controller ( control_socket )
def __init__ ( self , control_socket , is_authenticated = False ) :
self . _is_caching_enabled = True
self . _request_cache = { }
self . _last_newnym = 0.0
self . _cache_lock = threading . RLock ( )
# mapping of event types to their listeners
self . _event_listeners = { }
self . _event_listeners_lock = threading . RLock ( )
self . _enabled_features = [ ]
2018-12-15 00:08:54 +00:00
self . _is_geoip_unavailable = None
self . _last_address_exc = None
self . _last_fingerprint_exc = None
2015-11-23 21:13:53 +00:00
super ( Controller , self ) . __init__ ( control_socket , is_authenticated )
def _sighup_listener ( event ) :
if event . signal == Signal . RELOAD :
self . clear_cache ( )
self . _notify_status_listeners ( State . RESET )
self . add_event_listener ( _sighup_listener , EventType . SIGNAL )
def _confchanged_listener ( event ) :
if self . is_caching_enabled ( ) :
2018-12-15 00:08:54 +00:00
to_cache_changed = dict ( ( k . lower ( ) , v ) for k , v in event . changed . items ( ) )
to_cache_unset = dict ( ( k . lower ( ) , [ ] ) for k in event . unset ) # [] represents None value in cache
to_cache = { }
to_cache . update ( to_cache_changed )
to_cache . update ( to_cache_unset )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
self . _set_cache ( to_cache , ' getconf ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
self . _confchanged_cache_invalidation ( to_cache )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
self . add_event_listener ( _confchanged_listener , EventType . CONF_CHANGED )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
def _address_changed_listener ( event ) :
if event . action in ( ' EXTERNAL_ADDRESS ' , ' DNS_USELESS ' ) :
self . _set_cache ( { ' exit_policy ' : None } )
self . _set_cache ( { ' address ' : None } , ' getinfo ' )
self . _last_address_exc = None
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
self . add_event_listener ( _address_changed_listener , EventType . STATUS_SERVER )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
def close ( self ) :
self . clear_cache ( )
2015-11-23 21:13:53 +00:00
super ( Controller , self ) . close ( )
def authenticate ( self , * args , * * kwargs ) :
"""
A convenience method to authenticate the controller . This is just a
pass - through to : func : ` stem . connection . authenticate ` .
"""
import stem . connection
stem . connection . authenticate ( self , * args , * * kwargs )
2018-12-15 00:08:54 +00:00
def reconnect ( self , * args , * * kwargs ) :
"""
Reconnects and authenticates to our control socket .
. . versionadded : : 1.5 .0
: raises :
* : class : ` stem . SocketError ` if unable to re - establish socket
* : class : ` stem . connection . AuthenticationFailure ` if unable to authenticate
"""
with self . _msg_lock :
self . connect ( )
self . clear_cache ( )
self . authenticate ( * args , * * kwargs )
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_info ( self , params , default = UNDEFINED , get_bytes = False ) :
"""
get_info ( params , default = UNDEFINED , get_bytes = False )
Queries the control socket for the given GETINFO option . If provided a
default then that ' s returned if the GETINFO option is undefined or the
call fails for any reason ( error response , control port closed , initiated ,
etc ) .
. . versionchanged : : 1.1 .0
Added the get_bytes argument .
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.7 .0
Errors commonly provided a : class : ` stem . ProtocolError ` when we should
raise a : class : ` stem . OperationFailed ` .
2015-11-23 21:13:53 +00:00
: param str , list params : GETINFO option or options to be queried
: param object default : response if the query fails
: param bool get_bytes : provides * * bytes * * values rather than a * * str * * under python 3. x
: returns :
Response depends upon how we were called as follows . . .
* * * str * * with the response if our param was a * * str * *
* * * dict * * with the ' param => response ' mapping if our param was a * * list * *
* default if one was provided and our call failed
: raises :
* : class : ` stem . ControllerError ` if the call fails and we weren ' t
provided a default response
* : class : ` stem . InvalidArguments ` if the ' params ' requested was
invalid
2018-12-15 00:08:54 +00:00
* : class : ` stem . ProtocolError ` if the geoip database is unavailable
2015-11-23 21:13:53 +00:00
"""
start_time = time . time ( )
reply = { }
2018-12-15 00:08:54 +00:00
if stem . util . _is_str ( params ) :
2015-11-23 21:13:53 +00:00
is_multiple = False
params = set ( [ params ] )
else :
if not params :
return { }
is_multiple = True
params = set ( params )
2018-12-15 00:08:54 +00:00
for param in params :
if param . startswith ( ' ip-to-country/ ' ) and param != ' ip-to-country/0.0.0.0 ' and self . is_geoip_unavailable ( ) :
raise stem . ProtocolError ( ' Tor geoip database is unavailable ' )
elif param == ' address ' and self . _last_address_exc :
raise self . _last_address_exc # we already know we can't resolve an address
elif param == ' fingerprint ' and self . _last_fingerprint_exc and self . get_conf ( ' ORPort ' , None ) is None :
raise self . _last_fingerprint_exc # we already know we're not a relay
2015-11-23 21:13:53 +00:00
# check for cached results
from_cache = [ param . lower ( ) for param in params ]
cached_results = self . _get_cache_map ( from_cache , ' getinfo ' )
for key in cached_results :
user_expected_key = _case_insensitive_lookup ( params , key )
reply [ user_expected_key ] = cached_results [ key ]
params . remove ( user_expected_key )
# if everything was cached then short circuit making the query
if not params :
2018-12-15 00:08:54 +00:00
if LOG_CACHE_FETCHES :
log . trace ( ' GETINFO %s (cache fetch) ' % ' ' . join ( reply . keys ( ) ) )
2015-11-23 21:13:53 +00:00
if is_multiple :
return reply
else :
return list ( reply . values ( ) ) [ 0 ]
try :
response = self . msg ( ' GETINFO %s ' % ' ' . join ( params ) )
stem . response . convert ( ' GETINFO ' , response )
response . _assert_matches ( params )
# usually we want unicode values under python 3.x
if stem . prereq . is_python_3 ( ) and not get_bytes :
response . entries = dict ( ( k , stem . util . str_tools . _to_unicode ( v ) ) for ( k , v ) in response . entries . items ( ) )
reply . update ( response . entries )
if self . is_caching_enabled ( ) :
to_cache = { }
for key , value in response . entries . items ( ) :
key = key . lower ( ) # make case insensitive
2018-12-15 00:08:54 +00:00
if key in CACHEABLE_GETINFO_PARAMS or key in CACHEABLE_GETINFO_PARAMS_UNTIL_SETCONF :
2015-11-23 21:13:53 +00:00
to_cache [ key ] = value
elif key . startswith ( ' ip-to-country/ ' ) :
to_cache [ key ] = value
self . _set_cache ( to_cache , ' getinfo ' )
2018-12-15 00:08:54 +00:00
if ' address ' in params :
self . _last_address_exc = None
if ' fingerprint ' in params :
self . _last_fingerprint_exc = None
2015-11-23 21:13:53 +00:00
log . debug ( ' GETINFO %s (runtime: %0.4f ) ' % ( ' ' . join ( params ) , time . time ( ) - start_time ) )
if is_multiple :
return reply
else :
return list ( reply . values ( ) ) [ 0 ]
except stem . ControllerError as exc :
2018-12-15 00:08:54 +00:00
if ' address ' in params :
self . _last_address_exc = exc
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
if ' fingerprint ' in params :
self . _last_fingerprint_exc = exc
2015-11-23 21:13:53 +00:00
log . debug ( ' GETINFO %s (failed: %s ) ' % ( ' ' . join ( params ) , exc ) )
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_version ( self , default = UNDEFINED ) :
"""
get_version ( default = UNDEFINED )
A convenience method to get tor version that current controller is
connected to .
: param object default : response if the query fails
: returns : : class : ` ~ stem . version . Version ` of the tor instance that we ' re
connected to
: raises :
* : class : ` stem . ControllerError ` if unable to query the version
* * * ValueError * * if unable to parse the version
An exception is only raised if we weren ' t provided a default response.
"""
version = self . _get_cache ( ' version ' )
if not version :
2018-12-15 00:08:54 +00:00
version_str = self . get_info ( ' version ' )
version = stem . version . Version ( version_str [ 4 : ] if version_str . startswith ( ' Tor ' ) else version_str )
2015-11-23 21:13:53 +00:00
self . _set_cache ( { ' version ' : version } )
return version
@with_default ( )
def get_exit_policy ( self , default = UNDEFINED ) :
"""
get_exit_policy ( default = UNDEFINED )
2018-12-15 00:08:54 +00:00
Effective ExitPolicy for our relay .
. . versionchanged : : 1.7 .0
Policies retrieved through ' GETINFO exit-policy/full ' rather than
parsing the user ' s torrc entries. This should be more reliable for
some edge cases . ( : trac : ` 25739 ` )
2015-11-23 21:13:53 +00:00
: param object default : response if the query fails
: returns : : class : ` ~ stem . exit_policy . ExitPolicy ` of the tor instance that
we ' re connected to
: raises :
* : class : ` stem . ControllerError ` if unable to query the policy
* * * ValueError * * if unable to parse the policy
An exception is only raised if we weren ' t provided a default response.
"""
2018-12-15 00:08:54 +00:00
policy = self . _get_cache ( ' exit_policy ' )
if not policy :
try :
policy = stem . exit_policy . ExitPolicy ( * self . get_info ( ' exit-policy/full ' ) . splitlines ( ) )
self . _set_cache ( { ' exit_policy ' : policy } )
except stem . OperationFailed :
# There's a few situations where 'GETINFO exit-policy/full' will fail,
# most commonly...
#
# * Error 551: Descriptor still rebuilding - not ready yet
#
# Tor hasn't yet finished making our server descriptor. This often
# arises when tor has first started.
#
# * Error 552: Not running in server mode
#
# We're not configured to be a relay (no ORPort), or haven't yet
# been able to determine our externally facing IP address.
#
# When these arise best we can do is infer our policy from the torrc.
# Skipping caching so we'll retry GETINFO policy resolution next time
# we're called.
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
rules = [ ]
if self . get_conf ( ' ExitRelay ' ) == ' 0 ' :
rules . append ( ' reject *:* ' )
2015-11-23 21:13:53 +00:00
if self . get_conf ( ' ExitPolicyRejectPrivate ' ) == ' 1 ' :
2018-12-15 00:08:54 +00:00
rules . append ( ' reject private:* ' )
2015-11-23 21:13:53 +00:00
for policy_line in self . get_conf ( ' ExitPolicy ' , multiple = True ) :
2018-12-15 00:08:54 +00:00
rules + = policy_line . split ( ' , ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
rules + = self . get_info ( ' exit-policy/default ' ) . split ( ' , ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
policy = stem . exit_policy . get_config_policy ( rules , self . get_info ( ' address ' , None ) )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
return policy
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_ports ( self , listener_type , default = UNDEFINED ) :
"""
get_ports ( listener_type , default = UNDEFINED )
Provides the local ports where tor is listening for the given type of
connections . This is similar to
: func : ` ~ stem . control . Controller . get_listeners ` , but doesn ' t provide
addresses nor include non - local endpoints .
. . versionadded : : 1.2 .0
: param stem . control . Listener listener_type : connection type being handled
by the ports we return
: param object default : response if the query fails
: returns : * * list * * of * * ints * * for the local ports where tor handles
connections of the given type
: raises : : class : ` stem . ControllerError ` if unable to determine the ports
and no default was provided
"""
2018-12-15 00:08:54 +00:00
def is_localhost ( address ) :
if stem . util . connection . is_valid_ipv4_address ( address ) :
return address == ' 0.0.0.0 ' or address . startswith ( ' 127. ' )
elif stem . util . connection . is_valid_ipv6_address ( address ) :
return stem . util . connection . expand_ipv6_address ( address ) in (
' 0000:0000:0000:0000:0000:0000:0000:0000 ' ,
' 0000:0000:0000:0000:0000:0000:0000:0001 ' ,
)
else :
log . info ( " Request for %s ports got an address that ' s neither IPv4 or IPv6: %s " % ( listener_type , address ) )
return False
return [ port for ( addr , port ) in self . get_listeners ( listener_type ) if is_localhost ( addr ) ]
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_listeners ( self , listener_type , default = UNDEFINED ) :
"""
get_listeners ( listener_type , default = UNDEFINED )
Provides the addresses and ports where tor is listening for connections of
the given type . This is similar to
: func : ` ~ stem . control . Controller . get_ports ` but includes listener addresses
and non - local endpoints .
. . versionadded : : 1.2 .0
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.5 .0
Recognize listeners with IPv6 addresses .
2015-11-23 21:13:53 +00:00
: param stem . control . Listener listener_type : connection type being handled
by the listeners we return
: param object default : response if the query fails
: returns : * * list * * of * * ( address , port ) * * tuples for the available
listeners
: raises : : class : ` stem . ControllerError ` if unable to determine the listeners
and no default was provided
"""
2018-12-15 00:08:54 +00:00
listeners = self . _get_cache ( listener_type , ' listeners ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
if listeners is None :
proxy_addrs = [ ]
query = ' net/listeners/ %s ' % listener_type . lower ( )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
try :
for listener in self . get_info ( query ) . split ( ) :
if not ( listener . startswith ( ' " ' ) and listener . endswith ( ' " ' ) ) :
raise stem . ProtocolError ( " ' GETINFO %s ' responses are expected to be quoted: %s " % ( query , listener ) )
elif ' : ' not in listener :
raise stem . ProtocolError ( " ' GETINFO %s ' had a listener without a colon: %s " % ( query , listener ) )
listener = listener [ 1 : - 1 ] # strip quotes
addr , port = listener . rsplit ( ' : ' , 1 )
# Skip unix sockets, for instance...
#
# GETINFO net/listeners/control
# 250-net/listeners/control="unix:/tmp/tor/socket"
# 250 OK
if addr == ' unix ' :
continue
if addr . startswith ( ' [ ' ) and addr . endswith ( ' ] ' ) :
addr = addr [ 1 : - 1 ] # unbracket ipv6 address
2015-11-23 21:13:53 +00:00
proxy_addrs . append ( ( addr , port ) )
2018-12-15 00:08:54 +00:00
except stem . InvalidArguments :
# Tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead.
# Some options (like the ORPort) can have optional attributes after the
# actual port number.
port_option = {
Listener . OR : ' ORPort ' ,
Listener . DIR : ' DirPort ' ,
Listener . SOCKS : ' SocksPort ' ,
Listener . TRANS : ' TransPort ' ,
Listener . NATD : ' NatdPort ' ,
Listener . DNS : ' DNSPort ' ,
Listener . CONTROL : ' ControlPort ' ,
} [ listener_type ]
listener_option = {
Listener . OR : ' ORListenAddress ' ,
Listener . DIR : ' DirListenAddress ' ,
Listener . SOCKS : ' SocksListenAddress ' ,
Listener . TRANS : ' TransListenAddress ' ,
Listener . NATD : ' NatdListenAddress ' ,
Listener . DNS : ' DNSListenAddress ' ,
Listener . CONTROL : ' ControlListenAddress ' ,
} [ listener_type ]
port_value = self . get_conf ( port_option ) . split ( ) [ 0 ]
for listener in self . get_conf ( listener_option , multiple = True ) :
if ' : ' in listener :
addr , port = listener . rsplit ( ' : ' , 1 )
if addr . startswith ( ' [ ' ) and addr . endswith ( ' ] ' ) :
addr = addr [ 1 : - 1 ] # unbracket ipv6 address
proxy_addrs . append ( ( addr , port ) )
else :
proxy_addrs . append ( ( listener , port_value ) )
# validate that address/ports are valid, and convert ports to ints
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
for addr , port in proxy_addrs :
if not stem . util . connection . is_valid_ipv4_address ( addr ) and not stem . util . connection . is_valid_ipv6_address ( addr ) :
raise stem . ProtocolError ( ' Invalid address for a %s listener: %s ' % ( listener_type , addr ) )
elif not stem . util . connection . is_valid_port ( port ) :
raise stem . ProtocolError ( ' Invalid port for a %s listener: %s ' % ( listener_type , port ) )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
listeners = [ ( addr , int ( port ) ) for ( addr , port ) in proxy_addrs ]
self . _set_cache ( { listener_type : listeners } , ' listeners ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
return listeners
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_accounting_stats ( self , default = UNDEFINED ) :
"""
get_accounting_stats ( default = UNDEFINED )
Provides stats related to our relaying limitations if AccountingMax was set
2018-12-15 00:08:54 +00:00
in our torrc .
2015-11-23 21:13:53 +00:00
. . versionadded : : 1.3 .0
: param object default : response if the query fails
2018-12-15 00:08:54 +00:00
: returns : : class : ` ~ stem . control . AccountingStats ` with our accounting stats
2015-11-23 21:13:53 +00:00
: raises : : class : ` stem . ControllerError ` if unable to determine the listeners
and no default was provided
"""
if self . get_info ( ' accounting/enabled ' ) != ' 1 ' :
raise stem . ControllerError ( " Accounting isn ' t enabled " )
retrieved = time . time ( )
status = self . get_info ( ' accounting/hibernating ' )
interval_end = self . get_info ( ' accounting/interval-end ' )
used = self . get_info ( ' accounting/bytes ' )
left = self . get_info ( ' accounting/bytes-left ' )
interval_end = stem . util . str_tools . _parse_timestamp ( interval_end )
used_read , used_written = [ int ( val ) for val in used . split ( ' ' , 1 ) ]
left_read , left_written = [ int ( val ) for val in left . split ( ' ' , 1 ) ]
return AccountingStats (
retrieved = retrieved ,
status = status ,
interval_end = interval_end ,
time_until_reset = calendar . timegm ( interval_end . timetuple ( ) ) - int ( retrieved ) ,
read_bytes = used_read ,
read_bytes_left = left_read ,
read_limit = used_read + left_read ,
written_bytes = used_written ,
write_bytes_left = left_written ,
write_limit = used_written + left_written ,
)
def get_socks_listeners ( self , default = UNDEFINED ) :
"""
Provides the SOCKS * * ( address , port ) * * tuples that tor has open .
. . deprecated : : 1.2 .0
Use : func : ` ~ stem . control . Controller . get_listeners ` with
* * Listener . SOCKS * * instead .
: param object default : response if the query fails
: returns : list of * * ( address , port ) * * tuples for the available SOCKS
listeners
: raises : : class : ` stem . ControllerError ` if unable to determine the listeners
and no default was provided
"""
return self . get_listeners ( Listener . SOCKS , default )
@with_default ( )
def get_protocolinfo ( self , default = UNDEFINED ) :
"""
get_protocolinfo ( default = UNDEFINED )
A convenience method to get the protocol info of the controller .
: param object default : response if the query fails
: returns : : class : ` ~ stem . response . protocolinfo . ProtocolInfoResponse ` provided by tor
: raises :
* : class : ` stem . ProtocolError ` if the PROTOCOLINFO response is
malformed
* : class : ` stem . SocketError ` if problems arise in establishing or
using the socket
An exception is only raised if we weren ' t provided a default response.
"""
import stem . connection
return stem . connection . get_protocolinfo ( self )
@with_default ( )
def get_user ( self , default = UNDEFINED ) :
"""
get_user ( default = UNDEFINED )
Provides the user tor is running as . This often only works if tor is
running locally . Also , most of its checks are platform dependent , and hence
are not entirely reliable .
. . versionadded : : 1.1 .0
: param object default : response if the query fails
: returns : str with the username tor is running as
"""
user = self . _get_cache ( ' user ' )
if not user :
user = self . get_info ( ' process/user ' , None )
if not user and self . is_localhost ( ) :
pid = self . get_pid ( None )
if pid :
user = stem . util . system . user ( pid )
if user :
self . _set_cache ( { ' user ' : user } )
return user
else :
raise ValueError ( " Unable to resolve tor ' s user " if self . is_localhost ( ) else " Tor isn ' t running locally " )
@with_default ( )
def get_pid ( self , default = UNDEFINED ) :
"""
get_pid ( default = UNDEFINED )
Provides the process id of tor . This often only works if tor is running
locally . Also , most of its checks are platform dependent , and hence are not
entirely reliable .
. . versionadded : : 1.1 .0
: param object default : response if the query fails
: returns : * * int * * for tor ' s pid
: raises : * * ValueError * * if unable to determine the pid and no default was
provided
"""
pid = self . _get_cache ( ' pid ' )
if not pid :
getinfo_pid = self . get_info ( ' process/pid ' , None )
if getinfo_pid and getinfo_pid . isdigit ( ) :
pid = int ( getinfo_pid )
if not pid and self . is_localhost ( ) :
pid_file_path = self . get_conf ( ' PidFile ' , None )
if pid_file_path is not None :
with open ( pid_file_path ) as pid_file :
pid_file_contents = pid_file . read ( ) . strip ( )
if pid_file_contents . isdigit ( ) :
pid = int ( pid_file_contents )
if not pid :
pid = stem . util . system . pid_by_name ( ' tor ' )
if not pid :
control_socket = self . get_socket ( )
if isinstance ( control_socket , stem . socket . ControlPort ) :
2018-12-15 00:08:54 +00:00
pid = stem . util . system . pid_by_port ( control_socket . port )
2015-11-23 21:13:53 +00:00
elif isinstance ( control_socket , stem . socket . ControlSocketFile ) :
2018-12-15 00:08:54 +00:00
pid = stem . util . system . pid_by_open_file ( control_socket . path )
2015-11-23 21:13:53 +00:00
if pid :
self . _set_cache ( { ' pid ' : pid } )
return pid
else :
raise ValueError ( " Unable to resolve tor ' s pid " if self . is_localhost ( ) else " Tor isn ' t running locally " )
2018-12-15 00:08:54 +00:00
def is_user_traffic_allowed ( self ) :
"""
Checks if we ' re likely to service direct user traffic. This essentially
boils down to . . .
* If we ' re a bridge or guard relay, inbound connections are possibly from
users .
* If our exit policy allows traffic then output connections are possibly
from users .
Note the word ' likely ' . These is a decent guess in practice , but not always
correct . For instance , information about which flags we have are only
fetched periodically .
This method is intended to help you avoid eavesdropping on user traffic .
Monitoring user connections is not only unethical , but likely a violation
of wiretapping laws .
. . versionadded : : 1.5 .0
: returns : : class : ` ~ stem . cotroller . UserTrafficAllowed ` with * * inbound * * and
* * outbound * * boolean attributes to indicate if we ' re likely servicing
direct user traffic
"""
inbound_allowed , outbound_allowed = False , False
if self . get_conf ( ' BridgeRelay ' , None ) == ' 1 ' :
inbound_allowed = True
if self . get_conf ( ' ORPort ' , None ) :
if not inbound_allowed :
consensus_entry = self . get_network_status ( default = None )
inbound_allowed = consensus_entry and ' Guard ' in consensus_entry . flags
exit_policy = self . get_exit_policy ( None )
outbound_allowed = exit_policy and exit_policy . is_exiting_allowed ( )
return UserTrafficAllowed ( inbound_allowed , outbound_allowed )
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_microdescriptor ( self , relay = None , default = UNDEFINED ) :
"""
get_microdescriptor ( relay = None , default = UNDEFINED )
Provides the microdescriptor for the relay with the given fingerprint or
nickname . If the relay identifier could be either a fingerprint * or *
nickname then it ' s queried as a fingerprint.
If no * * relay * * is provided then this defaults to ourselves . Remember that
this requires that we ' ve retrieved our own descriptor from remote
authorities so this both won ' t be available for newly started relays and
may be up to around an hour out of date .
. . versionchanged : : 1.3 .0
Changed so we ' d fetch our own descriptor if no ' relay ' is provided.
: param str relay : fingerprint or nickname of the relay to be queried
: param object default : response if the query fails
: returns : : class : ` ~ stem . descriptor . microdescriptor . Microdescriptor ` for the given relay
: raises :
* : class : ` stem . DescriptorUnavailable ` if unable to provide a descriptor
for the given relay
* : class : ` stem . ControllerError ` if unable to query the descriptor
* * * ValueError * * if * * relay * * doesn ' t conform with the pattern for being
a fingerprint or nickname
An exception is only raised if we weren ' t provided a default response.
"""
if relay is None :
try :
relay = self . get_info ( ' fingerprint ' )
except stem . ControllerError as exc :
raise stem . ControllerError ( ' Unable to determine our own fingerprint: %s ' % exc )
if stem . util . tor_tools . is_valid_fingerprint ( relay ) :
query = ' md/id/ %s ' % relay
elif stem . util . tor_tools . is_valid_nickname ( relay ) :
query = ' md/name/ %s ' % relay
else :
raise ValueError ( " ' %s ' isn ' t a valid fingerprint or nickname " % relay )
try :
desc_content = self . get_info ( query , get_bytes = True )
except stem . InvalidArguments as exc :
if str ( exc ) . startswith ( ' GETINFO request contained unrecognized keywords: ' ) :
raise stem . DescriptorUnavailable ( " Tor was unable to provide the descriptor for ' %s ' " % relay )
else :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
if not desc_content :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
return stem . descriptor . microdescriptor . Microdescriptor ( desc_content )
@with_default ( yields = True )
def get_microdescriptors ( self , default = UNDEFINED ) :
"""
get_microdescriptors ( default = UNDEFINED )
Provides an iterator for all of the microdescriptors that tor currently
knows about .
2018-12-15 00:08:54 +00:00
Prior to Tor 0.3 .5 .1 this information was not available via the control
protocol . When connected to prior versions we read the microdescriptors
directly from disk instead , which will not work remotely or if our process
lacks read permissions .
2015-11-23 21:13:53 +00:00
: param list default : items to provide if the query fails
: returns : iterates over
: class : ` ~ stem . descriptor . microdescriptor . Microdescriptor ` for relays in
the tor network
: raises : : class : ` stem . ControllerError ` if unable to query tor and no
default was provided
"""
2018-12-15 00:08:54 +00:00
if self . get_version ( ) > = stem . version . Requirement . GETINFO_MICRODESCRIPTORS :
desc_content = self . get_info ( ' md/all ' , get_bytes = True )
if not desc_content :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
for desc in stem . descriptor . microdescriptor . _parse_file ( io . BytesIO ( desc_content ) ) :
yield desc
else :
# TODO: remove when tor versions that require this are obsolete
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
try :
data_directory = self . get_conf ( ' DataDirectory ' )
except stem . ControllerError as exc :
raise stem . OperationFailed ( message = ' Unable to determine the data directory ( %s ) ' % exc )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
cached_descriptor_path = os . path . join ( data_directory , ' cached-microdescs ' )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
if not os . path . exists ( data_directory ) :
raise stem . OperationFailed ( message = " Data directory reported by tor doesn ' t exist ( %s ) " % data_directory )
elif not os . path . exists ( cached_descriptor_path ) :
raise stem . OperationFailed ( message = " Data directory doesn ' t contain cached microdescriptors ( %s ) " % cached_descriptor_path )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
with stem . descriptor . reader . DescriptorReader ( [ cached_descriptor_path ] ) as reader :
for desc in reader :
# It shouldn't be possible for these to be something other than
# microdescriptors but as the saying goes: trust but verify.
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
if not isinstance ( desc , stem . descriptor . microdescriptor . Microdescriptor ) :
raise stem . OperationFailed ( message = ' BUG: Descriptor reader provided non-microdescriptor content ( %s ) ' % type ( desc ) )
yield desc
2015-11-23 21:13:53 +00:00
@with_default ( )
def get_server_descriptor ( self , relay = None , default = UNDEFINED ) :
"""
get_server_descriptor ( relay = None , default = UNDEFINED )
Provides the server descriptor for the relay with the given fingerprint or
nickname . If the relay identifier could be either a fingerprint * or *
nickname then it ' s queried as a fingerprint.
If no * * relay * * is provided then this defaults to ourselves . Remember that
this requires that we ' ve retrieved our own descriptor from remote
authorities so this both won ' t be available for newly started relays and
may be up to around an hour out of date .
* * As of Tor version 0.2 .3 .25 relays no longer get server descriptors by
default . * * It ' s advised that you use microdescriptors instead, but if you
really need server descriptors then you can get them by setting
' UseMicrodescriptors 0 ' .
. . versionchanged : : 1.3 .0
Changed so we ' d fetch our own descriptor if no ' relay ' is provided.
: param str relay : fingerprint or nickname of the relay to be queried
: param object default : response if the query fails
: returns : : class : ` ~ stem . descriptor . server_descriptor . RelayDescriptor ` for the given relay
: raises :
* : class : ` stem . DescriptorUnavailable ` if unable to provide a descriptor
for the given relay
* : class : ` stem . ControllerError ` if unable to query the descriptor
* * * ValueError * * if * * relay * * doesn ' t conform with the pattern for being
a fingerprint or nickname
An exception is only raised if we weren ' t provided a default response.
"""
try :
if relay is None :
try :
relay = self . get_info ( ' fingerprint ' )
except stem . ControllerError as exc :
raise stem . ControllerError ( ' Unable to determine our own fingerprint: %s ' % exc )
if stem . util . tor_tools . is_valid_fingerprint ( relay ) :
query = ' desc/id/ %s ' % relay
elif stem . util . tor_tools . is_valid_nickname ( relay ) :
query = ' desc/name/ %s ' % relay
else :
raise ValueError ( " ' %s ' isn ' t a valid fingerprint or nickname " % relay )
try :
desc_content = self . get_info ( query , get_bytes = True )
except stem . InvalidArguments as exc :
if str ( exc ) . startswith ( ' GETINFO request contained unrecognized keywords: ' ) :
raise stem . DescriptorUnavailable ( " Tor was unable to provide the descriptor for ' %s ' " % relay )
else :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
if not desc_content :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
return stem . descriptor . server_descriptor . RelayDescriptor ( desc_content )
2018-12-15 00:08:54 +00:00
except :
2015-11-23 21:13:53 +00:00
if not self . _is_server_descriptors_available ( ) :
raise ValueError ( SERVER_DESCRIPTORS_UNSUPPORTED )
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
@with_default ( yields = True )
def get_server_descriptors ( self , default = UNDEFINED ) :
"""
get_server_descriptors ( default = UNDEFINED )
Provides an iterator for all of the server descriptors that tor currently
knows about .
* * As of Tor version 0.2 .3 .25 relays no longer get server descriptors by
default . * * It ' s advised that you use microdescriptors instead, but if you
really need server descriptors then you can get them by setting
' UseMicrodescriptors 0 ' .
: param list default : items to provide if the query fails
: returns : iterates over
: class : ` ~ stem . descriptor . server_descriptor . RelayDescriptor ` for relays in
the tor network
: raises : : class : ` stem . ControllerError ` if unable to query tor and no
default was provided
"""
# TODO: We should iterate over the descriptors as they're read from the
# socket rather than reading the whole thing into memory.
#
# https://trac.torproject.org/8248
desc_content = self . get_info ( ' desc/all-recent ' , get_bytes = True )
if not desc_content :
if not self . _is_server_descriptors_available ( ) :
raise stem . ControllerError ( SERVER_DESCRIPTORS_UNSUPPORTED )
else :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
for desc in stem . descriptor . server_descriptor . _parse_file ( io . BytesIO ( desc_content ) ) :
yield desc
def _is_server_descriptors_available ( self ) :
"""
Checks to see if tor server descriptors should be available or not .
"""
2018-12-15 00:08:54 +00:00
# TODO: Replace with a 'GETINFO desc/download-enabled' request when they're
# widely available...
#
# https://gitweb.torproject.org/torspec.git/commit/?id=378699c
2015-11-23 21:13:53 +00:00
return self . get_version ( ) < stem . version . Requirement . MICRODESCRIPTOR_IS_DEFAULT or \
self . get_conf ( ' UseMicrodescriptors ' , None ) == ' 0 '
@with_default ( )
def get_network_status ( self , relay = None , default = UNDEFINED ) :
"""
get_network_status ( relay = None , default = UNDEFINED )
Provides the router status entry for the relay with the given fingerprint
or nickname . If the relay identifier could be either a fingerprint * or *
nickname then it ' s queried as a fingerprint.
If no * * relay * * is provided then this defaults to ourselves . Remember that
this requires that we ' ve retrieved our own descriptor from remote
authorities so this both won ' t be available for newly started relays and
may be up to around an hour out of date .
. . versionchanged : : 1.3 .0
Changed so we ' d fetch our own descriptor if no ' relay ' is provided.
: param str relay : fingerprint or nickname of the relay to be queried
: param object default : response if the query fails
2018-12-15 00:08:54 +00:00
: returns : : class : ` ~ stem . descriptor . router_status_entry . RouterStatusEntryV3 `
2015-11-23 21:13:53 +00:00
for the given relay
: raises :
* : class : ` stem . DescriptorUnavailable ` if unable to provide a descriptor
for the given relay
* : class : ` stem . ControllerError ` if unable to query the descriptor
* * * ValueError * * if * * relay * * doesn ' t conform with the pattern for being
a fingerprint or nickname
An exception is only raised if we weren ' t provided a default response.
"""
if relay is None :
try :
relay = self . get_info ( ' fingerprint ' )
except stem . ControllerError as exc :
raise stem . ControllerError ( ' Unable to determine our own fingerprint: %s ' % exc )
if stem . util . tor_tools . is_valid_fingerprint ( relay ) :
query = ' ns/id/ %s ' % relay
elif stem . util . tor_tools . is_valid_nickname ( relay ) :
query = ' ns/name/ %s ' % relay
else :
raise ValueError ( " ' %s ' isn ' t a valid fingerprint or nickname " % relay )
try :
desc_content = self . get_info ( query , get_bytes = True )
except stem . InvalidArguments as exc :
if str ( exc ) . startswith ( ' GETINFO request contained unrecognized keywords: ' ) :
raise stem . DescriptorUnavailable ( " Tor was unable to provide the descriptor for ' %s ' " % relay )
else :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
if not desc_content :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
2018-12-15 00:08:54 +00:00
return stem . descriptor . router_status_entry . RouterStatusEntryV3 ( desc_content )
2015-11-23 21:13:53 +00:00
@with_default ( yields = True )
def get_network_statuses ( self , default = UNDEFINED ) :
"""
get_network_statuses ( default = UNDEFINED )
Provides an iterator for all of the router status entries that tor
currently knows about .
: param list default : items to provide if the query fails
: returns : iterates over
2018-12-15 00:08:54 +00:00
: class : ` ~ stem . descriptor . router_status_entry . RouterStatusEntryV3 ` for
2015-11-23 21:13:53 +00:00
relays in the tor network
: raises : : class : ` stem . ControllerError ` if unable to query tor and no
default was provided
"""
# TODO: We should iterate over the descriptors as they're read from the
# socket rather than reading the whole thing into memory.
#
# https://trac.torproject.org/8248
desc_content = self . get_info ( ' ns/all ' , get_bytes = True )
if not desc_content :
raise stem . DescriptorUnavailable ( ' Descriptor information is unavailable, tor might still be downloading it ' )
desc_iterator = stem . descriptor . router_status_entry . _parse_file (
io . BytesIO ( desc_content ) ,
2018-12-15 00:08:54 +00:00
False ,
entry_class = stem . descriptor . router_status_entry . RouterStatusEntryV3 ,
2015-11-23 21:13:53 +00:00
)
for desc in desc_iterator :
yield desc
@with_default ( )
2018-12-15 00:08:54 +00:00
def get_hidden_service_descriptor ( self , address , default = UNDEFINED , servers = None , await_result = True , timeout = None ) :
2015-11-23 21:13:53 +00:00
"""
get_hidden_service_descriptor ( address , default = UNDEFINED , servers = None , await_result = True )
Provides the descriptor for a hidden service . The * * address * * is the
' .onion ' address of the hidden service ( for instance 3 g2upl4pq6kufc4m . onion
for DuckDuckGo ) .
If * * await_result * * is * * True * * then this blocks until we either receive
the descriptor or the request fails . If * * False * * this returns right away .
2018-12-15 00:08:54 +00:00
* * This method only supports v2 hidden services , not v3 . * * ( : trac : ` 25417 ` )
2015-11-23 21:13:53 +00:00
. . versionadded : : 1.4 .0
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.7 .0
Added the timeout argument .
2015-11-23 21:13:53 +00:00
: param str address : address of the hidden service descriptor , the ' .onion ' suffix is optional
: param object default : response if the query fails
: param list servers : requrest the descriptor from these specific servers
2018-12-15 00:08:54 +00:00
: param float timeout : seconds to wait when * * await_result * * is * * True * *
2015-11-23 21:13:53 +00:00
: returns : : class : ` ~ stem . descriptor . hidden_service_descriptor . HiddenServiceDescriptor `
for the given service if * * await_result * * is * * True * * , or * * None * * otherwise
: raises :
* : class : ` stem . DescriptorUnavailable ` if * * await_result * * is * * True * * and
unable to provide a descriptor for the given service
2018-12-15 00:08:54 +00:00
* : class : ` stem . Timeout ` if * * timeout * * was reached
2015-11-23 21:13:53 +00:00
* : class : ` stem . ControllerError ` if unable to query the descriptor
* * * ValueError * * if * * address * * doesn ' t conform with the pattern of a
hidden service address
An exception is only raised if we weren ' t provided a default response.
"""
if address . endswith ( ' .onion ' ) :
address = address [ : - 6 ]
if not stem . util . tor_tools . is_valid_hidden_service_address ( address ) :
raise ValueError ( " ' %s .onion ' isn ' t a valid hidden service address " % address )
if self . get_version ( ) < stem . version . Requirement . HSFETCH :
raise stem . UnsatisfiableRequest ( message = ' HSFETCH was added in tor version %s ' % stem . version . Requirement . HSFETCH )
hs_desc_queue , hs_desc_listener = queue . Queue ( ) , None
hs_desc_content_queue , hs_desc_content_listener = queue . Queue ( ) , None
2018-12-15 00:08:54 +00:00
start_time = time . time ( )
2015-11-23 21:13:53 +00:00
if await_result :
def hs_desc_listener ( event ) :
hs_desc_queue . put ( event )
def hs_desc_content_listener ( event ) :
hs_desc_content_queue . put ( event )
self . add_event_listener ( hs_desc_listener , EventType . HS_DESC )
self . add_event_listener ( hs_desc_content_listener , EventType . HS_DESC_CONTENT )
try :
request = ' HSFETCH %s ' % address
if servers :
2018-12-15 00:08:54 +00:00
request + = ' ' + ' ' . join ( [ ' SERVER= %s ' % s for s in servers ] )
2015-11-23 21:13:53 +00:00
response = self . msg ( request )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
raise stem . ProtocolError ( ' HSFETCH returned unexpected response code: %s ' % response . code )
if not await_result :
return None # not waiting, so nothing to provide back
else :
while True :
2018-12-15 00:08:54 +00:00
event = _get_with_timeout ( hs_desc_content_queue , timeout , start_time )
2015-11-23 21:13:53 +00:00
if event . address == address :
if event . descriptor :
return event . descriptor
else :
# no descriptor, looking through HS_DESC to figure out why
while True :
2018-12-15 00:08:54 +00:00
event = _get_with_timeout ( hs_desc_queue , timeout , start_time )
2015-11-23 21:13:53 +00:00
if event . address == address and event . action == stem . HSDescAction . FAILED :
if event . reason == stem . HSDescReason . NOT_FOUND :
raise stem . DescriptorUnavailable ( ' No running hidden service at %s .onion ' % address )
else :
raise stem . DescriptorUnavailable ( ' Unable to retrieve the descriptor for %s .onion (retrieved from %s ): %s ' % ( address , event . directory_fingerprint , event . reason ) )
finally :
if hs_desc_listener :
self . remove_event_listener ( hs_desc_listener )
if hs_desc_content_listener :
self . remove_event_listener ( hs_desc_content_listener )
def get_conf ( self , param , default = UNDEFINED , multiple = False ) :
"""
2018-12-15 00:08:54 +00:00
get_conf ( param , default = UNDEFINED , multiple = False )
2015-11-23 21:13:53 +00:00
Queries the current value for a configuration option . Some configuration
options ( like the ExitPolicy ) can have multiple values . This provides a
* * list * * with all of the values if * * multiple * * is * * True * * . Otherwise this
will be a * * str * * with the first value .
If provided with a * * default * * then that is provided if the configuration
option was unset or the query fails ( invalid configuration option , error
response , control port closed , initiated , etc ) .
If the configuration value is unset and no * * default * * was given then this
provides * * None * * if * * multiple * * was * * False * * and an empty list if it was
* * True * * .
: param str param : configuration option to be queried
: param object default : response if the option is unset or the query fails
: param bool multiple : if * * True * * then provides a list with all of the
present values ( this is an empty list if the config option is unset )
: returns :
Response depends upon how we were called as follows . . .
* * * str * * with the configuration value if * * multiple * * was * * False * * ,
* * None * * if it was unset
* * * list * * with the response strings if multiple was * * True * *
* default if one was provided and the configuration option was either
unset or our call failed
: raises :
* : class : ` stem . ControllerError ` if the call fails and we weren ' t
provided a default response
* : class : ` stem . InvalidArguments ` if the configuration option
requested was invalid
"""
# Config options are case insensitive and don't contain whitespace. Using
# strip so the following check will catch whitespace-only params.
param = param . lower ( ) . strip ( )
if not param :
return default if default != UNDEFINED else None
entries = self . get_conf_map ( param , default , multiple )
return _case_insensitive_lookup ( entries , param , default )
def get_conf_map ( self , params , default = UNDEFINED , multiple = True ) :
"""
2018-12-15 00:08:54 +00:00
get_conf_map ( params , default = UNDEFINED , multiple = True )
2015-11-23 21:13:53 +00:00
Similar to : func : ` ~ stem . control . Controller . get_conf ` but queries multiple
configuration options , providing back a mapping of those options to their
values .
There are three use cases for GETCONF :
1. a single value is provided ( e . g . * * ControlPort * * )
2. multiple values are provided for the option ( e . g . * * ExitPolicy * * )
3. a set of options that weren ' t necessarily requested are returned (for
instance querying * * HiddenServiceOptions * * gives * * HiddenServiceDir * * ,
* * HiddenServicePort * * , etc )
The vast majority of the options fall into the first two categories , in
which case calling : func : ` ~ stem . control . Controller . get_conf ` is sufficient .
However , for batch queries or the special options that give a set of values
this provides back the full response . As of tor version 0.2 .1 .25
* * HiddenServiceOptions * * was the only option that falls into the third
category .
2018-12-15 00:08:54 +00:00
* * Note : * * HiddenServiceOptions are best retrieved via the
: func : ` ~ stem . control . Controller . get_hidden_service_conf ` method instead .
2015-11-23 21:13:53 +00:00
: param str , list params : configuration option ( s ) to be queried
: param object default : value for the mappings if the configuration option
is either undefined or the query fails
: param bool multiple : if * * True * * then the values provided are lists with
all of the present values
: returns :
* * dict * * of the ' config key => value ' mappings . The value is a . . .
* * * str * * if * * multiple * * is * * False * * , * * None * * if the configuration
option is unset
* * * list * * if * * multiple * * is * * True * *
* the * * default * * if it was set and the value was either undefined or our
lookup failed
: raises :
* : class : ` stem . ControllerError ` if the call fails and we weren ' t provided
a default response
* : class : ` stem . InvalidArguments ` if the configuration option requested
was invalid
"""
start_time = time . time ( )
reply = { }
2018-12-15 00:08:54 +00:00
if stem . util . _is_str ( params ) :
2015-11-23 21:13:53 +00:00
params = [ params ]
# remove strings which contain only whitespace
params = [ entry for entry in params if entry . strip ( ) ]
if params == [ ] :
return { }
# translate context sensitive options
lookup_params = set ( [ MAPPED_CONFIG_KEYS . get ( entry , entry ) for entry in params ] )
# check for cached results
from_cache = [ param . lower ( ) for param in lookup_params ]
cached_results = self . _get_cache_map ( from_cache , ' getconf ' )
for key in cached_results :
user_expected_key = _case_insensitive_lookup ( lookup_params , key )
reply [ user_expected_key ] = cached_results [ key ]
lookup_params . remove ( user_expected_key )
# if everything was cached then short circuit making the query
if not lookup_params :
2018-12-15 00:08:54 +00:00
if LOG_CACHE_FETCHES :
log . trace ( ' GETCONF %s (cache fetch) ' % ' ' . join ( reply . keys ( ) ) )
2015-11-23 21:13:53 +00:00
return self . _get_conf_dict_to_response ( reply , default , multiple )
try :
response = self . msg ( ' GETCONF %s ' % ' ' . join ( lookup_params ) )
stem . response . convert ( ' GETCONF ' , response )
reply . update ( response . entries )
if self . is_caching_enabled ( ) :
to_cache = dict ( ( k . lower ( ) , v ) for k , v in response . entries . items ( ) )
self . _set_cache ( to_cache , ' getconf ' )
# Maps the entries back to the parameters that the user requested so the
# capitalization matches (ie, if they request "exitpolicy" then that
# should be the key rather than "ExitPolicy"). When the same
# configuration key is provided multiple times this determines the case
# based on the first and ignores the rest.
#
# This retains the tor provided camel casing of MAPPED_CONFIG_KEYS
# entries since the user didn't request those by their key, so we can't
# be sure what they wanted.
for key in reply :
if not key . lower ( ) in MAPPED_CONFIG_KEYS . values ( ) :
user_expected_key = _case_insensitive_lookup ( params , key , key )
if key != user_expected_key :
reply [ user_expected_key ] = reply [ key ]
del reply [ key ]
log . debug ( ' GETCONF %s (runtime: %0.4f ) ' % ( ' ' . join ( lookup_params ) , time . time ( ) - start_time ) )
return self . _get_conf_dict_to_response ( reply , default , multiple )
except stem . ControllerError as exc :
log . debug ( ' GETCONF %s (failed: %s ) ' % ( ' ' . join ( lookup_params ) , exc ) )
if default != UNDEFINED :
return dict ( ( param , default ) for param in params )
else :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
def _get_conf_dict_to_response ( self , config_dict , default , multiple ) :
"""
Translates a dictionary of ' config key => [value1, value2...] ' into the
return value of : func : ` ~ stem . control . Controller . get_conf_map ` , taking into
account what the caller requested .
"""
return_dict = { }
for key , values in list ( config_dict . items ( ) ) :
if values == [ ] :
# config option was unset
if default != UNDEFINED :
return_dict [ key ] = default
else :
return_dict [ key ] = [ ] if multiple else None
else :
return_dict [ key ] = values if multiple else values [ 0 ]
return return_dict
2018-12-15 00:08:54 +00:00
@with_default ( )
def is_set ( self , param , default = UNDEFINED ) :
"""
is_set ( param , default = UNDEFINED )
Checks if a configuration option differs from its default or not .
. . versionadded : : 1.5 .0
: param str param : configuration option to check
: param object default : response if the query fails
: returns : * * True * * if option differs from its default and * * False * *
otherwise
: raises : : class : ` stem . ControllerError ` if the call fails and we weren ' t
provided a default response
"""
return param in self . _get_custom_options ( )
def _get_custom_options ( self ) :
result = self . _get_cache ( ' get_custom_options ' )
if not result :
config_lines = self . get_info ( ' config-text ' ) . splitlines ( )
# Tor provides some config options even if they haven't been set...
#
# https://trac.torproject.org/projects/tor/ticket/2362
# https://trac.torproject.org/projects/tor/ticket/17909
default_lines = (
' Log notice stdout ' ,
' Log notice file /var/log/tor/log ' ,
' DataDirectory /home/ %s /.tor ' % self . get_user ( ' undefined ' ) ,
' HiddenServiceStatistics 0 ' ,
)
for line in default_lines :
if line in config_lines :
config_lines . remove ( line )
result = dict ( [ line . split ( ' ' , 1 ) for line in config_lines ] )
self . _set_cache ( { ' get_custom_options ' : result } )
return result
2015-11-23 21:13:53 +00:00
def set_conf ( self , param , value ) :
"""
Changes the value of a tor configuration option . Our value can be any of
the following . . .
* a string to set a single value
* a list of strings to set a series of values ( for instance the ExitPolicy )
* None to either set the value to 0 / NULL
: param str param : configuration option to be set
: param str , list value : value to set the parameter to
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . InvalidArguments ` if configuration options
requested was invalid
* : class : ` stem . InvalidRequest ` if the configuration setting is
impossible or if there ' s a syntax error in the configuration values
"""
self . set_options ( { param : value } , False )
def reset_conf ( self , * params ) :
"""
Reverts one or more parameters to their default values .
: param str params : configuration option to be reset
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . InvalidArguments ` if configuration options requested was invalid
* : class : ` stem . InvalidRequest ` if the configuration setting is
impossible or if there ' s a syntax error in the configuration values
"""
self . set_options ( dict ( [ ( entry , None ) for entry in params ] ) , True )
def set_options ( self , params , reset = False ) :
"""
Changes multiple tor configuration options via either a SETCONF or
RESETCONF query . Both behave identically unless our value is None , in which
case SETCONF sets the value to 0 or NULL , and RESETCONF returns it to its
default value . This accepts str , list , or None values in a similar fashion
to : func : ` ~ stem . control . Controller . set_conf ` . For example . . .
: :
my_controller . set_options ( {
' Nickname ' : ' caerSidi ' ,
' ExitPolicy ' : [ ' accept *:80 ' , ' accept *:443 ' , ' reject *:* ' ] ,
' ContactInfo ' : ' caerSidi-exit@someplace.com ' ,
' Log ' : None ,
} )
The params can optionally be a list of key / value tuples , though the only
reason this type of argument would be useful is for hidden service
configuration ( those options are order dependent ) .
: param dict , list params : mapping of configuration options to the values
we ' re setting it to
: param bool reset : issues a RESETCONF , returning * * None * * values to their
defaults if * * True * *
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . InvalidArguments ` if configuration options
requested was invalid
* : class : ` stem . InvalidRequest ` if the configuration setting is
impossible or if there ' s a syntax error in the configuration values
"""
start_time = time . time ( )
# constructs the SETCONF or RESETCONF query
query_comp = [ ' RESETCONF ' if reset else ' SETCONF ' ]
if isinstance ( params , dict ) :
params = list ( params . items ( ) )
for param , value in params :
if isinstance ( value , str ) :
query_comp . append ( ' %s = " %s " ' % ( param , value . strip ( ) ) )
2018-12-15 00:08:54 +00:00
elif isinstance ( value , collections . Iterable ) :
2015-11-23 21:13:53 +00:00
query_comp . extend ( [ ' %s = " %s " ' % ( param , val . strip ( ) ) for val in value ] )
2018-12-15 00:08:54 +00:00
elif not value :
2015-11-23 21:13:53 +00:00
query_comp . append ( param )
2018-12-15 00:08:54 +00:00
else :
raise ValueError ( ' Cannot set %s to %s since the value was a %s but we only accept strings ' % ( param , value , type ( value ) . __name__ ) )
2015-11-23 21:13:53 +00:00
query = ' ' . join ( query_comp )
response = self . msg ( query )
stem . response . convert ( ' SINGLELINE ' , response )
if response . is_ok ( ) :
log . debug ( ' %s (runtime: %0.4f ) ' % ( query , time . time ( ) - start_time ) )
if self . is_caching_enabled ( ) :
2018-12-15 00:08:54 +00:00
# clear cache for params; the CONF_CHANGED event will set cache for changes
to_cache = dict ( ( k . lower ( ) , None ) for k , v in params )
2015-11-23 21:13:53 +00:00
self . _set_cache ( to_cache , ' getconf ' )
2018-12-15 00:08:54 +00:00
self . _confchanged_cache_invalidation ( dict ( params ) )
2015-11-23 21:13:53 +00:00
else :
log . debug ( ' %s (failed, code: %s , message: %s ) ' % ( query , response . code , response . message ) )
2018-12-15 00:08:54 +00:00
immutable_params = [ k for k , v in params if stem . util . str_tools . _to_unicode ( k ) . lower ( ) in IMMUTABLE_CONFIG_OPTIONS ]
if immutable_params :
raise stem . InvalidArguments ( message = " %s cannot be changed while tor ' s running " % ' , ' . join ( sorted ( immutable_params ) ) , arguments = immutable_params )
2015-11-23 21:13:53 +00:00
if response . code == ' 552 ' :
if response . message . startswith ( " Unrecognized option: Unknown option ' " ) :
key = response . message [ 37 : response . message . find ( " ' " , 37 ) ]
raise stem . InvalidArguments ( response . code , response . message , [ key ] )
raise stem . InvalidRequest ( response . code , response . message )
elif response . code in ( ' 513 ' , ' 553 ' ) :
raise stem . InvalidRequest ( response . code , response . message )
else :
raise stem . ProtocolError ( ' Returned unexpected status code: %s ' % response . code )
@with_default ( )
def get_hidden_service_conf ( self , default = UNDEFINED ) :
"""
get_hidden_service_conf ( default = UNDEFINED )
This provides a mapping of hidden service directories to their
attribute ' s key/value pairs. All hidden services are assured to have a
' HiddenServicePort ' , but other entries may or may not exist .
: :
{
" /var/lib/tor/hidden_service_empty/ " : {
" HiddenServicePort " : [
]
} ,
" /var/lib/tor/hidden_service_with_two_ports/ " : {
" HiddenServiceAuthorizeClient " : " stealth a, b " ,
" HiddenServicePort " : [
( 8020 , " 127.0.0.1 " , 8020 ) , # the ports order is kept
( 8021 , " 127.0.0.1 " , 8021 )
] ,
" HiddenServiceVersion " : " 2 "
} ,
}
. . versionadded : : 1.3 .0
: param object default : response if the query fails
: returns : * * dict * * with the hidden service configuration
: raises : : class : ` stem . ControllerError ` if the call fails and we weren ' t
provided a default response
"""
2018-12-15 00:08:54 +00:00
service_dir_map = self . _get_cache ( ' hidden_service_conf ' )
if service_dir_map is not None :
if LOG_CACHE_FETCHES :
log . trace ( ' GETCONF HiddenServiceOptions (cache fetch) ' )
return service_dir_map
2015-11-23 21:13:53 +00:00
start_time = time . time ( )
try :
response = self . msg ( ' GETCONF HiddenServiceOptions ' )
stem . response . convert ( ' GETCONF ' , response )
log . debug ( ' GETCONF HiddenServiceOptions (runtime: %0.4f ) ' %
( time . time ( ) - start_time ) )
except stem . ControllerError as exc :
log . debug ( ' GETCONF HiddenServiceOptions (failed: %s ) ' % exc )
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
service_dir_map = OrderedDict ( )
directory = None
for status_code , divider , content in response . content ( ) :
if content == ' HiddenServiceOptions ' :
continue
if ' = ' not in content :
continue
k , v = content . split ( ' = ' , 1 )
if k == ' HiddenServiceDir ' :
directory = v
service_dir_map [ directory ] = { ' HiddenServicePort ' : [ ] }
elif k == ' HiddenServicePort ' :
port = target_port = v
target_address = ' 127.0.0.1 '
if not v . isdigit ( ) :
port , target = v . split ( )
if target . isdigit ( ) :
target_port = target
else :
2018-12-15 00:08:54 +00:00
target_address , target_port = target . rsplit ( ' : ' , 1 )
2015-11-23 21:13:53 +00:00
if not stem . util . connection . is_valid_port ( port ) :
raise stem . ProtocolError ( ' GETCONF provided an invalid HiddenServicePort port ( %s ): %s ' % ( port , content ) )
elif not stem . util . connection . is_valid_ipv4_address ( target_address ) :
raise stem . ProtocolError ( ' GETCONF provided an invalid HiddenServicePort target address ( %s ): %s ' % ( target_address , content ) )
elif not stem . util . connection . is_valid_port ( target_port ) :
raise stem . ProtocolError ( ' GETCONF provided an invalid HiddenServicePort target port ( %s ): %s ' % ( target_port , content ) )
service_dir_map [ directory ] [ ' HiddenServicePort ' ] . append ( ( int ( port ) , target_address , int ( target_port ) ) )
else :
service_dir_map [ directory ] [ k ] = v
2018-12-15 00:08:54 +00:00
self . _set_cache ( { ' hidden_service_conf ' : service_dir_map } )
2015-11-23 21:13:53 +00:00
return service_dir_map
def set_hidden_service_conf ( self , conf ) :
"""
Update all the configured hidden services from a dictionary having
the same format as
: func : ` ~ stem . control . Controller . get_hidden_service_conf ` .
For convenience the HiddenServicePort entries can be an integer , string , or
tuple . If an * * int * * then we treat it as just a port . If a * * str * * we pass
that directly as the HiddenServicePort . And finally , if a * * tuple * * then
it ' s expected to be the **(port, target_address, target_port)** as provided
by : func : ` ~ stem . control . Controller . get_hidden_service_conf ` .
This is to say the following three are equivalent . . .
: :
" HiddenServicePort " : [
80 ,
' 80 127.0.0.1:80 ' ,
( 80 , ' 127.0.0.1 ' , 80 ) ,
]
. . versionadded : : 1.3 .0
: param dict conf : configuration dictionary
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . InvalidArguments ` if configuration options
requested was invalid
* : class : ` stem . InvalidRequest ` if the configuration setting is
impossible or if there ' s a syntax error in the configuration values
"""
# If we're not adding or updating any hidden services then call RESETCONF
# so we drop existing values. Otherwise calling SETCONF is a no-op.
if not conf :
self . reset_conf ( ' HiddenServiceDir ' )
return
# Convert conf dictionary into a list of ordered config tuples
hidden_service_options = [ ]
for directory in conf :
hidden_service_options . append ( ( ' HiddenServiceDir ' , directory ) )
for k , v in list ( conf [ directory ] . items ( ) ) :
if k == ' HiddenServicePort ' :
for entry in v :
if isinstance ( entry , int ) :
entry = ' %s 127.0.0.1: %s ' % ( entry , entry )
elif isinstance ( entry , str ) :
pass # just pass along what the user gave us
elif isinstance ( entry , tuple ) :
port , target_address , target_port = entry
entry = ' %s %s : %s ' % ( port , target_address , target_port )
hidden_service_options . append ( ( ' HiddenServicePort ' , entry ) )
else :
hidden_service_options . append ( ( k , str ( v ) ) )
self . set_options ( hidden_service_options )
def create_hidden_service ( self , path , port , target_address = None , target_port = None , auth_type = None , client_names = None ) :
"""
Create a new hidden service . If the directory is already present , a
2018-12-15 00:08:54 +00:00
new port is added .
2015-11-23 21:13:53 +00:00
Our * . onion address is fetched by reading the hidden service directory .
However , this directory is only readable by the tor user , so if unavailable
the * * hostname * * will be * * None * * .
2018-12-15 00:08:54 +00:00
* * As of Tor 0.2 .7 .1 there ' s two ways for creating hidden services, and this
method is no longer recommended . * * Rather , try using
2015-11-23 21:13:53 +00:00
: func : ` ~ stem . control . Controller . create_ephemeral_hidden_service ` instead .
. . versionadded : : 1.3 .0
. . versionchanged : : 1.4 .0
Added the auth_type and client_names arguments .
: param str path : path for the hidden service ' s data directory
: param int port : hidden service port
: param str target_address : address of the service , by default 127.0 .0 .1
: param int target_port : port of the service , by default this is the same as
* * port * *
: param str auth_type : authentication type : basic , stealth or None to disable auth
: param list client_names : client names ( 1 - 16 characters " A-Za-z0-9+-_ " )
2018-12-15 00:08:54 +00:00
: returns : : class : ` ~ stem . cotroller . CreateHiddenServiceOutput ` if we create
or update a hidden service , * * None * * otherwise
2015-11-23 21:13:53 +00:00
: raises : : class : ` stem . ControllerError ` if the call fails
"""
if not stem . util . connection . is_valid_port ( port ) :
raise ValueError ( " %s isn ' t a valid port number " % port )
elif target_address and not stem . util . connection . is_valid_ipv4_address ( target_address ) :
raise ValueError ( " %s isn ' t a valid IPv4 address " % target_address )
elif target_port is not None and not stem . util . connection . is_valid_port ( target_port ) :
raise ValueError ( " %s isn ' t a valid port number " % target_port )
elif auth_type not in ( None , ' basic ' , ' stealth ' ) :
raise ValueError ( " %s isn ' t a recognized type of authentication " % auth_type )
port = int ( port )
target_address = target_address if target_address else ' 127.0.0.1 '
target_port = port if target_port is None else int ( target_port )
conf = self . get_hidden_service_conf ( )
if path in conf and ( port , target_address , target_port ) in conf [ path ] [ ' HiddenServicePort ' ] :
return None
conf . setdefault ( path , OrderedDict ( ) ) . setdefault ( ' HiddenServicePort ' , [ ] ) . append ( ( port , target_address , target_port ) )
if auth_type and client_names :
hsac = " %s %s " % ( auth_type , ' , ' . join ( client_names ) )
conf [ path ] [ ' HiddenServiceAuthorizeClient ' ] = hsac
2018-12-15 00:08:54 +00:00
# Tor 0.3.5 changes its default for HS creation from v2 to v3. This is
# fine, but there's a couple options that are incompatible with v3. If
# creating a service with one of those we should explicitly create a v2
# service instead.
#
# https://trac.torproject.org/projects/tor/ticket/27446
for path in conf :
if ' HiddenServiceAuthorizeClient ' in conf [ path ] or ' RendPostPeriod ' in conf [ path ] :
conf [ path ] [ ' HiddenServiceVersion ' ] = ' 2 '
2015-11-23 21:13:53 +00:00
self . set_hidden_service_conf ( conf )
hostname , hostname_for_client = None , { }
if self . is_localhost ( ) :
hostname_path = os . path . join ( path , ' hostname ' )
if not os . path . isabs ( hostname_path ) :
cwd = stem . util . system . cwd ( self . get_pid ( None ) )
if cwd :
hostname_path = stem . util . system . expand_path ( hostname_path , cwd )
if os . path . isabs ( hostname_path ) :
start_time = time . time ( )
while not os . path . exists ( hostname_path ) :
wait_time = time . time ( ) - start_time
if wait_time > = 3 :
break
else :
time . sleep ( 0.05 )
try :
with open ( hostname_path ) as hostname_file :
hostname = hostname_file . read ( ) . strip ( )
if client_names and ' \n ' in hostname :
# When there's multiple clients this looks like...
#
# ndisjxzkgcdhrwqf.onion sjUwjTSPznqWLdOPuwRUzg # client: c1
# ndisjxzkgcdhrwqf.onion sUu92axuL5bKnA76s2KRfw # client: c2
for line in hostname . splitlines ( ) :
if ' # client: ' in line :
address = line . split ( ) [ 0 ]
client = line . split ( ' # client: ' , 1 ) [ 1 ]
if len ( address ) == 22 and address . endswith ( ' .onion ' ) :
hostname_for_client [ client ] = address
except :
pass
return CreateHiddenServiceOutput (
path = path ,
hostname = hostname ,
hostname_for_client = hostname_for_client ,
config = conf ,
)
def remove_hidden_service ( self , path , port = None ) :
"""
Discontinues a given hidden service .
. . versionadded : : 1.3 .0
: param str path : path for the hidden service ' s data directory
: param int port : hidden service port
: returns : * * True * * if the hidden service is discontinued , * * False * * if it
wasn ' t running in the first place
: raises : : class : ` stem . ControllerError ` if the call fails
"""
if port and not stem . util . connection . is_valid_port ( port ) :
raise ValueError ( " %s isn ' t a valid port number " % port )
port = int ( port ) if port else None
conf = self . get_hidden_service_conf ( )
if path not in conf :
return False
if not port :
del conf [ path ]
else :
to_remove = [ entry for entry in conf [ path ] [ ' HiddenServicePort ' ] if entry [ 0 ] == port ]
if not to_remove :
return False
for entry in to_remove :
conf [ path ] [ ' HiddenServicePort ' ] . remove ( entry )
if not conf [ path ] [ ' HiddenServicePort ' ] :
del conf [ path ] # no ports left
self . set_hidden_service_conf ( conf )
return True
@with_default ( )
def list_ephemeral_hidden_services ( self , default = UNDEFINED , our_services = True , detached = False ) :
"""
list_ephemeral_hidden_services ( default = UNDEFINED , our_services = True , detached = False )
Lists hidden service addresses created by
: func : ` ~ stem . control . Controller . create_ephemeral_hidden_service ` .
. . versionadded : : 1.4 .0
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.6 .0
Tor change caused this to start providing empty strings if unset
( : trac : ` 21329 ` ) .
2015-11-23 21:13:53 +00:00
: param object default : response if the query fails
: param bool our_services : include services created with this controller
that weren ' t flagged as ' detached '
: param bool detached : include services whos contiuation isn ' t tied to a
controller
: returns : * * list * * of hidden service addresses without their ' .onion '
suffix
: raises : : class : ` stem . ControllerError ` if the call fails and we weren ' t
provided a default response
"""
if self . get_version ( ) < stem . version . Requirement . ADD_ONION :
raise stem . UnsatisfiableRequest ( message = ' Ephemeral hidden services were added in tor version %s ' % stem . version . Requirement . ADD_ONION )
result = [ ]
if our_services :
try :
result + = self . get_info ( ' onions/current ' ) . split ( ' \n ' )
except stem . ProtocolError as exc :
2018-12-15 00:08:54 +00:00
# TODO: Tor's behavior around this was changed in Feb 2017, we should
# drop it when all versions that did this are deprecated...
#
# https://trac.torproject.org/projects/tor/ticket/21329
2015-11-23 21:13:53 +00:00
if ' No onion services of the specified type. ' not in str ( exc ) :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
if detached :
try :
result + = self . get_info ( ' onions/detached ' ) . split ( ' \n ' )
except stem . ProtocolError as exc :
if ' No onion services of the specified type. ' not in str ( exc ) :
2018-12-15 00:08:54 +00:00
raise
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
return [ r for r in result if r ] # drop any empty responses (GETINFO is blank if unset)
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
def create_ephemeral_hidden_service ( self , ports , key_type = ' NEW ' , key_content = ' BEST ' , discard_key = False , detached = False , await_publication = False , timeout = None , basic_auth = None , max_streams = None ) :
2015-11-23 21:13:53 +00:00
"""
Creates a new hidden service . Unlike
: func : ` ~ stem . control . Controller . create_hidden_service ` this style of
hidden service doesn ' t touch disk, carrying with it a lot of advantages.
This is the suggested method for making hidden services .
Our * * ports * * argument can be a single port . . .
: :
create_ephemeral_hidden_service ( 80 )
. . . list of ports the service is available on . . .
: :
create_ephemeral_hidden_service ( [ 80 , 443 ] )
. . . or a mapping of hidden service ports to their targets . . .
: :
create_ephemeral_hidden_service ( { 80 : 80 , 443 : ' 173.194.33.133:443 ' } )
2018-12-15 00:08:54 +00:00
If * * basic_auth * * is provided this service will require basic
authentication to access . This means users must set HidServAuth in their
torrc with credentials to access it .
* * basic_auth * * is a mapping of usernames to their credentials . If the
credential is * * None * * one is generated and returned as part of the
response . For instance , only bob can access using the given newly generated
credentials . . .
: :
>> > response = controller . create_ephemeral_hidden_service ( 80 , basic_auth = { ' bob ' : None } )
>> > print ( response . client_auth )
{ ' bob ' : ' nKwfvVPmTNr2k2pG0pzV4g ' }
. . . while both alice and bob can access with existing credentials in the
following . . .
: :
controller . create_ephemeral_hidden_service ( 80 , basic_auth = {
' alice ' : ' l4BT016McqV2Oail+Bwe6w ' ,
' bob ' : ' vGnNRpWYiMBFTWD2gbBlcA ' ,
} )
To create a * * version 3 * * service simply specify * * ED25519 - V3 * * as the
our key type , and to create a * * version 2 * * service use * * RSA1024 * * . The
default version of newly created hidden services is based on the
* * HiddenServiceVersion * * value in your torrc . . .
: :
response = controller . create_ephemeral_hidden_service (
80 ,
key_content = ' ED25519-V3 ' ,
await_publication = True ,
)
print ( ' service established at %s .onion ' % response . service_id )
2015-11-23 21:13:53 +00:00
. . versionadded : : 1.4 .0
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.5 .0
Added the basic_auth argument .
. . versionchanged : : 1.5 .0
Added support for non - anonymous services . To do so set
' HiddenServiceSingleHopMode 1 ' and ' HiddenServiceNonAnonymousMode 1 ' in
your torrc .
. . versionchanged : : 1.7 .0
Added the timeout and max_streams arguments .
2015-11-23 21:13:53 +00:00
: param int , list , dict ports : hidden service port ( s ) or mapping of hidden
service ports to their targets
: param str key_type : type of key being provided , generates a new key if
2018-12-15 00:08:54 +00:00
' NEW ' ( options are : * * NEW * * , * * RSA1024 * * , and * * ED25519 - V3 * * )
2015-11-23 21:13:53 +00:00
: param str key_content : key for the service to use or type of key to be
2018-12-15 00:08:54 +00:00
generated ( options when * * key_type * * is * * NEW * * are * * BEST * * ,
* * RSA1024 * * , and * * ED25519 - V3 * * )
2015-11-23 21:13:53 +00:00
: param bool discard_key : avoid providing the key back in our response
: param bool detached : continue this hidden service even after this control
connection is closed if * * True * *
: param bool await_publication : blocks until our descriptor is successfully
published if * * True * *
2018-12-15 00:08:54 +00:00
: param float timeout : seconds to wait when * * await_result * * is * * True * *
: param dict basic_auth : required user credentials to access this service
: param int max_streams : maximum number of streams the hidden service will
accept , unlimited if zero or not set
2015-11-23 21:13:53 +00:00
: returns : : class : ` ~ stem . response . add_onion . AddOnionResponse ` with the response
2018-12-15 00:08:54 +00:00
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . Timeout ` if * * timeout * * was reached
2015-11-23 21:13:53 +00:00
"""
if self . get_version ( ) < stem . version . Requirement . ADD_ONION :
raise stem . UnsatisfiableRequest ( message = ' Ephemeral hidden services were added in tor version %s ' % stem . version . Requirement . ADD_ONION )
hs_desc_queue , hs_desc_listener = queue . Queue ( ) , None
2018-12-15 00:08:54 +00:00
start_time = time . time ( )
2015-11-23 21:13:53 +00:00
if await_publication :
def hs_desc_listener ( event ) :
hs_desc_queue . put ( event )
self . add_event_listener ( hs_desc_listener , EventType . HS_DESC )
request = ' ADD_ONION %s : %s ' % ( key_type , key_content )
flags = [ ]
if discard_key :
flags . append ( ' DiscardPK ' )
if detached :
flags . append ( ' Detach ' )
2018-12-15 00:08:54 +00:00
if basic_auth is not None :
if self . get_version ( ) < stem . version . Requirement . ADD_ONION_BASIC_AUTH :
raise stem . UnsatisfiableRequest ( message = ' Basic authentication support was added to ADD_ONION in tor version %s ' % stem . version . Requirement . ADD_ONION_BASIC_AUTH )
flags . append ( ' BasicAuth ' )
if max_streams is not None :
if self . get_version ( ) < stem . version . Requirement . ADD_ONION_MAX_STREAMS :
raise stem . UnsatisfiableRequest ( message = ' Limitation of the maximum number of streams to accept was added to ADD_ONION in tor version %s ' % stem . version . Requirement . ADD_ONION_MAX_STREAMS )
flags . append ( ' MaxStreamsCloseCircuit ' )
if self . get_version ( ) > = stem . version . Requirement . ADD_ONION_NON_ANONYMOUS :
if self . get_conf ( ' HiddenServiceSingleHopMode ' , None ) == ' 1 ' and self . get_conf ( ' HiddenServiceNonAnonymousMode ' , None ) == ' 1 ' :
flags . append ( ' NonAnonymous ' )
2015-11-23 21:13:53 +00:00
if flags :
request + = ' Flags= %s ' % ' , ' . join ( flags )
2018-12-15 00:08:54 +00:00
if max_streams is not None :
request + = ' MaxStreams= %s ' % max_streams
2015-11-23 21:13:53 +00:00
if isinstance ( ports , int ) :
request + = ' Port= %s ' % ports
elif isinstance ( ports , list ) :
for port in ports :
request + = ' Port= %s ' % port
elif isinstance ( ports , dict ) :
for port , target in ports . items ( ) :
request + = ' Port= %s , %s ' % ( port , target )
else :
raise ValueError ( " The ' ports ' argument of create_ephemeral_hidden_service() needs to be an int, list, or dict " )
2018-12-15 00:08:54 +00:00
if basic_auth is not None :
for client_name , client_blob in basic_auth . items ( ) :
if client_blob :
request + = ' ClientAuth= %s : %s ' % ( client_name , client_blob )
else :
request + = ' ClientAuth= %s ' % client_name
2015-11-23 21:13:53 +00:00
response = self . msg ( request )
stem . response . convert ( ' ADD_ONION ' , response )
if await_publication :
# We should receive five UPLOAD events, followed by up to another five
# UPLOADED to indicate they've finished. Presently tor seems to have an
# issue where the address is provided for UPLOAD but not UPLOADED so need
# to just guess that if it's for the same hidden service authority then
# it's what we're looking for.
directories_uploaded_to , failures = [ ] , [ ]
try :
while True :
2018-12-15 00:08:54 +00:00
event = _get_with_timeout ( hs_desc_queue , timeout , start_time )
2015-11-23 21:13:53 +00:00
if event . action == stem . HSDescAction . UPLOAD and event . address == response . service_id :
directories_uploaded_to . append ( event . directory_fingerprint )
elif event . action == stem . HSDescAction . UPLOADED and event . directory_fingerprint in directories_uploaded_to :
break # successfully uploaded to a HS authority... maybe
elif event . action == stem . HSDescAction . FAILED and event . directory_fingerprint in directories_uploaded_to :
failures . append ( ' %s ( %s ) ' % ( event . directory_fingerprint , event . reason ) )
if len ( directories_uploaded_to ) == len ( failures ) :
raise stem . OperationFailed ( message = ' Failed to upload our hidden service descriptor to %s ' % ' , ' . join ( failures ) )
finally :
self . remove_event_listener ( hs_desc_listener )
return response
def remove_ephemeral_hidden_service ( self , service_id ) :
"""
Discontinues a given hidden service that was created with
: func : ` ~ stem . control . Controller . create_ephemeral_hidden_service ` .
. . versionadded : : 1.4 .0
: param str service_id : hidden service address without the ' .onion ' suffix
: returns : * * True * * if the hidden service is discontinued , * * False * * if it
wasn ' t running in the first place
: raises : : class : ` stem . ControllerError ` if the call fails
"""
if self . get_version ( ) < stem . version . Requirement . ADD_ONION :
raise stem . UnsatisfiableRequest ( message = ' Ephemeral hidden services were added in tor version %s ' % stem . version . Requirement . ADD_ONION )
response = self . msg ( ' DEL_ONION %s ' % service_id )
stem . response . convert ( ' SINGLELINE ' , response )
if response . is_ok ( ) :
return True
elif response . code == ' 552 ' :
return False # no hidden service to discontinue
else :
raise stem . ProtocolError ( ' DEL_ONION returned unexpected response code: %s ' % response . code )
def add_event_listener ( self , listener , * events ) :
"""
Directs further tor controller events to a given function . The function is
expected to take a single argument , which is a
: class : ` ~ stem . response . events . Event ` subclass . For instance the following
would print the bytes sent and received by tor over five seconds . . .
: :
import time
from stem . control import Controller , EventType
def print_bw ( event ) :
print ( ' sent: %i , received: %i ' % ( event . written , event . read ) )
with Controller . from_port ( port = 9051 ) as controller :
controller . authenticate ( )
controller . add_event_listener ( print_bw , EventType . BW )
time . sleep ( 5 )
If a new control connection is initialized then this listener will be
reattached .
2018-12-15 00:08:54 +00:00
If tor emits a malformed event it can be received by listening for the
stem . control . MALFORMED_EVENTS constant .
. . versionchanged : : 1.7 .0
Listener exceptions and malformed events no longer break further event
processing . Added the * * MALFORMED_EVENTS * * constant .
2015-11-23 21:13:53 +00:00
: param functor listener : function to be called when an event is received
: param stem . control . EventType events : event types to be listened for
: raises : : class : ` stem . ProtocolError ` if unable to set the events
"""
# first checking that tor supports these event types
with self . _event_listeners_lock :
if self . is_authenticated ( ) :
for event_type in events :
event_type = stem . response . events . EVENT_TYPE_TO_CLASS . get ( event_type )
if event_type and ( self . get_version ( ) < event_type . _VERSION_ADDED ) :
raise stem . InvalidRequest ( 552 , ' %s event requires Tor version %s or later ' % ( event_type , event_type . _VERSION_ADDED ) )
for event_type in events :
self . _event_listeners . setdefault ( event_type , [ ] ) . append ( listener )
failed_events = self . _attach_listeners ( ) [ 1 ]
# restricted the failures to just things we requested
failed_events = set ( failed_events ) . intersection ( set ( events ) )
if failed_events :
raise stem . ProtocolError ( ' SETEVENTS rejected %s ' % ' , ' . join ( failed_events ) )
def remove_event_listener ( self , listener ) :
"""
Stops a listener from being notified of further tor events .
: param stem . control . EventListener listener : listener to be removed
: raises : : class : ` stem . ProtocolError ` if unable to set the events
"""
with self . _event_listeners_lock :
event_types_changed = False
for event_type , event_listeners in list ( self . _event_listeners . items ( ) ) :
if listener in event_listeners :
event_listeners . remove ( listener )
if len ( event_listeners ) == 0 :
event_types_changed = True
del self . _event_listeners [ event_type ]
if event_types_changed :
response = self . msg ( ' SETEVENTS %s ' % ' ' . join ( self . _event_listeners . keys ( ) ) )
if not response . is_ok ( ) :
raise stem . ProtocolError ( ' SETEVENTS received unexpected response \n %s ' % response )
def _get_cache ( self , param , namespace = None ) :
"""
Queries our request cache for the given key .
: param str param : key to be queried
: param str namespace : namespace in which to check for the key
: returns : cached value corresponding to key or * * None * * if the key wasn ' t found
"""
2018-12-15 00:08:54 +00:00
with self . _cache_lock :
if not self . is_caching_enabled ( ) :
return None
cache_key = ' %s . %s ' % ( namespace , param ) if namespace else param
return self . _request_cache . get ( cache_key , None )
2015-11-23 21:13:53 +00:00
def _get_cache_map ( self , params , namespace = None ) :
"""
Queries our request cache for multiple entries .
: param list params : keys to be queried
: param str namespace : namespace in which to check for the keys
: returns : * * dict * * of ' param => cached value ' pairs of keys present in cache
"""
with self . _cache_lock :
cached_values = { }
if self . is_caching_enabled ( ) :
for param in params :
2018-12-15 00:08:54 +00:00
cache_key = ' %s . %s ' % ( namespace , param ) if namespace else param
2015-11-23 21:13:53 +00:00
if cache_key in self . _request_cache :
cached_values [ param ] = self . _request_cache [ cache_key ]
return cached_values
def _set_cache ( self , params , namespace = None ) :
"""
Sets the given request cache entries . If the new cache value is * * None * *
then it is removed from our cache .
: param dict params : * * dict * * of ' cache_key => value ' pairs to be cached
: param str namespace : namespace for the keys
"""
with self . _cache_lock :
if not self . is_caching_enabled ( ) :
return
2018-12-15 00:08:54 +00:00
# if params is None then clear the namespace
if params is None and namespace :
for cache_key in list ( self . _request_cache . keys ( ) ) :
if cache_key . startswith ( ' %s . ' % namespace ) :
del self . _request_cache [ cache_key ]
return
# remove uncacheable items
if namespace == ' getconf ' :
# shallow copy before edit so as not to change it for the caller
params = params . copy ( )
for key in UNCACHEABLE_GETCONF_PARAMS :
if key in params :
del params [ key ]
2015-11-23 21:13:53 +00:00
for key , value in list ( params . items ( ) ) :
if namespace :
cache_key = ' %s . %s ' % ( namespace , key )
else :
cache_key = key
if value is None :
2018-12-15 00:08:54 +00:00
if cache_key in list ( self . _request_cache . keys ( ) ) :
2015-11-23 21:13:53 +00:00
del self . _request_cache [ cache_key ]
else :
self . _request_cache [ cache_key ] = value
2018-12-15 00:08:54 +00:00
def _confchanged_cache_invalidation ( self , params ) :
"""
Drops dependent portions of the cache when configuration changes .
: param dict params : * * dict * * of ' config_key => value ' pairs for configs
that changed . The entries ' values are currently unused.
"""
with self . _cache_lock :
if not self . is_caching_enabled ( ) :
return
if any ( ' hidden ' in param . lower ( ) for param in params . keys ( ) ) :
self . _set_cache ( { ' hidden_service_conf ' : None } )
# reset any getinfo parameters that can be changed by a SETCONF
self . _set_cache ( dict ( [ ( k . lower ( ) , None ) for k in CACHEABLE_GETINFO_PARAMS_UNTIL_SETCONF ] ) , ' getinfo ' )
self . _set_cache ( None , ' listeners ' )
self . _set_cache ( { ' get_custom_options ' : None } )
self . _set_cache ( { ' exit_policy ' : None } ) # numerous options can change our policy
2015-11-23 21:13:53 +00:00
def is_caching_enabled ( self ) :
"""
* * True * * if caching has been enabled , * * False * * otherwise .
: returns : bool to indicate if caching is enabled
"""
return self . _is_caching_enabled
def set_caching ( self , enabled ) :
"""
Enables or disables caching of information retrieved from tor .
: param bool enabled : * * True * * to enable caching , * * False * * to disable it
"""
self . _is_caching_enabled = enabled
if not self . _is_caching_enabled :
self . clear_cache ( )
def clear_cache ( self ) :
"""
Drops any cached results .
"""
with self . _cache_lock :
self . _request_cache = { }
self . _last_newnym = 0.0
2018-12-15 00:08:54 +00:00
self . _is_geoip_unavailable = None
2015-11-23 21:13:53 +00:00
def load_conf ( self , configtext ) :
"""
Sends the configuration text to Tor and loads it as if it has been read from
the torrc .
: param str configtext : the configuration text
: raises : : class : ` stem . ControllerError ` if the call fails
"""
response = self . msg ( ' LOADCONF \n %s ' % configtext )
stem . response . convert ( ' SINGLELINE ' , response )
if response . code in ( ' 552 ' , ' 553 ' ) :
if response . code == ' 552 ' and response . message . startswith ( ' Invalid config file: Failed to parse/validate config: Unknown option ' ) :
raise stem . InvalidArguments ( response . code , response . message , [ response . message [ 70 : response . message . find ( ' . ' , 70 ) - 1 ] ] )
raise stem . InvalidRequest ( response . code , response . message )
elif not response . is_ok ( ) :
raise stem . ProtocolError ( ' +LOADCONF Received unexpected response \n %s ' % str ( response ) )
2018-12-15 00:08:54 +00:00
def save_conf ( self , force = False ) :
2015-11-23 21:13:53 +00:00
"""
Saves the current configuration options into the active torrc file .
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.6 .0
Added the force argument .
: param bool force : overwrite the configuration even if it includes a
' %i nclude ' clause , this is ignored if tor doesn ' t support it
2015-11-23 21:13:53 +00:00
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . OperationFailed ` if the client is unable to save
the configuration file
"""
2018-12-15 00:08:54 +00:00
if self . get_version ( ) < stem . version . Requirement . SAVECONF_FORCE :
force = False
response = self . msg ( ' SAVECONF FORCE ' if force else ' SAVECONF ' )
2015-11-23 21:13:53 +00:00
stem . response . convert ( ' SINGLELINE ' , response )
if response . is_ok ( ) :
return True
elif response . code == ' 551 ' :
raise stem . OperationFailed ( response . code , response . message )
else :
raise stem . ProtocolError ( ' SAVECONF returned unexpected response code ' )
def is_feature_enabled ( self , feature ) :
"""
Checks if a control connection feature is enabled . These features can be
enabled using : func : ` ~ stem . control . Controller . enable_feature ` .
: param str feature : feature to be checked
: returns : * * True * * if feature is enabled , * * False * * otherwise
"""
feature = feature . upper ( )
if feature in self . _enabled_features :
return True
else :
# check if this feature is on by default
defaulted_version = None
if feature == ' EXTENDED_EVENTS ' :
defaulted_version = stem . version . Requirement . FEATURE_EXTENDED_EVENTS
elif feature == ' VERBOSE_NAMES ' :
defaulted_version = stem . version . Requirement . FEATURE_VERBOSE_NAMES
if defaulted_version :
our_version = self . get_version ( None )
if our_version and our_version > = defaulted_version :
self . _enabled_features . append ( feature )
return feature in self . _enabled_features
def enable_feature ( self , features ) :
"""
Enables features that are disabled by default to maintain backward
compatibility . Once enabled , a feature cannot be disabled and a new
control connection must be opened to get a connection with the feature
disabled . Feature names are case - insensitive .
The following features are currently accepted :
* EXTENDED_EVENTS - Requests the extended event syntax
* VERBOSE_NAMES - Replaces ServerID with LongName in events and GETINFO results
: param str , list features : a single feature or a list of features to be enabled
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . InvalidArguments ` if features passed were invalid
"""
2018-12-15 00:08:54 +00:00
if stem . util . _is_str ( features ) :
2015-11-23 21:13:53 +00:00
features = [ features ]
response = self . msg ( ' USEFEATURE %s ' % ' ' . join ( features ) )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
if response . code == ' 552 ' :
invalid_feature = [ ]
if response . message . startswith ( ' Unrecognized feature " ' ) :
invalid_feature = [ response . message [ 22 : response . message . find ( ' " ' , 22 ) ] ]
raise stem . InvalidArguments ( response . code , response . message , invalid_feature )
raise stem . ProtocolError ( ' USEFEATURE provided an invalid response code: %s ' % response . code )
self . _enabled_features + = [ entry . upper ( ) for entry in features ]
@with_default ( )
def get_circuit ( self , circuit_id , default = UNDEFINED ) :
"""
get_circuit ( circuit_id , default = UNDEFINED )
Provides a circuit currently available from tor .
: param int circuit_id : circuit to be fetched
: param object default : response if the query fails
: returns : : class : ` stem . response . events . CircuitEvent ` for the given circuit
: raises :
* : class : ` stem . ControllerError ` if the call fails
* * * ValueError * * if the circuit doesn ' t exist
An exception is only raised if we weren ' t provided a default response.
"""
for circ in self . get_circuits ( ) :
if circ . id == circuit_id :
return circ
raise ValueError ( " Tor currently does not have a circuit with the id of ' %s ' " % circuit_id )
@with_default ( )
def get_circuits ( self , default = UNDEFINED ) :
"""
get_circuits ( default = UNDEFINED )
Provides tor ' s currently available circuits.
: param object default : response if the query fails
: returns : * * list * * of : class : ` stem . response . events . CircuitEvent ` for our circuits
: raises : : class : ` stem . ControllerError ` if the call fails and no default was provided
"""
circuits = [ ]
response = self . get_info ( ' circuit-status ' )
for circ in response . splitlines ( ) :
2018-12-15 00:08:54 +00:00
circ_message = stem . socket . recv_message ( io . BytesIO ( stem . util . str_tools . _to_bytes ( ' 650 CIRC %s \r \n ' % circ ) ) )
2015-11-23 21:13:53 +00:00
stem . response . convert ( ' EVENT ' , circ_message , arrived_at = 0 )
circuits . append ( circ_message )
return circuits
2018-12-15 00:08:54 +00:00
def new_circuit ( self , path = None , purpose = ' general ' , await_build = False , timeout = None ) :
2015-11-23 21:13:53 +00:00
"""
Requests a new circuit . If the path isn ' t provided, one is automatically
selected .
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.7 .0
Added the timeout argument .
2015-11-23 21:13:53 +00:00
: param list , str path : one or more relays to make a circuit through
: param str purpose : ' general ' or ' controller '
: param bool await_build : blocks until the circuit is built if * * True * *
2018-12-15 00:08:54 +00:00
: param float timeout : seconds to wait when * * await_build * * is * * True * *
2015-11-23 21:13:53 +00:00
: returns : str of the circuit id of the newly created circuit
2018-12-15 00:08:54 +00:00
: raises :
* : class : ` stem . ControllerError ` if the call fails
* : class : ` stem . Timeout ` if * * timeout * * was reached
2015-11-23 21:13:53 +00:00
"""
2018-12-15 00:08:54 +00:00
return self . extend_circuit ( ' 0 ' , path , purpose , await_build , timeout )
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
def extend_circuit ( self , circuit_id = ' 0 ' , path = None , purpose = ' general ' , await_build = False , timeout = None ) :
2015-11-23 21:13:53 +00:00
"""
Either requests the creation of a new circuit or extends an existing one .
When called with a circuit value of zero ( the default ) a new circuit is
created , and when non - zero the circuit with that id is extended . If the
path isn ' t provided, one is automatically selected.
A python interpreter session used to create circuits could look like this . . .
: :
>> > controller . extend_circuit ( ' 0 ' , [ ' 718BCEA286B531757ACAFF93AE04910EA73DE617 ' , ' 30BAB8EE7606CBD12F3CC269AE976E0153E7A58D ' , ' 2765D8A8C4BBA3F89585A9FFE0E8575615880BEB ' ] )
19
>> > controller . extend_circuit ( ' 0 ' )
20
>> > print ( controller . get_info ( ' circuit-status ' ) )
20 EXTENDED $ 718 BCEA286B531757ACAFF93AE04910EA73DE617 = KsmoinOK , $ 649 F2D0ACF418F7CFC6539AB2257EB2D5297BAFA = Eskimo BUILD_FLAGS = NEED_CAPACITY PURPOSE = GENERAL TIME_CREATED = 2012 - 12 - 06 T13 : 51 : 11.433755
19 BUILT $ 718 BCEA286B531757ACAFF93AE04910EA73DE617 = KsmoinOK , $ 30 BAB8EE7606CBD12F3CC269AE976E0153E7A58D = Pascal1 , $ 2765 D8A8C4BBA3F89585A9FFE0E8575615880BEB = Anthracite PURPOSE = GENERAL TIME_CREATED = 2012 - 12 - 06 T13 : 50 : 56.969938
2018-12-15 00:08:54 +00:00
. . versionchanged : : 1.7 .0
Added the timeout argument .
2015-11-23 21:13:53 +00:00
: param str circuit_id : id of a circuit to be extended
: param list , str path : one or more relays to make a circuit through , this is
required if the circuit id is non - zero
: param str purpose : ' general ' or ' controller '
: param bool await_build : blocks until the circuit is built if * * True * *
2018-12-15 00:08:54 +00:00
: param float timeout : seconds to wait when * * await_build * * is * * True * *
2015-11-23 21:13:53 +00:00
: returns : str of the circuit id of the created or extended circuit
: raises :
* : class : ` stem . InvalidRequest ` if one of the parameters were invalid
* : class : ` stem . CircuitExtensionFailed ` if we were waiting for the circuit
to build but it failed
2018-12-15 00:08:54 +00:00
* : class : ` stem . Timeout ` if * * timeout * * was reached
2015-11-23 21:13:53 +00:00
* : class : ` stem . ControllerError ` if the call fails
"""
# Attaches a temporary listener for CIRC events if we'll be waiting for it
# to build. This is icky, but we can't reliably do this via polling since
# we then can't get the failure if it can't be created.
circ_queue , circ_listener = queue . Queue ( ) , None
2018-12-15 00:08:54 +00:00
start_time = time . time ( )
2015-11-23 21:13:53 +00:00
if await_build :
def circ_listener ( event ) :
circ_queue . put ( event )
self . add_event_listener ( circ_listener , EventType . CIRC )
try :
# we might accidently get integer circuit ids
circuit_id = str ( circuit_id )
if path is None and circuit_id == ' 0 ' :
path_opt_version = stem . version . Requirement . EXTENDCIRCUIT_PATH_OPTIONAL
if not self . get_version ( ) > = path_opt_version :
raise stem . InvalidRequest ( 512 , ' EXTENDCIRCUIT requires the path prior to version %s ' % path_opt_version )
args = [ circuit_id ]
2018-12-15 00:08:54 +00:00
if stem . util . _is_str ( path ) :
2015-11-23 21:13:53 +00:00
path = [ path ]
if path :
args . append ( ' , ' . join ( path ) )
if purpose :
args . append ( ' purpose= %s ' % purpose )
response = self . msg ( ' EXTENDCIRCUIT %s ' % ' ' . join ( args ) )
stem . response . convert ( ' SINGLELINE ' , response )
if response . code in ( ' 512 ' , ' 552 ' ) :
raise stem . InvalidRequest ( response . code , response . message )
elif not response . is_ok ( ) :
raise stem . ProtocolError ( ' EXTENDCIRCUIT returned unexpected response code: %s ' % response . code )
if not response . message . startswith ( ' EXTENDED ' ) :
raise stem . ProtocolError ( ' EXTENDCIRCUIT response invalid: \n %s ' , response )
new_circuit = response . message . split ( ' ' , 1 ) [ 1 ]
if await_build :
while True :
2018-12-15 00:08:54 +00:00
circ = _get_with_timeout ( circ_queue , timeout , start_time )
2015-11-23 21:13:53 +00:00
if circ . id == new_circuit :
if circ . status == CircStatus . BUILT :
break
elif circ . status == CircStatus . FAILED :
raise stem . CircuitExtensionFailed ( ' Circuit failed to be created: %s ' % circ . reason , circ )
elif circ . status == CircStatus . CLOSED :
raise stem . CircuitExtensionFailed ( ' Circuit was closed prior to build ' , circ )
return new_circuit
finally :
if circ_listener :
self . remove_event_listener ( circ_listener )
def repurpose_circuit ( self , circuit_id , purpose ) :
"""
Changes a circuit ' s purpose. Currently, two purposes are recognized...
* general
* controller
: param str circuit_id : id of the circuit whose purpose is to be changed
: param str purpose : purpose ( either ' general ' or ' controller ' )
: raises : : class : ` stem . InvalidArguments ` if the circuit doesn ' t exist or if the purpose was invalid
"""
response = self . msg ( ' SETCIRCUITPURPOSE %s purpose= %s ' % ( circuit_id , purpose ) )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
if response . code == ' 552 ' :
raise stem . InvalidRequest ( response . code , response . message )
else :
raise stem . ProtocolError ( ' SETCIRCUITPURPOSE returned unexpected response code: %s ' % response . code )
def close_circuit ( self , circuit_id , flag = ' ' ) :
"""
Closes the specified circuit .
: param str circuit_id : id of the circuit to be closed
: param str flag : optional value to modify closing , the only flag available
is ' IfUnused ' which will not close the circuit unless it is unused
: raises : : class : ` stem . InvalidArguments ` if the circuit is unknown
: raises : : class : ` stem . InvalidRequest ` if not enough information is provided
"""
response = self . msg ( ' CLOSECIRCUIT %s %s ' % ( circuit_id , flag ) )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
if response . code in ( ' 512 ' , ' 552 ' ) :
if response . message . startswith ( ' Unknown circuit ' ) :
raise stem . InvalidArguments ( response . code , response . message , [ circuit_id ] )
raise stem . InvalidRequest ( response . code , response . message )
else :
raise stem . ProtocolError ( ' CLOSECIRCUIT returned unexpected response code: %s ' % response . code )
@with_default ( )
def get_streams ( self , default = UNDEFINED ) :
"""
get_streams ( default = UNDEFINED )
Provides the list of streams tor is currently handling .
: param object default : response if the query fails
: returns : list of : class : ` stem . response . events . StreamEvent ` objects
: raises : : class : ` stem . ControllerError ` if the call fails and no default was
provided
"""
streams = [ ]
response = self . get_info ( ' stream-status ' )
for stream in response . splitlines ( ) :
2018-12-15 00:08:54 +00:00
message = stem . socket . recv_message ( io . BytesIO ( stem . util . str_tools . _to_bytes ( ' 650 STREAM %s \r \n ' % stream ) ) )
2015-11-23 21:13:53 +00:00
stem . response . convert ( ' EVENT ' , message , arrived_at = 0 )
streams . append ( message )
return streams
def attach_stream ( self , stream_id , circuit_id , exiting_hop = None ) :
"""
Attaches a stream to a circuit .
Note : Tor attaches streams to circuits automatically unless the
__LeaveStreamsUnattached configuration variable is set to ' 1 '
: param str stream_id : id of the stream that must be attached
: param str circuit_id : id of the circuit to which it must be attached
: param int exiting_hop : hop in the circuit where traffic should exit
: raises :
* : class : ` stem . InvalidRequest ` if the stream or circuit id were unrecognized
* : class : ` stem . UnsatisfiableRequest ` if the stream isn ' t in a state where it can be attached
* : class : ` stem . OperationFailed ` if the stream couldn ' t be attached for any other reason
"""
query = ' ATTACHSTREAM %s %s ' % ( stream_id , circuit_id )
if exiting_hop :
query + = ' HOP= %s ' % exiting_hop
response = self . msg ( query )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
if response . code == ' 552 ' :
raise stem . InvalidRequest ( response . code , response . message )
elif response . code == ' 551 ' :
raise stem . OperationFailed ( response . code , response . message )
elif response . code == ' 555 ' :
raise stem . UnsatisfiableRequest ( response . code , response . message )
else :
raise stem . ProtocolError ( ' ATTACHSTREAM returned unexpected response code: %s ' % response . code )
def close_stream ( self , stream_id , reason = stem . RelayEndReason . MISC , flag = ' ' ) :
"""
Closes the specified stream .
: param str stream_id : id of the stream to be closed
: param stem . RelayEndReason reason : reason the stream is closing
: param str flag : not currently used
: raises :
* : class : ` stem . InvalidArguments ` if the stream or reason are not recognized
* : class : ` stem . InvalidRequest ` if the stream and / or reason are missing
"""
# there's a single value offset between RelayEndReason.index_of() and the
# value that tor expects since tor's value starts with the index of one
response = self . msg ( ' CLOSESTREAM %s %s %s ' % ( stream_id , stem . RelayEndReason . index_of ( reason ) + 1 , flag ) )
stem . response . convert ( ' SINGLELINE ' , response )
if not response . is_ok ( ) :
if response . code in ( ' 512 ' , ' 552 ' ) :
if response . message . startswith ( ' Unknown stream ' ) :
raise stem . InvalidArguments ( response . code , response . message , [ stream_id ] )
elif response . message . startswith ( ' Unrecognized reason ' ) :
raise stem . InvalidArguments ( response . code , response . message , [ reason ] )
raise stem . InvalidRequest ( response . code , response . message )
else :
raise stem . ProtocolError ( ' CLOSESTREAM returned unexpected response code: %s ' % response . code )
def signal ( self , signal ) :
"""
Sends a signal to the Tor client .
: param stem . Signal signal : type of signal to be sent
2018-12-15 00:08:54 +00:00
: raises :
* : class : ` stem . ControllerError ` if sending the signal failed
* : class : ` stem . InvalidArguments ` if signal provided wasn ' t recognized
2015-11-23 21:13:53 +00:00
"""
response = self . msg ( ' SIGNAL %s ' % signal )
stem . response . convert ( ' SINGLELINE ' , response )
if response . is_ok ( ) :
if signal == stem . Signal . NEWNYM :
self . _last_newnym = time . time ( )
else :
if response . code == ' 552 ' :
raise stem . InvalidArguments ( response . code , response . message , [ signal ] )
raise stem . ProtocolError ( ' SIGNAL response contained unrecognized status code: %s ' % response . code )
def is_newnym_available ( self ) :
"""
Indicates if tor would currently accept a NEWNYM signal . This can only
account for signals sent via this controller .
. . versionadded : : 1.2 .0
: returns : * * True * * if tor would currently accept a NEWNYM signal , * * False * *
otherwise
"""
if self . is_alive ( ) :
return self . get_newnym_wait ( ) == 0.0
else :
return False
def get_newnym_wait ( self ) :
"""
Provides the number of seconds until a NEWNYM signal would be respected .
This can only account for signals sent via this controller .
. . versionadded : : 1.2 .0
: returns : * * float * * for the number of seconds until tor would respect
another NEWNYM signal
"""
return max ( 0.0 , self . _last_newnym + 10 - time . time ( ) )
@with_default ( )
def get_effective_rate ( self , default = UNDEFINED , burst = False ) :
"""
get_effective_rate ( default = UNDEFINED , burst = False )
Provides the maximum rate this relay is configured to relay in bytes per
second . This is based on multiple torrc parameters if they ' re set...
* Effective Rate = min ( BandwidthRate , RelayBandwidthRate , MaxAdvertisedBandwidth )
* Effective Burst = min ( BandwidthBurst , RelayBandwidthBurst )
. . versionadded : : 1.3 .0
: param object default : response if the query fails
: param bool burst : provides the burst bandwidth , otherwise this provides
the standard rate
: returns : * * int * * with the effective bandwidth rate in bytes per second
: raises : : class : ` stem . ControllerError ` if the call fails and no default was
provided
"""
if not burst :
attributes = ( ' BandwidthRate ' , ' RelayBandwidthRate ' , ' MaxAdvertisedBandwidth ' )
else :
attributes = ( ' BandwidthBurst ' , ' RelayBandwidthBurst ' )
value = None
for attr in attributes :
attr_value = int ( self . get_conf ( attr ) )
if attr_value == 0 and attr . startswith ( ' Relay ' ) :
continue # RelayBandwidthRate and RelayBandwidthBurst default to zero
value = min ( value , attr_value ) if value else attr_value
return value
def is_geoip_unavailable ( self ) :
"""
2018-12-15 00:08:54 +00:00
Provides * * True * * if tor ' s geoip database is unavailable, **False**
otherwise .
. . versionchanged : : 1.6 .0
No longer requires previously failed GETINFO requests to determine this .
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
. . deprecated : : 1.6 .0
This is available as of Tor 0.3 .2 .1 through the following instead . . .
2015-11-23 21:13:53 +00:00
2018-12-15 00:08:54 +00:00
: :
controller . get_info ( ' ip-to-country/ipv4-available ' , 0 ) == ' 1 '
: returns : * * bool * * indicating if we ' ve determined tor ' s geoip database to
be unavailable or not
2015-11-23 21:13:53 +00:00
"""
2018-12-15 00:08:54 +00:00
if self . _is_geoip_unavailable is None :
try :
self . get_info ( ' ip-to-country/0.0.0.0 ' )
self . _is_geoip_unavailable = False
except stem . ControllerError as exc :
if ' GeoIP data not loaded ' in str ( exc ) :
self . _is_geoip_unavailable = True
else :
return False # unexpected issue, fail open and don't cache
return self . _is_geoip_unavailable
2015-11-23 21:13:53 +00:00
def map_address ( self , mapping ) :
"""
Map addresses to replacement addresses . Tor replaces subseqent connections
to the original addresses with the replacement addresses .
If the original address is a null address , i . e . , one of ' 0.0.0.0 ' , ' ::0 ' , or
' . ' Tor picks an original address itself and returns it in the reply . If the
original address is already mapped to a different address the mapping is
removed .
: param dict mapping : mapping of original addresses to replacement addresses
: raises :
* : class : ` stem . InvalidRequest ` if the addresses are malformed
* : class : ` stem . OperationFailed ` if Tor couldn ' t fulfill the request
: returns : * * dict * * with ' original -> replacement ' address mappings
"""
mapaddress_arg = ' ' . join ( [ ' %s = %s ' % ( k , v ) for ( k , v ) in list ( mapping . items ( ) ) ] )
response = self . msg ( ' MAPADDRESS %s ' % mapaddress_arg )
stem . response . convert ( ' MAPADDRESS ' , response )
return response . entries
def drop_guards ( self ) :
"""
Drops our present guard nodes and picks a new set .
. . versionadded : : 1.2 .0
: raises : : class : ` stem . ControllerError ` if Tor couldn ' t fulfill the request
"""
if self . get_version ( ) < stem . version . Requirement . DROPGUARDS :
raise stem . UnsatisfiableRequest ( message = ' DROPGUARDS was added in tor version %s ' % stem . version . Requirement . DROPGUARDS )
self . msg ( ' DROPGUARDS ' )
def _post_authentication ( self ) :
super ( Controller , self ) . _post_authentication ( )
# try to re-attach event listeners to the new instance
with self . _event_listeners_lock :
try :
failed_events = self . _attach_listeners ( ) [ 1 ]
if failed_events :
# remove our listeners for these so we don't keep failing
for event_type in failed_events :
del self . _event_listeners [ event_type ]
logging_id = ' stem.controller.event_reattach- %s ' % ' - ' . join ( failed_events )
log . log_once ( logging_id , log . WARN , ' We were unable to re-attach our event listeners to the new tor instance for: %s ' % ' , ' . join ( failed_events ) )
except stem . ProtocolError as exc :
log . warn ( ' Unable to issue the SETEVENTS request to re-attach our listeners ( %s ) ' % exc )
# issue TAKEOWNERSHIP if we're the owning process for this tor instance
owning_pid = self . get_conf ( ' __OwningControllerProcess ' , None )
if owning_pid == str ( os . getpid ( ) ) and self . is_localhost ( ) :
response = self . msg ( ' TAKEOWNERSHIP ' )
stem . response . convert ( ' SINGLELINE ' , response )
if response . is_ok ( ) :
# Now that tor is tracking our ownership of the process via the control
# connection, we can stop having it check for us via our pid.
try :
self . reset_conf ( ' __OwningControllerProcess ' )
except stem . ControllerError as exc :
log . warn ( " We were unable to reset tor ' s __OwningControllerProcess configuration. It will continue to periodically check if our pid exists. ( %s ) " % exc )
else :
log . warn ( ' We were unable assert ownership of tor through TAKEOWNERSHIP, despite being configured to be the owning process through __OwningControllerProcess. ( %s ) ' % response )
def _handle_event ( self , event_message ) :
2018-12-15 00:08:54 +00:00
try :
stem . response . convert ( ' EVENT ' , event_message , arrived_at = time . time ( ) )
event_type = event_message . type
except stem . ProtocolError as exc :
log . error ( ' Tor sent a malformed event ( %s ): %s ' % ( exc , event_message ) )
event_type = MALFORMED_EVENTS
2015-11-23 21:13:53 +00:00
with self . _event_listeners_lock :
2018-12-15 00:08:54 +00:00
for listener_type , event_listeners in list ( self . _event_listeners . items ( ) ) :
if listener_type == event_type :
2015-11-23 21:13:53 +00:00
for listener in event_listeners :
2018-12-15 00:08:54 +00:00
try :
listener ( event_message )
except Exception as exc :
log . warn ( ' Event listener raised an uncaught exception ( %s ): %s ' % ( exc , event_message ) )
2015-11-23 21:13:53 +00:00
def _attach_listeners ( self ) :
"""
Attempts to subscribe to the self . _event_listeners events from tor . This is
a no - op if we ' re not currently authenticated.
: returns : tuple of the form ( set_events , failed_events )
: raises : : class : ` stem . ControllerError ` if unable to make our request to tor
"""
set_events , failed_events = [ ] , [ ]
with self . _event_listeners_lock :
if self . is_authenticated ( ) :
# try to set them all
response = self . msg ( ' SETEVENTS %s ' % ' ' . join ( self . _event_listeners . keys ( ) ) )
if response . is_ok ( ) :
set_events = list ( self . _event_listeners . keys ( ) )
else :
# One of the following likely happened...
#
# * Our user attached listeners before having an authenticated
# connection, so we couldn't check if we met the version
# requirement.
#
# * User attached listeners to one tor instance, then connected us to
# an older tor instancce.
#
# * Some other controller hiccup (far less likely).
#
# See if we can set some subset of our events.
for event in list ( self . _event_listeners . keys ( ) ) :
response = self . msg ( ' SETEVENTS %s ' % ' ' . join ( set_events + [ event ] ) )
if response . is_ok ( ) :
set_events . append ( event )
else :
failed_events . append ( event )
return ( set_events , failed_events )
def _parse_circ_path ( path ) :
"""
Parses a circuit path as a list of * * ( fingerprint , nickname ) * * tuples . Tor
circuit paths are defined as being of the form . . .
: :
Path = LongName * ( " , " LongName )
LongName = Fingerprint [ ( " = " / " ~ " ) Nickname ]
example :
$ 999 A226EBED397F331B612FE1E4CFAE5C1F201BA = piyaz
. . . * unless * this is prior to tor version 0.2 .2 .1 with the VERBOSE_NAMES
feature turned off ( or before version 0.1 .2 .2 where the feature was
introduced ) . In that case either the fingerprint or nickname in the tuple
will be * * None * * , depending on which is missing .
: :
Path = ServerID * ( " , " ServerID )
ServerID = Nickname / Fingerprint
example :
$ E57A476CD4DFBD99B4EE52A100A58610AD6E80B9 , hamburgerphone , PrivacyRepublic14
: param str path : circuit path to be parsed
: returns : list of * * ( fingerprint , nickname ) * * tuples , fingerprints do not have a proceeding ' $ '
: raises : : class : ` stem . ProtocolError ` if the path is malformed
"""
if path :
try :
return [ _parse_circ_entry ( entry ) for entry in path . split ( ' , ' ) ]
except stem . ProtocolError as exc :
# include the path with the exception
raise stem . ProtocolError ( ' %s : %s ' % ( exc , path ) )
else :
return [ ]
def _parse_circ_entry ( entry ) :
"""
Parses a single relay ' s ' LongName ' or ' ServerID ' . See the
: func : ` ~ stem . control . _parse_circ_path ` function for more information .
: param str entry : relay information to be parsed
: returns : * * ( fingerprint , nickname ) * * tuple
: raises : : class : ` stem . ProtocolError ` if the entry is malformed
"""
if ' = ' in entry :
# common case
fingerprint , nickname = entry . split ( ' = ' )
elif ' ~ ' in entry :
# this is allowed for by the spec, but I've never seen it used
fingerprint , nickname = entry . split ( ' ~ ' )
elif entry [ 0 ] == ' $ ' :
# old style, fingerprint only
fingerprint , nickname = entry , None
else :
# old style, nickname only
fingerprint , nickname = None , entry
if fingerprint is not None :
if not stem . util . tor_tools . is_valid_fingerprint ( fingerprint , True ) :
raise stem . ProtocolError ( ' Fingerprint in the circuit path is malformed ( %s ) ' % fingerprint )
fingerprint = fingerprint [ 1 : ] # strip off the leading '$'
if nickname is not None and not stem . util . tor_tools . is_valid_nickname ( nickname ) :
raise stem . ProtocolError ( ' Nickname in the circuit path is malformed ( %s ) ' % nickname )
return ( fingerprint , nickname )
@with_default ( )
def _case_insensitive_lookup ( entries , key , default = UNDEFINED ) :
"""
Makes a case insensitive lookup within a list or dictionary , providing the
first matching entry that we come across .
: param list , dict entries : list or dictionary to be searched
: param str key : entry or key value to look up
: param object default : value to be returned if the key doesn ' t exist
: returns : case insensitive match or default if one was provided and key wasn ' t found
: raises : * * ValueError * * if no such value exists
"""
if entries is not None :
if isinstance ( entries , dict ) :
for k , v in list ( entries . items ( ) ) :
if k . lower ( ) == key . lower ( ) :
return v
else :
for entry in entries :
if entry . lower ( ) == key . lower ( ) :
return entry
raise ValueError ( " key ' %s ' doesn ' t exist in dict: %s " % ( key , entries ) )
2018-12-15 00:08:54 +00:00
def _get_with_timeout ( event_queue , timeout , start_time ) :
"""
Pulls an item from a queue with a given timeout .
"""
if timeout :
time_left = timeout - ( time . time ( ) - start_time )
if time_left < = 0 :
raise stem . Timeout ( ' Reached our %0.1f second timeout ' % timeout )
try :
return event_queue . get ( True , time_left )
except queue . Empty :
raise stem . Timeout ( ' Reached our %0.1f second timeout ' % timeout )
else :
return event_queue . get ( )