Fix Collabora: Fetch versioned editor URL from discovery endpoint

- Added getCollaboraEditorURL() to fetch correct /browser/{version}/cool.html path
- Cache discovery response for 5 minutes to avoid repeated requests
- Fixed 'Invalid URI' error caused by version mismatch
This commit is contained in:
Leon Bösche
2026-01-13 14:38:08 +01:00
parent 20e9ae3e4d
commit 749672509b

View File

@@ -2,6 +2,7 @@ package http
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
@@ -21,6 +22,89 @@ import (
"go.b0esche.cloud/backend/pkg/jwt"
)
// Collabora discovery cache
var (
collaboraEditorURL string
collaboraDiscoveryCache time.Time
collaboraDiscoveryMu sync.RWMutex
)
// getCollaboraEditorURL fetches the editor URL from Collabora's discovery endpoint
func getCollaboraEditorURL(collaboraBaseURL string) string {
collaboraDiscoveryMu.RLock()
// Cache for 5 minutes
if collaboraEditorURL != "" && time.Since(collaboraDiscoveryCache) < 5*time.Minute {
url := collaboraEditorURL
collaboraDiscoveryMu.RUnlock()
return url
}
collaboraDiscoveryMu.RUnlock()
// Fetch discovery
collaboraDiscoveryMu.Lock()
defer collaboraDiscoveryMu.Unlock()
// Double-check after acquiring write lock
if collaboraEditorURL != "" && time.Since(collaboraDiscoveryCache) < 5*time.Minute {
return collaboraEditorURL
}
discoveryURL := collaboraBaseURL + "/hosting/discovery"
resp, err := http.Get(discoveryURL)
if err != nil {
fmt.Printf("[COLLABORA] Failed to fetch discovery: %v\n", err)
// Fallback to guessed URL
return collaboraBaseURL + "/browser/dist/cool.html"
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[COLLABORA] Failed to read discovery: %v\n", err)
return collaboraBaseURL + "/browser/dist/cool.html"
}
// Parse XML to extract urlsrc
type Action struct {
Name string `xml:"name,attr"`
Ext string `xml:"ext,attr"`
URLSrc string `xml:"urlsrc,attr"`
}
type App struct {
Name string `xml:"name,attr"`
Actions []Action `xml:"action"`
}
type NetZone struct {
Apps []App `xml:"app"`
}
type WopiDiscovery struct {
NetZone NetZone `xml:"net-zone"`
}
var discovery WopiDiscovery
if err := xml.Unmarshal(body, &discovery); err != nil {
fmt.Printf("[COLLABORA] Failed to parse discovery XML: %v\n", err)
return collaboraBaseURL + "/browser/dist/cool.html"
}
// Find the first edit action URL (they all have the same base)
for _, app := range discovery.NetZone.Apps {
for _, action := range app.Actions {
if action.URLSrc != "" {
// Extract base URL (remove query string marker)
url := strings.TrimSuffix(action.URLSrc, "?")
collaboraEditorURL = url
collaboraDiscoveryCache = time.Now()
fmt.Printf("[COLLABORA] Discovered editor URL: %s\n", url)
return url
}
}
}
fmt.Printf("[COLLABORA] No editor URL found in discovery\n")
return collaboraBaseURL + "/browser/dist/cool.html"
}
// WOPILockManager manages file locks to prevent concurrent editing conflicts
type WOPILockManager struct {
locks map[string]*models.WOPILockInfo
@@ -665,6 +749,9 @@ func collaboraProxyHandler(w http.ResponseWriter, r *http.Request, db *database.
// Build WOPISrc URL (without access_token - that goes in a separate form field)
wopiSrc := fmt.Sprintf("https://go.b0esche.cloud/wopi/files/%s", fileID)
// Get the correct Collabora editor URL from discovery (includes version hash)
editorURL := getCollaboraEditorURL(collaboraURL)
// Return HTML page with auto-submitting form
// The form POSTs to Collabora from within an iframe to work around CSP frame-ancestors restrictions
// The iframe is hosted on the same domain as the embedded page, allowing the POST to complete
@@ -684,7 +771,7 @@ func collaboraProxyHandler(w http.ResponseWriter, r *http.Request, db *database.
<div class="loading">
<p>Loading Collabora Online...</p>
</div>
<form method="POST" action="%s/browser/dist/cool.html" target="_self" id="collaboraForm" style="display: none;">
<form method="POST" action="%s" target="_self" id="collaboraForm" style="display: none;">
<input type="hidden" name="WOPISrc" value="%s">
<input type="hidden" name="access_token" value="%s">
</form>
@@ -692,19 +779,19 @@ func collaboraProxyHandler(w http.ResponseWriter, r *http.Request, db *database.
// Auto-submit the form to Collabora
var form = document.getElementById('collaboraForm');
if (form) {
console.log('[COLLABORA] Submitting form to /browser/dist/cool.html');
console.log('[COLLABORA] Submitting form to %s');
form.submit();
} else {
console.error('[COLLABORA] Form not found');
}
</script>
</body>
</html>`, collaboraURL, wopiSrc, accessToken)
</html>`, editorURL, wopiSrc, accessToken, editorURL)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// Don't set X-Frame-Options - this endpoint is meant to be loaded in an iframe
w.WriteHeader(http.StatusOK)
w.Write([]byte(htmlContent))
fmt.Printf("[COLLABORA-PROXY] Served HTML form: file=%s user=%s wopi_src=%s\n", fileID, userID.String(), wopiSrc)
fmt.Printf("[COLLABORA-PROXY] Served HTML form: file=%s user=%s wopi_src=%s editor_url=%s\n", fileID, userID.String(), wopiSrc, editorURL)
}