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

This commit is contained in:
HelloZeroNet 2015-03-24 01:33:09 +01:00
parent 3b8d49207e
commit 78f97dcbe8
26 changed files with 789 additions and 308 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

0
src/Plugin/__init__.py Normal file
View file

View file

@ -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: <dict> 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("<td>%s</td>" % formatted)
return "<tr>%s</tr>" % "".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 """
<style>
* { font-family: monospace }
table * { text-align: right; padding: 0px 10px }
</style>
"""
# 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 <a href='?size=1'>on</a> <a href='?size=0'>off</a><br>"
except Exception, err:
pass
yield "Connections (%s):<br>" % len(main.file_server.connections)
yield "<table><tr> <th>id</th> <th>protocol</th> <th>type</th> <th>ip</th> <th>ping</th> <th>buff</th>"
yield "<th>idle</th> <th>open</th> <th>delay</th> <th>sent</th> <th>received</th> <th>last sent</th> <th>waiting</th> <th>version</th> <th>peerid</th> </tr>"
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 "</table>"
from greenlet import greenlet
objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
yield "<br>Greenlets (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (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 "<br>Workers (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (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 "<br>Connections (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (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 "<br>Sites (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj)))
objs = [obj for obj in gc.get_objects() if isinstance(obj, self.server.log.__class__)]
yield "<br>Loggers (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj.name)))
objs = [obj for obj in gc.get_objects() if isinstance(obj, UiRequest)]
yield "<br>UiRequest (%s):<br>" % len(objs)
for obj in objs:
yield " - %.3fkb: %s<br>" % (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()

View file

@ -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()

View file

@ -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()

View file

@ -33,7 +33,7 @@ class Notifications
$(".notification-icon", elem).html("i")
if typeof(body) == "string"
$(".body", elem).html(body)
$(".body", elem).html("<span class='message'>"+body+"</span>")
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", =>

View file

@ -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 = $("<span class='message'>"+message.params[1]+"</span>")
@notifications.add("notification-#{message.id}", message.params[0], body, message.params[2])
wrapperConfirm: (message, caption, cb) ->
body = $("<span>"+message+"</span>")
displayConfirm: (message, caption, cb) ->
body = $("<span class='message'>"+message+"</span>")
button = $("<a href='##{caption}' class='button button-#{caption}'>#{caption}</a>") # 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 = $("<span>"+message.params[0]+"</span>")
displayPrompt: (message, type, caption, cb) ->
body = $("<span class='message'>"+message+"</span>")
input = $("<input type='#{type}' class='input button-#{type}'/>") # Add input
input.on "keyup", (e) => # Send on enter
if e.keyCode == 13
button.trigger "click" # Response to confirm
body.append(input)
button = $("<a href='##{caption}' class='button button-#{caption}'>#{caption}</a>") # 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
$('<meta name="viewport" id="viewport">').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)

View file

@ -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 */

View file

@ -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 */

View file

@ -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("<span class='message'>" + body + "</span>");
} 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 = $("<span class='message'>" + message.params[1] + "</span>");
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 = $("<span class='message'>" + message + "</span>");
button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
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 = $("<span>" + message + "</span>");
button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
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 = $("<span>" + message.params[0] + "</span>");
Wrapper.prototype.displayPrompt = function(message, type, caption, cb) {
var body, button, input;
body = $("<span class='message'>" + message + "</span>");
input = $("<input type='" + type + "' class='input button-" + type + "'/>");
input.on("keyup", (function(_this) {
return function(e) {
@ -887,11 +894,7 @@ jQuery.extend( jQuery.easing,
button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
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);

View file

@ -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)

View file

@ -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()

View file

@ -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):")