From d294cacbe4a40853dcba55940db8052e17b881c1 Mon Sep 17 00:00:00 2001 From: Alan Castro Date: Fri, 19 Jan 2024 23:28:34 -0800 Subject: [PATCH] feat: only-office integration pt 2, performed server-side signing, addressed other comments --- cmd/config.go | 3 + files/file.go | 16 +- files/utils.go | 15 ++ frontend/package-lock.json | 9 + frontend/package.json | 1 + frontend/src/types/file.d.ts | 3 +- frontend/src/types/settings.d.ts | 6 + frontend/src/utils/constants.ts | 4 +- frontend/src/views/Files.vue | 8 +- frontend/src/views/files/OnlyOfficeEditor.vue | 190 ++++-------------- go.mod | 1 + go.sum | 2 + http/auth.go | 1 + http/data.go | 11 +- http/http.go | 1 + http/onlyoffice.go | 168 +++++++++++++++- http/static.go | 2 +- test.docx | Bin 24698 -> 0 bytes 18 files changed, 258 insertions(+), 183 deletions(-) delete mode 100644 test.docx diff --git a/cmd/config.go b/cmd/config.go index de55c28e..3c8878cc 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -48,6 +48,9 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("branding.files", "", "path to directory with images and custom styles") flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links") flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") + + flags.String("onlyoffice.url", "", "onlyoffice integration url") + flags.String("onlyoffice.jwtSecret", "", "onlyoffice integration secret") } //nolint:gocyclo diff --git a/files/file.go b/files/file.go index 7d3a695b..c228cfca 100644 --- a/files/file.go +++ b/files/file.go @@ -205,19 +205,10 @@ func (i *FileInfo) Checksum(algo string) error { } func (i *FileInfo) RealPath() string { - if realPathFs, ok := i.Fs.(interface { - RealPath(name string) (fPath string, err error) - }); ok { - realPath, err := realPathFs.RealPath(i.Path) - if err == nil { - return realPath - } - } - - return i.Path + return GetRealPath(i.Fs, i.Path) } -//nolint:goconst +//nolint:goconst,gocyclo func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { if IsNamedPipe(i.Mode) { i.Type = "blob" @@ -276,7 +267,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { i.Content = string(content) } return nil - case strings.HasPrefix(mimetype, "application/vnd.openxmlformats-officedocument"): + case strings.HasPrefix(mimetype, "application/vnd.openxmlformats-officedocument"), + strings.HasPrefix(mimetype, "application/vnd.oasis.opendocument"): i.Type = "officedocument" return nil default: diff --git a/files/utils.go b/files/utils.go index f4b0365d..756c940f 100644 --- a/files/utils.go +++ b/files/utils.go @@ -3,6 +3,8 @@ package files import ( "os" "unicode/utf8" + + "github.com/spf13/afero" ) func isBinary(content []byte) bool { @@ -57,3 +59,16 @@ func IsNamedPipe(mode os.FileMode) bool { func IsSymlink(mode os.FileMode) bool { return mode&os.ModeSymlink != 0 } + +func GetRealPath(fs afero.Fs, path string) string { + if realPathFs, ok := fs.(interface { + RealPath(name string) (fPath string, err error) + }); ok { + realPath, err := realPathFs.RealPath(path) + if err == nil { + return realPath + } + } + + return path +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ce3dcc31..a91166ec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0", "dependencies": { "@chenfengyuan/vue-number-input": "^2.0.1", + "@onlyoffice/document-editor-vue": "^1.4.0", "@vueuse/core": "^10.9.0", "@vueuse/integrations": "^10.9.0", "ace-builds": "^1.32.9", @@ -2506,6 +2507,14 @@ "node": ">= 8" } }, + "node_modules/@onlyoffice/document-editor-vue": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@onlyoffice/document-editor-vue/-/document-editor-vue-1.4.0.tgz", + "integrity": "sha512-Fg5gSc1zF6bmpRapUd7rMpm7kEDF7mQIHQKfcsfJcILdFX9bwIhnkXEucETEA9zdt92nWMS6qiAgVeT61TdCyw==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index a5089c94..47aac7ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@chenfengyuan/vue-number-input": "^2.0.1", + "@onlyoffice/document-editor-vue": "^1.4.0", "@vueuse/core": "^10.9.0", "@vueuse/integrations": "^10.9.0", "ace-builds": "^1.32.9", diff --git a/frontend/src/types/file.d.ts b/frontend/src/types/file.d.ts index db2aa5fe..e0ffe007 100644 --- a/frontend/src/types/file.d.ts +++ b/frontend/src/types/file.d.ts @@ -35,7 +35,8 @@ type ResourceType = | "pdf" | "text" | "blob" - | "textImmutable"; + | "textImmutable" + | "officedocument"; type DownloadFormat = | "zip" diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts index a2c19f76..2eedd064 100644 --- a/frontend/src/types/settings.d.ts +++ b/frontend/src/types/settings.d.ts @@ -8,6 +8,7 @@ interface ISettings { tus: SettingsTus; shell: string[]; commands: SettingsCommand; + onlyoffice: SettingsOnlyOffice; } interface SettingsDefaults { @@ -55,3 +56,8 @@ interface SettingsUnit { GB: number; TB: number; } + +interface SettingsOnlyOffice { + url: string; + jwtSecret: string; +} diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index c437b0d2..59c1db41 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -18,7 +18,7 @@ const enableExec: boolean = window.FileBrowser.EnableExec; const tusSettings = window.FileBrowser.TusSettings; const origin = window.location.origin; const tusEndpoint = `/api/tus`; -const onlyOffice = window.FileBrowser.OnlyOffice; +const onlyOfficeUrl = window.FileBrowser.OnlyOfficeUrl; export { name, @@ -40,5 +40,5 @@ export { tusSettings, origin, tusEndpoint, - onlyOffice, + onlyOfficeUrl, }; diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue index ea6196d2..89314ede 100644 --- a/frontend/src/views/Files.vue +++ b/frontend/src/views/Files.vue @@ -37,7 +37,7 @@ import { storeToRefs } from "pinia"; import { useFileStore } from "@/stores/file"; import { useLayoutStore } from "@/stores/layout"; import { useUploadStore } from "@/stores/upload"; -import { onlyOffice } from "@/utils/constants"; +import { onlyOfficeUrl } from "@/utils/constants"; import HeaderBar from "@/components/header/HeaderBar.vue"; import Breadcrumbs from "@/components/Breadcrumbs.vue"; @@ -48,7 +48,9 @@ import FileListing from "@/views/files/FileListing.vue"; import { StatusError } from "@/api/utils"; const Editor = defineAsyncComponent(() => import("@/views/files/Editor.vue")); const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue")); -const OnlyOfficeEditor = defineAsyncComponent(() => import("@/views/files/OnlyOfficeEditor.vue")); +const OnlyOfficeEditor = defineAsyncComponent( + () => import("@/views/files/OnlyOfficeEditor.vue") +); const layoutStore = useLayoutStore(); const fileStore = useFileStore(); @@ -79,7 +81,7 @@ const currentView = computed(() => { fileStore.req.type === "textImmutable" ) { return Editor; - } else if (fileStore.req.type === "officedocument" && onlyOffice !== "") { + } else if (fileStore.req.type === "officedocument" && onlyOfficeUrl) { return OnlyOfficeEditor; } else { return Preview; diff --git a/frontend/src/views/files/OnlyOfficeEditor.vue b/frontend/src/views/files/OnlyOfficeEditor.vue index bb76dce8..2758694b 100644 --- a/frontend/src/views/files/OnlyOfficeEditor.vue +++ b/frontend/src/views/files/OnlyOfficeEditor.vue @@ -2,169 +2,57 @@
- {{ req.name }} + {{ fileStore.req?.name ?? "" }} - - -
+ +
+ +
- - - diff --git a/go.mod b/go.mod index 18bd5895..58f3d466 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( ) require ( + github.com/allegro/bigcache v1.2.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/asticode/go-astikit v0.42.0 // indirect github.com/asticode/go-astits v1.13.0 // indirect diff --git a/go.sum b/go.sum index 8287feff..ed43e6b9 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= +github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= diff --git a/http/auth.go b/http/auth.go index 23dc7b77..1f02ebfc 100644 --- a/http/auth.go +++ b/http/auth.go @@ -84,6 +84,7 @@ func withUser(fn handleFunc) handleFunc { w.Header().Add("X-Renew-Token", "true") } + d.authToken = token.Raw d.user, err = d.store.Users.Get(d.server.Root, tk.User.ID) if err != nil { return http.StatusInternalServerError, err diff --git a/http/data.go b/http/data.go index 5ba87313..2c86fa46 100644 --- a/http/data.go +++ b/http/data.go @@ -18,11 +18,12 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, erro type data struct { *runner.Runner - settings *settings.Settings - server *settings.Server - store *storage.Storage - user *users.User - raw interface{} + authToken string + settings *settings.Settings + server *settings.Server + store *storage.Storage + user *users.User + raw interface{} } // Check implements rules.Checker. diff --git a/http/http.go b/http/http.go index 1da832bd..3b2cd488 100644 --- a/http/http.go +++ b/http/http.go @@ -60,6 +60,7 @@ func NewHandler( users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET") users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE") + api.PathPrefix("/onlyoffice").Handler(monkey(onlyofficeClientConfigGetHandler, "/api/onlyoffice/client-config")).Methods("GET") api.PathPrefix("/onlyoffice").Handler(monkey(onlyofficeCallbackHandler, "/api/onlyoffice/callback")).Methods("POST") api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET") diff --git a/http/onlyoffice.go b/http/onlyoffice.go index 55d6416e..ad509949 100644 --- a/http/onlyoffice.go +++ b/http/onlyoffice.go @@ -1,21 +1,125 @@ package http import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "io" "net/http" + "net/url" + "strconv" + "time" + + "github.com/allegro/bigcache" + "github.com/golang-jwt/jwt/v4" + + "github.com/filebrowser/filebrowser/v2/files" +) + +const ( + onlyOfficeStatusDocumentClosedWithChanges = 2 + onlyOfficeStatusDocumentClosedWithNoChanges = 4 + onlyOfficeStatusForceSaveWhileDocumentStillOpen = 6 + trueString = "true" // linter-enforced constant + twoDays = 48 * time.Hour // linter enforced constant +) + +var ( + // Refer to only-office documentation on co-editing + // https://api.onlyoffice.com/editors/coedit + // + // a 48 hour TTL here is not required, because the document server will notify + // us when keys should be evicted. However, it is added defensively in order to + // prevent potential memory leaks. + coeditingDocumentKeys, _ = bigcache.NewBigCache(bigcache.DefaultConfig(twoDays)) ) type OnlyOfficeCallback struct { ChangesURL string `json:"changesurl,omitempty"` - Key string `json:"key"` - Status int `json:"status"` + Key string `json:"key,omitempty"` + Status int `json:"status,omitempty"` URL string `json:"url,omitempty"` Users []string `json:"users,omitempty"` UserData string `json:"userdata,omitempty"` } +var onlyofficeClientConfigGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if d.settings.OnlyOffice.JWTSecret == "" { + return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings") + } + + if !d.user.Perm.Modify || !d.Check(r.URL.Path) { + return http.StatusForbidden, nil + } + + referrer, err := getReferer(r) + if err != nil { + return http.StatusInternalServerError, errors.Join(errors.New("could not determine request referrer"), err) + } + + file, err := files.NewFileInfo(&files.FileOptions{ + Fs: d.user.Fs, + Path: r.URL.Path, + Modify: d.user.Perm.Modify, + Expand: false, + ReadHeader: d.server.TypeDetectionByHeader, + Checker: d, + }) + + if err != nil { + return errToStatus(err), err + } + + clientConfig := map[string]interface{}{ + "document": map[string]interface{}{ + "fileType": file.Extension[1:], + "key": getDocumentKey(file.RealPath()), + "title": file.Name, + "url": (&url.URL{ + Scheme: referrer.Scheme, + Host: referrer.Host, + RawQuery: "auth=" + url.QueryEscape(d.authToken), + }).JoinPath(d.server.BaseURL, "/api/raw", file.Path).String(), + "permissions": map[string]interface{}{ + "edit": d.user.Perm.Modify, + "download": d.user.Perm.Download, + "print": d.user.Perm.Download, + }, + }, + "editorConfig": map[string]interface{}{ + "callbackUrl": (&url.URL{ + Scheme: referrer.Scheme, + Host: referrer.Host, + RawQuery: "auth=" + url.QueryEscape(d.authToken) + "&save=" + url.QueryEscape(file.Path), + }).JoinPath(d.server.BaseURL, "/api/onlyoffice/callback").String(), + "user": map[string]interface{}{ + "id": strconv.FormatUint(uint64(d.user.ID), 10), + "name": d.user.Username, + }, + "customization": map[string]interface{}{ + "autosave": true, + "forcesave": true, + "uiTheme": ternary(d.Settings.Branding.Theme == "dark", "default-dark", "default-light"), + }, + "lang": d.user.Locale, + "mode": ternary(d.user.Perm.Modify, "edit", "view"), + }, + "type": ternary(r.URL.Query().Get("isMobile") == trueString, "mobile", "desktop"), + } + + signature, err := jwt. + NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(clientConfig)). + SignedString([]byte(d.Settings.OnlyOffice.JWTSecret)) + + if err != nil { + return http.StatusInternalServerError, errors.Join(errors.New("could not sign only-office client-config"), err) + } + clientConfig["token"] = signature + + return renderJSON(w, r, clientConfig) +}) + var onlyofficeCallbackHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { body, err := io.ReadAll(r.Body) if err != nil { @@ -28,12 +132,25 @@ var onlyofficeCallbackHandler = withUser(func(w http.ResponseWriter, r *http.Req return http.StatusInternalServerError, err } - if data.Status == 2 || data.Status == 6 { - docPath := r.URL.Query().Get("save") - if docPath == "" { - return http.StatusInternalServerError, errors.New("unable to get file save path") - } + docPath := r.URL.Query().Get("save") + if docPath == "" { + return http.StatusInternalServerError, errors.New("unable to get file save path") + } + if data.Status == onlyOfficeStatusDocumentClosedWithChanges || + data.Status == onlyOfficeStatusDocumentClosedWithNoChanges { + // Refer to only-office documentation + // - https://api.onlyoffice.com/editors/coedit + // - https://api.onlyoffice.com/editors/callback + // + // When the document is fully closed by all editors, + // then the document key should no longer be re-used. + realPath := files.GetRealPath(d.user.Fs, docPath) + _ = coeditingDocumentKeys.Delete(realPath) + } + + if data.Status == onlyOfficeStatusDocumentClosedWithChanges || + data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen { if !d.user.Perm.Modify || !d.Check(docPath) { return http.StatusForbidden, nil } @@ -44,7 +161,7 @@ var onlyofficeCallbackHandler = withUser(func(w http.ResponseWriter, r *http.Req } defer doc.Body.Close() - err = d.RunHook(func() error { + err = d.Runner.RunHook(func() error { _, writeErr := writeFile(d.user.Fs, docPath, doc.Body) if writeErr != nil { return writeErr @@ -62,3 +179,38 @@ var onlyofficeCallbackHandler = withUser(func(w http.ResponseWriter, r *http.Req } return renderJSON(w, r, resp) }) + +func getReferer(r *http.Request) (*url.URL, error) { + if len(r.Header["Referer"]) != 1 { + return nil, errors.New("expected exactly one Referer header") + } + + return url.ParseRequestURI(r.Header["Referer"][0]) +} + +func getDocumentKey(realPath string) string { + // error is intentionally ignored in order treat errors + // the same as a cache-miss + cachedDocumentKey, _ := coeditingDocumentKeys.Get(realPath) + + if cachedDocumentKey != nil { + return string(cachedDocumentKey) + } + + timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) + documentKey := hashSHA256(realPath + timestamp) + _ = coeditingDocumentKeys.Set(realPath, []byte(documentKey)) + return documentKey +} + +func hashSHA256(data string) string { + bytes := sha256.Sum256([]byte(data)) + return hex.EncodeToString(bytes[:]) +} + +func ternary(condition bool, trueValue, falseValue string) string { + if condition { + return trueValue + } + return falseValue +} diff --git a/http/static.go b/http/static.go index d8f19c4c..54b7f565 100644 --- a/http/static.go +++ b/http/static.go @@ -46,7 +46,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys "ResizePreview": d.server.ResizePreview, "EnableExec": d.server.EnableExec, "TusSettings": d.settings.Tus, - "OnlyOffice": d.settings.OnlyOffice, + "OnlyOfficeUrl": d.settings.OnlyOffice.URL, } if d.settings.Branding.Files != "" { diff --git a/test.docx b/test.docx deleted file mode 100644 index adf9a8c004ab9f4965602b5c8cf748e335541a6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24698 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