Clarify Collabora proxy token handling for iframe cross-origin requests

This commit is contained in:
Leon Bösche
2026-01-12 16:08:35 +01:00
parent 2ded9b00f9
commit 350eb27e30
3 changed files with 97 additions and 42 deletions

View File

@@ -403,12 +403,28 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
final wopiSession = snapshot.data!;
// Don't build URL with query parameters - pass WOPISrc separately to JavaScript
return _buildWebView(wopiSession.wopisrc);
// Use backend proxy endpoint to serve the Collabora form
final proxyUrl = _buildProxyUrl(token);
return _buildWebView(proxyUrl);
},
);
}
String _buildProxyUrl(String token) {
// Build the proxy URL based on whether we're in org or user workspace
String baseUrl = 'https://go.b0esche.cloud';
String endpoint;
if (widget.orgId.isNotEmpty && widget.orgId != 'personal') {
endpoint = '/orgs/${widget.orgId}/files/${widget.fileId}/collabora-proxy';
} else {
endpoint = '/user/files/${widget.fileId}/collabora-proxy';
}
// Pass token as query parameter for iframe (which cannot send Authorization header)
return '$baseUrl$endpoint?token=$token';
}
Future<WOPISession> _createWOPISession(String token) async {
try {
// Use default base URL from backend
@@ -448,32 +464,29 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
}
}
Widget _buildCollaboraIframe(String wopisrc) {
// For Collabora Online, POST the WOPISrc to loleaflet.html
// Use JavaScript to submit the form reliably
Widget _buildCollaboraIframe(String proxyUrl) {
// Load the backend proxy page which handles Collabora form submission
final String viewType =
'collabora-viewer-${DateTime.now().millisecondsSinceEpoch}';
final String iframeName = 'collabora-iframe-$viewType';
'collabora-${DateTime.now().millisecondsSinceEpoch}';
ui.platformViewRegistry.registerViewFactory(viewType, (int viewId) {
// Create the iframe that will receive the form submission
// Create iframe pointing to the proxy endpoint
final iframe = html.IFrameElement()
..name = iframeName
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..style.margin = '0'
..style.padding = '0'
..src = proxyUrl
..setAttribute(
'allow',
'microphone; camera; usb; autoplay; clipboard-read; clipboard-write',
)
..setAttribute(
'sandbox',
'allow-same-origin allow-scripts allow-popups allow-forms allow-pointer-lock allow-presentation allow-modals allow-downloads',
'allow-same-origin allow-scripts allow-popups allow-forms',
);
// Create container
final container = html.DivElement()
..style.width = '100%'
..style.height = '100%'
@@ -482,43 +495,15 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
..style.overflow = 'hidden'
..append(iframe);
// Create JavaScript to submit the form
final jsCode =
'''
(function() {
var form = document.createElement('form');
form.method = 'POST';
form.action = 'https://of.b0esche.cloud/loleaflet/dist/loleaflet.html';
form.target = '$iframeName';
form.style.display = 'none';
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'WOPISrc';
input.value = '$wopisrc';
form.appendChild(input);
document.body.appendChild(form);
form.submit();
})();
''';
// Create and execute script
final script = html.ScriptElement()
..type = 'text/javascript'
..text = jsCode;
html.document.head!.append(script);
return container;
});
return HtmlElementView(viewType: viewType);
}
Widget _buildWebView(String wopisrc) {
// Embed Collabora Online in an iframe for web platform
return _buildCollaboraIframe(wopisrc);
Widget _buildWebView(String proxyUrl) {
// Embed Collabora Online via proxy endpoint
return _buildCollaboraIframe(proxyUrl);
}
@override

View File

@@ -185,6 +185,10 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
r.Post("/user/files/{fileId}/wopi-session", func(w http.ResponseWriter, req *http.Request) {
wopiSessionHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
})
// Collabora form proxy for user files
r.Get("/user/files/{fileId}/collabora-proxy", func(w http.ResponseWriter, req *http.Request) {
collaboraProxyHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
})
// Org routes
r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) {
@@ -241,6 +245,10 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Post("/wopi-session", func(w http.ResponseWriter, req *http.Request) {
wopiSessionHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
})
// Collabora form proxy for org files
r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/collabora-proxy", func(w http.ResponseWriter, req *http.Request) {
collaboraProxyHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
})
})
r.Get("/activity", func(w http.ResponseWriter, req *http.Request) {
activityHandler(w, req, db)

View File

@@ -603,3 +603,65 @@ func wopiSessionHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
fmt.Printf("[WOPI-REQUEST] Session created: file=%s user=%s\n", fileID, userID.String())
}
// CollaboraProxyHandler serves an HTML page that POSTs WOPISrc to Collabora
// This avoids CORS issues by having the POST originate from our domain
func collaboraProxyHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, collaboraURL string) {
fileID := r.PathValue("fileId")
if fileID == "" {
errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
return
}
// Get user from context (from auth middleware)
userIDStr, ok := middleware.GetUserID(r.Context())
if !ok {
errors.WriteError(w, errors.CodeUnauthenticated, "Not authenticated", http.StatusUnauthorized)
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
return
}
// Create WOPI session
wopiSrc, accessToken, err := createWOPISession(r.Context(), db, jwtManager, userID, fileID)
if err != nil {
fmt.Printf("[WOPI-ERROR] Failed to create session: %v\n", err)
errors.WriteError(w, errors.CodeInternal, "Failed to create WOPI session", http.StatusInternalServerError)
return
}
// Return HTML page with auto-submitting form
htmlContent := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<title>Loading Document...</title>
<style>
body { margin: 0; padding: 0; background: #f5f5f5; }
.container { display: flex; justify-content: center; align-items: center; height: 100vh; }
.message { font-family: sans-serif; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="message">Loading document in Collabora Online...</div>
</div>
<form method="POST" action="%s/loleaflet/dist/loleaflet.html" id="collaboraForm" style="display: none;">
<input type="hidden" name="WOPISrc" value="%s">
</form>
<script>
// Submit the form immediately to Collabora
document.getElementById('collaboraForm').submit();
</script>
</body>
</html>`, collaboraURL, wopiSrc)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.WriteHeader(http.StatusOK)
w.Write([]byte(htmlContent))
fmt.Printf("[COLLABORA-PROXY] Served HTML form: file=%s user=%s\n", fileID, userID.String())
}