I think we're pretty much there

Now using file_keys so anyone can't just steal the files just by path.

We're also getting en setting the URL properly now.
This commit is contained in:
Brandon Doornbos 2026-06-03 23:27:54 +02:00
parent a5f3c61958
commit a29609640c
4 changed files with 88 additions and 23 deletions

View file

@ -1570,8 +1570,9 @@ def add_opds(ap):
def add_wopi(ap): def add_wopi(ap):
ap2 = ap.add_argument_group("WOPI options") ap2 = ap.add_argument_group("WOPI options")
ap2.add_argument("--wopi", action="store_true", help="enable WOPI -- allows for integrating with office suites (volflag=wopi)") ap2.add_argument("--wopi", action="store_true", help="allows for integrating with office suites using WOPI (volflag=wopi)")
ap2.add_argument("--wopi-path", action="store_true", default="wopi", help="where to expose WOPI (needs to start with 'wopi'); defaults to <volname>/wopi/ (volflag=wopi-path)") ap2.add_argument("--wopi-client", type=u, default="https://localhost:9980", help="where to find your WOPI client, this is what actually hosts e.g. Collabora Online")
ap2.add_argument("--wopi-self-signed", action="store_true", help="disable certificate verification of the WOPI client, only use with local use")
def add_handlers(ap): def add_handlers(ap):

View file

@ -240,7 +240,6 @@ flagcats = {
"gz": "allows server-side gzip compression of uploads with ?gz", "gz": "allows server-side gzip compression of uploads with ?gz",
"xz": "allows server-side lzma compression of uploads with ?xz", "xz": "allows server-side lzma compression of uploads with ?xz",
"pk": "forces server-side compression, optional arg: xz,9", "pk": "forces server-side compression, optional arg: xz,9",
"wopi": "enable WOPI support for integrating with online office suites",
}, },
"upload rules": { "upload rules": {
"apnd_who=dw": "who can append? (aw/dw/w/no)", "apnd_who=dw": "who can append? (aw/dw/w/no)",
@ -395,6 +394,11 @@ flagcats = {
"og_no_head": "you want to add tags manually with og_tpl", "og_no_head": "you want to add tags manually with og_tpl",
"og_ua": "if defined: only send OG html if useragent matches this regex", "og_ua": "if defined: only send OG html if useragent matches this regex",
}, },
"wopi": {
"wopi": "enable WOPI support for integrating with online office suites",
"wopi-client": "address of WOPI client, e.g. Collabora Online",
"wopi-self-signed": "disable certificate verification of the WOPI client, only use with local use"
},
"opds": { "opds": {
"opds": "enable OPDS", "opds": "enable OPDS",
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything", "opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",

View file

@ -16,6 +16,10 @@ import sys
import threading # typechk import threading # typechk
import time import time
import uuid import uuid
import urllib.request
import urllib.parse
import ssl
import xml.etree.ElementTree as ET
from datetime import datetime from datetime import datetime
from operator import itemgetter from operator import itemgetter
@ -1524,35 +1528,34 @@ class HttpCli(object):
if "rss" in self.uparam: if "rss" in self.uparam:
return self.tx_rss() return self.tx_rss()
if self.args.wopi:
if "wopi" in self.uparam: if "wopi" in self.uparam:
return self.tx_wopi() return self.tx_wopi()
if self.vpath.startswith("wopi"):
return self.tx_wopi_api()
return self.tx_browser() return self.tx_browser()
def tx_wopi(self) -> bool: def tx_wopi_api(self) -> bool:
self.log("%s" % self.uparam.get("wopi"))
return False
# https://' + location.hostname + ':9980/browser/4610258811/cool.html?WOPISrc=' + location.origin + '/wopi/files' + top + bhref +
path = self.vpath.split('/') path = self.vpath.split('/')
if path[1] == "files": if "files" in path:
if path[-1] == "contents": real_path = self.conn.hsrv.wopi_files[path[2]]
vfs, _ = self.asrv.vfs.get("/".join(path[2:-1]), self.uname, False, True) vfs, _ = self.asrv.vfs.get(real_path, self.uname, False, True)
full_path = vfs.realpath + "/" + "/".join(path[2:-1]) full_path = vfs.realpath + "/" + real_path
if "contents" in path:
if self.do_log: if self.do_log:
self.log("WOPI GET 'contents': %s" % (full_path)) self.log("WOPI GET 'contents': %s" % (full_path))
return self.tx_file("oh_f", full_path) return self.tx_file("oh_f", full_path)
else: else:
vfs, _ = self.asrv.vfs.get("/".join(path[2:]), self.uname, False, True)
full_path = vfs.realpath + "/" + "/".join(path[2:])
if self.do_log: if self.do_log:
self.log("WOPI GET 'file_info': %s" % (full_path)) self.log("WOPI GET 'file_info': %s" % (full_path))
file_info = { file_info = {
"BaseFileName": path[-1], "BaseFileName": real_path.split("/")[-1],
"OwnerId": self.uname, "OwnerId": self.uname,
"Size": os.path.getsize(full_path), "Size": os.path.getsize(full_path),
"UserId": self.uname, "UserId": self.uname,
@ -1567,7 +1570,7 @@ class HttpCli(object):
# "HideRepairOption": , # "HideRepairOption": ,
# "DisableExport": , # "DisableExport": ,
# "DisableCopy": , # "DisableCopy": ,
"EnableOwnerTermination": True, # "EnableOwnerTermination": True,
"LastModifiedTime": "LastModifiedTime":
time.strftime( time.strftime(
"%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%SZ",
@ -1582,7 +1585,61 @@ class HttpCli(object):
self.reply(ret, 200, "application/json; charset=utf-8") self.reply(ret, 200, "application/json; charset=utf-8")
return True return True
return self.tx_404(True) return self.tx_404()
def tx_wopi(self) -> bool:
path = self.vpath + "/" + str(self.uparam["wopi"])
file_key = self.gen_fk(2, self.args.fk_salt, path, 0, 0)
self.conn.hsrv.wopi_files[file_key] = path
try:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ctx.verify_mode = ssl.CERT_NONE if self.args.wopi_self_signed else ssl.CERT_REQUIRED
discovery = urllib.request.urlopen(self.args.wopi_client + "/hosting/discovery", context=ctx)
response = ET.fromstring(discovery.read())
ext = path.split('.')[-1]
wopi_url = response.find(".//action[@ext='%s'][@urlsrc]" % ext).get("urlsrc")
favicon_url = response.find(".//action[@ext='%s'].." % ext).get("favIconUrl")
url = wopi_url + "WOPISrc=https://" + self.host + "/wopi/files/" + file_key
except Exception as error:
self.log("Couldn't get urls from WOPI client: %s" % error)
return False
ret = [
"""\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" href="%s" />
<title>Load Collabora Online</title>
<style>
body {
margin: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
iframe {
width: 100%%;
height: 100%%;
border: 0;
}
</style>
</head>
<body>
<iframe src="%s" />
</body>
</html>
"""
% (favicon_url, url)
]
bret = "".join(ret).encode("utf-8", "replace")
self.reply(bret, 200, "text/html; charset=utf-8")
return True
def tx_rss(self) -> bool: def tx_rss(self) -> bool:
if self.do_log: if self.do_log:
@ -3207,7 +3264,7 @@ class HttpCli(object):
except: except:
raise Pebkac(400, "you must supply a content-length for binary POST") raise Pebkac(400, "you must supply a content-length for binary POST")
if self.vpath.startswith("wopi"): if self.args.wopi and self.vpath.startswith("wopi"):
return self.rx_wopi() return self.rx_wopi()
try: try:
@ -3424,9 +3481,10 @@ class HttpCli(object):
def rx_wopi(self) -> bool: def rx_wopi(self) -> bool:
path = self.vpath.split('/') path = self.vpath.split('/')
if path[1] == "files" and path[-1] == "contents": if "files" in path and "contents" in path:
vfs, _ = self.asrv.vfs.get("/".join(path[2:-1]), self.uname, False, True) real_path = self.conn.hsrv.wopi_files[path[2]]
full_path = vfs.realpath + "/" + "/".join(path[2:-1]) vfs, _ = self.asrv.vfs.get(real_path, self.uname, False, True)
full_path = vfs.realpath + "/" + real_path
if self.do_log: if self.do_log:
self.log("WOPI POST 'contents': %s" % (full_path)) self.log("WOPI POST 'contents': %s" % (full_path))

View file

@ -242,6 +242,8 @@ class HttpSrv(object):
if (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb: if (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb:
Daemon(self.post_init, "hsrv-init2") Daemon(self.post_init, "hsrv-init2")
self.wopi_files: dict[str, str] = {}
def post_init(self) -> None: def post_init(self) -> None:
try: try:
x = self.broker.ask("thumbsrv.getcfg") x = self.broker.ask("thumbsrv.getcfg")