diff --git a/client/src/components/files.vue b/client/src/components/files.vue index e8c3005e..b5f263f9 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -9,7 +9,7 @@

{{ item.name }}

{{ fileSize(item.size) }}

- +
@@ -73,6 +73,7 @@
{ ///////////////////////////// // File Transfer Events ///////////////////////////// - protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) { + protected [EVENT.FILETRANSFER.LIST]({ cwd, user_download, user_upload, files }: FileTransferListPayload) { this.$accessor.files.setCwd(cwd) this.$accessor.files.setFileList(files) + this.$accessor.files.setUserDownload(user_download) + this.$accessor.files.setUserUpload(user_upload) } ///////////////////////////// diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index ac12ef23..1c3546d4 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -204,6 +204,8 @@ export interface FileTransferListMessage extends WebSocketMessage, FileTransferL export interface FileTransferListPayload { cwd: string + user_download: boolean + user_upload: boolean files: FileListItem[] } diff --git a/client/src/store/files.ts b/client/src/store/files.ts index 1a4ca842..c45a4ac4 100644 --- a/client/src/store/files.ts +++ b/client/src/store/files.ts @@ -7,6 +7,8 @@ export const state = () => ({ cwd: '', files: [] as FileListItem[], transfers: [] as FileTransfer[], + userDownload: false, + userUpload: false, }) export const getters = getterTree(state, { @@ -22,6 +24,14 @@ export const mutations = mutationTree(state, { state.files = files }, + _setUserDownload(state, val: boolean) { + state.userDownload = val + }, + + _setUserUpload(state, val: boolean) { + state.userUpload = val + }, + _addTransfer(state, transfer: FileTransfer) { state.transfers = [...state.transfers, transfer] }, @@ -42,6 +52,14 @@ export const actions = actionTree( accessor.files._setFileList(files) }, + setUserDownload(store, val: boolean) { + accessor.files._setUserDownload(val) + }, + + setUserUpload(store, val: boolean) { + accessor.files._setUserUpload(val) + }, + addTransfer(store, transfer: FileTransfer) { if (transfer.status !== 'pending') { return diff --git a/server/internal/http/legacy/message/messages.go b/server/internal/http/legacy/message/messages.go index 89cf4e4c..17d527bd 100644 --- a/server/internal/http/legacy/message/messages.go +++ b/server/internal/http/legacy/message/messages.go @@ -109,9 +109,11 @@ type EmoteSend struct { } type FileTransferList struct { - Event string `json:"event"` - Cwd string `json:"cwd"` - Files []types.FileListItem `json:"files"` + Event string `json:"event"` + Cwd string `json:"cwd"` + UserDownload bool `json:"user_download"` + UserUpload bool `json:"user_upload"` + Files []types.FileListItem `json:"files"` } type Admin struct { diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go index 8304acfe..d5609d6c 100644 --- a/server/internal/http/legacy/wstoclient.go +++ b/server/internal/http/legacy/wstoclient.go @@ -607,9 +607,11 @@ func (s *session) wsToClient(msg []byte) error { } return s.toClient(&oldMessage.FileTransferList{ - Event: oldEvent.FILETRANSFER_LIST, - Cwd: request.RootDir, - Files: files, + Event: oldEvent.FILETRANSFER_LIST, + Cwd: request.RootDir, + UserDownload: request.UserDownload, + UserUpload: request.UserUpload, + Files: files, }) // Screen Events diff --git a/server/internal/plugins/filetransfer/config.go b/server/internal/plugins/filetransfer/config.go index 04ed8109..b84f8b51 100644 --- a/server/internal/plugins/filetransfer/config.go +++ b/server/internal/plugins/filetransfer/config.go @@ -12,6 +12,8 @@ type Config struct { Enabled bool RootDir string RefreshInterval time.Duration + UserDownload bool + UserUpload bool } func (Config) Init(cmd *cobra.Command) error { @@ -30,6 +32,16 @@ func (Config) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("filetransfer.user_download", false, "allow non-admin users to download files") + if err := viper.BindPFlag("filetransfer.user_download", cmd.PersistentFlags().Lookup("filetransfer.user_download")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("filetransfer.user_upload", false, "allow non-admin users to upload files") + if err := viper.BindPFlag("filetransfer.user_upload", cmd.PersistentFlags().Lookup("filetransfer.user_upload")); err != nil { + return err + } + // v2 config cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") @@ -50,6 +62,8 @@ func (s *Config) Set() { rootDir := viper.GetString("filetransfer.dir") s.RootDir = filepath.Clean(rootDir) s.RefreshInterval = viper.GetDuration("filetransfer.refresh_interval") + s.UserDownload = viper.GetBool("filetransfer.user_download") + s.UserUpload = viper.GetBool("filetransfer.user_upload") // v2 config diff --git a/server/internal/plugins/filetransfer/manager.go b/server/internal/plugins/filetransfer/manager.go index e805b35a..28e2919a 100644 --- a/server/internal/plugins/filetransfer/manager.go +++ b/server/internal/plugins/filetransfer/manager.go @@ -103,9 +103,11 @@ func (m *Manager) broadcastUpdate() { m.mu.RUnlock() m.sessions.Broadcast(FILETRANSFER_UPDATE, Message{ - Enabled: m.config.Enabled, - RootDir: m.config.RootDir, - Files: fileList, + Enabled: m.config.Enabled, + RootDir: m.config.RootDir, + UserDownload: m.config.UserDownload, + UserUpload: m.config.UserUpload, + Files: fileList, }) } @@ -115,9 +117,11 @@ func (m *Manager) sendUpdate(session types.Session) { m.mu.RUnlock() session.Send(FILETRANSFER_UPDATE, Message{ - Enabled: m.config.Enabled, - RootDir: m.config.RootDir, - Files: fileList, + Enabled: m.config.Enabled, + RootDir: m.config.RootDir, + UserDownload: m.config.UserDownload, + UserUpload: m.config.UserUpload, + Files: fileList, }) } @@ -208,8 +212,8 @@ func (m *Manager) Shutdown() error { } func (m *Manager) Route(r types.Router) { - r.With(auth.AdminsOnly).Get("/", m.downloadFileHandler) - r.With(auth.AdminsOnly).Post("/", m.uploadFileHandler) + r.Get("/", m.downloadFileHandler) + r.Post("/", m.uploadFileHandler) } func (m *Manager) WebSocketHandler(session types.Session, msg types.WebSocketMessage) bool { @@ -251,6 +255,10 @@ func (m *Manager) downloadFileHandler(w http.ResponseWriter, r *http.Request) er return utils.HttpForbidden("file transfer is disabled") } + if !session.Profile().IsAdmin && !m.config.UserDownload { + return utils.HttpForbidden("file download is not allowed for non-admin users") + } + filename := r.URL.Query().Get("filename") badChars, err := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename) if filename == "" || badChars || err != nil { @@ -285,6 +293,10 @@ func (m *Manager) uploadFileHandler(w http.ResponseWriter, r *http.Request) erro return utils.HttpForbidden("file transfer is disabled") } + if !session.Profile().IsAdmin && !m.config.UserUpload { + return utils.HttpForbidden("file upload is not allowed for non-admin users") + } + err = r.ParseMultipartForm(multipartFormMaxMemory) if err != nil || r.MultipartForm == nil { return utils.HttpBadRequest(). diff --git a/server/internal/plugins/filetransfer/types.go b/server/internal/plugins/filetransfer/types.go index 32748d46..dcb6ec50 100644 --- a/server/internal/plugins/filetransfer/types.go +++ b/server/internal/plugins/filetransfer/types.go @@ -11,9 +11,11 @@ const ( ) type Message struct { - Enabled bool `json:"enabled"` - RootDir string `json:"root_dir"` - Files []Item `json:"files"` + Enabled bool `json:"enabled"` + RootDir string `json:"root_dir"` + UserDownload bool `json:"user_download"` + UserUpload bool `json:"user_upload"` + Files []Item `json:"files"` } type ItemType string diff --git a/webpage/docs/configuration/plugins.md b/webpage/docs/configuration/plugins.md index 5f6d5e38..b511f21b 100644 --- a/webpage/docs/configuration/plugins.md +++ b/webpage/docs/configuration/plugins.md @@ -59,11 +59,15 @@ The file transfer plugin is a simple pre-loaded internal plugin that allows you type: 'duration', defaultValue: '30s', }, + 'filetransfer.user_download': false, + 'filetransfer.user_upload': false, }} /> - enables the file transfer support. If set to `false`, the file transfer is disabled. - refers to the directory where the files are stored. - refers to the interval at which the file list is refreshed. +- allows non-admin users to download files. If set to `false` (default), only admins can download files. +- allows non-admin users to upload files. If set to `false` (default), only admins can upload files. The file transfer plugin extends user profile and room settings by adding the following fields: