Browse Source

rev307, Plugin for password protected web ui, Faster page load times by yielding wrapper html, Reworked configuration parser to support plugin extensions, Initial site sync bugfix, Test for configuration parsing, Parse posted data function

HelloZeroNet 8 years ago
parent
commit
a93ca2c3b4

+ 117 - 0
plugins/disabled-UiPassword/UiPasswordPlugin.py

@@ -0,0 +1,117 @@
+import string
+import random
+import time
+import json
+import re
+
+from Config import config
+from Plugin import PluginManager
+
+if "sessions" not in locals().keys():  # To keep sessions between module reloads
+    sessions = {}
+
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+    sessions = sessions
+    last_cleanup = time.time()
+
+    def route(self, path):
+        if path.endswith("favicon.ico"):
+            return self.actionFile("src/Ui/media/img/favicon.ico")
+        else:
+            if config.ui_password:
+                if time.time() - self.last_cleanup > 60 * 60:  # Cleanup expired sessions every hour
+                    self.cleanup()
+                # Validate session
+                session_id = self.getCookies().get("session_id")
+                if session_id not in self.sessions:  # Invalid session id, display login
+                    return self.actionLogin()
+            return super(UiRequestPlugin, self).route(path)
+
+    # Action: Login
+    def actionLogin(self):
+        template = open("plugins/UiPassword/login.html").read()
+        self.sendHeader()
+        posted = self.getPosted()
+        if posted:  # Validate http posted data
+            if self.checkPassword(posted.get("password")):
+                # Valid password, create session
+                session_id = self.randomString(26)
+                self.sessions[session_id] = {
+                    "added": time.time(),
+                    "keep": posted.get("keep")
+                }
+
+                # Redirect to homepage or referer
+                url = self.env.get("HTTP_REFERER", "")
+                if not url or re.sub("\?.*", "", url).endswith("/Login"):
+                    url = "/" + config.homepage
+                cookie_header = ('Set-Cookie', "session_id=%s;path=/;max-age=2592000;" % session_id)  # Max age = 30 days
+                self.start_response('301 Redirect', [('Location', url), cookie_header])
+                yield "Redirecting..."
+
+            else:
+                # Invalid password, show login form again
+                template = template.replace("{result}", "bad_password")
+        yield template
+
+    def checkPassword(self, password):
+        if password == config.ui_password:
+            return True
+        else:
+            return False
+
+    def randomString(self, chars):
+        return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(chars))
+
+    @classmethod
+    def cleanup(cls):
+        for session_id, session in cls.sessions.items():
+            if session["keep"] and time.time() - session["added"] > 60 * 60 * 24 * 60:  # Max 60days for keep sessions
+                del(cls.sessions[session_id])
+            elif not session["keep"] and time.time() - session["added"] > 60 * 60 * 24:  # Max 24h for non-keep sessions
+                del(cls.sessions[session_id])
+
+    # Action: Display sessions
+    def actionSessions(self):
+        self.sendHeader()
+        yield "<pre>"
+        yield json.dumps(self.sessions, indent=4)
+
+    # Action: Logout
+    def actionLogout(self):
+        # Session id has to passed as get parameter or called without referer to avoid remote logout
+        session_id = self.getCookies().get("session_id")
+        if not self.env.get("HTTP_REFERER") or session_id == self.get.get("session_id"):
+            if session_id in self.sessions:
+                del self.sessions[session_id]
+            self.start_response('301 Redirect', [
+                ('Location', "/"),
+                ('Set-Cookie', "session_id=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT")
+            ])
+            yield "Redirecting..."
+        else:
+            self.sendHeader()
+            yield "Error: Invalid session id"
+
+
+@PluginManager.registerTo("ConfigPlugin")
+class ConfigPlugin(object):
+    def createArguments(self):
+        group = self.parser.add_argument_group("UiPassword plugin")
+        group.add_argument('--ui_password', help='Password to access UiServer', default=None, metavar="password")
+
+        return super(ConfigPlugin, self).createArguments()
+
+
+@PluginManager.registerTo("UiWebsocket")
+class UiWebsocketPlugin(object):
+    def actionUiLogout(self, to):
+        permissions = self.getPermissions(to)
+        if "ADMIN" not in permissions:
+            return self.response(to, "You don't have permission to run this command")
+
+        session_id = self.request.getCookies().get("session_id", "")
+        message = "<script>document.location.href = '/Logout?session_id=%s'</script>" % session_id
+        self.cmd("notification", ["done", message])

+ 1 - 0
plugins/disabled-UiPassword/__init__.py

@@ -0,0 +1 @@
+import UiPasswordPlugin

+ 116 - 0
plugins/disabled-UiPassword/login.html

@@ -0,0 +1,116 @@
+<html>
+<head>
+ <title>Log In</title>
+ <meta name="viewport" id="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+
+<style>
+body {
+	background-color: #323C4D; font-family: "Segoe UI", Helvetica, Arial; font-weight: lighter;
+    font-size: 22px; color: #333; letter-spacing: 1px; color: white; overflow: hidden;
+}
+.login { left: 50%; position: absolute; top: 50%; transform: translateX(-50%) translateY(-50%); width: 100%; max-width: 370px; text-align: center; }
+
+*:focus { outline: 0; }
+input[type=text], input[type=password] {
+	padding: 10px 0px; border: 0px; display: block; margin: 15px 0px; width: 100%; border-radius: 30px; transition: 0.3s ease-out; background-color: #DDD;
+	text-align: center; font-family: "Segoe UI", Helvetica, Arial; font-weight: lighter; font-size: 28px; border: 2px solid #323C4D;
+}
+input[type=text]:focus, input[type=password]:focus {
+	border: 2px solid #FFF; background-color: #FFF;
+}
+input[type=checkbox] { opacity: 0; }
+input[type=checkbox]:checked + label { color: white; }
+input[type=checkbox]:focus + label::before { background-color: #435065; }
+input[type=checkbox]:checked + label::before { box-shadow: inset 0px 0px 0px 5px white; background-color: #4DCC6E; }
+input.error { border: 2px solid #F44336 !important; animation: shake 1s }
+label::before {
+	content: ""; width: 20px; height: 20px; background-color: #323C4D;
+	display: inline-block; margin-left: -20px; border-radius: 15px; box-shadow: inset 0px 0px 0px 2px #9EA5B3;
+	transition: all 0.1s; margin-right: 7px; position: relative; top: 2px;
+}
+label { vertical-align: -1px; color: #9EA5B3; transition: all 0.3s; }
+
+.button {
+	padding: 13px; display: inline-block; margin: 15px 0px; width: 100%; border-radius: 30px; text-align: center; white-space: nowrap;
+	font-size: 28px; color: #333; background: linear-gradient(45deg, #6B14D3 0, #7A26E2 25%, #4962DD 90%);
+    box-sizing: border-box; margin-top: 50px; color: white; text-decoration: none; transition: 0.3s ease-out;
+}
+.button:hover, .button:focus { box-shadow: 0px 5px 30px rgba(0,0,0,0.3); }
+.button:active { transform: translateY(1px); box-shadow: 0px 0px 20px rgba(0,0,0,0.5); transition: none; }
+
+#login_form_submit { display: none; }
+
+.login-anim { animation: login 1s cubic-bezier(0.785, 0.135, 0.15, 0.86) forwards; }
+
+@keyframes login {
+    0%   { width: 100%; }
+    60%  { width: 63px; transform: scale(1); color: rgba(255,255,255,0); }
+    70%  { width: 63px; transform: scale(1); color: rgba(255,255,255,0); }
+    100% { transform: scale(80); width: 63px; color: rgba(255,255,255,0); }
+}
+
+@keyframes shake {
+    0%, 100% { transform: translateX(0); }
+    10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
+    20%, 40%, 60%, 80% { transform: translateX(10px); }
+}
+</style>
+
+<body>
+
+
+<div class="login">
+ <form action="" method="post" id="login_form" onkeypress="return onFormKeypress(event)">
+  <!--<input type="text" name="username" placeholder="Username" required/>-->
+  <input type="password" name="password" placeholder="Password" required/>
+  <input type="checkbox" name="keep" id="keep"><label for="keep">Keep me logged in</label>
+  <div style="clear: both"></div>
+  <a href="Login" class="button" onclick="return submit()" id="login_button"><span>Log In</span></a>
+  <input type="submit" id="login_form_submit"/>
+ </form>
+</div>
+
+
+<script>
+
+function onFormKeypress(e) {
+	if (event.keyCode == 13) {
+		submit()
+		return false
+	}
+}
+
+function submit() {
+	var form = document.getElementById("login_form")
+	if (form.checkValidity()) {
+		document.getElementById("login_button").className = "button login-anim"
+		setTimeout(function () {
+			document.getElementById("login_form_submit").click()
+		}, 1000)
+	} else {
+		document.getElementById("login_form_submit").click()
+	}
+	return false
+}
+
+function badPassword() {
+	var elem = document.getElementsByName("password")[0]
+	elem.className = "error"
+	elem.placeholder = "Wrong Password"
+	elem.focus()
+	elem.addEventListener('input', function() {
+		elem.className = ""
+		elem.placeholder = "Password"
+	})
+}
+
+result = "{result}"
+
+if (result == "bad_password")
+	badPassword()
+
+</script>
+
+</body>
+</html>

+ 6 - 33
plugins/disabled-Zeroname-local/domainLookup.py

@@ -5,32 +5,32 @@ import time, json, os, sys, re, socket, json
 # Supports subdomains and .bit on the end
 def lookupDomain(domain):
 	domain = domain.lower()
-	
+
 	#remove .bit on end
 	if domain[-4:] == ".bit":
 		domain = domain[0:-4]
-		
+
 	#check for subdomain
 	if domain.find(".") != -1:
 		subdomain = domain[0:domain.find(".")]
 		domain = domain[domain.find(".")+1:]
 	else:
 		subdomain = ""
-	
+
 	try:
 		domain_object = rpc.name_show("d/"+domain)
 	except:
 		#domain doesn't exist
 		return None
-	
+
 	domain_json = json.loads(domain_object['value'])
-	
+
 	try:
 		domain_address = domain_json["zeronet"][subdomain]
 	except:
 		#domain exists but doesn't have any zeronet value
 		return None
-	
+
 	return domain_address
 
 # Loading config...
@@ -52,30 +52,3 @@ rpc_pass = re.search("rpcpassword=(.*)$", namecoin_conf, re.M).group(1)
 rpc_url = "http://%s:%s@127.0.0.1:8336" % (rpc_user, rpc_pass)
 
 rpc = AuthServiceProxy(rpc_url, timeout=60*5)
-
-"""
-while 1:
-	print "Waiting for new block",
-	sys.stdout.flush()
-	while 1:
-		try:
-			rpc = AuthServiceProxy(rpc_url, timeout=60*5)
-			if (int(rpc.getinfo()["blocks"]) > last_block): break
-			time.sleep(1)
-			rpc.waitforblock()
-			print "Found"
-			break # Block found
-		except socket.timeout: # Timeout
-			print ".",
-			sys.stdout.flush()
-		except Exception, err:
-			print "Exception", err.__class__, err
-			time.sleep(5)
-
-	last_block = int(rpc.getinfo()["blocks"])
-	for block_id in range(config["lastprocessed"]+1, last_block+1):
-		processBlock(block_id)
-
-	config["lastprocessed"] = last_block
-	open(config_path, "w").write(json.dumps(config, indent=2))
-"""

+ 136 - 72
src/Config.py

@@ -6,14 +6,19 @@ import ConfigParser
 
 class Config(object):
 
-    def __init__(self):
+    def __init__(self, argv):
         self.version = "0.3.1"
-        self.rev = 281
-        self.parser = self.createArguments()
-        argv = sys.argv[:]  # Copy command line arguments
-        argv = self.parseConfig(argv)  # Add arguments from config file
-        self.parseCommandline(argv)  # Parse argv
-        self.setAttributes()
+        self.rev = 307
+        self.argv = argv
+        self.action = None
+        self.createParser()
+        self.createArguments()
+
+    def createParser(self):
+        # Create parser
+        self.parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+        self.parser.register('type', 'bool', self.strToBool)
+        self.subparsers = self.parser.add_subparsers(title="Action to perform", dest="action")
 
     def __str__(self):
         return str(self.arguments).replace("Namespace", "Config")  # Using argparse str output
@@ -29,28 +34,17 @@ class Config(object):
             coffeescript = "type %s | tools\\coffee\\coffee.cmd"
         else:
             coffeescript = None
-        """ Probably fixed
-        if sys.platform.lower().startswith("darwin"):
-            # For some reasons openssl doesnt works on mac yet (https://github.com/HelloZeroNet/ZeroNet/issues/94)
-            use_openssl = False
-        else:
-            use_openssl = True
-        """
-        use_openssl = True
 
-        # Create parser
-        parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-        parser.register('type', 'bool', self.strToBool)
-        subparsers = parser.add_subparsers(title="Action to perform", dest="action")
+        use_openssl = True
 
         # Main
-        action = subparsers.add_parser("main", help='Start UiServer and FileServer (default)')
+        action = self.subparsers.add_parser("main", help='Start UiServer and FileServer (default)')
 
         # SiteCreate
-        action = subparsers.add_parser("siteCreate", help='Create a new site')
+        action = self.subparsers.add_parser("siteCreate", help='Create a new site')
 
         # SiteSign
-        action = subparsers.add_parser("siteSign", help='Update and sign content.json: address [privatekey]')
+        action = self.subparsers.add_parser("siteSign", help='Update and sign content.json: address [privatekey]')
         action.add_argument('address', help='Site to sign')
         action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?')
         action.add_argument('--inner_path', help='File you want to sign (default: content.json)',
@@ -58,7 +52,7 @@ class Config(object):
         action.add_argument('--publish', help='Publish site after the signing', action='store_true')
 
         # SitePublish
-        action = subparsers.add_parser("sitePublish", help='Publish site to other peers: address')
+        action = self.subparsers.add_parser("sitePublish", help='Publish site to other peers: address')
         action.add_argument('address', help='Site to publish')
         action.add_argument('peer_ip', help='Peer ip to publish (default: random peers ip from tracker)',
                             default=None, nargs='?')
@@ -68,76 +62,76 @@ class Config(object):
                             default="content.json", metavar="inner_path")
 
         # SiteVerify
-        action = subparsers.add_parser("siteVerify", help='Verify site files using sha512: address')
+        action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address')
         action.add_argument('address', help='Site to verify')
 
         # dbRebuild
-        action = subparsers.add_parser("dbRebuild", help='Rebuild site database cache')
+        action = self.subparsers.add_parser("dbRebuild", help='Rebuild site database cache')
         action.add_argument('address', help='Site to rebuild')
 
         # dbQuery
-        action = subparsers.add_parser("dbQuery", help='Query site sql cache')
+        action = self.subparsers.add_parser("dbQuery", help='Query site sql cache')
         action.add_argument('address', help='Site to query')
         action.add_argument('query', help='Sql query')
 
         # PeerPing
-        action = subparsers.add_parser("peerPing", help='Send Ping command to peer')
+        action = self.subparsers.add_parser("peerPing", help='Send Ping command to peer')
         action.add_argument('peer_ip', help='Peer ip')
         action.add_argument('peer_port', help='Peer port', nargs='?')
 
         # PeerGetFile
-        action = subparsers.add_parser("peerGetFile", help='Request and print a file content from peer')
+        action = self.subparsers.add_parser("peerGetFile", help='Request and print a file content from peer')
         action.add_argument('peer_ip', help='Peer ip')
         action.add_argument('peer_port', help='Peer port')
         action.add_argument('site', help='Site address')
         action.add_argument('filename', help='File name to request')
 
         # PeerGetFile
-        action = subparsers.add_parser("peerCmd", help='Request and print a file content from peer')
+        action = self.subparsers.add_parser("peerCmd", help='Request and print a file content from peer')
         action.add_argument('peer_ip', help='Peer ip')
         action.add_argument('peer_port', help='Peer port')
         action.add_argument('cmd', help='Command to execute')
         action.add_argument('parameters', help='Parameters to command', nargs='?')
 
         # CryptSign
-        action = subparsers.add_parser("cryptSign", help='Sign message using Bitcoin private key')
+        action = self.subparsers.add_parser("cryptSign", help='Sign message using Bitcoin private key')
         action.add_argument('message', help='Message to sign')
         action.add_argument('privatekey', help='Private key')
 
         # Config parameters
-        parser.add_argument('--debug', help='Debug mode', action='store_true')
-        parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true')
-
-        parser.add_argument('--config_file', help='Path of config file', default="zeronet.conf", metavar="path")
-        parser.add_argument('--data_dir', help='Path of data directory', default="data", metavar="path")
-        parser.add_argument('--log_dir', help='Path of logging directory', default="log", metavar="path")
-
-        parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
-        parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port')
-        parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
-        parser.add_argument('--open_browser', help='Open homepage in web browser automatically',
-                            nargs='?', const="default_browser", metavar='browser_name')
-        parser.add_argument('--homepage', help='Web interface Homepage', default='1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr',
-                            metavar='address')
-        parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, metavar='size')
-
-        parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip')
-        parser.add_argument('--fileserver_port', help='FileServer bind port', default=15441, type=int, metavar='port')
-        parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
-        parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
-        parser.add_argument('--ip_external', help='External ip (tested on start if None)', metavar='ip')
-        parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup',
-                            type='bool', choices=[True, False], default=use_openssl)
-        parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true')
-        parser.add_argument('--disable_sslcompression', help='Disable SSL compression to save memory',
-                            type='bool', choices=[True, False], default=True)
-
-        parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript,
-                            metavar='executable_path')
-
-        parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
-
-        return parser
+        self.parser.add_argument('--debug', help='Debug mode', action='store_true')
+        self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true')
+
+        self.parser.add_argument('--config_file', help='Path of config file', default="zeronet.conf", metavar="path")
+        self.parser.add_argument('--data_dir', help='Path of data directory', default="data", metavar="path")
+        self.parser.add_argument('--log_dir', help='Path of logging directory', default="log", metavar="path")
+
+        self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip')
+        self.parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port')
+        self.parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*')
+        self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically',
+                                 nargs='?', const="default_browser", metavar='browser_name')
+        self.parser.add_argument('--homepage', help='Web interface Homepage', default='1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr',
+                                 metavar='address')
+        self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, metavar='size')
+
+        self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip')
+        self.parser.add_argument('--fileserver_port', help='FileServer bind port', default=15441, type=int, metavar='port')
+        self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true')
+        self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port')
+        self.parser.add_argument('--ip_external', help='External ip (tested on start if None)', metavar='ip')
+        self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup',
+                                 type='bool', choices=[True, False], default=use_openssl)
+        self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true')
+        self.parser.add_argument('--disable_sslcompression', help='Disable SSL compression to save memory',
+                                 type='bool', choices=[True, False], default=True)
+
+        self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript,
+                                 metavar='executable_path')
+
+        self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev))
+
+        return self.parser
 
     # Find arguments specificed for current action
     def getActionArguments(self):
@@ -147,23 +141,78 @@ class Config(object):
             back[argument.dest] = getattr(self, argument.dest)
         return back
 
-    # Try to find action from sys.argv
+    # Try to find action from argv
     def getAction(self, argv):
         actions = [action.choices.keys() for action in self.parser._actions if action.dest == "action"][0]  # Valid actions
         found_action = False
-        for action in actions:  # See if any in sys.argv
+        for action in actions:  # See if any in argv
             if action in argv:
                 found_action = action
                 break
         return found_action
 
+    # Move plugin parameters to end of argument list
+    def moveUnknownToEnd(self, argv, default_action):
+        valid_actions = sum([action.option_strings for action in self.parser._actions], [])
+        valid_parameters = []
+        plugin_parameters = []
+        plugin = False
+        for arg in argv:
+            if arg.startswith("--"):
+                if arg not in valid_actions:
+                    plugin = True
+                else:
+                    plugin = False
+            elif arg == default_action:
+                plugin = False
+
+            if plugin:
+                plugin_parameters.append(arg)
+            else:
+                valid_parameters.append(arg)
+        return valid_parameters + plugin_parameters
+
+    # Parse arguments from config file and command line
+    def parse(self, silent=False, parse_config=True):
+        if silent:  # Don't display messages or quit on unknown parameter
+            original_print_message = self.parser._print_message
+            original_exit = self.parser.exit
+
+            def silent(parser, function_name):
+                parser.exited = True
+                return None
+            self.parser.exited = False
+            self.parser._print_message = lambda *args, **kwargs: silent(self.parser, "_print_message")
+            self.parser.exit = lambda *args, **kwargs: silent(self.parser, "exit")
+
+        argv = self.argv[:]  # Copy command line arguments
+        if parse_config:
+            argv = self.parseConfig(argv)  # Add arguments from config file
+        self.parseCommandline(argv, silent)  # Parse argv
+        self.setAttributes()
+
+        if silent:  # Restore original functions
+            if self.parser.exited and self.action == "main":  # Argument parsing halted, don't start ZeroNet with main action
+                self.action = None
+            self.parser._print_message = original_print_message
+            self.parser.exit = original_exit
+
     # Parse command line arguments
-    def parseCommandline(self, argv):
+    def parseCommandline(self, argv, silent=False):
         # Find out if action is specificed on start
         action = self.getAction(argv)
-        if len(argv) == 1 or not action:  # If no action specificed set the main action
+        if not action:
             argv.append("main")
-        self.arguments = self.parser.parse_args(argv[1:])
+            action = "main"
+        argv = self.moveUnknownToEnd(argv, action)
+        if silent:
+            res = self.parser.parse_known_args(argv[1:])
+            if res:
+                self.arguments = res[0]
+            else:
+                self.arguments = {}
+        else:
+            self.arguments = self.parser.parse_args(argv[1:])
 
     # Parse config file
     def parseConfig(self, argv):
@@ -187,9 +236,24 @@ class Config(object):
     # Expose arguments as class attributes
     def setAttributes(self):
         # Set attributes from arguments
-        args = vars(self.arguments)
-        for key, val in args.items():
-            setattr(self, key, val)
+        if self.arguments:
+            args = vars(self.arguments)
+            for key, val in args.items():
+                setattr(self, key, val)
+
+    def loadPlugins(self):
+        from Plugin import PluginManager
+
+        @PluginManager.acceptPlugins
+        class ConfigPlugin(object):
+            def __init__(self, config):
+                self.parser = config.parser
+                self.createArguments()
+
+            def createArguments(self):
+                pass
+
+        ConfigPlugin(self)
 
 
-config = Config()
+config = Config(sys.argv)

+ 1 - 1
src/Site/Site.py

@@ -207,7 +207,7 @@ class Site:
             elif len(peers_try) < 5:  # Backup peers, add to end of the try list
                 peers_try.append(peer)
 
-        if since is not None:  # No since definied, download from last modification time-1day
+        if since is None:  # No since definied, download from last modification time-1day
             since = self.settings.get("modified", 60 * 60 * 24) - 60 * 60 * 24
         self.log.debug("Try to get listModifications from peers: %s since: %s" % (peers_try, since))
 

+ 29 - 4
src/Test/test.py

@@ -6,6 +6,7 @@ import time
 sys.path.append(os.path.abspath("src"))  # Imports relative to src dir
 
 from Config import config
+config.parse()
 config.data_dir = "src/Test/testdata"  # Use test data for unittests
 
 from Crypt import CryptBitcoin
@@ -382,13 +383,16 @@ class TestCase(unittest.TestCase):
             1458664252141532163166741013621928587528255888800826689784628722366466547364755811L
         )
 
+        # Re-generate privatekey based on address_index
         address, address_index, site_data = user.getNewSiteData()
-        self.assertEqual(CryptBitcoin.hdPrivatekey(user.master_seed, address_index), site_data["privatekey"])  # Re-generate privatekey based on address_index
+        self.assertEqual(CryptBitcoin.hdPrivatekey(user.master_seed, address_index), site_data["privatekey"])
 
         user.sites = {}  # Reset user data
 
-        self.assertNotEqual(user.getSiteData(address)["auth_address"], address)  # Site address and auth address is different
-        self.assertEqual(user.getSiteData(address)["auth_privatekey"], site_data["auth_privatekey"])  # Re-generate auth_privatekey for site
+        # Site address and auth address is different
+        self.assertNotEqual(user.getSiteData(address)["auth_address"], address)
+        # Re-generate auth_privatekey for site
+        self.assertEqual(user.getSiteData(address)["auth_privatekey"], site_data["auth_privatekey"])
 
     def testSslCert(self):
         from Crypt import CryptConnection
@@ -409,8 +413,29 @@ class TestCase(unittest.TestCase):
         os.unlink("%s/cert-rsa.pem" % config.data_dir)
         os.unlink("%s/key-rsa.pem" % config.data_dir)
 
+    def testConfigParse(self):
+        import Config
+        config_test = Config.Config("zeronet.py".split(" "))
+        config_test.parse(silent=True, parse_config=False)
+        self.assertFalse(config_test.debug)
+        self.assertFalse(config_test.debug_socket)
+
+        config_test = Config.Config("zeronet.py --debug --debug_socket --ui_password hello".split(" "))
+        config_test.parse(silent=True, parse_config=False)
+        self.assertTrue(config_test.debug)
+        self.assertTrue(config_test.debug_socket)
+
+        args = "zeronet.py --unknown_arg --debug --debug_socket --ui_restrict 127.0.0.1 1.2.3.4 "
+        args += "--another_unknown argument --use_openssl False siteSign address privatekey --inner_path users/content.json"
+        config_test = Config.Config(args.split(" "))
+        config_test.parse(silent=True, parse_config=False)
+        self.assertTrue(config_test.debug)
+        self.assertIn("1.2.3.4", config_test.ui_restrict)
+        self.assertFalse(config_test.use_openssl)
+        self.assertEqual(config_test.inner_path, "users/content.json")
+
 if __name__ == "__main__":
     import logging
     logging.getLogger().setLevel(level=logging.CRITICAL)
     unittest.main(verbosity=2)
-    # unittest.main(verbosity=2, defaultTest="TestCase.testUserContentCert")
+    # unittest.main(verbosity=2, defaultTest="TestCase.testConfigParse")

+ 84 - 70
src/Ui/UiRequest.py

@@ -29,19 +29,15 @@ class UiRequest(object):
             self.log = server.log
         self.get = get  # Get parameters
         self.env = env  # Enviroment settings
+        # ['CONTENT_LENGTH', 'CONTENT_TYPE', 'GATEWAY_INTERFACE', 'HTTP_ACCEPT', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE',
+        #  'HTTP_COOKIE', 'HTTP_CACHE_CONTROL', 'HTTP_HOST', 'HTTP_HTTPS', 'HTTP_ORIGIN', 'HTTP_PROXY_CONNECTION', 'HTTP_REFERER',
+        #  'HTTP_USER_AGENT', 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REMOTE_PORT', 'REQUEST_METHOD', 'SCRIPT_NAME',
+        #  'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE', 'werkzeug.request', 'wsgi.errors',
+        #  'wsgi.input', 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version']
 
         self.start_response = start_response  # Start response function
         self.user = None
 
-    # Return posted variables as dict
-    def getPosted(self):
-        if self.env['REQUEST_METHOD'] == "POST":
-            return dict(cgi.parse_qsl(
-                self.env['wsgi.input'].readline().decode()
-            ))
-        else:
-            return {}
-
     # Call the request handler function base on path
     def route(self, path):
         if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:  # Restict Ui access by ip
@@ -69,7 +65,10 @@ class UiRequest(object):
             return self.actionConsole()
         # Site media wrapper
         else:
-            body = self.actionWrapper(path)
+            if self.get.get("wrapper") == "False":
+                return self.actionSiteMedia("/media" + path)  # Only serve html files with frame
+            else:
+                body = self.actionWrapper(path)
             if body:
                 return body
             else:
@@ -96,7 +95,16 @@ class UiRequest(object):
                 content_type = "application/octet-stream"
         return content_type
 
-    # Returns: <dict> Cookies based on self.env
+    # Return: <dict> Posted variables
+    def getPosted(self):
+        if self.env['REQUEST_METHOD'] == "POST":
+            return dict(cgi.parse_qsl(
+                self.env['wsgi.input'].readline().decode()
+            ))
+        else:
+            return {}
+
+    # Return: <dict> Cookies based on self.env
     def getCookies(self):
         raw_cookies = self.env.get('HTTP_COOKIE')
         if raw_cookies:
@@ -122,10 +130,11 @@ class UiRequest(object):
         headers.append(("Access-Control-Allow-Origin", "*"))  # Allow json access
         if self.env["REQUEST_METHOD"] == "OPTIONS":
             # Allow json access
-            headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"))
+            headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Cookie"))
+            headers.append(("Access-Control-Allow-Credentials", "true"))
 
         cacheable_type = (
-            content_type == "text/css" or content_type.startswith("image") or 
+            content_type == "text/css" or content_type.startswith("image") or
             self.env["REQUEST_METHOD"] == "OPTIONS" or content_type == "application/javascript"
         )
 
@@ -157,8 +166,6 @@ class UiRequest(object):
     def actionWrapper(self, path, extra_headers=None):
         if not extra_headers:
             extra_headers = []
-        if self.get.get("wrapper") == "False":
-            return self.actionSiteMedia("/media" + path)  # Only serve html files with frame
 
         match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
         if match:
@@ -169,14 +176,6 @@ class UiRequest(object):
             if self.env.get("HTTP_X_REQUESTED_WITH"):
                 return self.error403("Ajax request not allowed to load wrapper")  # No ajax allowed on wrapper
 
-            file_inner_path = inner_path
-            if not file_inner_path:
-                file_inner_path = "index.html"  # If inner path defaults to index.html
-
-            if not inner_path and not path.endswith("/"):
-                inner_path = address + "/"  # Fix relative resources loading if missing / end of site address
-            inner_path = re.sub(".*/(.+)", "\\1", inner_path)  # Load innerframe relative to current url
-
             site = SiteManager.site_manager.get(address)
 
             if (
@@ -190,56 +189,71 @@ class UiRequest(object):
 
                 if not site:
                     return False
+            return self.renderWrapper(site, path, inner_path, title, extra_headers)
 
-            self.sendHeader(extra_headers=extra_headers[:])
+        else:  # Bad url
+            return False
 
-            # Wrapper variable inits
-            query_string = ""
-            body_style = ""
-            meta_tags = ""
 
-            if self.env.get("QUERY_STRING"):
-                query_string = "?" + self.env["QUERY_STRING"] + "&wrapper=False"
-            else:
-                query_string = "?wrapper=False"
-
-            if self.isProxyRequest():  # Its a remote proxy request
-                if self.env["REMOTE_ADDR"] == "127.0.0.1":  # Local client, the server address also should be 127.0.0.1
-                    server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"]
-                else:  # Remote client, use SERVER_NAME as server's real address
-                    server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"])
-                homepage = "http://zero/" + config.homepage
-            else:  # Use relative path
-                server_url = ""
-                homepage = "/" + config.homepage
-
-            if site.content_manager.contents.get("content.json"):  # Got content.json
-                content = site.content_manager.contents["content.json"]
-                if content.get("background-color"):
-                    body_style += "background-color: %s;" % \
-                        cgi.escape(site.content_manager.contents["content.json"]["background-color"], True)
-                if content.get("viewport"):
-                    meta_tags += '<meta name="viewport" id="viewport" content="%s">' % cgi.escape(content["viewport"], True)
-
-            return self.render(
-                "src/Ui/template/wrapper.html",
-                server_url=server_url,
-                inner_path=inner_path,
-                file_inner_path=file_inner_path,
-                address=address,
-                title=title,
-                body_style=body_style,
-                meta_tags=meta_tags,
-                query_string=query_string,
-                wrapper_key=site.settings["wrapper_key"],
-                permissions=json.dumps(site.settings["permissions"]),
-                show_loadingscreen=json.dumps(not site.storage.isFile(file_inner_path)),
-                rev=config.rev,
-                homepage=homepage
-            )
+    def renderWrapper(self, site, path, inner_path, title, extra_headers):
+        self.sendHeader(extra_headers=extra_headers[:])
+
+        file_inner_path = inner_path
+        if not file_inner_path:
+            file_inner_path = "index.html"  # If inner path defaults to index.html
+
+        address = re.sub("/.*", "", path.lstrip("/"))
+        if self.isProxyRequest() and (not path or "/" in path[1:]):
+            file_url = re.sub(".*/", "", inner_path)
+        else:
+            file_url = "/" + address + "/" + inner_path
+
+        # Wrapper variable inits
+        query_string = ""
+        body_style = ""
+        meta_tags = ""
+
+        if self.env.get("QUERY_STRING"):
+            query_string = "?" + self.env["QUERY_STRING"] + "&wrapper=False"
+        else:
+            query_string = "?wrapper=False"
+
+        if self.isProxyRequest():  # Its a remote proxy request
+            if self.env["REMOTE_ADDR"] == "127.0.0.1":  # Local client, the server address also should be 127.0.0.1
+                server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"]
+            else:  # Remote client, use SERVER_NAME as server's real address
+                server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"])
+            homepage = "http://zero/" + config.homepage
+        else:  # Use relative path
+            server_url = ""
+            homepage = "/" + config.homepage
+
+        if site.content_manager.contents.get("content.json"):  # Got content.json
+            content = site.content_manager.contents["content.json"]
+            if content.get("background-color"):
+                body_style += "background-color: %s;" % \
+                    cgi.escape(site.content_manager.contents["content.json"]["background-color"], True)
+            if content.get("viewport"):
+                meta_tags += '<meta name="viewport" id="viewport" content="%s">' % cgi.escape(content["viewport"], True)
+
+        yield self.render(
+            "src/Ui/template/wrapper.html",
+            server_url=server_url,
+            inner_path=inner_path,
+            file_url=file_url,
+            file_inner_path=file_inner_path,
+            address=site.address,
+            title=title,
+            body_style=body_style,
+            meta_tags=meta_tags,
+            query_string=query_string,
+            wrapper_key=site.settings["wrapper_key"],
+            permissions=json.dumps(site.settings["permissions"]),
+            show_loadingscreen=json.dumps(not site.storage.isFile(file_inner_path)),
+            rev=config.rev,
+            homepage=homepage
+        )
 
-        else:  # Bad url
-            return False
 
     # Returns if media request allowed from that referer
     def isMediaRequestAllowed(self, site_address, referer):
@@ -351,7 +365,7 @@ class UiRequest(object):
                 if not user:
                     self.log.error("No user found")
                     return self.error403()
-                ui_websocket = UiWebsocket(ws, site, self.server, user)
+                ui_websocket = UiWebsocket(ws, site, self.server, user, self)
                 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():

+ 11 - 7
src/Ui/UiWebsocket.py

@@ -15,11 +15,12 @@ from Plugin import PluginManager
 @PluginManager.acceptPlugins
 class UiWebsocket(object):
 
-    def __init__(self, ws, site, server, user):
+    def __init__(self, ws, site, server, user, request):
         self.ws = ws
         self.site = site
         self.user = user
         self.log = site.log
+        self.request = request
         self.server = server
         self.next_message_id = 1
         self.waiting_cb = {}  # Waiting for callback. Key: message_id, Value: function pointer
@@ -101,21 +102,24 @@ class UiWebsocket(object):
         except Exception, err:
             self.log.debug("Websocket send error: %s" % Debug.formatException(err))
 
+    def getPermissions(self, req_id):
+        permissions = self.site.settings["permissions"]
+        if req_id >= 1000000:  # Its a wrapper command, allow admin commands
+            permissions = permissions[:]
+            permissions.append("ADMIN")
+        return permissions
+
     # Handle incoming messages
     def handleRequest(self, data):
         req = json.loads(data)
 
         cmd = req.get("cmd")
         params = req.get("params")
-        permissions = self.site.settings["permissions"]
-        if req["id"] >= 1000000:  # Its a wrapper command, allow admin commands
-            permissions = permissions[:]
-            permissions.append("ADMIN")
+        permissions = self.getPermissions(req["id"])
 
         admin_commands = (
             "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone",
-            "channelJoinAllsite",
-            "serverUpdate", "certSet"
+            "channelJoinAllsite", "serverUpdate", "certSet"
         )
 
         if cmd == "response":  # It's a response to a command

+ 2 - 2
src/Ui/template/wrapper.html

@@ -13,7 +13,7 @@
 
 <script>
 // If we are inside iframe escape from it
-if (window.self !== window.top) window.open(window.location.toString(), "_top"); 
+if (window.self !== window.top) window.open(window.location.toString(), "_top");
 if (window.self !== window.top) window.stop();
 </script>
 
@@ -46,7 +46,7 @@ if (window.self !== window.top) window.stop();
 
 
 <!-- Site Iframe -->
-<iframe src='{inner_path}{query_string}' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups"></iframe>
+<iframe src='{file_url}{query_string}' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups"></iframe>
 
 <!-- Site info -->
 <script>

+ 11 - 9
src/main.py

@@ -1,5 +1,4 @@
 # Included modules
-
 import os
 import sys
 import time
@@ -14,6 +13,7 @@ update_after_shutdown = False  # If set True then update and restart zeronet aft
 
 # Load config
 from Config import config
+config.parse(silent=True)  # Plugins need to access the configuration
 
 # Create necessary files and dirs
 if not os.path.isdir(config.log_dir):
@@ -25,9 +25,7 @@ if not os.path.isfile("%s/sites.json" % config.data_dir):
 if not os.path.isfile("%s/users.json" % config.data_dir):
     open("%s/users.json" % config.data_dir, "w").write("{}")
 
-
 # Setup logging
-
 if config.action == "main":
     if os.path.isfile("%s/debug.log" % config.log_dir):  # Simple logrotate
         if os.path.isfile("%s/debug-last.log" % config.log_dir):
@@ -58,6 +56,11 @@ else:
 
 monkey.patch_all(thread=False)  # Not thread: pyfilesystem and system tray icon not compatible
 
+# Load plugins
+from Plugin import PluginManager
+PluginManager.plugin_manager.loadPlugins()
+config.loadPlugins()
+config.parse()  # Parse again to add plugin configuration options
 
 # Log current config
 logging.debug("Config: %s" % config)
@@ -72,17 +75,17 @@ if config.proxy:
     SocksProxy.monkeyPath(*config.proxy.split(":"))
 
 
-# Load plugins
-from Plugin import PluginManager
-PluginManager.plugin_manager.loadPlugins()
 
 
 # -- Actions --
 
 @PluginManager.acceptPlugins
 class Actions(object):
-    # Default action: Start serving UiServer and FileServer
+    def call(self, function_name, kwargs):
+        func = getattr(self, function_name, None)
+        func(**kwargs)
 
+    # Default action: Start serving UiServer and FileServer
     def main(self):
         logging.info("Version: %s r%s, Python %s, Gevent: %s" % (config.version, config.rev, sys.version, gevent.__version__))
         global ui_server, file_server
@@ -295,6 +298,5 @@ actions = Actions()
 
 def start():
     # Call function
-    func = getattr(actions, config.action, None)
     action_kwargs = config.getActionArguments()
-    func(**action_kwargs)
+    actions.call(config.action, action_kwargs)