You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
263 lines
10 KiB
263 lines
10 KiB
|
|
from zope.interface import implementer |
|
from twisted.internet.error import ReactorNotRunning |
|
from twisted.internet import reactor, defer |
|
from twisted.internet.endpoints import (TCP4ClientEndpoint, |
|
UNIXClientEndpoint, serverFromString) |
|
from twisted.web.client import Agent, BrowserLikePolicyForHTTPS |
|
import txtorcon |
|
from txtorcon.web import tor_agent |
|
from txtorcon import TorControlProtocol, TorConfig |
|
|
|
_custom_stop_reactor_is_set = False |
|
custom_stop_reactor = None |
|
|
|
# This removes `CONF_CHANGED` requests |
|
# over the Tor control port, which aren't needed for our use case. |
|
def patch_add_event_listener(self, evt, callback): |
|
if evt not in self.valid_events.values(): |
|
try: |
|
evt = self.valid_events[evt] |
|
except KeyError: |
|
raise RuntimeError("Unknown event type: " + evt) |
|
|
|
if evt.name not in self.events and evt.name != "CONF_CHANGED": |
|
self.events[evt.name] = evt |
|
d = self.queue_command('SETEVENTS %s' % ' '.join(self.events.keys())) |
|
else: |
|
d = defer.succeed(None) |
|
evt.listen(callback) |
|
return d |
|
TorControlProtocol.add_event_listener = patch_add_event_listener |
|
|
|
# Similar to above, but more important: |
|
# txtorcon making too nosy requests for config data; this |
|
# simply prevents the request, which the package allows. |
|
def patch_get_defaults(self): |
|
return dict() |
|
TorConfig._get_defaults = patch_get_defaults |
|
|
|
from twisted.web.server import Site |
|
from twisted.web.resource import Resource |
|
from twisted.web.iweb import IPolicyForHTTPS |
|
from twisted.internet.ssl import CertificateOptions |
|
from .support import wrapped_urlparse |
|
|
|
# txtorcon outputs erroneous warnings about hiddenservice directory strings, |
|
# annoyingly, so we suppress it here: |
|
import warnings |
|
warnings.filterwarnings("ignore") |
|
|
|
""" This whitelister allows us to accept any cert for a specific |
|
domain, and is to be used for testing only; the default Agent |
|
behaviour of twisted.web.client.Agent for https URIs is |
|
the correct one in production (i.e. uses local trust store). |
|
""" |
|
@implementer(IPolicyForHTTPS) |
|
class WhitelistContextFactory(object): |
|
def __init__(self, good_domains=None): |
|
""" |
|
:param good_domains: List of domains. The URLs must be in bytes |
|
""" |
|
if not good_domains: |
|
self.good_domains = [] |
|
else: |
|
self.good_domains = good_domains |
|
# by default, handle requests like a browser would |
|
self.default_policy = BrowserLikePolicyForHTTPS() |
|
|
|
def creatorForNetloc(self, hostname, port): |
|
# check if the hostname is in the whitelist, |
|
# otherwise return the default policy |
|
if hostname in self.good_domains: |
|
return CertificateOptions(verify=False) |
|
return self.default_policy.creatorForNetloc(hostname, port) |
|
|
|
def set_custom_stop_reactor(fn): |
|
global _custom_stop_reactor_is_set |
|
global custom_stop_reactor |
|
_custom_stop_reactor_is_set = True |
|
custom_stop_reactor = fn |
|
|
|
def stop_reactor(): |
|
if not _custom_stop_reactor_is_set: |
|
_stop_reactor() |
|
else: |
|
custom_stop_reactor() |
|
|
|
def _stop_reactor(): |
|
""" The value of the bool `reactor.running` |
|
does not reliably tell us whether the |
|
reactor is running (!). There are startup |
|
and shutdown phases not reported externally |
|
by IReactorCore. So we must catch Exceptions |
|
raised by trying to stop the reactor. |
|
""" |
|
try: |
|
reactor.stop() |
|
except ReactorNotRunning: |
|
pass |
|
|
|
def is_hs_uri(s): |
|
x = wrapped_urlparse(s) |
|
if x.hostname.endswith(".onion"): |
|
return x.scheme, x.hostname, x.port |
|
return False |
|
|
|
def get_tor_agent(socks5_host, socks5_port): |
|
torEndpoint = TCP4ClientEndpoint(reactor, socks5_host, socks5_port) |
|
return tor_agent(reactor, torEndpoint) |
|
|
|
def get_nontor_agent(tls_whitelist=[]): |
|
""" The tls_whitelist argument must be a list of hosts for which |
|
TLS certificate verification may be omitted, default none. |
|
""" |
|
if len(tls_whitelist) == 0: |
|
agent = Agent(reactor) |
|
else: |
|
agent = Agent(reactor, |
|
contextFactory=WhitelistContextFactory(tls_whitelist)) |
|
return agent |
|
|
|
def config_to_hs_ports(virtual_port, host, port): |
|
# See https://github.com/meejah/txtorcon/blob/0c416cc8fe18b913cd0c7422935885a1bfecf4c0/txtorcon/onion.py#L1320 |
|
# for non default config, pass port mapping strings like: |
|
# "80 127.0.0.1:1234" |
|
return "{} {}:{}".format(virtual_port, host, port) |
|
|
|
class JMHiddenService(object): |
|
""" Wrapper class around the actions needed to |
|
create and serve on a hidden service; an object of |
|
type either Resource or server.ProtocolFactory must |
|
be provided in the constructor, which does the HTTP |
|
(GET, POST) or other protocol serving actions. |
|
""" |
|
def __init__(self, proto_factory_or_resource, info_callback, |
|
error_callback, onion_hostname_callback, tor_control_host, |
|
tor_control_port, serving_host, serving_port, |
|
virtual_port=None, |
|
shutdown_callback=None, |
|
hidden_service_dir=""): |
|
if isinstance(proto_factory_or_resource, Resource): |
|
# TODO bad naming, in this case it doesn't start |
|
# out as a protocol factory; a Site is one, a Resource isn't. |
|
self.proto_factory = Site(proto_factory_or_resource) |
|
self.proto_factory.displayTracebacks = False |
|
else: |
|
self.proto_factory = proto_factory_or_resource |
|
self.info_callback = info_callback |
|
self.error_callback = error_callback |
|
# this has a separate callback for convenience, it should |
|
# be passed the literal *.onion string (port is already |
|
# known and is 80 by default) |
|
self.onion_hostname_callback = onion_hostname_callback |
|
self.shutdown_callback = shutdown_callback |
|
if not virtual_port: |
|
self.virtual_port = 80 |
|
else: |
|
self.virtual_port = virtual_port |
|
self.tor_control_host = tor_control_host |
|
self.tor_control_port = tor_control_port |
|
# note that defaults only exist in jmclient |
|
# config object, so no default here: |
|
self.serving_host = serving_host |
|
self.serving_port = serving_port |
|
# this is used to serve an onion from the filesystem, |
|
# NB: Because of how txtorcon is set up, this option |
|
# uses a *separate tor instance* owned by the owner of |
|
# this script (because txtorcon needs to read the |
|
# HS dir), whereas if this option is "", we set up |
|
# an ephemeral HS on the global or pre-existing tor. |
|
self.hidden_service_dir = hidden_service_dir |
|
|
|
def start_tor(self): |
|
""" This function executes the workflow |
|
of starting the hidden service and returning its hostname |
|
""" |
|
self.info_callback("Attempting to start onion service on port: {} " |
|
"...".format(self.virtual_port)) |
|
if self.hidden_service_dir == "": |
|
if str(self.tor_control_host).startswith('unix:'): |
|
control_endpoint = UNIXClientEndpoint(reactor, |
|
self.tor_control_host[5:]) |
|
else: |
|
control_endpoint = TCP4ClientEndpoint(reactor, |
|
self.tor_control_host, self.tor_control_port) |
|
d = txtorcon.connect(reactor, control_endpoint) |
|
d.addCallback(self.create_onion_ep) |
|
d.addErrback(self.setup_failed) |
|
# TODO: add errbacks to the next two calls in |
|
# the chain: |
|
d.addCallback(self.onion_listen) |
|
d.addCallback(self.print_host) |
|
else: |
|
ep = "onion:" + str(self.virtual_port) + ":localPort=" |
|
ep += str(self.serving_port) |
|
# endpoints.TCPHiddenServiceEndpoint creates version 2 by |
|
# default for backwards compat (err, txtorcon needs to update that ...) |
|
ep += ":version=3" |
|
ep += ":hiddenServiceDir="+self.hidden_service_dir |
|
onion_endpoint = serverFromString(reactor, ep) |
|
d = onion_endpoint.listen(self.proto_factory) |
|
d.addCallback(self.print_host_filesystem) |
|
|
|
|
|
def setup_failed(self, arg): |
|
# Note that actions based on this failure are deferred to callers: |
|
self.error_callback("Setup failed: " + str(arg)) |
|
|
|
def create_onion_ep(self, t): |
|
self.tor_connection = t |
|
portmap_string = config_to_hs_ports(self.virtual_port, |
|
self.serving_host, self.serving_port) |
|
return t.create_onion_service( |
|
ports=[portmap_string], private_key=txtorcon.DISCARD) |
|
|
|
def onion_listen(self, onion): |
|
# 'onion' arg is the created EphemeralOnionService object; |
|
# now we know it exists, we start serving the Site on the |
|
# relevant port: |
|
self.onion = onion |
|
serverstring = "tcp:{}:interface={}".format(self.serving_port, |
|
self.serving_host) |
|
onion_endpoint = serverFromString(reactor, serverstring) |
|
print("created the onion endpoint, now calling listen") |
|
return onion_endpoint.listen(self.proto_factory) |
|
|
|
def print_host(self, ep): |
|
""" Callback fired once the HS is available |
|
and the site is up ready to receive requests. |
|
The hidden service hostname will be used in the BIP21 uri. |
|
""" |
|
self.onion_hostname_callback(self.onion.hostname) |
|
|
|
def print_host_filesystem(self, port): |
|
""" As above but needed to respect slightly different |
|
callback chain for this case (where we start our own tor |
|
instance for the filesystem-based onion). |
|
""" |
|
self.onion = port.onion_service |
|
self.onion_hostname_callback(self.onion.hostname) |
|
|
|
def shutdown(self): |
|
self.tor_connection.protocol.transport.loseConnection() |
|
self.info_callback("Hidden service shutdown complete") |
|
if self.shutdown_callback: |
|
self.shutdown_callback() |
|
|
|
class JMHTTPResource(Resource): |
|
""" Object acting as HTTP serving resource |
|
""" |
|
def __init__(self, info_callback, shutdown_callback): |
|
self.info_callback = info_callback |
|
self.shutdown_callback = shutdown_callback |
|
super().__init__() |
|
|
|
isLeaf = True |
|
|
|
def render_GET(self, request): |
|
""" by default, we serve a simple string which can be used e.g. |
|
to check if an ephemeral HS is upon Tor Browser; child classes |
|
may override. |
|
""" |
|
return "<html>Only for testing.</html>".encode("utf-8")
|
|
|