From fa45619dbba3f63d4c61b74f428023b088055513 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Sun, 16 Oct 2022 20:54:23 -0400 Subject: [PATCH 01/13] added basic UI for file transfer --- client/src/components/files.vue | 194 ++++++++++++++++++++++++++++++++ client/src/components/side.vue | 7 ++ client/src/locale/en-us.ts | 1 + 3 files changed, 202 insertions(+) create mode 100644 client/src/components/files.vue diff --git a/client/src/components/files.vue b/client/src/components/files.vue new file mode 100644 index 0000000..7e81fb9 --- /dev/null +++ b/client/src/components/files.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/client/src/components/side.vue b/client/src/components/side.vue index db1ff81..2122e78 100644 --- a/client/src/components/side.vue +++ b/client/src/components/side.vue @@ -6,6 +6,10 @@ {{ $t('side.chat') }} +
  • + + {{ $t('side.files') }} +
  • {{ $t('side.settings') }} @@ -14,6 +18,7 @@
    +
    @@ -78,12 +83,14 @@ import Settings from '~/components/settings.vue' import Chat from '~/components/chat.vue' + import Files from '~/components/files.vue' @Component({ name: 'neko', components: { 'neko-settings': Settings, 'neko-chat': Chat, + 'neko-files': Files }, }) export default class extends Vue { diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index 91d98d9..b397e2d 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -7,6 +7,7 @@ export const send_a_message = 'Send a message' export const side = { chat: 'Chat', + files: 'Files', settings: 'Settings', } From 1505abb70375eac4130aabbae498aa07ebc0612c Mon Sep 17 00:00:00 2001 From: William Harrell Date: Sun, 30 Oct 2022 21:06:05 -0400 Subject: [PATCH 02/13] http endpoints for transferring files --- server/internal/config/websocket.go | 23 ++++++ server/internal/http/http.go | 97 ++++++++++++++++++++++++++ server/internal/types/websocket.go | 2 + server/internal/websocket/websocket.go | 25 +++++++ 4 files changed, 147 insertions(+) diff --git a/server/internal/config/websocket.go b/server/internal/config/websocket.go index 9105018..653411e 100644 --- a/server/internal/config/websocket.go +++ b/server/internal/config/websocket.go @@ -12,6 +12,10 @@ type WebSocket struct { Locks []string ControlProtection bool + + FileTransfer bool + UnprivFileTransfer bool + FileTransferPath string } func (WebSocket) Init(cmd *cobra.Command) error { @@ -40,6 +44,21 @@ func (WebSocket) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("file_transfer", false, "allow file transfer for admins") + if err := viper.BindPFlag("file_transfer", cmd.PersistentFlags().Lookup("file_transfer")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("unpriv_file_transfer", false, "allow file transfer for non admins") + if err := viper.BindPFlag("unpriv_file_transfer", cmd.PersistentFlags().Lookup("unpriv_file_transfer")); err != nil { + return err + } + + cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer") + if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil { + return err + } + return nil } @@ -50,4 +69,8 @@ func (s *WebSocket) Set() { s.Locks = viper.GetStringSlice("locks") s.ControlProtection = viper.GetBool("control_protection") + + s.FileTransfer = viper.GetBool("file_transfer") + s.UnprivFileTransfer = viper.GetBool("unpriv_file_transfer") + s.FileTransferPath = viper.GetString("file_transfer_path") } diff --git a/server/internal/http/http.go b/server/internal/http/http.go index 06b0863..4b8989c 100644 --- a/server/internal/http/http.go +++ b/server/internal/http/http.go @@ -3,9 +3,11 @@ package http import ( "context" "encoding/json" + "fmt" "image/jpeg" "net/http" "os" + "regexp" "strconv" "github.com/go-chi/chi" @@ -17,6 +19,8 @@ import ( "m1k1o/neko/internal/types" ) +const FILE_UPLOAD_BUF_SIZE = 65000 + type Server struct { logger zerolog.Logger router *chi.Mux @@ -99,6 +103,99 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t } }) + router.Get("/file", func(w http.ResponseWriter, r *http.Request) { + password := r.URL.Query().Get("pwd") + isAuthorized, err := webSocketHandler.CanTransferFiles(password) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + if !isAuthorized { + http.Error(w, "bad authorization", http.StatusUnauthorized) + return + } + + filename := r.URL.Query().Get("filename") + badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename) + if filename == "" || badChars { + http.Error(w, "bad filename", http.StatusBadRequest) + return + } + + path := webSocketHandler.MakeFilePath(filename) + f, err := os.Open(path) + if err != nil { + http.Error(w, "not found or unable to open", http.StatusNotFound) + return + } + defer f.Close() + fileinfo, err := f.Stat() + if err != nil { + http.Error(w, "unable to stat file", http.StatusInternalServerError) + return + } + + buffer := make([]byte, fileinfo.Size()) + _, err = f.Read(buffer) + if err != nil { + http.Error(w, "error reading file", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + w.Write(buffer) + }) + + router.Post("/file", func(w http.ResponseWriter, r *http.Request) { + password := r.URL.Query().Get("pwd") + isAuthorized, err := webSocketHandler.CanTransferFiles(password) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + if !isAuthorized { + http.Error(w, "bad authorization", http.StatusUnauthorized) + return + } + + r.ParseMultipartForm(32 << 20) + buffer := make([]byte, FILE_UPLOAD_BUF_SIZE) + for _, formheader := range r.MultipartForm.File["files"] { + formfile, err := formheader.Open() + if err != nil { + logger.Warn().Err(err).Msg("failed to open formdata file") + http.Error(w, "error writing file", http.StatusInternalServerError) + return + } + f, err := os.OpenFile(webSocketHandler.MakeFilePath(formheader.Filename), os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + http.Error(w, "unable to open file for writing", http.StatusInternalServerError) + return + } + + var copied int64 = 0 + for copied < formheader.Size { + var limit int64 = int64(len(buffer)) + if limit > formheader.Size-copied { + limit = formheader.Size - copied + } + bytesRead, err := formfile.ReadAt(buffer[:limit], copied) + if err != nil { + logger.Warn().Err(err).Msg("failed copying file in upload") + http.Error(w, "error writing file", http.StatusInternalServerError) + return + } + f.Write(buffer[:bytesRead]) + copied += int64(bytesRead) + } + + formfile.Close() + f.Close() + } + }) + router.Get("/health", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("true")) }) diff --git a/server/internal/types/websocket.go b/server/internal/types/websocket.go index 968bbb8..374d4f9 100644 --- a/server/internal/types/websocket.go +++ b/server/internal/types/websocket.go @@ -34,4 +34,6 @@ type WebSocketHandler interface { Stats() Stats IsLocked(resource string) bool IsAdmin(password string) (bool, error) + CanTransferFiles(password string) (bool, error) + MakeFilePath(filename string) string } diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index a4ec572..00e8eda 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -3,6 +3,7 @@ package websocket import ( "fmt" "net/http" + "os" "sync" "sync/atomic" "time" @@ -33,6 +34,14 @@ func New(sessions types.SessionManager, desktop types.DesktopManager, capture ty logger.Info().Msgf("control locked on behalf of control protection") } + if conf.FileTransferPath[len(conf.FileTransferPath)-1] != '/' { + conf.FileTransferPath += "/" + } + err := os.Mkdir(conf.FileTransferPath, 0755) + if err != nil && !os.IsExist(err) { + logger.Panic().Err(err).Msg("unable to create file transfer directory") + } + // apply default locks for _, lock := range conf.Locks { state.Lock(lock, "") // empty session ID @@ -314,6 +323,22 @@ func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) { return false, fmt.Errorf("invalid password") } +func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { + if !ws.conf.FileTransfer { + return false, nil + } + + if !ws.conf.UnprivFileTransfer { + return ws.IsAdmin(password) + } + + return password == ws.conf.Password, nil +} + +func (ws *WebSocketHandler) MakeFilePath(filename string) string { + return fmt.Sprintf("%s%s", ws.conf.FileTransferPath, filename) +} + func (ws *WebSocketHandler) authenticate(r *http.Request) (bool, error) { passwords, ok := r.URL.Query()["password"] if !ok || len(passwords[0]) < 1 { From 70e84c584075b8d2964330cdf10d94fac0455256 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Wed, 2 Nov 2022 22:20:32 -0400 Subject: [PATCH 03/13] listing of files on connect --- client/src/components/files.vue | 32 ++++++--------------- client/src/neko/events.ts | 16 +++++++++++ client/src/neko/index.ts | 9 ++++++ client/src/neko/messages.ts | 17 ++++++++++- client/src/neko/types.ts | 5 ++++ client/src/store/files.ts | 35 +++++++++++++++++++++++ client/src/store/index.ts | 3 +- server/internal/types/event/events.go | 8 ++++++ server/internal/types/message/messages.go | 11 +++++++ server/internal/types/websocket.go | 5 ++++ server/internal/utils/files.go | 30 +++++++++++++++++++ server/internal/websocket/websocket.go | 15 ++++++++++ 12 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 client/src/store/files.ts create mode 100644 server/internal/utils/files.go diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 7e81fb9..d663b31 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -8,7 +8,8 @@

    {{ item.name }}

    - +
    @@ -111,7 +112,6 @@ From 57e89bb1cc77168aac440191c1274f1ca69da9f9 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Tue, 15 Nov 2022 20:39:06 -0500 Subject: [PATCH 09/13] file transfer permission state management --- client/src/components/files.vue | 20 +++++++ client/src/components/settings.vue | 30 ++++++++++ client/src/components/side.vue | 16 +++++- client/src/neko/events.ts | 10 +--- client/src/neko/index.ts | 7 ++- client/src/neko/messages.ts | 14 ++++- client/src/store/files.ts | 9 +++ client/src/store/settings.ts | 28 ++++++++++ server/internal/types/event/events.go | 9 +-- server/internal/types/message/messages.go | 7 ++- server/internal/websocket/handler/files.go | 30 ++++++++++ server/internal/websocket/handler/handler.go | 6 ++ server/internal/websocket/state/state.go | 5 ++ server/internal/websocket/websocket.go | 58 ++++++++++++-------- 14 files changed, 208 insertions(+), 41 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index bfae9cf..cc4c7b9 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -131,6 +131,26 @@ max-height: 50vh; overflow-y: scroll; overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: $background-tertiary transparent; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $background-tertiary; + border: 2px solid $background-primary; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background-color: $background-floating; + } } .transfers > p { diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index e6c991b..7b02b83 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -44,6 +44,20 @@
  • +
  • + File transfer + +
  • +
  • + Non-admin file transfer + +
  • {{ $t('setting.broadcast_title') }} @@ -366,6 +380,22 @@ return this.$accessor.settings.keyboard_layout } + get file_transfer() { + return this.$accessor.settings.file_transfer + } + + set file_transfer(value: boolean) { + this.$accessor.settings.setGlobalFileTransferStatus({ admin: value, unpriv: false }) + } + + get unpriv_file_transfer() { + return this.$accessor.settings.unpriv_file_transfer + } + + set unpriv_file_transfer(value: boolean) { + this.$accessor.settings.setGlobalFileTransferStatus({ admin: this.file_transfer, unpriv: value }) + } + get broadcast_is_active() { return this.$accessor.settings.broadcast_is_active } diff --git a/client/src/components/side.vue b/client/src/components/side.vue index 2122e78..282065a 100644 --- a/client/src/components/side.vue +++ b/client/src/components/side.vue @@ -6,7 +6,7 @@ {{ $t('side.chat') }}
  • -
  • +
  • {{ $t('side.files') }}
  • @@ -94,6 +94,20 @@ }, }) export default class extends Vue { + + constructor() { + super() + if (this.tab === 'files' && (!this.$accessor.settings.file_transfer || + !this.$accessor.user.admin && this.$accessor.settings.unpriv_file_transfer)) { + this.change('chat') + } + } + + get filetransferAllowed() { + return this.$accessor.user.admin && this.$accessor.settings.file_transfer || + this.$accessor.settings.unpriv_file_transfer + } + get tab() { return this.$accessor.client.tab } diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index d12046a..d73c185 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -39,10 +39,7 @@ export const EVENT = { EMOTE: 'chat/emote', }, FILETRANSFER: { - ENABLE: 'filetransfer/enable', - DISABLE: 'filetransfer/disable', - UNPRIVENABLE: 'filetransfer/unprivenable', - UNPRIVDISABLE: 'filetransfer/unprivdisable', + STATUS: 'filetransfer/status', LIST: 'filetransfer/list', REFRESH: 'filetransfer/refresh' }, @@ -102,10 +99,7 @@ export type SignalEvents = export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE export type FileTransferEvents = - | typeof EVENT.FILETRANSFER.ENABLE - | typeof EVENT.FILETRANSFER.DISABLE - | typeof EVENT.FILETRANSFER.UNPRIVENABLE - | typeof EVENT.FILETRANSFER.UNPRIVDISABLE + | typeof EVENT.FILETRANSFER.STATUS | typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index 15033db..dc4b5ee 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -25,6 +25,7 @@ import { SystemInitPayload, AdminLockResource, FileTransferListPayload, + FileTransferStatusPayload, } from './messages' interface NekoEvents extends BaseEvents {} @@ -361,8 +362,12 @@ export class NekoClient extends BaseClient implements EventEmitter { } ///////////////////////////// - // Chat Events + // Filetransfer Events ///////////////////////////// + protected [EVENT.FILETRANSFER.STATUS]({ admin, unpriv }: FileTransferStatusPayload) { + this.$accessor.settings.setLocalFileTransferStatus({ admin, unpriv }) + } + protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) { this.$accessor.files.setCwd(cwd) this.$accessor.files.setFileList(files) diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index feb6073..b71d73d 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -44,6 +44,7 @@ export type WebSocketPayloads = | ChatPayload | ChatSendPayload | EmojiSendPayload + | FileTransferStatusPayload | ScreenResolutionPayload | ScreenConfigurationsPayload | AdminPayload @@ -198,8 +199,17 @@ export interface EmojiSendPayload { emote: string } -// file transfer -export interface FileTransferMessage extends WebSocketMessage, FileTransferListPayload { +// file transfer enabled +export interface FileTransferStatusMessage extends WebSocketMessage, FileTransferStatusPayload { + event: typeof EVENT.FILETRANSFER.STATUS +} +export interface FileTransferStatusPayload { + admin: boolean, + unpriv: boolean +} + +// file transfer list +export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload { event: FileTransferEvents } export interface FileTransferListPayload { diff --git a/client/src/store/files.ts b/client/src/store/files.ts index 5eb2c1f..c4122af 100644 --- a/client/src/store/files.ts +++ b/client/src/store/files.ts @@ -52,6 +52,15 @@ export const actions = actionTree( accessor.files._removeTransfer(transfer) }, + cancelAllTransfers(store) { + for (const t of accessor.files.transfers) { + if (t.status !== 'completed') { + t.abortController?.abort() + } + accessor.files.removeTransfer(t) + } + }, + refresh(store) { if (!accessor.connected) { return diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 3d99fad..f68a468 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -20,6 +20,9 @@ export const state = () => { keyboard_layouts_list: {} as KeyboardLayouts, + file_transfer: false, + unpriv_file_transfer: false, + broadcast_is_active: false, broadcast_url: '', } @@ -58,6 +61,14 @@ export const mutations = mutationTree(state, { set('keyboard_layout', value) }, + setFileTransfer(state, value: boolean) { + state.file_transfer = value + }, + + setUnprivFileTransfer(state, value: boolean) { + state.unpriv_file_transfer = value + }, + setKeyboardLayoutsList(state, value: KeyboardLayouts) { state.keyboard_layouts_list = value }, @@ -79,6 +90,23 @@ export const actions = actionTree( } }, + setLocalFileTransferStatus({ getters }, { admin, unpriv }) { + accessor.settings.setFileTransfer(admin) + accessor.settings.setUnprivFileTransfer(unpriv) + + if (!admin || !accessor.user.admin && !unpriv) { + accessor.files.cancelAllTransfers() + } + + if (accessor.client.tab === 'files' && !unpriv) { + accessor.client.setTab('chat') + } + }, + + setGlobalFileTransferStatus({ getters}, { admin, unpriv }) { + $client.sendMessage(EVENT.FILETRANSFER.STATUS, { admin, unpriv }) + }, + broadcastStatus({ getters }, { url, isActive }) { accessor.settings.setBroadcastStatus({ url, isActive }) }, diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go index 8c5e12f..4b6b047 100644 --- a/server/internal/types/event/events.go +++ b/server/internal/types/event/events.go @@ -35,12 +35,9 @@ const ( ) const ( - FILETRANSFER_ENABLE = "filetransfer/enable" - FILETRANSFER_DISABLE = "filetransfer/disable" - FILETRANSFER_UNPRIVENABLE = "filetransfer/unprivenable" - FILETRANSFER_UNPRIVDISABLE = "filetransfer/unprivdisable" - FILETRANSFER_LIST = "filetransfer/list" - FILETRANSFER_REFRESH = "filetransfer/refresh" + FILETRANSFER_STATUS = "filetransfer/status" + FILETRANSFER_LIST = "filetransfer/list" + FILETRANSFER_REFRESH = "filetransfer/refresh" ) const ( diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go index 2364360..27a83d6 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/types/message/messages.go @@ -108,7 +108,12 @@ type EmoteSend struct { type FileTransferTarget struct { Event string `json:"event"` - ID string `json:"id"` +} + +type FileTransferStatus struct { + Event string `json:"event"` + Admin bool `json:"admin"` + Unpriv bool `json:"unpriv"` } type FileList struct { diff --git a/server/internal/websocket/handler/files.go b/server/internal/websocket/handler/files.go index cda7ec8..fb1d190 100644 --- a/server/internal/websocket/handler/files.go +++ b/server/internal/websocket/handler/files.go @@ -8,6 +8,36 @@ import ( "m1k1o/neko/internal/utils" ) +func (h *MessageHandler) setFileTransferStatus(session types.Session, payload *message.FileTransferStatus) error { + if !session.Admin() { + return errors.New(session.Member().Name + " tried to toggle file transfer but they're not admin") + } + h.state.SetFileTransferState(payload.Admin, payload.Unpriv) + err := h.sessions.Broadcast(message.FileTransferStatus{ + Event: event.FILETRANSFER_STATUS, + Admin: payload.Admin, + Unpriv: payload.Admin && payload.Unpriv, + }, nil) + if err != nil { + return err + } + + files, err := utils.ListFiles(h.state.FileTransferPath()) + if err != nil { + return err + } + msg := message.FileList{ + Event: event.FILETRANSFER_LIST, + Cwd: h.state.FileTransferPath(), + Files: *files, + } + if payload.Unpriv { + return h.sessions.Broadcast(msg, nil) + } else { + return h.sessions.AdminBroadcast(msg, nil) + } +} + func (h *MessageHandler) refresh(session types.Session) error { if !(h.state.FileTransferEnabled() && session.Admin() || h.state.UnprivFileTransferEnabled()) { return errors.New(session.Member().Name + " tried to refresh file list when they can't") diff --git a/server/internal/websocket/handler/handler.go b/server/internal/websocket/handler/handler.go index 004a4b4..985f027 100644 --- a/server/internal/websocket/handler/handler.go +++ b/server/internal/websocket/handler/handler.go @@ -127,6 +127,12 @@ func (h *MessageHandler) Message(id string, raw []byte) error { }), "%s failed", header.Event) // File Transfer Events + case event.FILETRANSFER_STATUS: + payload := &message.FileTransferStatus{} + return errors.Wrapf( + utils.Unmarshal(payload, raw, func() error { + return h.setFileTransferStatus(session, payload) + }), "%s failed", header.Event) case event.FILETRANSFER_REFRESH: return errors.Wrapf(h.refresh(session), "%s failed", header.Event) diff --git a/server/internal/websocket/state/state.go b/server/internal/websocket/state/state.go index 1c2719c..3eba130 100644 --- a/server/internal/websocket/state/state.go +++ b/server/internal/websocket/state/state.go @@ -78,6 +78,11 @@ func (s *State) UnprivFileTransferEnabled() bool { return s.fileTransferUnprivEnabled } +func (s *State) SetFileTransferState(admin bool, unpriv bool) { + s.fileTransferEnabled = admin + s.fileTransferUnprivEnabled = unpriv +} + func (s *State) FileTransferPath() string { return s.fileTransferPath } diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index f461765..022067a 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -135,7 +135,19 @@ func (ws *WebSocketHandler) Start() { } // send file list if necessary - if session.Admin() && ws.conf.FileTransfer || ws.conf.FileTransfer && ws.conf.UnprivFileTransfer { + if session.Admin() && ws.state.FileTransferEnabled() || + ws.state.FileTransferEnabled() && ws.state.UnprivFileTransferEnabled() { + err := session.Send( + message.FileTransferStatus{ + Event: event.FILETRANSFER_STATUS, + Admin: ws.state.FileTransferEnabled(), + Unpriv: ws.state.UnprivFileTransferEnabled(), + }) + if err != nil { + ws.logger.Warn().Err(err).Msgf("file transfer status event has failed") + return + } + files, err := utils.ListFiles(ws.conf.FileTransferPath) if err == nil { if err := session.Send( @@ -214,27 +226,25 @@ func (ws *WebSocketHandler) Start() { }) // watch for file changes - if ws.conf.FileTransfer { - watcher, err := fsnotify.NewWatcher() - if err != nil { - ws.logger.Err(err).Msg("unable to start file transfer dir watcher") - return - } + watcher, err := fsnotify.NewWatcher() + if err != nil { + ws.logger.Err(err).Msg("unable to start file transfer dir watcher") + return + } - go func() { - for { - select { - case <-watcher.Events: - ws.sendFileTransferUpdate() - case err := <-watcher.Errors: - ws.logger.Err(err).Msg("error in file transfer dir watcher") - } + go func() { + for { + select { + case <-watcher.Events: + ws.sendFileTransferUpdate() + case err := <-watcher.Errors: + ws.logger.Err(err).Msg("error in file transfer dir watcher") } - }() - - if err := watcher.Add(ws.conf.FileTransferPath); err != nil { - ws.logger.Err(err).Msg("unable to add file transfer path to watcher") } + }() + + if err := watcher.Add(ws.conf.FileTransferPath); err != nil { + ws.logger.Err(err).Msg("unable to add file transfer path to watcher") } } @@ -364,11 +374,11 @@ func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) { } func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { - if !ws.conf.FileTransfer { + if !ws.state.FileTransferEnabled() { return false, nil } - if !ws.conf.UnprivFileTransfer { + if !ws.state.UnprivFileTransferEnabled() { return ws.IsAdmin(password) } @@ -380,6 +390,10 @@ func (ws *WebSocketHandler) MakeFilePath(filename string) string { } func (ws *WebSocketHandler) sendFileTransferUpdate() { + if !ws.state.FileTransferEnabled() { + return + } + files, err := utils.ListFiles(ws.conf.FileTransferPath) if err != nil { ws.logger.Err(err).Msg("unable to ls file transfer path") @@ -393,7 +407,7 @@ func (ws *WebSocketHandler) sendFileTransferUpdate() { } var broadcastErr error - if ws.conf.UnprivFileTransfer { + if ws.state.UnprivFileTransferEnabled() { broadcastErr = ws.sessions.Broadcast(message, nil) } else { broadcastErr = ws.sessions.AdminBroadcast(message, nil) From b65df3e3bfd2c9434e06c21a2b409c6bce713a6a Mon Sep 17 00:00:00 2001 From: William Harrell Date: Wed, 16 Nov 2022 20:06:36 -0500 Subject: [PATCH 10/13] more efficient file upload/download --- server/internal/http/http.go | 37 +++++------------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/server/internal/http/http.go b/server/internal/http/http.go index 82eea6b..9b8a237 100644 --- a/server/internal/http/http.go +++ b/server/internal/http/http.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "image/jpeg" + "io" "net/http" "os" "regexp" @@ -131,21 +132,10 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t return } defer f.Close() - fileinfo, err := f.Stat() - if err != nil { - http.Error(w, "unable to stat file", http.StatusInternalServerError) - return - } - buffer := make([]byte, fileinfo.Size()) - _, err = f.Read(buffer) - if err != nil { - http.Error(w, "error reading file", http.StatusInternalServerError) - return - } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) - w.Write(buffer) + io.Copy(w, f) }) router.Post("/file", func(w http.ResponseWriter, r *http.Request) { @@ -162,7 +152,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t } r.ParseMultipartForm(32 << 20) - buffer := make([]byte, FILE_UPLOAD_BUF_SIZE) for _, formheader := range r.MultipartForm.File["files"] { formfile, err := formheader.Open() if err != nil { @@ -170,30 +159,14 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t http.Error(w, "error writing file", http.StatusInternalServerError) return } + defer formfile.Close() f, err := os.OpenFile(webSocketHandler.MakeFilePath(formheader.Filename), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { http.Error(w, "unable to open file for writing", http.StatusInternalServerError) return } - - var copied int64 = 0 - for copied < formheader.Size { - var limit int64 = int64(len(buffer)) - if limit > formheader.Size-copied { - limit = formheader.Size - copied - } - bytesRead, err := formfile.ReadAt(buffer[:limit], copied) - if err != nil { - logger.Warn().Err(err).Msg("failed copying file in upload") - http.Error(w, "error writing file", http.StatusInternalServerError) - return - } - f.Write(buffer[:bytesRead]) - copied += int64(bytesRead) - } - - formfile.Close() - f.Close() + defer f.Close() + io.Copy(f, formfile) } }) From 4885c2d69eb54117e5c9431ffdeabaf325724606 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Wed, 16 Nov 2022 20:58:39 -0500 Subject: [PATCH 11/13] frontend improvements --- client/src/components/files.vue | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index cc4c7b9..4261afb 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -18,6 +18,7 @@

    Downloads

    +

    {{ download.name }}

    {{ Math.min(100, Math.round(download.progress / download.size * 100))}}%

    @@ -28,6 +29,7 @@

    Uploads

    +

    {{ upload.name }}

    {{ Math.min(100, Math.round(upload.progress / upload.size * 100))}}%

    @@ -97,7 +99,7 @@ flex-direction: row; } - .file-icon { + .file-icon, .transfer-status { width: 14px; margin-right: 0.5em; } @@ -286,6 +288,9 @@ document.body.appendChild(link) link.click() document.body.removeChild(link) + + transfer.progress = transfer.size + transfer.status = 'completed' }).catch((err) => { this.$log.error(err) }) @@ -370,15 +375,31 @@ } const ext = parts[parts.length - 1] switch (ext) { - case 'mp3': + case 'aac': case 'flac': + case 'midi': + case 'mp3': + case 'ogg': + case 'wav': className += 'fa-music' break - case 'webm': - case 'mp4': case 'mkv': + case 'mov': + case 'mpeg': + case 'mp4': + case 'webm': className += 'fa-film' break + case 'bmp': + case 'gif': + case 'jpeg': + case 'jpg': + case 'png': + case 'svg': + case 'tiff': + case 'webp': + className += 'fa-image' + break; default: className += 'fa-file' } From bbfa0d5834af6fc82ec4912726e80c1834115318 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Wed, 16 Nov 2022 22:03:25 -0500 Subject: [PATCH 12/13] added translations --- client/src/components/files.vue | 6 +++--- client/src/components/settings.vue | 4 ++-- client/src/locale/de-de.ts | 9 +++++++++ client/src/locale/en-us.ts | 8 ++++++++ client/src/locale/es-sp.ts | 9 +++++++++ client/src/locale/fi-fi.ts | 9 +++++++++ client/src/locale/fr-fr.ts | 9 +++++++++ client/src/locale/ko-kr.ts | 9 +++++++++ client/src/locale/nb-no.ts | 9 +++++++++ client/src/locale/ru-ru.ts | 9 +++++++++ client/src/locale/sk-sk.ts | 9 +++++++++ client/src/locale/sv-se.ts | 9 +++++++++ client/src/locale/zh-cn.ts | 9 +++++++++ 13 files changed, 103 insertions(+), 5 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 4261afb..b40553a 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -15,7 +15,7 @@
    -

    Downloads

    +

    {{ $t('files.downloads') }}

    @@ -26,7 +26,7 @@
    -

    Uploads

    +

    {{ $t('files.uploads' )}}

    @@ -42,7 +42,7 @@ @dragover.prevent="() => uploadAreaDrag = true" @dragleave.prevent="() => uploadAreaDrag = false" @drop.prevent="(e) => upload(e.dataTransfer)" @click="openFileBrowser"> -

    Click or drag files here to upload

    +

    {{ $t('files.upload_here') }}

    diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index 7b02b83..c95bd67 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -45,14 +45,14 @@
  • - File transfer + {{ $t('setting.file_transfer') }}
  • - Non-admin file transfer + {{ $t('setting.unpriv_file_transfer') }}