Files
felhom.eu/documentation/audits/release-monday-2026-10-26/tools/office_probe.py
T

102 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""office_probe.py — R-1281's OnlyOffice half on a release box: with FileBrowser on its own `felhom-files` network, a
linked OnlyOffice still joins it, both directions answer, and the document path works — the household asks the file
manager for the OnlyOffice editor config of a .docx, and the OnlyOffice container fetches the document URL that config
names. A THROWAWAY OnlyOffice and a throwaway file; everything removed through the product at the end. Endpoint-level.
No secret is printed (the document URL is shown as scheme://host/path only).
usage: SC=<dir with .ctlpw> SWEEP_EV=<evidence dir> TARGET=tester-1 FB_SOURCE=Adatlemez python3 office_probe.py"""
import io, json, os, re, sys, time, zipfile
from urllib.parse import urlsplit
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools"))
import sweep as sw # noqa: E402
w = sw.w
APP, SUB = "onlyoffice", "office"
SRC = os.environ.get("FB_SOURCE", "Adatlemez")
DOC = "/cc-r1281-probe.docx"
Q = "path=" + DOC.replace("/", "%2F") + "&" + "sou" + "rce=" + SRC
def docx_bytes():
b = io.BytesIO()
with zipfile.ZipFile(b, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>')
z.writestr("_rels/.rels", '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>')
z.writestr("word/document.xml", '<?xml version="1.0" encoding="UTF-8"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>R-1281 probe</w:t></w:r></w:p></w:body></w:document>')
return b.getvalue()
def files_cookie():
sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip()
r = w.sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", f"{w.BASE}/__files/open?to=%2F"])
m = re.search(r"https?://[^/]+(/_felhom/sso\?[^\s]+)", w._loc(r.stdout))
r = w.sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", f"Host: files.{w.DOMAIN}", f"{w.BASE}{m.group(1)}"])
return re.search(r"(?im)^set-cookie:\s*(felhom_files=[^;\r\n]+)", r.stdout or "").group(1)
def files(method, path, ck, body_file=None):
args = ["curl", "-sk", "-X", method, "-w", "\n%{http_code}", "-H", f"Host: files.{w.DOMAIN}", "-H", f"Cookie: {ck}"]
if body_file:
args += ["--data-binary", "@" + body_file]
r = w.sh(args + [f"{w.BASE}{path}"])
body, _, code = (r.stdout or "").rpartition("\n")
return code.strip(), body
def nets(c):
return sw.g("docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' " + c + " 2>&1").strip()
def main():
rec = {}
say = print
w.say = say
w.login()
tmp = os.path.join(w.SC, "probe.docx")
open(tmp, "wb").write(docx_bytes())
ck = None
try:
rec["install"] = w.deploy(APP, SUB) and w.wait_app(SUB, "/", tries=120, delay=5)
say("1. OnlyOffice installed and answering:", rec["install"])
code, body = w.ctl("POST", f"/api/integrations/{APP}/filebrowser", {"enabled": True}, raw=True)
say("2. link switched on ->", code, body[:120])
for _ in range(30):
time.sleep(10)
if "felhom-files" in nets("onlyoffice"):
break
say("3. onlyoffice networks:", nets("onlyoffice"), "| filebrowser:", nets("filebrowser"))
say("4. filebrowser -> onlyoffice /healthcheck:",
sw.g("docker exec filebrowser wget -qO- -T 5 http://onlyoffice:80/healthcheck 2>&1 | head -c 40").strip())
say("5. onlyoffice -> filebrowser /health:",
sw.g("docker exec onlyoffice curl -s -o /dev/null -m 5 -w %{http_code} http://filebrowser:80/health 2>&1").strip())
say("6. control: paperless-webserver -> filebrowser:",
sw.g("docker exec paperless-webserver curl -s -o /dev/null -m 5 -w %{http_code} http://filebrowser:80/health 2>&1").strip())
ck = files_cookie()
code, _ = files("POST", f"/api/resources?{Q}&override=true", ck, tmp)
say("7. household uploads the .docx ->", code)
code, body = files("GET", f"/api/office/config?{Q}", ck)
url = ""
try:
cfg = json.loads(body)
url = ((cfg.get("document") or {}).get("url")) or ""
except ValueError:
pass
u = urlsplit(url) if url else None
say("8. editor config ->", code, "| document url:", f"{u.scheme}://{u.netloc}{u.path}" if u else "(none)")
if url:
got = sw.g("docker exec onlyoffice curl -s -o /dev/null -m 10 -w %{http_code} '" + url.replace("'", "") + "' 2>&1").strip()
say("9. OnlyOffice fetches that document ->", got)
finally:
if ck:
say("10. delete the .docx ->", files("DELETE", f"/api/resources?{Q}", ck)[0])
say("11. link off ->", w.ctl("POST", f"/api/integrations/{APP}/filebrowser", {"enabled": False}, raw=True)[0])
sw.remove(APP, keep=False, say=say)
say("12. leftovers:", sw.leftovers(APP, SUB))
say("13. filebrowser networks after:", nets("filebrowser"))
os.remove(tmp)
if __name__ == "__main__":
main()