From 1fb6123f5abf4a87b812298681c090addf6cb576 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Tue, 26 Feb 2019 13:07:06 +0100 Subject: [PATCH 1/3] Extend mediasrv to also serve media files in addons directory RequestsHandler now rewrites "/_addons" references to addons folder. --- aqt/main.py | 3 ++- aqt/mediasrv.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/aqt/main.py b/aqt/main.py index 279e9f1d2..fe5495107 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -1350,7 +1350,8 @@ Please ensure a profile is open and Anki is not busy, then try again."""), ########################################################################## def setupMediaServer(self): - self.mediaServer = aqt.mediasrv.MediaServer() + self.mediaServer = aqt.mediasrv.MediaServer( + addonFolder=self.pm.addonFolder()) self.mediaServer.start() def baseHTML(self): diff --git a/aqt/mediasrv.py b/aqt/mediasrv.py index 7ae437ca7..9d1218d32 100644 --- a/aqt/mediasrv.py +++ b/aqt/mediasrv.py @@ -47,7 +47,12 @@ class MediaServer(threading.Thread): _port = None _ready = threading.Event() + def __init__(self, addonFolder=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self._addonFolder = addonFolder + def run(self): + RequestHandler = createRequestHandler(self._addonFolder) self.server = ThreadedHTTPServer(("127.0.0.1", 0), RequestHandler) self._ready.set() self.server.serve_forever() @@ -59,9 +64,15 @@ class MediaServer(threading.Thread): def shutdown(self): self.server.shutdown() +def createRequestHandler(addonFolder): + """RequestHandler factory""" + return type("RequestHandler", (RequestHandler, ), + {"_addonFolder": addonFolder}) + class RequestHandler(http.server.SimpleHTTPRequestHandler): timeout = 1 + _addonFolder = None def do_GET(self): f = self.send_head() @@ -121,11 +132,16 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): format%args)) # catch /_anki references and rewrite them to web export folder + # catch /_addons references and rewrite them to addons folder def _redirectWebExports(self, path): targetPath = os.path.join(os.getcwd(), "_anki", "") if path.startswith(targetPath): newPath = os.path.join(_exportFolder, path[len(targetPath):]) return newPath + targetPath = os.path.join(os.getcwd(), "_addons", "") + if self._addonFolder and path.startswith(targetPath): + newPath = os.path.join(self._addonFolder, path[len(targetPath):]) + return newPath return path # work around Windows machines with incorrect mime type From 5e90758f3996e2c216ee62274526a4878952b16c Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Sat, 2 Mar 2019 18:57:51 +0100 Subject: [PATCH 2/3] Allow add-on authors to set a whitelist of webview-accessible files Extends the AddonManager API with two new methods, setWebExports and getWebExports. setWebExports expects an add-on module name and a valid RegEx pattern to match subpaths in the add-on folder against. Any matching subpaths will be accessible in Anki's web views by referencing them with /_addons/{addon_id}/{subpath}. For instance, to allow access to .css and .png files in your add-on's user_files directory you would call: > mw.addonManager.setWebExports(__name__, r"user_files/.+(\.png|\.css)") You could then reference these files in web views as such: > Please note that this bypasses the default security policies used in Anki webviews. You should take care to construct your RegEx patterns specific enough so that they do not match any sensitive files of our add-on. --- aqt/addons.py | 13 +++++++++++++ aqt/main.py | 3 +-- aqt/mediasrv.py | 46 +++++++++++++++++++++++++++++++++------------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/aqt/addons.py b/aqt/addons.py index 209b71105..cf8dc8cdf 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -408,6 +408,19 @@ Are you sure you want to continue?""" if not os.path.exists(bp): return os.rename(bp, p) + + # Web Exports + ###################################################################### + + _webExports = {} + + def setWebExports(self, module, pattern): + addon = self.addonFromModule(module) + self._webExports[addon] = pattern + + def getWebExports(self, addon): + return self._webExports.get(addon) + # Add-ons Dialog ###################################################################### diff --git a/aqt/main.py b/aqt/main.py index fe5495107..2b5cbc8f1 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -1350,8 +1350,7 @@ Please ensure a profile is open and Anki is not busy, then try again."""), ########################################################################## def setupMediaServer(self): - self.mediaServer = aqt.mediasrv.MediaServer( - addonFolder=self.pm.addonFolder()) + self.mediaServer = aqt.mediasrv.MediaServer(self) self.mediaServer.start() def baseHTML(self): diff --git a/aqt/mediasrv.py b/aqt/mediasrv.py index 9d1218d32..fd74e3089 100644 --- a/aqt/mediasrv.py +++ b/aqt/mediasrv.py @@ -9,6 +9,7 @@ import socketserver import socket from anki.utils import devMode import threading +import re # locate web folder in source/binary distribution def _getExportFolder(): @@ -47,12 +48,12 @@ class MediaServer(threading.Thread): _port = None _ready = threading.Event() - def __init__(self, addonFolder=None, *args, **kwargs): + def __init__(self, mw, *args, **kwargs): super().__init__(*args, **kwargs) - self._addonFolder = addonFolder + self.mw = mw def run(self): - RequestHandler = createRequestHandler(self._addonFolder) + RequestHandler.mw = self.mw self.server = ThreadedHTTPServer(("127.0.0.1", 0), RequestHandler) self._ready.set() self.server.serve_forever() @@ -64,15 +65,10 @@ class MediaServer(threading.Thread): def shutdown(self): self.server.shutdown() -def createRequestHandler(addonFolder): - """RequestHandler factory""" - return type("RequestHandler", (RequestHandler, ), - {"_addonFolder": addonFolder}) - class RequestHandler(http.server.SimpleHTTPRequestHandler): timeout = 1 - _addonFolder = None + mw = None def do_GET(self): f = self.send_head() @@ -131,17 +127,41 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): self.log_date_time_string(), format%args)) - # catch /_anki references and rewrite them to web export folder - # catch /_addons references and rewrite them to addons folder + def _redirectWebExports(self, path): + # catch /_anki references and rewrite them to web export folder targetPath = os.path.join(os.getcwd(), "_anki", "") if path.startswith(targetPath): newPath = os.path.join(_exportFolder, path[len(targetPath):]) return newPath + + # catch /_addons references and rewrite them to addons folder targetPath = os.path.join(os.getcwd(), "_addons", "") - if self._addonFolder and path.startswith(targetPath): - newPath = os.path.join(self._addonFolder, path[len(targetPath):]) + if path.startswith(targetPath): + try: + addMgr = self.mw.addonManager + except AttributeError: + return path + + addonPath = path[len(targetPath):] + + try: + addon, subPath = addonPath.split(os.path.sep, 1) + except ValueError: + return path + if not addon: + return path + + pattern = addMgr.getWebExports(addon) + if not pattern: + return path + + if not re.match("^{}$".format(pattern), subPath): + return path + + newPath = os.path.join(addMgr.addonsFolder(), addonPath) return newPath + return path # work around Windows machines with incorrect mime type From 58d095539200f02beff80e613671f096f6700bc3 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Sun, 3 Mar 2019 17:04:01 +0100 Subject: [PATCH 3/3] Refactor: re.match --> re.fullmatch --- aqt/mediasrv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqt/mediasrv.py b/aqt/mediasrv.py index fd74e3089..9574609c5 100644 --- a/aqt/mediasrv.py +++ b/aqt/mediasrv.py @@ -156,7 +156,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): if not pattern: return path - if not re.match("^{}$".format(pattern), subPath): + if not re.fullmatch(pattern, subPath): return path newPath = os.path.join(addMgr.addonsFolder(), addonPath)