From 78f97dcbe835dbdb2cd7c39b168ced11ce9ea2e7 Mon Sep 17 00:00:00 2001 From: HelloZeroNet Date: Tue, 24 Mar 2015 01:33:09 +0100 Subject: [PATCH] version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status --- plugins/Stats/StatsPlugin.py | 135 ++++++++++++ plugins/Stats/__init__.py | 1 + .../DonationMessagePlugin.py | 24 +++ plugins/disabled-DonationMessage/__init__.py | 1 + plugins/disabled-Multiuser/MultiuserPlugin.py | 169 +++++++++++++++ plugins/disabled-Multiuser/__init__.py | 1 + src/Config.py | 2 +- src/Crypt/CryptBitcoin.py | 4 +- src/Debug/DebugReloader.py | 2 + src/File/FileServer.py | 2 +- src/Peer/Peer.py | 2 +- src/Plugin/PluginManager.py | 97 +++++++++ src/Plugin/__init__.py | 0 src/Ui/UiRequest.py | 195 +++++------------- src/Ui/UiServer.py | 11 +- src/Ui/UiWebsocket.py | 39 ++-- src/Ui/media/Notifications.coffee | 4 +- src/Ui/media/Wrapper.coffee | 64 ++++-- src/Ui/media/Wrapper.css | 15 +- src/Ui/media/all.css | 15 +- src/Ui/media/all.js | 110 ++++++---- src/User/User.py | 24 ++- src/User/UserManager.py | 121 ++++++----- src/main.py | 29 ++- update.py | 26 +++ zeronet.py | 4 +- 26 files changed, 789 insertions(+), 308 deletions(-) create mode 100644 plugins/Stats/StatsPlugin.py create mode 100644 plugins/Stats/__init__.py create mode 100644 plugins/disabled-DonationMessage/DonationMessagePlugin.py create mode 100644 plugins/disabled-DonationMessage/__init__.py create mode 100644 plugins/disabled-Multiuser/MultiuserPlugin.py create mode 100644 plugins/disabled-Multiuser/__init__.py create mode 100644 src/Plugin/PluginManager.py create mode 100644 src/Plugin/__init__.py 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