diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py new file mode 100644 index 00000000..72d3be22 --- /dev/null +++ b/plugins/Stats/StatsPlugin.py @@ -0,0 +1,135 @@ +import re, time, cgi, os +from Plugin import PluginManager + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def formatTableRow(self, row): + back = [] + for format, val in row: + if val == None: + formatted = "n/a" + elif format == "since": + if val: + formatted = "%.0f" % (time.time()-val) + else: + formatted = "n/a" + else: + formatted = format % val + back.append("%s" % formatted) + return "%s" % "".join(back) + + + def getObjSize(self, obj, hpy = None): + if hpy: + return float(hpy.iso(obj).domisize)/1024 + else: + return 0 + + + # /Stats entry point + def actionStats(self): + import gc, sys + from Ui import UiRequest + + hpy = None + if self.get.get("size") == "1": # Calc obj size + try: + import guppy + hpy = guppy.hpy() + except: + pass + self.sendHeader() + s = time.time() + main = sys.modules["main"] + + # Style + yield """ + + """ + + # Memory + try: + import psutil + process = psutil.Process(os.getpid()) + mem = process.get_memory_info()[0] / float(2 ** 20) + yield "Memory usage: %.2fMB | " % mem + yield "Threads: %s | " % len(process.threads()) + yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times() + yield "Open files: %s | " % len(process.open_files()) + yield "Sockets: %s" % len(process.connections()) + yield " | Calc size on off
" + except Exception, err: + pass + + yield "Connections (%s):
" % len(main.file_server.connections) + yield "" + yield "" + for connection in main.file_server.connections: + yield self.formatTableRow([ + ("%3d", connection.id), + ("%s", connection.protocol), + ("%s", connection.type), + ("%s", connection.ip), + ("%6.3f", connection.last_ping_delay), + ("%s", connection.incomplete_buff_recv), + ("since", max(connection.last_send_time, connection.last_recv_time)), + ("since", connection.start_time), + ("%.3f", connection.last_sent_time-connection.last_send_time), + ("%.0fkB", connection.bytes_sent/1024), + ("%.0fkB", connection.bytes_recv/1024), + ("%s", connection.last_cmd), + ("%s", connection.waiting_requests.keys()), + ("%s", connection.handshake.get("version")), + ("%s", connection.handshake.get("peer_id")), + ]) + yield "
id protocol type ip ping buffidle open delay sent received last sent waiting version peerid
" + + from greenlet import greenlet + objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)] + yield "
Greenlets (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + + + from Worker import Worker + objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)] + yield "
Workers (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + + + from Connection import Connection + objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)] + yield "
Connections (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + + + from Site import Site + objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)] + yield "
Sites (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + + + objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)] + yield "
Loggers (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj.name))) + + + objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)] + yield "
UiRequest (%s):
" % len(objs) + for obj in objs: + yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + + objs = [(key, val) for key, val in sys.modules.iteritems() if val is not None] + objs.sort() + yield "
Modules (%s):
" % len(objs) + for module_name, module in objs: + yield " - %.3fkb: %s %s
" % (self.getObjSize(module, hpy), module_name, cgi.escape(repr(module))) + + yield "Done in %.3f" % (time.time()-s) diff --git a/plugins/Stats/__init__.py b/plugins/Stats/__init__.py new file mode 100644 index 00000000..90bd9d6e --- /dev/null +++ b/plugins/Stats/__init__.py @@ -0,0 +1 @@ +import StatsPlugin \ No newline at end of file diff --git a/plugins/disabled-DonationMessage/DonationMessagePlugin.py b/plugins/disabled-DonationMessage/DonationMessagePlugin.py new file mode 100644 index 00000000..00be03f1 --- /dev/null +++ b/plugins/disabled-DonationMessage/DonationMessagePlugin.py @@ -0,0 +1,24 @@ +import re +from Plugin import PluginManager + +# Warning: If you modify the donation address then renmae the plugin's directory to "MyDonationMessage" to prevent the update script overwrite + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + # Inject a donation message to every page top right corner + def actionWrapper(self, path): + back = super(UiRequestPlugin, self).actionWrapper(path) + if not back or not hasattr(back, "endswith"): return back # Wrapper error or not string returned, injection not possible + + back = re.sub("\s*\s*$", + """ + + Please donate to help to keep this ZeroProxy alive + + + """, back) + + return back diff --git a/plugins/disabled-DonationMessage/__init__.py b/plugins/disabled-DonationMessage/__init__.py new file mode 100644 index 00000000..f8dcae2f --- /dev/null +++ b/plugins/disabled-DonationMessage/__init__.py @@ -0,0 +1 @@ +import DonationMessagePlugin diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py new file mode 100644 index 00000000..ed05dba2 --- /dev/null +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -0,0 +1,169 @@ +import re, time, sys +from Plugin import PluginManager +from Crypt import CryptBitcoin + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def __init__(self, server = None): + self.user_manager = sys.modules["User.UserManager"].user_manager + super(UiRequestPlugin, self).__init__(server) + + + # Create new user and inject user welcome message if necessary + # Return: Html body also containing the injection + def actionWrapper(self, path): + user_created = False + user = self.getCurrentUser() # Get user from cookie + + if not user: # No user found by cookie + user = self.user_manager.create() + user_created = True + + master_address = user.master_address + master_seed = user.master_seed + + if user_created: + extra_headers = [('Set-Cookie', "master_address=%s;path=/;max-age=2592000;" % user.master_address)] # Max age = 30 days + else: + extra_headers = [] + + loggedin = self.get.get("login") == "done" + + back = super(UiRequestPlugin, self).actionWrapper(path, extra_headers) # Get the wrapper frame output + + if not user_created and not loggedin: return back # No injection necessary + + if not back or not hasattr(back, "endswith"): return back # Wrapper error or not string returned, injection not possible + + if user_created: + # Inject the welcome message + inject_html = """ + + + + + + """.replace("\t", "") + inject_html = inject_html.replace("{master_seed}", master_seed) # Set the master seed in the message + + back = re.sub("\s*\s*$", inject_html, back) # Replace the tags with the injection + + elif loggedin: + inject_html = """ + + + + + """.replace("\t", "") + back = re.sub("\s*\s*$", inject_html, back) # Replace the tags with the injection + + return back + + + # Get the current user based on request's cookies + # Return: User object or None if no match + def getCurrentUser(self): + cookies = self.getCookies() + user_manager = self.user_manager + user = None + if "master_address" in cookies: + users = self.user_manager.list() + user = users.get(cookies["master_address"]) + return user + + +@PluginManager.registerTo("UserManager") +class UserManagerPlugin(object): + # In multiuser mode do not load the users + def load(self): + if not self.users: self.users = {} + return self.users + + + # Find user by master address + # Return: User or None + def get(self, master_address=None): + users = self.list() + if master_address in users: + user = users[master_address] + else: + user = None + return user + + +@PluginManager.registerTo("User") +class UserPlugin(object): + # In multiuser mode users data only exits in memory, dont write to data/user.json + def save(self): + return False + + +@PluginManager.registerTo("UiWebsocket") +class UiWebsocketPlugin(object): + # Let the page know we running in multiuser mode + def formatServerInfo(self): + server_info = super(UiWebsocketPlugin, self).formatServerInfo() + server_info["multiuser"] = True + if "ADMIN" in self.site.settings["permissions"]: + server_info["master_address"] = self.user.master_address + return server_info + + + # Show current user's master seed + def actionUserShowMasterSeed(self, to): + if not "ADMIN" in self.site.settings["permissions"]: return self.response(to, "Show master seed not allowed") + message = "Your unique private key:" + message+= "
%s
" % self.user.master_seed + message+= "(Save it, you can access your account using this information)" + self.cmd("notification", ["info", message]) + + + # Logout user + def actionUserLogout(self, to): + if not "ADMIN" in self.site.settings["permissions"]: return self.response(to, "Logout not allowed") + message = "You have been logged out. Login to another account" + message+= "" + self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :) + # Delete from user_manager + user_manager = sys.modules["User.UserManager"].user_manager + if self.user.master_address in user_manager.users: + del user_manager.users[self.user.master_address] + self.response(to, "Successful logout") + else: + self.response(to, "User not found") + + + # Show login form + def actionUserLoginForm(self, to): + self.cmd("prompt", ["Login
Your private key:", "password", "Login"], self.responseUserLogin) + + + # Login form submit + def responseUserLogin(self, master_seed): + user_manager = sys.modules["User.UserManager"].user_manager + user = user_manager.create(master_seed=master_seed) + if user.master_address: + message = "Successfull login, reloading page..." + message+= "" % user.master_address + message+= "" + self.cmd("notification", ["done", message]) + else: + self.cmd("notification", ["error", "Error: Invalid master seed"]) + self.actionUserLoginForm(0) + diff --git a/plugins/disabled-Multiuser/__init__.py b/plugins/disabled-Multiuser/__init__.py new file mode 100644 index 00000000..154d6008 --- /dev/null +++ b/plugins/disabled-Multiuser/__init__.py @@ -0,0 +1 @@ +import MultiuserPlugin diff --git a/src/Config.py b/src/Config.py index 3136372c..d3421c19 100644 --- a/src/Config.py +++ b/src/Config.py @@ -3,7 +3,7 @@ import ConfigParser class Config(object): def __init__(self): - self.version = "0.2.6" + self.version = "0.2.7" self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index 19c6fc1e..7735c1f7 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,5 +1,5 @@ -from src.lib.BitcoinECC import BitcoinECC -from src.lib.pybitcointools import bitcoin as btctools +from lib.BitcoinECC import BitcoinECC +from lib.pybitcointools import bitcoin as btctools def newPrivatekey(uncompressed=True): # Return new private key diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py index f6045210..78c1fd5d 100644 --- a/src/Debug/DebugReloader.py +++ b/src/Debug/DebugReloader.py @@ -6,6 +6,7 @@ if config.debug: # Only load pyfilesytem if using debug mode try: from fs.osfs import OSFS pyfilesystem = OSFS("src") + pyfilesystem_plugins = OSFS("plugins") except Exception, err: logging.debug("%s: For autoreload please download pyfilesystem (https://code.google.com/p/pyfilesystem/)" % err) pyfilesystem = False @@ -28,6 +29,7 @@ class DebugReloader: try: time.sleep(1) # Wait for .pyc compiles pyfilesystem.add_watcher(self.changed, path=self.directory, events=None, recursive=recursive) + pyfilesystem_plugins.add_watcher(self.changed, path=self.directory, events=None, recursive=recursive) except Exception, err: print "File system watcher failed: %s (on linux pyinotify not gevent compatible yet :( )" % err diff --git a/src/File/FileServer.py b/src/File/FileServer.py index b9a5b42c..8a6c9c6c 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -1,4 +1,4 @@ -import os, logging, urllib2, urllib, re, time +import os, logging, urllib2, re, time import gevent, msgpack import zmq.green as zmq from Config import config diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index d5b11d42..85b85733 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -12,7 +12,7 @@ class Peer: self.site = site self.key = "%s:%s" % (ip, port) self.log = None - self.connection_server = sys.modules["src.main"].file_server + self.connection_server = sys.modules["main"].file_server self.connection = None self.last_found = None # Time of last found in the torrent tracker diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py new file mode 100644 index 00000000..6b6f2594 --- /dev/null +++ b/src/Plugin/PluginManager.py @@ -0,0 +1,97 @@ +import logging, os, sys +from Debug import Debug +from Config import config + +class PluginManager: + def __init__(self): + self.log = logging.getLogger("PluginManager") + self.plugin_path = "plugins" # Plugin directory + self.plugins = {} # Registered plugins (key: class name, value: list of plugins for class) + + sys.path.append(self.plugin_path) + + + if config.debug: # Auto reload Plugins on file change + from Debug import DebugReloader + DebugReloader(self.reloadPlugins) + + + # -- Load / Unload -- + + # Load all plugin + def loadPlugins(self): + for dir_name in os.listdir(self.plugin_path): + dir_path = os.path.join(self.plugin_path, dir_name) + if dir_name.startswith("disabled"): continue # Dont load if disabled + if not os.path.isdir(dir_path): continue # Dont load if not dir + if dir_name.startswith("Debug") and not config.debug: continue # Only load in debug mode if module name starts with Debug + self.log.debug("Loading plugin: %s" % dir_name) + try: + __import__(dir_name) + except Exception, err: + self.log.error("Plugin %s load error: %s" % (dir_name, Debug.formatException(err))) + + + # Reload all plugins + def reloadPlugins(self): + self.plugins = {} # Reset registered plugins + for module_name, module in sys.modules.items(): + if module and "__file__" in dir(module) and self.plugin_path in module.__file__: # Module file within plugin_path + if "allow_reload" not in dir(module) or module.allow_reload: # Check if reload disabled + try: + reload(module) + except Exception, err: + self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) + + self.loadPlugins() # Load new plugins + + +plugin_manager = PluginManager() # Singletone + +# -- Decorators -- + +# Accept plugin to class decorator +def acceptPlugins(base_class): + class_name = base_class.__name__ + if class_name in plugin_manager.plugins: # Has plugins + classes = plugin_manager.plugins[class_name][:] # Copy the current plugins + classes.reverse() + classes.append(base_class) # Add the class itself to end of inherience line + PluginedClass = type(class_name, tuple(classes), dict()) # Create the plugined class + plugin_manager.log.debug("New class accepts plugins: %s (Loaded plugins: %s)" % (class_name, classes)) + else: # No plugins just use the original + PluginedClass = base_class + return PluginedClass + + +# Register plugin to class name decorator +def registerTo(class_name): + plugin_manager.log.debug("New plugin registered to: %s" % class_name) + if class_name not in plugin_manager.plugins: plugin_manager.plugins[class_name] = [] + + def classDecorator(self): + plugin_manager.plugins[class_name].append(self) + return self + return classDecorator + + + +# - Example usage - + +if __name__ == "__main__": + @registerTo("Request") + class RequestPlugin(object): + def actionMainPage(self, path): + return "Hello MainPage!" + + + @accept + class Request(object): + def route(self, path): + func = getattr(self, "action"+path, None) + if func: + return func(path) + else: + return "Can't route to", path + + print Request().route("MainPage") diff --git a/src/Plugin/__init__.py b/src/Plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a47c7974..333ee1b2 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -2,6 +2,7 @@ import time, re, os, mimetypes, json, cgi from Config import config from Site import SiteManager from User import UserManager +from Plugin import PluginManager from Ui.UiWebsocket import UiWebsocket status_texts = { @@ -12,15 +13,15 @@ status_texts = { } - -class UiRequest: +@PluginManager.acceptPlugins +class UiRequest(object): def __init__(self, server = None): if server: self.server = server self.log = server.log self.get = {} # Get parameters self.env = {} # Enviroment settings - self.user = UserManager.getCurrent() + self.user = None self.start_response = None # Start response function @@ -46,16 +47,17 @@ class UiRequest: return self.actionDebug() elif path == "/Console" and config.debug: return self.actionConsole() - elif path == "/Stats": - return self.actionStats() - # Test - elif path == "/Test/Websocket": - return self.actionFile("Data/temp/ws_test.html") - elif path == "/Test/Stream": - return self.actionTestStream() # Site media wrapper else: - return self.actionWrapper(path) + body = self.actionWrapper(path) + if body: + return body + else: + func = getattr(self, "action"+path.lstrip("/"), None) # Check if we have action+request_path function + if func: + return func() + else: + return self.error404(path) # Get mime by filename @@ -69,6 +71,24 @@ class UiRequest: return content_type + # Returns: Cookies based on self.env + def getCookies(self): + raw_cookies = self.env.get('HTTP_COOKIE') + if raw_cookies: + cookies = cgi.parse_qsl(raw_cookies) + return {key.strip(): val for key, val in cookies} + else: + return {} + + + def getCurrentUser(self): + if self.user: return self.user # Cache + self.user = UserManager.user_manager.get() # Get user + if not self.user: + self.user = UserManager.user_manager.create() + return self.user + + # Send response headers def sendHeader(self, status=200, content_type="text/html", extra_headers=[]): if content_type == "text/html": content_type = "text/html; charset=utf-8" @@ -89,7 +109,7 @@ class UiRequest: #template = SimpleTemplate(open(template_path), lookup=[os.path.dirname(template_path)]) #yield str(template.render(*args, **kwargs).encode("utf8")) template = open(template_path).read().decode("utf8") - yield template.format(**kwargs).encode("utf8") + return template.format(**kwargs).encode("utf8") # - Actions - @@ -105,7 +125,7 @@ class UiRequest: # Render a file from media with iframe site wrapper - def actionWrapper(self, path): + def actionWrapper(self, path, extra_headers=[]): if "." in path and not path.endswith(".html"): return self.actionSiteMedia("/media"+path) # Only serve html files with frame if self.get.get("wrapper") == "False": return self.actionSiteMedia("/media"+path) # Only serve html files with frame if self.env.get("HTTP_X_REQUESTED_WITH"): return self.error403() # No ajax allowed on wrapper @@ -121,9 +141,11 @@ class UiRequest: else: title = "Loading %s..." % match.group("site") site = SiteManager.need(match.group("site")) # Start download site - if not site: return self.error404(path) + if not site: return False - self.sendHeader(extra_headers=[("X-Frame-Options", "DENY")]) + extra_headers.append(("X-Frame-Options", "DENY")) + + self.sendHeader(extra_headers=extra_headers) # Wrapper variable inits query_string = "" @@ -152,7 +174,7 @@ class UiRequest: ) else: # Bad url - return self.error404(path) + return False # Serve a media for site @@ -241,7 +263,11 @@ class UiRequest: if site_check.settings["wrapper_key"] == wrapper_key: site = site_check if site: # Correct wrapper key - ui_websocket = UiWebsocket(ws, site, self.server, self.user) + user = self.getCurrentUser() + if not user: + self.log.error("No user found") + return self.error403() + ui_websocket = UiWebsocket(ws, site, self.server, user) site.websockets.append(ui_websocket) # Add to site websockets to allow notify on events ui_websocket.start() for site_check in self.server.sites.values(): # Remove websocket from every site (admin sites allowed to join other sites event channels) @@ -260,7 +286,7 @@ class UiRequest: def actionDebug(self): # Raise last error from DebugHook import sys - last_error = sys.modules["src.main"].DebugHook.last_error + last_error = sys.modules["main"].DebugHook.last_error if last_error: raise last_error[0], last_error[1], last_error[2] else: @@ -272,134 +298,10 @@ class UiRequest: def actionConsole(self): import sys sites = self.server.sites - main = sys.modules["src.main"] + main = sys.modules["main"] raise Exception("Here is your console") - def formatTableRow(self, row): - back = [] - for format, val in row: - if val == None: - formatted = "n/a" - elif format == "since": - if val: - formatted = "%.0f" % (time.time()-val) - else: - formatted = "n/a" - else: - formatted = format % val - back.append("%s" % formatted) - return "%s" % "".join(back) - - - def getObjSize(self, obj, hpy = None): - if hpy: - return float(hpy.iso(obj).domisize)/1024 - else: - return 0 - - - - def actionStats(self): - import gc, sys - hpy = None - if self.get.get("size") == "1": # Calc obj size - try: - import guppy - hpy = guppy.hpy() - except: - pass - self.sendHeader() - s = time.time() - main = sys.modules["src.main"] - - # Style - yield """ - - """ - - # Memory - try: - import psutil - process = psutil.Process(os.getpid()) - mem = process.get_memory_info()[0] / float(2 ** 20) - yield "Memory usage: %.2fMB | " % mem - yield "Threads: %s | " % len(process.threads()) - yield "CPU: usr %.2fs sys %.2fs | " % process.cpu_times() - yield "Open files: %s | " % len(process.open_files()) - yield "Sockets: %s" % len(process.connections()) - yield " | Calc size on off
" - except Exception, err: - pass - - yield "Connections (%s):
" % len(main.file_server.connections) - yield "" - yield "" - for connection in main.file_server.connections: - yield self.formatTableRow([ - ("%3d", connection.id), - ("%s", connection.protocol), - ("%s", connection.type), - ("%s", connection.ip), - ("%6.3f", connection.last_ping_delay), - ("%s", connection.incomplete_buff_recv), - ("since", max(connection.last_send_time, connection.last_recv_time)), - ("since", connection.start_time), - ("%.3f", connection.last_sent_time-connection.last_send_time), - ("%.0fkB", connection.bytes_sent/1024), - ("%.0fkB", connection.bytes_recv/1024), - ("%s", connection.last_cmd), - ("%s", connection.waiting_requests.keys()), - ("%s", connection.handshake.get("version")), - ("%s", connection.handshake.get("peer_id")), - ]) - yield "
id protocol type ip ping buffidle open delay sent received last sent waiting version peerid
" - - from greenlet import greenlet - objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)] - yield "
Greenlets (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) - - - from Worker import Worker - objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)] - yield "
Workers (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) - - - from Connection import Connection - objs = [obj for obj in gc.get_objects() if isinstance(obj, Connection)] - yield "
Connections (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) - - - from Site import Site - objs = [obj for obj in gc.get_objects() if isinstance(obj, Site)] - yield "
Sites (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) - - - objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)] - yield "
Loggers (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj.name))) - - - objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)] - yield "
UiRequest (%s):
" % len(objs) - for obj in objs: - yield " - %.3fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) - - yield "Done in %.3f" % (time.time()-s) - - # - Tests - def actionTestStream(self): @@ -433,8 +335,9 @@ class UiRequest: # - Reload for eaiser developing - def reload(self): - import imp + import imp, sys global UiWebsocket UiWebsocket = imp.load_source("UiWebsocket", "src/Ui/UiWebsocket.py").UiWebsocket - UserManager.reload() - self.user = UserManager.getCurrent() + #reload(sys.modules["User.UserManager"]) + #UserManager.reloadModule() + #self.user = UserManager.user_manager.getCurrent() diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 5fa00690..b677887d 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -3,11 +3,12 @@ import logging, time, cgi, string, random from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIHandler from lib.geventwebsocket.handler import WebSocketHandler -from Ui import UiRequest +from UiRequest import UiRequest from Site import SiteManager from Config import config from Debug import Debug + # Skip websocket handler if not necessary class UiWSGIHandler(WSGIHandler): def __init__(self, *args, **kwargs): @@ -28,7 +29,10 @@ class UiWSGIHandler(WSGIHandler): try: return super(UiWSGIHandler, self).run_application() except Exception, err: - logging.debug("UiWSGIHandler error: %s" % err) + logging.debug("UiWSGIHandler error: %s" % Debug.formatException(err)) + if config.debug: # Allow websocket errors to appear on /Debug + import sys + sys.modules["main"].DebugHook.handleError() del self.server.sockets[self.client_address] @@ -59,7 +63,8 @@ class UiServer: # Reload the UiRequest class to prevent restarts in debug mode def reload(self): - import imp + import imp, sys + reload(sys.modules["User.UserManager"]) self.ui_request = imp.load_source("UiRequest", "src/Ui/UiRequest.py").UiRequest(self) self.ui_request.reload() diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index a2ad559d..602692ab 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -3,9 +3,10 @@ from Config import config from Site import SiteManager from Debug import Debug from util import QueryJson +from Plugin import PluginManager - -class UiWebsocket: +@PluginManager.acceptPlugins +class UiWebsocket(object): def __init__(self, ws, site, server, user): self.ws = ws self.site = site @@ -41,7 +42,7 @@ class UiWebsocket: if err.message != 'Connection is already closed': if config.debug: # Allow websocket errors to appear on /Debug import sys - sys.modules["src.main"].DebugHook.handleError() + sys.modules["main"].DebugHook.handleError() self.log.error("WebSocket error: %s" % Debug.formatException(err)) return "Bye." @@ -133,10 +134,12 @@ class UiWebsocket: func = self.actionChannelJoinAllsite elif cmd == "serverUpdate" and "ADMIN" in permissions: func = self.actionServerUpdate - # Unknown command else: - self.response(req["id"], "Unknown command: %s" % cmd) - return + func_name = "action" + cmd[0].upper() + cmd[1:] + func = getattr(self, func_name, None) + if not func: # Unknown command + self.response(req["id"], "Unknown command: %s" % cmd) + return # Support calling as named, unnamed paramters and raw first argument too if type(params) is dict: @@ -152,7 +155,7 @@ class UiWebsocket: # Do callback on response {"cmd": "response", "to": message_id, "result": result} def actionResponse(self, to, result): if to in self.waiting_cb: - self.waiting_cb(result) # Call callback function + self.waiting_cb[to](result) # Call callback function else: self.log.error("Websocket callback not found: %s, %s" % (to, result)) @@ -163,7 +166,7 @@ class UiWebsocket: # Format site info - def formatSiteInfo(self, site): + def formatSiteInfo(self, site, create_user=True): content = site.content_manager.contents.get("content.json") if content: # Remove unnecessary data transfer content = content.copy() @@ -179,7 +182,7 @@ class UiWebsocket: ret = { "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed "auth_key_sha512": hashlib.sha512(self.site.settings["auth_key"]).hexdigest()[0:64], # Obsolete, will be removed - "auth_address": self.user.getAuthAddress(site.address), + "auth_address": self.user.getAuthAddress(site.address, create=create_user), "address": site.address, "settings": settings, "content_updated": site.content_updated, @@ -208,9 +211,8 @@ class UiWebsocket: self.channels.append(channel) - # Server variables - def actionServerInfo(self, to): - ret = { + def formatServerInfo(self): + return { "ip_external": bool(config.ip_external), "platform": sys.platform, "fileserver_ip": config.fileserver_ip, @@ -220,6 +222,11 @@ class UiWebsocket: "version": config.version, "debug": config.debug } + + + # Server variables + def actionServerInfo(self, to): + ret = self.formatServerInfo() self.response(to, ret) @@ -323,7 +330,7 @@ class UiWebsocket: SiteManager.load() # Reload sites for site in self.server.sites.values(): if not site.content_manager.contents.get("content.json"): continue # Broken site - ret.append(self.formatSiteInfo(site)) + ret.append(self.formatSiteInfo(site, create_user=False)) self.response(to, ret) @@ -395,7 +402,7 @@ class UiWebsocket: def actionServerUpdate(self, to): import sys self.cmd("updating") - sys.modules["src.main"].update_after_shutdown = True - sys.modules["src.main"].file_server.stop() - sys.modules["src.main"].ui_server.stop() + sys.modules["main"].update_after_shutdown = True + sys.modules["main"].file_server.stop() + sys.modules["main"].ui_server.stop() diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index ec26f425..cb277612 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -33,7 +33,7 @@ class Notifications $(".notification-icon", elem).html("i") if typeof(body) == "string" - $(".body", elem).html(body) + $(".body", elem).html(""+body+"") else $(".body", elem).html("").append(body) @@ -49,9 +49,11 @@ class Notifications # Animate width = elem.outerWidth() if not timeout then width += 20 # Add space for close button + if elem.outerHeight() > 55 then elem.addClass("long") elem.css({"width": "50px", "transform": "scale(0.01)"}) elem.animate({"scale": 1}, 800, "easeOutElastic") elem.animate({"width": width}, 700, "easeInOutCubic") + $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000) # Close button $(".close", elem).on "click", => diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 04571af5..e451f851 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -42,6 +42,9 @@ class Wrapper @sendInner message # Pass message to inner frame else if cmd == "notification" # Display notification @notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2]) + else if cmd == "prompt" # Prompt input + @displayPrompt message.params[0], message.params[1], message.params[2], (res) => + @ws.response message.id, res else if cmd == "setSiteInfo" @sendInner message # Pass to inner frame if message.params.address == window.address # Current page @@ -63,18 +66,19 @@ class Wrapper @sendInner {"cmd": "wrapperOpenedWebsocket"} @wrapperWsInited = true else if cmd == "wrapperNotification" # Display notification - message.params = @toHtmlSafe(message.params) # Escape html - @notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2]) + @actionNotification(message) else if cmd == "wrapperConfirm" # Display confirm message - @actionWrapperConfirm(message) + @actionConfirm(message) else if cmd == "wrapperPrompt" # Prompt input - @actionWrapperPrompt(message) + @actionPrompt(message) else if cmd == "wrapperSetViewport" # Set the viewport @actionSetViewport(message) + else if cmd == "wrapperReload" # Reload current page + @actionReload(message) else if cmd == "wrapperGetLocalStorage" @actionGetLocalStorage(message) else if cmd == "wrapperSetLocalStorage" - @actionSetLocalStorage(message) + @actionSetLocalStorage(message) else # Send to websocket if message.id < 1000000 @ws.send(message) # Pass message to websocket @@ -84,46 +88,58 @@ class Wrapper # - Actions - - actionWrapperConfirm: (message, cb=false) -> + actionNotification: (message) -> message.params = @toHtmlSafe(message.params) # Escape html - if message.params[1] then caption = message.params[1] else caption = "ok" - @wrapperConfirm message.params[0], caption, => - @sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm - return false + body = $(""+message.params[1]+"") + @notifications.add("notification-#{message.id}", message.params[0], body, message.params[2]) - wrapperConfirm: (message, caption, cb) -> - body = $(""+message+"") + + displayConfirm: (message, caption, cb) -> + body = $(""+message+"") button = $("#{caption}") # Add confirm button button.on "click", cb body.append(button) @notifications.add("notification-#{caption}", "ask", body) - - actionWrapperPrompt: (message) -> + actionConfirm: (message, cb=false) -> message.params = @toHtmlSafe(message.params) # Escape html - if message.params[1] then type = message.params[1] else type = "text" - caption = "OK" + if message.params[1] then caption = message.params[1] else caption = "ok" + @displayConfirm message.params[0], caption, => + @sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm + return false - body = $(""+message.params[0]+"") + + + displayPrompt: (message, type, caption, cb) -> + body = $(""+message+"") input = $("") # Add input input.on "keyup", (e) => # Send on enter if e.keyCode == 13 button.trigger "click" # Response to confirm - body.append(input) button = $("#{caption}") # Add confirm button button.on "click", => # Response on button click - @sendInner {"cmd": "response", "to": message.id, "result": input.val()} # Response to confirm + cb input.val() return false body.append(button) @notifications.add("notification-#{message.id}", "ask", body) + actionPrompt: (message) -> + message.params = @toHtmlSafe(message.params) # Escape html + if message.params[1] then type = message.params[1] else type = "text" + caption = "OK" + + @displayPrompt message.params[0], type, caption, (res) => + @sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm + + + actionSetViewport: (message) -> @log "actionSetViewport", message if $("#viewport").length > 0 @@ -132,6 +148,16 @@ class Wrapper $('').attr("content", @toHtmlSafe message.params).appendTo("head") + reload: (url_post="") -> + if url_post + if window.location.toString().indexOf("?") > 0 + window.location += "&"+url_post + else + window.location += "?"+url_post + else + window.location.reload() + + actionGetLocalStorage: (message) -> data = localStorage.getItem "site.#{window.address}" if data then data = JSON.parse(data) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 283b0d9b..2f7a4690 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -33,21 +33,28 @@ a { color: black } /* Notification */ -.notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap } +.notifications { position: absolute; top: 0px; right: 80px; display: inline-block; z-index: 999; white-space: nowrap } .notification { - position: relative; float: right; clear: both; margin: 10px; height: 50px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; perspective: 1000px; - background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ + position: relative; float: right; clear: both; margin: 10px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; perspective: 1000px; padding-bottom: 5px; + color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white; } -.notification .body { max-width: 420px; padding-left: 68px; padding-right: 17px; height: 50px; vertical-align: middle; display: table-cell } +.notification .body { + max-width: 420px; padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table; + background-color: white; left: 50px; top: 0px; position: relative; padding-top: 5px; padding-bottom: 5px; +} +.notification.long .body { padding-top: 10px; padding-bottom: 10px } +.notification .message { display: table-cell; vertical-align: middle } + .notification.visible { max-width: 350px } .notification .close { position: absolute; top: 0px; right: 0px; font-size: 19px; line-height: 13px; color: #DDD; padding: 7px; text-decoration: none } .notification .close:hover { color: black } .notification .close:active, .notification .close:focus { color: #AF3BFF } +.notification small { color: #AAA } .body-white .notification { box-shadow: 0px 1px 9px rgba(0,0,0,0.1) } /* Notification types */ diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index d5c48986..3d9d37cc 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -38,21 +38,28 @@ a { color: black } /* Notification */ -.notifications { position: absolute; top: 0px; right: 85px; display: inline-block; z-index: 999; white-space: nowrap } +.notifications { position: absolute; top: 0px; right: 80px; display: inline-block; z-index: 999; white-space: nowrap } .notification { - position: relative; float: right; clear: both; margin: 10px; height: 50px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; - background-color: white; color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ + position: relative; float: right; clear: both; margin: 10px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; padding-bottom: 5px; + color: #4F4F4F; font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white; } -.notification .body { max-width: 420px; padding-left: 68px; padding-right: 17px; height: 50px; vertical-align: middle; display: table-cell } +.notification .body { + max-width: 420px; padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table; + background-color: white; left: 50px; top: 0px; position: relative; padding-top: 5px; padding-bottom: 5px; +} +.notification.long .body { padding-top: 10px; padding-bottom: 10px } +.notification .message { display: table-cell; vertical-align: middle } + .notification.visible { max-width: 350px } .notification .close { position: absolute; top: 0px; right: 0px; font-size: 19px; line-height: 13px; color: #DDD; padding: 7px; text-decoration: none } .notification .close:hover { color: black } .notification .close:active, .notification .close:focus { color: #AF3BFF } +.notification small { color: #AAA } .body-white .notification { -webkit-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -moz-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -o-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -ms-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; box-shadow: 0px 1px 9px rgba(0,0,0,0.1) } /* Notification types */ diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 0ff4f45d..c1eba6a5 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -149,7 +149,6 @@ }).call(this); - /* ---- src/Ui/media/lib/jquery.cssanim.js ---- */ @@ -247,7 +246,6 @@ jQuery.fx.step.scale = function(fx) { }).call(this); - /* ---- src/Ui/media/lib/jquery.easing.1.3.js ---- */ @@ -542,7 +540,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/Notifications.coffee ---- */ @@ -593,7 +590,7 @@ jQuery.extend( jQuery.easing, $(".notification-icon", elem).html("i"); } if (typeof body === "string") { - $(".body", elem).html(body); + $(".body", elem).html("" + body + ""); } else { $(".body", elem).html("").append(body); } @@ -610,6 +607,9 @@ jQuery.extend( jQuery.easing, if (!timeout) { width += 20; } + if (elem.outerHeight() > 55) { + elem.addClass("long"); + } elem.css({ "width": "50px", "transform": "scale(0.01)" @@ -620,6 +620,7 @@ jQuery.extend( jQuery.easing, elem.animate({ "width": width }, 700, "easeInOutCubic"); + $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000); $(".close", elem).on("click", (function(_this) { return function() { _this.close(elem); @@ -725,7 +726,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/Wrapper.coffee ---- */ @@ -786,6 +786,12 @@ jQuery.extend( jQuery.easing, } } else if (cmd === "notification") { return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]); + } else if (cmd === "prompt") { + return this.displayPrompt(message.params[0], message.params[1], message.params[2], (function(_this) { + return function(res) { + return _this.ws.response(message.id, res); + }; + })(this)); } else if (cmd === "setSiteInfo") { this.sendInner(message); if (message.params.address === window.address) { @@ -812,14 +818,15 @@ jQuery.extend( jQuery.easing, return this.wrapperWsInited = true; } } else if (cmd === "wrapperNotification") { - message.params = this.toHtmlSafe(message.params); - return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]); + return this.actionNotification(message); } else if (cmd === "wrapperConfirm") { - return this.actionWrapperConfirm(message); + return this.actionConfirm(message); } else if (cmd === "wrapperPrompt") { - return this.actionWrapperPrompt(message); + return this.actionPrompt(message); } else if (cmd === "wrapperSetViewport") { return this.actionSetViewport(message); + } else if (cmd === "wrapperReload") { + return this.actionReload(message); } else if (cmd === "wrapperGetLocalStorage") { return this.actionGetLocalStorage(message); } else if (cmd === "wrapperSetLocalStorage") { @@ -833,7 +840,23 @@ jQuery.extend( jQuery.easing, } }; - Wrapper.prototype.actionWrapperConfirm = function(message, cb) { + Wrapper.prototype.actionNotification = function(message) { + var body; + message.params = this.toHtmlSafe(message.params); + body = $("" + message.params[1] + ""); + return this.notifications.add("notification-" + message.id, message.params[0], body, message.params[2]); + }; + + Wrapper.prototype.displayConfirm = function(message, caption, cb) { + var body, button; + body = $("" + message + ""); + button = $("" + caption + ""); + button.on("click", cb); + body.append(button); + return this.notifications.add("notification-" + caption, "ask", body); + }; + + Wrapper.prototype.actionConfirm = function(message, cb) { var caption; if (cb == null) { cb = false; @@ -844,7 +867,7 @@ jQuery.extend( jQuery.easing, } else { caption = "ok"; } - return this.wrapperConfirm(message.params[0], caption, (function(_this) { + return this.displayConfirm(message.params[0], caption, (function(_this) { return function() { _this.sendInner({ "cmd": "response", @@ -856,25 +879,9 @@ jQuery.extend( jQuery.easing, })(this)); }; - Wrapper.prototype.wrapperConfirm = function(message, caption, cb) { - var body, button; - body = $("" + message + ""); - button = $("" + caption + ""); - button.on("click", cb); - body.append(button); - return this.notifications.add("notification-" + caption, "ask", body); - }; - - Wrapper.prototype.actionWrapperPrompt = function(message) { - var body, button, caption, input, type; - message.params = this.toHtmlSafe(message.params); - if (message.params[1]) { - type = message.params[1]; - } else { - type = "text"; - } - caption = "OK"; - body = $("" + message.params[0] + ""); + Wrapper.prototype.displayPrompt = function(message, type, caption, cb) { + var body, button, input; + body = $("" + message + ""); input = $(""); input.on("keyup", (function(_this) { return function(e) { @@ -887,11 +894,7 @@ jQuery.extend( jQuery.easing, button = $("" + caption + ""); button.on("click", (function(_this) { return function() { - _this.sendInner({ - "cmd": "response", - "to": message.id, - "result": input.val() - }); + cb(input.val()); return false; }; })(this)); @@ -899,6 +902,26 @@ jQuery.extend( jQuery.easing, return this.notifications.add("notification-" + message.id, "ask", body); }; + Wrapper.prototype.actionPrompt = function(message) { + var caption, type; + message.params = this.toHtmlSafe(message.params); + if (message.params[1]) { + type = message.params[1]; + } else { + type = "text"; + } + caption = "OK"; + return this.displayPrompt(message.params[0], type, caption, (function(_this) { + return function(res) { + return _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": res + }); + }; + })(this)); + }; + Wrapper.prototype.actionSetViewport = function(message) { this.log("actionSetViewport", message); if ($("#viewport").length > 0) { @@ -908,6 +931,21 @@ jQuery.extend( jQuery.easing, } }; + Wrapper.prototype.reload = function(url_post) { + if (url_post == null) { + url_post = ""; + } + if (url_post) { + if (window.location.toString().indexOf("?") > 0) { + return window.location += "&" + url_post; + } else { + return window.location += "?" + url_post; + } + } else { + return window.location.reload(); + } + }; + Wrapper.prototype.actionGetLocalStorage = function(message) { var data; data = localStorage.getItem("site." + window.address); @@ -1116,4 +1154,4 @@ jQuery.extend( jQuery.easing, window.wrapper = new Wrapper(ws_url); -}).call(this); +}).call(this); \ No newline at end of file diff --git a/src/User/User.py b/src/User/User.py index 14b7072e..65f64c13 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -1,9 +1,14 @@ import logging, json, time from Crypt import CryptBitcoin +from Plugin import PluginManager -class User: - def __init__(self, master_address=None): - if master_address: +@PluginManager.acceptPlugins +class User(object): + def __init__(self, master_address=None, master_seed=None): + if master_seed: + self.master_seed = master_seed + self.master_address = CryptBitcoin.privatekeyToAddress(self.master_seed) + elif master_address: self.master_address = master_address self.master_seed = None else: @@ -27,8 +32,9 @@ class User: # Get user site data # Return: {"auth_address": "xxx", "auth_privatekey": "xxx"} - def getSiteData(self, address): + def getSiteData(self, address, create=True): if not address in self.sites: # Genreate new BIP32 child key based on site address + if not create: return {"auth_address": None, "auth_privatekey": None} # Dont create user yet s = time.time() address_id = int(address.encode("hex"), 16) # Convert site address to int auth_privatekey = CryptBitcoin.hdPrivatekey(self.master_seed, address_id) @@ -43,17 +49,15 @@ class User: # Get BIP32 address from site address # Return: BIP32 auth address - def getAuthAddress(self, address): - return self.getSiteData(address)["auth_address"] + def getAuthAddress(self, address, create=True): + return self.getSiteData(address, create)["auth_address"] - def getAuthPrivatekey(self, address): - return self.getSiteData(address)["auth_privatekey"] - + def getAuthPrivatekey(self, address, create=True): + return self.getSiteData(address, create)["auth_privatekey"] # Set user attributes from dict def setData(self, data): for key, val in data.items(): setattr(self, key, val) - diff --git a/src/User/UserManager.py b/src/User/UserManager.py index 805b50ea..2f07c375 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -1,66 +1,79 @@ import json, logging, os from User import User - -users = None - -# Load all user from data/users.json -def load(): - global users - if not users: users = {} - - user_found = [] - added = 0 - # Load new users - for master_address, data in json.load(open("data/users.json")).items(): - if master_address not in users: - user = User(master_address) - user.setData(data) - users[master_address] = user - added += 1 - user_found.append(master_address) - - # Remove deleted adresses - for master_address in users.keys(): - if master_address not in user_found: - del(users[master_address]) - logging.debug("Removed user: %s" % master_address) - - if added: logging.debug("UserManager added %s users" % added) +from Plugin import PluginManager -# Create new user -# Return: User -def create(): - user = User() - logging.debug("Created user: %s" % user.master_address) - users[user.master_address] = user - user.save() - return user +@PluginManager.acceptPlugins +class UserManager(object): + def __init__(self): + self.users = {} -# List all users from data/users.json -# Return: {"usermasteraddr": User} -def list(): - if users == None: # Not loaded yet - load() - return users + # Load all user from data/users.json + def load(self): + if not self.users: self.users = {} + + user_found = [] + added = 0 + # Load new users + for master_address, data in json.load(open("data/users.json")).items(): + if master_address not in self.users: + user = User(master_address) + user.setData(data) + self.users[master_address] = user + added += 1 + user_found.append(master_address) + + # Remove deleted adresses + for master_address in self.users.keys(): + if master_address not in user_found: + del(self.users[master_address]) + logging.debug("Removed user: %s" % master_address) + + if added: logging.debug("UserManager added %s users" % added) -# Get current authed user -# Return: User -def getCurrent(): - users = list() - if users: - return users.values()[0] - else: - return create() + # Create new user + # Return: User + def create(self, master_address=None, master_seed=None): + user = User(master_address, master_seed) + logging.debug("Created user: %s" % user.master_address) + if user.master_address: # If successfully created + self.users[user.master_address] = user + user.save() + return user + # List all users from data/users.json + # Return: {"usermasteraddr": User} + def list(self): + if self.users == {}: # Not loaded yet + self.load() + return self.users + + + # Get user based on master_address + # Return: User or None + def get(self, master_address=None): + users = self.list() + if users: + return users.values()[0] # Single user mode, always return the first + else: + return None + + +user_manager = UserManager() # Singletone + # Debug: Reload User.py -def reload(): - return False # Disabled - """import imp - global users, User +def reloadModule(): + return "Not used" + + import imp + global User, UserManager, user_manager User = imp.load_source("User", "src/User/User.py").User # Reload source - users.clear() # Remove all items - load()""" + #module = imp.load_source("UserManager", "src/User/UserManager.py") # Reload module + #UserManager = module.UserManager + #user_manager = module.user_manager + # Reload users + user_manager = UserManager() + user_manager.load() diff --git a/src/main.py b/src/main.py index 346ea3db..ade19447 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,5 @@ import os, sys -update_after_shutdown = False -sys.path.insert(0, os.path.dirname(__file__)) # Imports relative to main.py +update_after_shutdown = False # If set True then update and restart zeronet after main loop ended # Create necessary files and dirs if not os.path.isdir("log"): os.mkdir("log") @@ -11,7 +10,8 @@ if not os.path.isfile("data/users.json"): open("data/users.json", "w").write("{} # Load config from Config import config -# Init logging + +# Setup logging import logging if config.action == "main": if os.path.isfile("log/debug.log"): # Simple logrotate @@ -28,24 +28,35 @@ if config.action == "main": # Add time if main action else: console_log.setFormatter(logging.Formatter('%(name)s %(message)s', "%H:%M:%S")) - logging.getLogger('').addHandler(console_log) # Add console logger logging.getLogger('').name = "-" # Remove root prefix + # Debug dependent configuration from Debug import DebugHook if config.debug: - console_log.setLevel(logging.DEBUG) + console_log.setLevel(logging.DEBUG) # Display everything to console from gevent import monkey; monkey.patch_all(thread=False) # thread=False because of pyfilesystem else: - console_log.setLevel(logging.INFO) - from gevent import monkey; monkey.patch_all() + console_log.setLevel(logging.INFO) # Display only important info to console + from gevent import monkey; monkey.patch_all() # Make time, thread, socket gevent compatible + import gevent import time + +# Log current config logging.debug("Config: %s" % config) + +# Load plugins +from Plugin import PluginManager +PluginManager.plugin_manager.loadPlugins() + + +# -- Actions -- + # Starts here when running zeronet.py def start(): action_func = globals()[config.action] # Function reference @@ -74,7 +85,7 @@ def main(): def siteCreate(): logging.info("Generating new privatekey...") - from src.Crypt import CryptBitcoin + from Crypt import CryptBitcoin privatekey = CryptBitcoin.newPrivatekey() logging.info("----------------------------------------------------------------------") logging.info("Site private key: %s" % privatekey) @@ -196,7 +207,7 @@ def sitePublish(address, peer_ip=None, peer_port=15441, inner_path="content.json # Crypto commands def cryptoPrivatekeyToAddress(privatekey=None): - from src.Crypt import CryptBitcoin + from Crypt import CryptBitcoin if not privatekey: # If no privatekey in args then ask it now import getpass privatekey = getpass.getpass("Private key (input hidden):") diff --git a/update.py b/update.py index e27cda45..cefa3744 100644 --- a/update.py +++ b/update.py @@ -16,15 +16,41 @@ def update(): if not buff: break data.write(buff) print ".", + print "Downloaded." + + # Checking plugins + plugins_enabled = [] + plugins_disabled = [] + if os.path.isdir("plugins"): + for dir in os.listdir("plugins"): + if dir.startswith("disabled-"): + plugins_disabled.append(dir.replace("disabled-", "")) + else: + plugins_enabled.append(dir) + print "Plugins:", plugins_enabled, plugins_disabled + print "Extracting...", zip = zipfile.ZipFile(data) for inner_path in zip.namelist(): + inner_path = inner_path.replace("\\", "/") # Make sure we have unix path print ".", dest_path = inner_path.replace("ZeroNet-master/", "") if not dest_path: continue + + # Keep plugin disabled/enabled status + match = re.match("plugins/([^/]+)", dest_path) + if match: + plugin_name = match.group(1).replace("disabled-","") + if plugin_name in plugins_enabled: # Plugin was enabled + dest_path = dest_path.replace("plugins/disabled-"+plugin_name, "plugins/"+plugin_name) + else: # Plugin was disabled + dest_path = dest_path.replace("plugins/"+plugin_name, "plugins/disabled-"+plugin_name) + print "P", + dest_dir = os.path.dirname(dest_path) + if dest_dir and not os.path.isdir(dest_dir): os.makedirs(dest_dir) diff --git a/zeronet.py b/zeronet.py index a88a4f0a..62084b2c 100644 --- a/zeronet.py +++ b/zeronet.py @@ -2,8 +2,10 @@ def main(): print " - Starging ZeroNet..." + import sys, os try: - from src import main + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) # Imports relative to src + import main main.start() if main.update_after_shutdown: # Updater import update, sys, os, gc