Merge branch 'master' into always-nonce
This commit is contained in:
commit
93ed7418ab
4 changed files with 77 additions and 146 deletions
|
@ -126,96 +126,3 @@ class UiWebsocketPlugin(object):
|
||||||
plugin_manager.saveConfig()
|
plugin_manager.saveConfig()
|
||||||
|
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
def pluginAction(self, action, address, inner_path):
|
|
||||||
site = self.server.sites.get(address)
|
|
||||||
plugin_manager = PluginManager.plugin_manager
|
|
||||||
|
|
||||||
# Install/update path should exists
|
|
||||||
if action in ("add", "update", "add_request"):
|
|
||||||
if not site:
|
|
||||||
raise Exception("Site not found")
|
|
||||||
|
|
||||||
if not site.storage.isDir(inner_path):
|
|
||||||
raise Exception("Directory not found on the site")
|
|
||||||
|
|
||||||
try:
|
|
||||||
plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json")
|
|
||||||
plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"])
|
|
||||||
except Exception as err:
|
|
||||||
raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err))
|
|
||||||
|
|
||||||
source_path = site.storage.getPath(inner_path)
|
|
||||||
|
|
||||||
target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path
|
|
||||||
plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {})
|
|
||||||
|
|
||||||
# Make sure plugin (not)installed
|
|
||||||
if action in ("add", "add_request") and os.path.isdir(target_path):
|
|
||||||
raise Exception("Plugin already installed")
|
|
||||||
|
|
||||||
if action in ("update", "remove") and not os.path.isdir(target_path):
|
|
||||||
raise Exception("Plugin not installed")
|
|
||||||
|
|
||||||
# Do actions
|
|
||||||
if action == "add":
|
|
||||||
shutil.copytree(source_path, target_path)
|
|
||||||
|
|
||||||
plugin_config["date_added"] = int(time.time())
|
|
||||||
plugin_config["rev"] = plugin_info["rev"]
|
|
||||||
plugin_config["enabled"] = True
|
|
||||||
|
|
||||||
if action == "update":
|
|
||||||
shutil.rmtree(target_path)
|
|
||||||
|
|
||||||
shutil.copytree(source_path, target_path)
|
|
||||||
|
|
||||||
plugin_config["rev"] = plugin_info["rev"]
|
|
||||||
plugin_config["date_updated"] = time.time()
|
|
||||||
|
|
||||||
if action == "remove":
|
|
||||||
del plugin_manager.config[address][inner_path]
|
|
||||||
shutil.rmtree(target_path)
|
|
||||||
|
|
||||||
def doPluginAdd(self, to, inner_path, res):
|
|
||||||
if not res:
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.pluginAction("add", self.site.address, inner_path)
|
|
||||||
PluginManager.plugin_manager.saveConfig()
|
|
||||||
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
["Plugin installed!<br>You have to restart the client to load the plugin", "Restart"],
|
|
||||||
lambda res: self.actionServerShutdown(to, restart=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.response(to, "ok")
|
|
||||||
|
|
||||||
@flag.no_multiuser
|
|
||||||
def actionPluginAddRequest(self, to, inner_path):
|
|
||||||
self.pluginAction("add_request", self.site.address, inner_path)
|
|
||||||
plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json")
|
|
||||||
warning = "<b>Warning!<br/>Plugins has the same permissions as the ZeroNet client.<br/>"
|
|
||||||
warning += "Do not install it if you don't trust the developer.</b>"
|
|
||||||
|
|
||||||
self.cmd(
|
|
||||||
"confirm",
|
|
||||||
["Install new plugin: %s?<br>%s" % (plugin_info["name"], warning), "Trust & Install"],
|
|
||||||
lambda res: self.doPluginAdd(to, inner_path, res)
|
|
||||||
)
|
|
||||||
|
|
||||||
@flag.admin
|
|
||||||
@flag.no_multiuser
|
|
||||||
def actionPluginRemove(self, to, address, inner_path):
|
|
||||||
self.pluginAction("remove", address, inner_path)
|
|
||||||
PluginManager.plugin_manager.saveConfig()
|
|
||||||
return "ok"
|
|
||||||
|
|
||||||
@flag.admin
|
|
||||||
@flag.no_multiuser
|
|
||||||
def actionPluginUpdate(self, to, address, inner_path):
|
|
||||||
self.pluginAction("update", address, inner_path)
|
|
||||||
PluginManager.plugin_manager.saveConfig()
|
|
||||||
PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True
|
|
||||||
return "ok"
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ class PluginManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log = logging.getLogger("PluginManager")
|
self.log = logging.getLogger("PluginManager")
|
||||||
self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__))
|
self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__))
|
||||||
self.path_installed_plugins = config.data_dir + "/__plugins__"
|
|
||||||
self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class)
|
self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class)
|
||||||
self.subclass_order = {} # Record the load order of the plugins, to keep it after reload
|
self.subclass_order = {} # Record the load order of the plugins, to keep it after reload
|
||||||
self.pluggable = {}
|
self.pluggable = {}
|
||||||
|
@ -93,34 +92,6 @@ class PluginManager:
|
||||||
plugin["loaded"] = plugin_name in self.plugin_names
|
plugin["loaded"] = plugin_name in self.plugin_names
|
||||||
plugins.append(plugin)
|
plugins.append(plugin)
|
||||||
|
|
||||||
plugins += self.listInstalledPlugins(list_disabled)
|
|
||||||
return plugins
|
|
||||||
|
|
||||||
def listInstalledPlugins(self, list_disabled=False):
|
|
||||||
plugins = []
|
|
||||||
|
|
||||||
for address, site_plugins in sorted(self.config.items()):
|
|
||||||
if address == "builtin":
|
|
||||||
continue
|
|
||||||
for plugin_inner_path, plugin_config in sorted(site_plugins.items()):
|
|
||||||
is_enabled = plugin_config.get("enabled", False)
|
|
||||||
if not is_enabled and not list_disabled:
|
|
||||||
continue
|
|
||||||
plugin_name = os.path.basename(plugin_inner_path)
|
|
||||||
|
|
||||||
dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path)
|
|
||||||
|
|
||||||
plugin = {}
|
|
||||||
plugin["source"] = address
|
|
||||||
plugin["name"] = plugin_name
|
|
||||||
plugin["dir_name"] = plugin_name
|
|
||||||
plugin["dir_path"] = dir_path
|
|
||||||
plugin["inner_path"] = plugin_inner_path
|
|
||||||
plugin["enabled"] = is_enabled
|
|
||||||
plugin["rev"] = plugin_config.get("rev", 0)
|
|
||||||
plugin["loaded"] = plugin_name in self.plugin_names
|
|
||||||
plugins.append(plugin)
|
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
# Load all plugin
|
# Load all plugin
|
||||||
|
@ -156,10 +127,10 @@ class PluginManager:
|
||||||
for module_name, module in list(sys.modules.items()):
|
for module_name, module in list(sys.modules.items()):
|
||||||
if not module or not getattr(module, "__file__", None):
|
if not module or not getattr(module, "__file__", None):
|
||||||
continue
|
continue
|
||||||
if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__:
|
if self.path_plugins not in module.__file__:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled
|
if not getattr(module, 'allow_reload', True): # Reload disabled
|
||||||
# Re-add non-reloadable plugins
|
# Re-add non-reloadable plugins
|
||||||
for class_name, classes in self.plugins_before.items():
|
for class_name, classes in self.plugins_before.items():
|
||||||
for c in classes:
|
for c in classes:
|
||||||
|
|
|
@ -49,7 +49,7 @@ class SecurityError(Exception):
|
||||||
|
|
||||||
|
|
||||||
@PluginManager.acceptPlugins
|
@PluginManager.acceptPlugins
|
||||||
class UiRequest(object):
|
class UiRequest:
|
||||||
|
|
||||||
def __init__(self, server, get, env, start_response):
|
def __init__(self, server, get, env, start_response):
|
||||||
if server:
|
if server:
|
||||||
|
@ -99,8 +99,52 @@ class UiRequest(object):
|
||||||
def resolveDomain(self, domain):
|
def resolveDomain(self, domain):
|
||||||
return self.server.site_manager.resolveDomainCached(domain)
|
return self.server.site_manager.resolveDomainCached(domain)
|
||||||
|
|
||||||
# Call the request handler function base on path
|
def isCrossOriginRequest(self):
|
||||||
|
"""Prevent detecting sites on this 0net instance
|
||||||
|
|
||||||
|
In particular, we block non-user requests from other hosts as well as
|
||||||
|
cross-site
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = self.getRequestUrl()
|
||||||
|
fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE')
|
||||||
|
origin = self.env.get('HTTP_ORIGIN')
|
||||||
|
referer = self.env.get('HTTP_REFERER')
|
||||||
|
|
||||||
|
# Allow all user-initiated requests
|
||||||
|
if fetch_mode == 'navigate':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Deny requests that cannot be traced
|
||||||
|
if not origin and not referer:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Deny requests from non-0net origins
|
||||||
|
if origin and not self.isSameHost(origin, url):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Allow non-site specific requests
|
||||||
|
if self.getRequestSite() == '/':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Deny cross site requests
|
||||||
|
if not self.isSameOrigin(referer, url):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def route(self, path):
|
def route(self, path):
|
||||||
|
"""Main routing
|
||||||
|
|
||||||
|
If no internal action is performed, calls action[Path] from plugins
|
||||||
|
|
||||||
|
This behaviour is not very flexible or easy to follow, so perhaps
|
||||||
|
we'd want something else..
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.isCrossOriginRequest():
|
||||||
|
return self.error404()
|
||||||
|
|
||||||
# Restict Ui access by ip
|
# Restict Ui access by ip
|
||||||
if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
|
if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
|
||||||
return self.error403(details=False)
|
return self.error403(details=False)
|
||||||
|
@ -274,25 +318,31 @@ class UiRequest(object):
|
||||||
else:
|
else:
|
||||||
return referer
|
return referer
|
||||||
|
|
||||||
|
def getRequestSite(self):
|
||||||
|
"""Return 0net site addr associated with current request
|
||||||
|
|
||||||
|
If request is site-agnostic, returns /
|
||||||
|
"""
|
||||||
|
path = self.env["PATH_INFO"]
|
||||||
|
match = re.match(r'(/raw)?(?P<site>/1[a-zA-Z0-9]*)', path)
|
||||||
|
if not match:
|
||||||
|
match = re.match(r'(/raw)?/(?P<domain>[a-zA-Z0-9\.\-_]*)', path)
|
||||||
|
if match:
|
||||||
|
domain = match.group('domain')
|
||||||
|
if self.isDomain(domain):
|
||||||
|
addr = self.resolveDomain(domain)
|
||||||
|
return '/'+addr
|
||||||
|
return '/'
|
||||||
|
return match.group('site')
|
||||||
|
|
||||||
# Send response headers
|
# Send response headers
|
||||||
def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
|
def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
|
||||||
url = self.getRequestUrl()
|
|
||||||
referer = self.env.get('HTTP_REFERER')
|
|
||||||
origin = self.env.get('HTTP_ORIGIN')
|
|
||||||
fetch_site = self.env.get('HTTP_SEC_FETCH_SITE')
|
|
||||||
fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE')
|
|
||||||
not_same_ref = referer and not self.isSameHost(referer, url)
|
|
||||||
not_same_origin = origin and not self.isSameHost(origin, url)
|
|
||||||
cross_site_not_navigate = not referer and fetch_site == 'cross-site' and not fetch_mode == 'navigate'
|
|
||||||
if status != 404 and (not_same_ref or not_same_origin or cross_site_not_navigate):
|
|
||||||
# pretend nothing is here for third-party access
|
|
||||||
return self.error404()
|
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["Version"] = "HTTP/1.1"
|
headers["Version"] = "HTTP/1.1"
|
||||||
headers["Connection"] = "Keep-Alive"
|
headers["Connection"] = "Keep-Alive"
|
||||||
headers["Keep-Alive"] = "max=25, timeout=30"
|
headers["Keep-Alive"] = "max=25, timeout=30"
|
||||||
headers["X-Frame-Options"] = "SAMEORIGIN"
|
headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||||
|
headers["Referrer-Policy"] = "same-origin"
|
||||||
|
|
||||||
if noscript:
|
if noscript:
|
||||||
headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
|
headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
|
||||||
|
@ -304,7 +354,7 @@ class UiRequest(object):
|
||||||
|
|
||||||
if self.env["REQUEST_METHOD"] == "OPTIONS":
|
if self.env["REQUEST_METHOD"] == "OPTIONS":
|
||||||
# Allow json access
|
# Allow json access
|
||||||
headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range"
|
headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range, Referer"
|
||||||
headers["Access-Control-Allow-Credentials"] = "true"
|
headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
|
||||||
# Download instead of display file types that can be dangerous
|
# Download instead of display file types that can be dangerous
|
||||||
|
@ -614,15 +664,18 @@ class UiRequest(object):
|
||||||
if not url_a or not url_b:
|
if not url_a or not url_b:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
url_a = url_a.replace("/raw/", "/")
|
host_pattern = r'(?P<host>http[s]?://.*?)(/|$)'
|
||||||
url_b = url_b.replace("/raw/", "/")
|
|
||||||
|
|
||||||
origin_pattern = "http[s]{0,1}://(.*?/).*"
|
match_a = re.match(host_pattern, url_a)
|
||||||
|
match_b = re.match(host_pattern, url_b)
|
||||||
|
|
||||||
origin_a = re.sub(origin_pattern, "\\1", url_a)
|
if not match_a or not match_b:
|
||||||
origin_b = re.sub(origin_pattern, "\\1", url_b)
|
return False
|
||||||
|
|
||||||
return origin_a == origin_b
|
host_a = match_a.group('host')
|
||||||
|
host_b = match_b.group('host')
|
||||||
|
|
||||||
|
return host_a == host_b
|
||||||
|
|
||||||
def isSameOrigin(self, url_a, url_b):
|
def isSameOrigin(self, url_a, url_b):
|
||||||
"""Check if 0net origin is the same"""
|
"""Check if 0net origin is the same"""
|
||||||
|
|
|
@ -74,7 +74,7 @@ else if (window.opener && window.opener.location.toString()) {
|
||||||
|
|
||||||
|
|
||||||
<!-- Site Iframe -->
|
<!-- Site Iframe -->
|
||||||
<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox {sandbox_permissions}" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true"></iframe>
|
<iframe src='about:blank' id='inner-iframe' sandbox="allow-forms allow-scripts allow-top-navigation allow-popups allow-modals allow-presentation allow-pointer-lock allow-popups-to-escape-sandbox allow-same-origin {sandbox_permissions}" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true" referrerpolicy="same-origin"></iframe>
|
||||||
|
|
||||||
<!-- Site info -->
|
<!-- Site info -->
|
||||||
<script id="script_init" nonce="{script_nonce}">
|
<script id="script_init" nonce="{script_nonce}">
|
||||||
|
|
Loading…
Reference in a new issue