|
| 1 | +import os |
1 | 2 |
|
2 | | -from zope.interface import implementer |
| 3 | +import txtorcon |
| 4 | +from twisted.internet import defer, reactor |
| 5 | +from twisted.internet.endpoints import ( |
| 6 | + TCP4ClientEndpoint, |
| 7 | + UNIXClientEndpoint, |
| 8 | + serverFromString, |
| 9 | +) |
3 | 10 | from twisted.internet.error import ReactorNotRunning |
4 | | -from twisted.internet import reactor, defer |
5 | | -from twisted.internet.endpoints import (TCP4ClientEndpoint, |
6 | | - UNIXClientEndpoint, serverFromString) |
7 | 11 | from twisted.web.client import Agent, BrowserLikePolicyForHTTPS |
8 | | -import txtorcon |
| 12 | +from txtorcon import TorConfig, TorControlProtocol |
9 | 13 | from txtorcon.web import tor_agent |
10 | | -from txtorcon import TorControlProtocol, TorConfig |
| 14 | +from zope.interface import implementer |
11 | 15 |
|
12 | 16 | _custom_stop_reactor_is_set = False |
13 | 17 | custom_stop_reactor = None |
@@ -170,48 +174,86 @@ def __init__(self, proto_factory_or_resource, info_callback, |
170 | 174 | # an ephemeral HS on the global or pre-existing tor. |
171 | 175 | self.hidden_service_dir = hidden_service_dir |
172 | 176 |
|
| 177 | + self.tor_connection = None |
| 178 | + |
173 | 179 | def start_tor(self): |
174 | 180 | """ This function executes the workflow |
175 | 181 | of starting the hidden service and returning its hostname |
176 | 182 | """ |
177 | | - self.info_callback("Attempting to start onion service on port: {} " |
178 | | - "...".format(self.virtual_port)) |
| 183 | + self.info_callback( |
| 184 | + f"Attempting to start onion service on port: {self.virtual_port} ..." |
| 185 | + ) |
| 186 | + |
| 187 | + # Check if using Tor-managed mode (via torrc, not control port) |
| 188 | + if self.hidden_service_dir.startswith("tor-managed:"): |
| 189 | + self.start_tor_managed_onion() |
| 190 | + return |
| 191 | + |
| 192 | + # Ephemeral or txtorcon-managed hidden service (via control port) |
| 193 | + if str(self.tor_control_host).startswith("unix:"): |
| 194 | + control_endpoint = UNIXClientEndpoint(reactor, self.tor_control_host[5:]) |
| 195 | + else: |
| 196 | + control_endpoint = TCP4ClientEndpoint( |
| 197 | + reactor, self.tor_control_host, self.tor_control_port |
| 198 | + ) |
| 199 | + d = txtorcon.connect(reactor, control_endpoint) |
| 200 | + |
179 | 201 | if self.hidden_service_dir == "": |
180 | | - if str(self.tor_control_host).startswith('unix:'): |
181 | | - control_endpoint = UNIXClientEndpoint(reactor, |
182 | | - self.tor_control_host[5:]) |
183 | | - else: |
184 | | - control_endpoint = TCP4ClientEndpoint(reactor, |
185 | | - self.tor_control_host, self.tor_control_port) |
186 | | - d = txtorcon.connect(reactor, control_endpoint) |
| 202 | + # Ephemeral hidden service (no persistence) |
187 | 203 | d.addCallback(self.create_onion_ep) |
188 | 204 | d.addErrback(self.setup_failed) |
189 | | - # TODO: add errbacks to the next two calls in |
190 | | - # the chain: |
191 | 205 | d.addCallback(self.onion_listen) |
192 | 206 | d.addCallback(self.print_host) |
193 | 207 | else: |
194 | | - ep = "onion:" + str(self.virtual_port) + ":localPort=" |
195 | | - ep += str(self.serving_port) |
196 | | - # endpoints.TCPHiddenServiceEndpoint creates version 2 by |
197 | | - # default for backwards compat (err, txtorcon needs to update that ...) |
198 | | - ep += ":version=3" |
199 | | - ep += ":hiddenServiceDir="+self.hidden_service_dir |
200 | | - onion_endpoint = serverFromString(reactor, ep) |
201 | | - d = onion_endpoint.listen(self.proto_factory) |
| 208 | + # txtorcon-managed filesystem hidden service |
| 209 | + d.addCallback(self.create_filesystem_onion_ep) |
| 210 | + d.addErrback(self.setup_failed) |
202 | 211 | d.addCallback(self.print_host_filesystem) |
203 | 212 |
|
204 | | - |
205 | 213 | def setup_failed(self, arg): |
206 | 214 | # Note that actions based on this failure are deferred to callers: |
207 | 215 | self.error_callback("Setup failed: " + str(arg)) |
208 | 216 |
|
209 | 217 | def create_onion_ep(self, t): |
210 | 218 | self.tor_connection = t |
211 | | - portmap_string = config_to_hs_ports(self.virtual_port, |
212 | | - self.serving_host, self.serving_port) |
| 219 | + portmap_string = config_to_hs_ports( |
| 220 | + self.virtual_port, self.serving_host, self.serving_port |
| 221 | + ) |
213 | 222 | return t.create_onion_service( |
214 | | - ports=[portmap_string], private_key=txtorcon.DISCARD) |
| 223 | + ports=[portmap_string], private_key=txtorcon.DISCARD |
| 224 | + ) |
| 225 | + |
| 226 | + def create_filesystem_onion_ep(self, t): |
| 227 | + """Create a persistent hidden service using txtorcon's filesystem support. |
| 228 | + Requires local Tor control port access. |
| 229 | + """ |
| 230 | + self.tor_connection = t |
| 231 | + ep = "onion:" + str(self.virtual_port) + ":localPort=" |
| 232 | + ep += str(self.serving_port) |
| 233 | + ep += ":version=3" |
| 234 | + ep += ":hiddenServiceDir=" + self.hidden_service_dir |
| 235 | + onion_endpoint = serverFromString(reactor, ep) |
| 236 | + return onion_endpoint.listen(self.proto_factory) |
| 237 | + |
| 238 | + def start_tor_managed_onion(self) -> None: |
| 239 | + """ |
| 240 | + For Tor-managed hidden services: read hostname, start listening. |
| 241 | + No control port connection needed. |
| 242 | + """ |
| 243 | + hs_dir = self.hidden_service_dir.removeprefix("tor-managed:") |
| 244 | + hostname_file = os.path.join(hs_dir, "hostname") |
| 245 | + |
| 246 | + if not os.path.exists(hostname_file): |
| 247 | + self.error_callback(f"Hostname file {hostname_file} does not exist") |
| 248 | + return |
| 249 | + |
| 250 | + try: |
| 251 | + with open(hostname_file, "r") as f: |
| 252 | + hostname = f.read().strip() |
| 253 | + self.info_callback(f"Using Tor-managed hidden service: {hostname}") |
| 254 | + self.onion_hostname_callback(hostname) |
| 255 | + except Exception as e: |
| 256 | + self.error_callback(f"Failed to read {hostname_file}: {e}") |
215 | 257 |
|
216 | 258 | def onion_listen(self, onion): |
217 | 259 | # 'onion' arg is the created EphemeralOnionService object; |
@@ -240,11 +282,13 @@ def print_host_filesystem(self, port): |
240 | 282 | self.onion_hostname_callback(self.onion.hostname) |
241 | 283 |
|
242 | 284 | def shutdown(self): |
243 | | - self.tor_connection.protocol.transport.loseConnection() |
| 285 | + if self.tor_connection: |
| 286 | + self.tor_connection.protocol.transport.loseConnection() |
244 | 287 | self.info_callback("Hidden service shutdown complete") |
245 | 288 | if self.shutdown_callback: |
246 | 289 | self.shutdown_callback() |
247 | 290 |
|
| 291 | + |
248 | 292 | class JMHTTPResource(Resource): |
249 | 293 | """ Object acting as HTTP serving resource |
250 | 294 | """ |
|
0 commit comments