diff --git a/src/Config.py b/src/Config.py
index 401473a71..a5ca8f341 100644
--- a/src/Config.py
+++ b/src/Config.py
@@ -167,9 +167,10 @@ def createArguments(self):
self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript,
metavar='executable_path')
- self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable')
+ self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always", "inside"], default='enable')
self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051')
self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050')
+ self.parser.add_argument('--tor_hidden_services', help='Tor hidden services file', default='')
self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py
index 4843da58f..fa579e8e4 100644
--- a/src/Connection/Connection.py
+++ b/src/Connection/Connection.py
@@ -177,11 +177,11 @@ def getHandshakeInfo(self):
# Setup peer lock from requested onion address
if self.handshake and self.handshake.get("target_ip", "").endswith(".onion"):
target_onion = self.handshake.get("target_ip").replace(".onion", "") # My onion address
- onion_sites = {v: k for k, v in self.server.tor_manager.site_onions.items()} # Inverse, Onion: Site address
- self.site_lock = onion_sites.get(target_onion)
- if not self.site_lock:
- self.server.log.error("Unknown target onion address: %s" % target_onion)
- self.site_lock = "unknown"
+ self.site_lock = self.server.tor_manager.onion_sites.get(target_onion)
+ if not self.site_lock: # TODO Need to also verify that this is the same site connection was opened for
+ self.log("Unknown target onion address %s, closing connection" % target_onion)
+ self.close()
+ return
handshake = {
"version": config.version,
diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py
index 95d671053..111cbc97d 100644
--- a/src/Connection/ConnectionServer.py
+++ b/src/Connection/ConnectionServer.py
@@ -13,6 +13,7 @@
from Crypt import CryptConnection
from Crypt import CryptHash
from Tor import TorManager
+from Tor import TorManagerInside
class ConnectionServer:
@@ -23,8 +24,10 @@ def __init__(self, ip=None, port=None, request_handler=None):
self.log = logging.getLogger("ConnServer")
self.port_opened = None
- if config.tor != "disabled":
+ if config.tor == "enable" or config.tor == "always":
self.tor_manager = TorManager(self.ip, self.port)
+ elif config.tor == "inside":
+ self.tor_manager = TorManagerInside(config.tor_hidden_services)
else:
self.tor_manager = None
diff --git a/src/File/FileServer.py b/src/File/FileServer.py
index 0861daf7b..529c139f3 100644
--- a/src/File/FileServer.py
+++ b/src/File/FileServer.py
@@ -64,7 +64,7 @@ def openport(self, port=None, check=True):
if self.testOpenport(port, use_alternative=False)["result"] is True:
return True # Port already opened
- if config.tor == "always": # Port opening won't work in Tor mode
+ if config.tor == "always" or config.tor == "inside": # Port opening won't work in Tor mode
return False
self.log.info("Trying to open port using UpnpPunch...")
@@ -102,7 +102,7 @@ def testOpenportPortchecker(self, port=None):
data = ""
if "closed" in message or "Error" in message:
- if config.tor != "always":
+ if config.tor != "always" and config.tor != "inside":
self.log.info("[BAD :(] Port closed: %s" % message)
if port == self.port:
self.port_opened = False # Self port, update port_opened status
@@ -135,7 +135,7 @@ def testOpenportCanyouseeme(self, port=None):
message = "Error: %s" % Debug.formatException(err)
if "Error" in message:
- if config.tor != "always":
+ if config.tor != "always" and config.tor != "inside":
self.log.info("[BAD :(] Port closed: %s" % message)
if port == self.port:
self.port_opened = False # Self port, update port_opened status
@@ -186,7 +186,7 @@ def checkSites(self, check_files=True, force_port_check=False):
gevent.spawn(self.checkSite, site, check_files)
self.openport()
- if self.port_opened is False:
+ if self.port_opened is False and config.tor != "inside":
self.tor_manager.startOnions()
if not sites_checking:
diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py
index 52df41aa3..5912b583d 100644
--- a/src/Site/SiteManager.py
+++ b/src/Site/SiteManager.py
@@ -3,6 +3,7 @@
import re
import os
import time
+import sys
import gevent
@@ -24,6 +25,7 @@ def load(self):
self.sites = {}
address_found = []
added = 0
+ serving = 0
# Load new adresses
for address in json.load(open("%s/sites.json" % config.data_dir)):
if address not in self.sites and os.path.isfile("%s/%s/content.json" % (config.data_dir, address)):
@@ -31,8 +33,16 @@ def load(self):
self.sites[address] = Site(address)
logging.debug("Loaded site %s in %.3fs" % (address, time.time() - s))
added += 1
+ if self.sites[address].settings["serving"]:
+ serving += 1
address_found.append(address)
+ # check if there are enough onions available
+ if sys.modules.get("main") and sys.modules["main"].file_server:
+ tor_manager = sys.modules["main"].file_server.tor_manager
+ if tor_manager and tor_manager.numOnions() < serving+1:
+ sys.exit("Insufficient number of onions: supplied %u, need at least %u, recommended to have %u+ onions" % (tor_manager.numOnions(), serving+1, serving*3))
+
# Remove deleted adresses
for address in self.sites.keys():
if address not in address_found:
diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py
index 465dc4691..595289125 100644
--- a/src/Tor/TorManager.py
+++ b/src/Tor/TorManager.py
@@ -23,6 +23,7 @@ class TorManager:
def __init__(self, fileserver_ip=None, fileserver_port=None):
self.privatekeys = {} # Onion: Privatekey
self.site_onions = {} # Site address: Onion
+ self.onion_sites = {} # Onion: Site address
self.tor_exe = "tools/tor/tor.exe"
self.tor_process = None
self.log = logging.getLogger("TorManager")
@@ -141,6 +142,7 @@ def connect(self):
if not self.enabled:
return False
self.site_onions = {}
+ self.onion_sites = {}
self.privatekeys = {}
if "socket_noproxy" in dir(socket): # Socket proxy-patched, use non-proxy one
@@ -193,6 +195,12 @@ def resetCircuits(self):
self.status = u"Reset circuits error (%s)" % res
self.log.error("Tor reset circuits error: %s" % res)
+ def haveOnionsAvailable(self):
+ return True # Can always create more onions
+
+ def numOnions(self):
+ return 100000 # Need the real limit here, but it is not very important to have am exact number
+
def addOnion(self):
res = self.request("ADD_ONION NEW:RSA1024 port=%s" % self.fileserver_port)
match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=RSA1024:(.*?)[\r\n]", res, re.DOTALL)
@@ -207,18 +215,28 @@ def addOnion(self):
self.log.error("Tor addOnion error: %s" % res)
return False
- def delOnion(self, address):
- res = self.request("DEL_ONION %s" % address)
+ def delSiteOnion(self, site_address, onion):
+ res = self.request("DEL_ONION %s" % onion)
if "250 OK" in res:
- del self.privatekeys[address]
- self.status = "OK (%s onion running)" % len(self.privatekeys)
+ del self.privatekeys[onion]
+ del self.site_onions[site_address]
+ del self.onion_sites[onion]
+ self.status = "OK (%s onion running)" % len(self.onion_sites)
return True
else:
self.status = u"DelOnion error (%s)" % res
- self.log.error("Tor delOnion error: %s" % res)
+ self.log.error("Tor delOnion %s for site %s error: %s" % (onion, site_address, res))
self.disconnect()
return False
+ def delOnion(self, onion):
+ site_address = self.onion_sites[onion]
+ return self.delSiteOnion(site_address, onion)
+
+ def delSite(self, site_address):
+ onion = self.site_onions[site_address]
+ return self.delSiteOnion(site_address, onion)
+
def request(self, cmd):
with self.lock:
if not self.enabled:
@@ -255,6 +273,7 @@ def getOnion(self, site_address):
if not onion:
self.site_onions[site_address] = self.addOnion()
onion = self.site_onions[site_address]
+ self.onion_sites[onion] = site_address
self.log.debug("Created new hidden service for %s: %s" % (site_address, onion))
return onion
diff --git a/src/Tor/TorManagerInside.py b/src/Tor/TorManagerInside.py
new file mode 100644
index 000000000..9a5d41c45
--- /dev/null
+++ b/src/Tor/TorManagerInside.py
@@ -0,0 +1,126 @@
+import logging
+import socket
+import random
+import sys
+
+from Config import config
+from Crypt import CryptRsa
+from Site import SiteManager
+
+# TorManagerInside is the version of TorManager reduced to the needs of the Tor-connected VM.
+
+class TorManagerInside:
+ def __init__(self, tor_hidden_services_fname):
+ self.privatekeys = {} # Onion: Privatekey
+ self.site_onions = {} # Site address: Onion
+ self.onion_sites = {} # Onion: Site address
+ self.log = logging.getLogger("TorManagerInside")
+
+ self.ip, self.port = config.tor_controller.split(":")
+ self.port = int(self.port)
+
+ self.hss_all = self.reshuffleList(self.readHiddenServices(tor_hidden_services_fname))
+ self.hss_unused = self.hss_all[:]
+
+ # Add our onions to the black list
+ for hs in self.hss_all:
+ SiteManager.peer_blacklist.append((hs[0], config.fileserver_port))
+
+ self.enabled = True
+ self.start_onions = True
+ self.updateStatus()
+
+ def readHiddenServices(self, fname):
+ with open(fname) as f:
+ lines = f.readlines()
+ hss = []
+ hs = []
+ phase = 'O'
+ onion = ""
+ key = ""
+ for line in lines:
+ line = line.strip('\n')
+ if line == "":
+ continue
+ if phase == 'O':
+ if not line.endswith(".onion"):
+ sys.exit("Onion address is expected in the hidden services file, got the line >%s<" % line)
+ onion = line
+ phase = 'H'
+ elif phase == 'H':
+ if line != "-----BEGIN RSA PRIVATE KEY-----":
+ sys.exit("Key header is expected in the hidden services file, got the line >%s<" % line)
+ phase = 'K'
+ elif phase == 'K':
+ if line != "-----END RSA PRIVATE KEY-----":
+ key = key+line
+ else:
+ hss.append([onion, key])
+ key = ""
+ phase = 'O'
+ if phase != 'O':
+ sys.exit("Unexpected end of the hidden services file")
+ return hss
+
+ def reshuffleList(self, lst):
+ idxs = range(0, len(lst))
+ shuf = []
+ while len(idxs) > 0:
+ n = random.randrange(0, len(idxs))
+ shuf.append(lst[idxs[n]])
+ del idxs[n]
+ return shuf
+
+ def updateStatus(self):
+ self.status = u"OK (%s onion used of %s available)" % (len(self.onion_sites), len(self.hss_all))
+
+ def getPrivatekey(self, address):
+ return self.privatekeys[address]
+
+ def getPublickey(self, address):
+ return CryptRsa.privatekeyToPublickey(self.privatekeys[address])
+
+ def haveOnionsAvailable(self):
+ return len(self.hss_unused) > 0
+
+ def numOnions(self):
+ return len(self.hss_all)
+
+ def getOnion(self, site_address):
+ onion = self.site_onions.get(site_address)
+ if onion:
+ return onion
+ if len(self.hss_unused) == 0:
+ sys.exit("TorManager ran out of onions (%u onions were supplied)" % len(self.hss_all))
+ hs_info = self.hss_unused[0]
+ del self.hss_unused[0]
+ onion = hs_info[0].replace(".onion", "")
+ self.site_onions[site_address] = onion
+ self.onion_sites[onion] = site_address
+ self.privatekeys[onion] = hs_info[1]
+ self.log.debug("Using the next onion for the site %s: %s.onion" % (site_address, onion))
+ self.updateStatus()
+ return onion
+
+ def delSiteOnion(self, site_address, onion):
+ self.log.debug("Deleting the site %s, recycling its onion %s.onion" % (site_address, onion))
+ self.hss_unused.append([onion+".onion", self.privatekeys[onion]])
+ del self.privatekeys[onion]
+ del self.site_onions[site_address]
+ del self.onion_sites[onion]
+ self.updateStatus()
+ return True
+
+ def delOnion(self, onion):
+ site_address = self.onion_sites[onion]
+ return self.delSiteOnion(site_address, onion)
+
+ def delSite(self, site_address):
+ onion = self.site_onions[site_address]
+ return self.delSiteOnion(site_address, onion)
+
+ def createSocket(self, onion, port):
+ self.log.debug("Creating new socket to %s:%s" % (onion, port))
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((onion, int(port)))
+ return sock
diff --git a/src/Tor/__init__.py b/src/Tor/__init__.py
index 250eac2d8..11936f2ac 100644
--- a/src/Tor/__init__.py
+++ b/src/Tor/__init__.py
@@ -1 +1,2 @@
-from TorManager import TorManager
\ No newline at end of file
+from TorManager import TorManager
+from TorManagerInside import TorManagerInside
diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py
index dde774547..8aa0fbdb9 100644
--- a/src/Ui/UiWebsocket.py
+++ b/src/Ui/UiWebsocket.py
@@ -56,6 +56,15 @@ def start(self):
""",
10000
])
+ elif config.tor == "inside" and file_server.tor_manager.start_onions:
+ self.site.notifications.append([
+ "done",
+ """
+ Tor mode active, every connection using Onion route.
+ Using externally supplied Tor onion hidden services.
+ """,
+ 10000
+ ])
elif config.tor == "always" and file_server.tor_manager.start_onions is not False:
self.site.notifications.append([
"error",
@@ -608,6 +617,8 @@ def actionSitePause(self, to, address):
site.saveSettings()
site.updateWebsocket()
site.worker_manager.stopWorkers()
+ if sys.modules["main"].file_server.tor_manager:
+ sys.modules["main"].file_server.tor_manager.delSite(address)
self.response(to, "Paused")
else:
self.response(to, {"error": "Unknown site: %s" % address})
@@ -616,6 +627,8 @@ def actionSitePause(self, to, address):
def actionSiteResume(self, to, address):
site = self.server.sites.get(address)
if site:
+ if sys.modules["main"].file_server.tor_manager and not sys.modules["main"].file_server.tor_manager.haveOnionsAvailable():
+ return # Failed to resume
site.settings["serving"] = True
site.saveSettings()
gevent.spawn(site.update, announce=True)
@@ -636,6 +649,8 @@ def actionSiteDelete(self, to, address):
site.updateWebsocket()
SiteManager.site_manager.delete(address)
self.user.deleteSiteData(address)
+ if sys.modules["main"].file_server.tor_manager:
+ sys.modules["main"].file_server.tor_manager.delSite(address)
self.response(to, "Deleted")
else:
self.response(to, {"error": "Unknown site: %s" % address})
diff --git a/src/main.py b/src/main.py
index c9bb1a9cf..cc7989e02 100644
--- a/src/main.py
+++ b/src/main.py
@@ -26,6 +26,14 @@
if not config.arguments: # Config parse failed, show the help screen and exit
config.parse()
+# Check config sanity
+if config.tor == "inside" and (config.tor_hidden_services == "" or not os.access(config.tor_hidden_services, os.R_OK)):
+ print "have to specify --tor_hidden_services with tor=inside"
+ sys.exit(1)
+if config.tor != "inside" and config.tor_hidden_services != "":
+ print "--tor_hidden_services can only be specified with tor=inside"
+ sys.exit(1)
+
# Create necessary files and dirs
if not os.path.isdir(config.log_dir):
os.mkdir(config.log_dir)