Skip to content
Permalink
Browse files
Add hidden services configuration handling
  • Loading branch information
FedericoCeratto committed Aug 4, 2014
1 parent 1486167 commit 15dc81c5c74562d58095b1f897c5478fb155b624
Showing with 262 additions and 0 deletions.
  1. +189 −0 stem/control.py
  2. +73 −0 test/integ/control/controller.py
@@ -216,6 +216,7 @@
import StringIO
import threading
import time
from collections import OrderedDict

import stem.descriptor.microdescriptor
import stem.descriptor.reader
@@ -1747,6 +1748,194 @@ def get_conf_map(self, params, default = UNDEFINED, multiple = True):
else:
raise exc

def get_hidden_services_conf(self):
"""Get hidden services configuration
>>> controller.get_hidden_services_conf()
{
"/var/lib/tor/hidden_service_empty/": {
"ports": [
]
},
"/var/lib/tor/hidden_service_with_two_ports/": {
"authorize_client": "stealth a, b",
"ports": [
"8020 127.0.0.1:8020", # the ports order is kept
"8021 127.0.0.1:8021"
],
"service_version": "2"
},
}
:raises:
* :class:`stem.ControllerError` if the call fails and we weren't provided
a default response
* :class:`RuntimeError` if the configuration contains unexpected entries
"""
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)
raise

service_dir_map = OrderedDict()
directory = None
ports = []
for status_code, divider, content in response.content():

if content == 'HiddenServiceOptions':
continue

k, v = content.split('=', 1)
if k == 'HiddenServiceDir':
directory = v
service_dir_map[directory] = dict(
ports = []
)

elif k == 'HiddenServicePort':
service_dir_map[directory]['ports'].append(v)

elif k == 'HiddenServiceVersion':
service_dir_map[directory]['service_version'] = int(v)

elif k == 'HiddenServiceAuthorizeClient':
service_dir_map[directory]['authorize_client'] = v

else:
raise RuntimeError("Unexpected entry %r" % content)

return service_dir_map

def set_hidden_services_conf(self, conf):
"""Update all the configured hidden services from a dictionary having
the same format as the output of get_hidden_services_conf()
: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
"""

start_time = time.time()

# Convert conf dictionary into a single configuration line
queryitems = []
for directory in conf:
queryitems.append("HiddenServiceDir=%s" % directory)
for k, v in conf[directory].iteritems():
if k == 'authorize_client':
queryitems.append("HiddenServiceAuthorizeClient=\"%s\"" % v)

elif k == 'service_version':
queryitems.append("HiddenServiceVersion=%s" % v)

elif k == 'ports':
for port in v:
queryitems.append("HiddenServicePort=\"%s\"" % port)

query = 'SETCONF ' + ' '.join(queryitems)
response = self.msg(query)
stem.response.convert('SINGLELINE', response)

if response.is_ok():
log.debug('%s (runtime: %0.4f)' % (query, time.time() - start_time))

else:
log.debug('%s (failed, code: %s, message: %s)' % (query, response.code, response.message))

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)

def create_new_hidden_service(self, dirname, virtport, target=None):
"""Create a new hidden service+port. If the directory is already present, a
new port will be added. If the port is already present, return False.
:param str dirname: directory name
:param int virtport: virtual port
:param str target: optional ipaddr:port target e.g. '127.0.0.1:8080'
:returns: False if the hidden service and port is already in place
True if the creation is successful
"""
virtport = int(virtport)
assert 0 <= virtport <= 2 **16
conf = self.get_hidden_services_conf()

if dirname in conf:
ports = conf[dirname]['ports']
if target is None:
if str(virtport) in ports:
return False

if "%d 127.0.0.1:%d" % (virtport, virtport) in ports:
return False

elif "%d %s" % (virtport, target) in ports:
return False

else:
conf[dirname] = dict(ports=[])

if target is None:
conf[dirname]['ports'].append("%d" % virtport)

else:
conf[dirname]['ports'].append("%d %s" % (virtport, target))

self.set_hidden_services_conf(conf)
return True

def delete_hidden_service(self, dirname, virtport, target=None):
"""Delete a hidden service+port.
:param str dirname: directory name
:param int virtport: virtual port
:param str target: optional ipaddr:port target e.g. '127.0.0.1:8080'
:raises:
"""
virtport = int(virtport)
assert 0 <= virtport <= 2 **16
conf = self.get_hidden_services_conf()

if dirname not in conf:
raise RuntimeError("HiddenServiceDir %r not found" % dirname)

ports = conf[dirname]['ports']

if target is None:
longport = "%d 127.0.0.1:%d" % (virtport, virtport)
try:
ports.pop(ports.index(str(virtport)))
except ValueError:
ports.pop(ports.index(longport))

else:
longport = "%d %s" % (virtport, target)
ports.pop(ports.index(longport))

if not ports:
del(conf[dirname])

self.set_hidden_services_conf(conf)
return True

def _get_conf_dict_to_response(self, config_dict, default, multiple):
"""
Translates a dictionary of 'config key => [value1, value2...]' into the
@@ -455,6 +455,79 @@ def test_getconf(self):
self.assertEqual({}, controller.get_conf_map('', 'la-di-dah'))
self.assertEqual({}, controller.get_conf_map([], 'la-di-dah'))

def test_hidden_services_conf(self):
"""
Exercises get_hidden_services_conf with valid and invalid queries.
"""

if test.runner.require_control(self):
return

runner = test.runner.get_runner()

with runner.get_tor_controller() as controller:

conf = controller.get_hidden_services_conf()
self.assertDictEqual({}, conf)
controller.set_hidden_services_conf(conf)

initialconf = {
"test_hidden_service1/": {
"ports": [
"8020 127.0.0.1:8020",
"8021 127.0.0.1:8021"
],
"service_version": 2,
},
"test_hidden_service2/": {
"authorize_client": "stealth a, b",
"ports": [
"8030 127.0.0.1:8030",
"8031 127.0.0.1:8031",
"8032 127.0.0.1:8032"
]
},
"test_hidden_service_empty/": {
"ports": []
}
}
controller.set_hidden_services_conf(initialconf)

conf = controller.get_hidden_services_conf()
self.assertDictEqual(initialconf, dict(conf))

# Add already existing services, with/without explicit target
r = controller.create_new_hidden_service('test_hidden_service1/', 8020)
self.assertFalse(r)
r = controller.create_new_hidden_service('test_hidden_service1/', 8021, target="127.0.0.1:8021")
self.assertFalse(r)

# Add new services, with/without explicit target
r = controller.create_new_hidden_service('test_hidden_serviceX/', 8888)
self.assertTrue(r)
r = controller.create_new_hidden_service('test_hidden_serviceX/', 8989, target="127.0.0.1:8021")
self.assertTrue(r)

conf = controller.get_hidden_services_conf()
self.assertEqual(len(conf), 4)
ports = conf['test_hidden_serviceX/']['ports']
self.assertEqual(len(ports), 2)

# Delete services
controller.delete_hidden_service('test_hidden_serviceX/', 8888)

# The service dir should be still there
conf = controller.get_hidden_services_conf()
self.assertEqual(len(conf), 4)

# Delete service
controller.delete_hidden_service('test_hidden_serviceX/', 8989, target="127.0.0.1:8021")

# The service dir should be gone
conf = controller.get_hidden_services_conf()
self.assertEqual(len(conf), 3)


def test_set_conf(self):
"""
Exercises set_conf(), reset_conf(), and set_options() methods with valid

0 comments on commit 15dc81c

Please sign in to comment.