From fa45619dbba3f63d4c61b74f428023b088055513 Mon Sep 17 00:00:00 2001 From: William Harrell Date: Sun, 16 Oct 2022 20:54:23 -0400 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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') }}
  • @@ -18,29 +17,51 @@

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

    - +

    {{ download.name }}

    -

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

    +

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

    - +
    -

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

    +

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

    - +

    {{ upload.name }}

    -

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

    +

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

    - +
    -
    +

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

    @@ -94,12 +115,13 @@ .files-list-item { padding: 0.5em; - border-bottom: 2px solid rgba($color: #fff, $alpha: 0.10); + border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1); display: flex; flex-direction: row; } - .file-icon, .transfer-status { + .file-icon, + .transfer-status { width: 14px; margin-right: 0.5em; } @@ -115,10 +137,12 @@ .file-size { margin-left: auto; margin-right: 0.5em; - color: rgba($color: #fff, $alpha: 0.40); + color: rgba($color: #fff, $alpha: 0.4); } - .refresh:hover, .download:hover, .remove-transfer:hover { + .refresh:hover, + .download:hover, + .remove-transfer:hover { cursor: pointer; } @@ -186,8 +210,9 @@ cursor: pointer; } - .upload-area-drag, .upload-area:hover { - background-color: rgba($color: #fff, $alpha: 0.10); + .upload-area-drag, + .upload-area:hover { + background-color: rgba($color: #fff, $alpha: 0.1); } .upload-area > i { @@ -198,12 +223,10 @@ .upload-area > p { margin: 0px 10px 10px 10px; } - } diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index c95bd67..2535cb2 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -385,7 +385,7 @@ } set file_transfer(value: boolean) { - this.$accessor.settings.setGlobalFileTransferStatus({ admin: value, unpriv: false }) + this.$accessor.settings.setRemoteFileTransferStatus({ admin: value, unpriv: false }) } get unpriv_file_transfer() { @@ -393,7 +393,7 @@ } set unpriv_file_transfer(value: boolean) { - this.$accessor.settings.setGlobalFileTransferStatus({ admin: this.file_transfer, unpriv: value }) + this.$accessor.settings.setRemoteFileTransferStatus({ admin: this.file_transfer, unpriv: value }) } get broadcast_is_active() { diff --git a/client/src/components/side.vue b/client/src/components/side.vue index 282065a..7404a98 100644 --- a/client/src/components/side.vue +++ b/client/src/components/side.vue @@ -79,7 +79,7 @@ From 950cb118cca13dfc1f14127356d512fc9433af86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 16:57:00 +0100 Subject: [PATCH 17/25] remove unused property. --- client/src/components/files.vue | 4 +--- client/src/neko/types.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 972c069..359eda8 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -285,11 +285,10 @@ size: item.size, progress: 0, status: 'pending', - axios: null, abortController: abortController, } - transfer.axios = this.$http + this.$http .get(url, { responseType: 'blob', signal: abortController.signal, @@ -342,7 +341,6 @@ size: file.size, progress: 0, status: 'pending', - axios: null, abortController: abortController, } diff --git a/client/src/neko/types.ts b/client/src/neko/types.ts index 14123f4..ab6a813 100644 --- a/client/src/neko/types.ts +++ b/client/src/neko/types.ts @@ -36,6 +36,5 @@ export interface FileTransfer { size: number progress: number status: 'pending' | 'inprogress' | 'completed' - axios: Promise | null abortController: AbortController | null } From 76b44b949c84928a6e1668040f78ea7577274959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 17:35:02 +0100 Subject: [PATCH 18/25] add status failed + error. --- client/src/components/files.vue | 52 ++++++++++++++++++++++++++++----- client/src/neko/types.ts | 5 ++-- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 359eda8..84b34c8 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -14,39 +14,57 @@
    -

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

    +

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

    {{ download.name }}

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

    +
    {{ download.error }}
    -

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

    +

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

    {{ upload.name }}

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

    +
    {{ upload.error }}
    { - this.$log.error(err) + .catch((error) => { + this.$log.error(error) + + transfer.status = 'failed' + transfer.error = error.message }) this.$accessor.files.addTransfer(transfer) @@ -360,8 +393,11 @@ } }, }) - .catch((err) => { - this.$log.error(err) + .catch((error) => { + this.$log.error(error) + + transfer.status = 'failed' + transfer.error = error.message }) this.$accessor.files.addTransfer(transfer) diff --git a/client/src/neko/types.ts b/client/src/neko/types.ts index ab6a813..bc4d046 100644 --- a/client/src/neko/types.ts +++ b/client/src/neko/types.ts @@ -35,6 +35,7 @@ export interface FileTransfer { direction: 'upload' | 'download' size: number progress: number - status: 'pending' | 'inprogress' | 'completed' - abortController: AbortController | null + status: 'pending' | 'inprogress' | 'completed' | 'failed' + error?: string + abortController?: AbortController } From cdb9b185f2ae773a06465955f9c6f1231d376a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 18:29:21 +0100 Subject: [PATCH 19/25] filepath clean. --- server/internal/config/websocket.go | 3 ++ server/internal/utils/files.go | 4 +- server/internal/websocket/handler/files.go | 12 ++++-- server/internal/websocket/websocket.go | 47 ++++++++++++---------- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/server/internal/config/websocket.go b/server/internal/config/websocket.go index 653411e..377e141 100644 --- a/server/internal/config/websocket.go +++ b/server/internal/config/websocket.go @@ -1,6 +1,8 @@ package config import ( + "path/filepath" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -73,4 +75,5 @@ func (s *WebSocket) Set() { s.FileTransfer = viper.GetBool("file_transfer") s.UnprivFileTransfer = viper.GetBool("unpriv_file_transfer") s.FileTransferPath = viper.GetString("file_transfer_path") + s.FileTransferPath = filepath.Clean(s.FileTransferPath) } diff --git a/server/internal/utils/files.go b/server/internal/utils/files.go index 56c714d..d9f24db 100644 --- a/server/internal/utils/files.go +++ b/server/internal/utils/files.go @@ -6,7 +6,7 @@ import ( "m1k1o/neko/internal/types" ) -func ListFiles(path string) (*[]types.FileListItem, error) { +func ListFiles(path string) ([]types.FileListItem, error) { items, err := os.ReadDir(path) if err != nil { return nil, err @@ -32,5 +32,5 @@ func ListFiles(path string) (*[]types.FileListItem, error) { } } - return &out, nil + return out, nil } diff --git a/server/internal/websocket/handler/files.go b/server/internal/websocket/handler/files.go index fb1d190..4ac85f5 100644 --- a/server/internal/websocket/handler/files.go +++ b/server/internal/websocket/handler/files.go @@ -10,9 +10,12 @@ import ( 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.logger.Debug().Msg("user not admin") + return nil } + h.state.SetFileTransferState(payload.Admin, payload.Unpriv) + err := h.sessions.Broadcast(message.FileTransferStatus{ Event: event.FILETRANSFER_STATUS, Admin: payload.Admin, @@ -26,11 +29,13 @@ func (h *MessageHandler) setFileTransferStatus(session types.Session, payload *m if err != nil { return err } + msg := message.FileList{ Event: event.FILETRANSFER_LIST, Cwd: h.state.FileTransferPath(), - Files: *files, + Files: files, } + if payload.Unpriv { return h.sessions.Broadcast(msg, nil) } else { @@ -47,10 +52,11 @@ func (h *MessageHandler) refresh(session types.Session) error { if err != nil { return err } + return session.Send( message.FileList{ Event: event.FILETRANSFER_LIST, Cwd: h.state.FileTransferPath(), - Files: *files, + Files: files, }) } diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index 022067a..96d6cd6 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "sync" "sync/atomic" "time" @@ -35,12 +36,9 @@ 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") + if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) { + err = os.Mkdir(conf.FileTransferPath, os.ModePerm) + logger.Err(err).Msg("creating file transfer directory") } // apply default locks @@ -135,8 +133,7 @@ func (ws *WebSocketHandler) Start() { } // send file list if necessary - if session.Admin() && ws.state.FileTransferEnabled() || - ws.state.FileTransferEnabled() && ws.state.UnprivFileTransferEnabled() { + if ws.state.FileTransferEnabled() && (session.Admin() || ws.state.UnprivFileTransferEnabled()) { err := session.Send( message.FileTransferStatus{ Event: event.FILETRANSFER_STATUS, @@ -154,7 +151,7 @@ func (ws *WebSocketHandler) Start() { message.FileList{ Event: event.FILETRANSFER_LIST, Cwd: ws.conf.FileTransferPath, - Files: *files, + Files: files, }); err != nil { ws.logger.Warn().Err(err).Msg("file list event has failed") } @@ -235,8 +232,14 @@ func (ws *WebSocketHandler) Start() { go func() { for { select { - case <-watcher.Events: - ws.sendFileTransferUpdate() + case e, ok := <-watcher.Events: + if !ok { + ws.logger.Info().Msg("file transfer dir watcher closed") + return + } + if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) { + ws.sendFileTransferUpdate() + } case err := <-watcher.Errors: ws.logger.Err(err).Msg("error in file transfer dir watcher") } @@ -378,15 +381,17 @@ func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { return false, nil } - if !ws.state.UnprivFileTransferEnabled() { - return ws.IsAdmin(password) + isAdmin, err := ws.IsAdmin(password) + if err != nil { + return false, err } - return password == ws.conf.Password, nil + return isAdmin || ws.state.UnprivFileTransferEnabled(), nil } func (ws *WebSocketHandler) MakeFilePath(filename string) string { - return fmt.Sprintf("%s%s", ws.conf.FileTransferPath, filename) + cleanPath := filepath.Clean(filename) + return filepath.Join(ws.conf.FileTransferPath, cleanPath) } func (ws *WebSocketHandler) sendFileTransferUpdate() { @@ -403,17 +408,17 @@ func (ws *WebSocketHandler) sendFileTransferUpdate() { message := message.FileList{ Event: event.FILETRANSFER_LIST, Cwd: ws.conf.FileTransferPath, - Files: *files, + Files: files, } - var broadcastErr error if ws.state.UnprivFileTransferEnabled() { - broadcastErr = ws.sessions.Broadcast(message, nil) + err = ws.sessions.Broadcast(message, nil) } else { - broadcastErr = ws.sessions.AdminBroadcast(message, nil) + err = ws.sessions.AdminBroadcast(message, nil) } - if broadcastErr != nil { - ws.logger.Err(broadcastErr).Msg("unable to broadcast file list") + + if err != nil { + ws.logger.Err(err).Msg("unable to broadcast file list") } } From d17a7e8d82e6f0cc29af81810825ba62f9496495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:26:45 +0100 Subject: [PATCH 20/25] move filetransfer to locks. --- client/src/components/files.vue | 12 +- client/src/components/header.vue | 39 +++-- client/src/components/settings.vue | 30 ---- client/src/components/side.vue | 13 +- client/src/locale/de-de.ts | 10 +- client/src/locale/en-us.ts | 10 +- client/src/locale/es-sp.ts | 11 +- client/src/locale/fi-fi.ts | 11 +- client/src/locale/fr-fr.ts | 11 +- client/src/locale/ko-kr.ts | 11 +- client/src/locale/nb-no.ts | 11 +- client/src/locale/ru-ru.ts | 11 +- client/src/locale/sk-sk.ts | 11 +- client/src/locale/sv-se.ts | 11 +- client/src/locale/zh-cn.ts | 11 +- client/src/neko/events.ts | 6 +- client/src/neko/index.ts | 10 +- client/src/neko/messages.ts | 14 +- client/src/store/index.ts | 17 +- client/src/store/remote.ts | 5 + client/src/store/settings.ts | 27 --- server/internal/config/websocket.go | 17 +- server/internal/http/http.go | 121 ++++++------- server/internal/types/event/events.go | 1 - server/internal/types/message/messages.go | 17 +- server/internal/types/websocket.go | 5 +- server/internal/websocket/handler/admin.go | 7 +- server/internal/websocket/handler/files.go | 62 ------- .../websocket/handler/filetransfer.go | 42 +++++ server/internal/websocket/handler/handler.go | 8 +- server/internal/websocket/handler/session.go | 12 +- server/internal/websocket/state/state.go | 38 ++--- server/internal/websocket/websocket.go | 160 +++++++----------- 33 files changed, 377 insertions(+), 405 deletions(-) delete mode 100644 server/internal/websocket/handler/files.go create mode 100644 server/internal/websocket/handler/filetransfer.go diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 84b34c8..58d4ddf 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -9,7 +9,7 @@

    {{ item.name }}

    {{ fileSize(item.size) }}

    - +
    @@ -31,7 +31,7 @@ >

    {{ download.name }}

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

    - +
    {{ download.error }}

    {{ upload.name }}

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

    - +
    {{ upload.error }}
    @@ -322,6 +322,7 @@ .get(url, { responseType: 'blob', signal: abortController.signal, + withCredentials: false, onDownloadProgress: (x) => { transfer.progress = x.loaded @@ -380,6 +381,7 @@ this.$http .post(url, formdata, { signal: abortController.signal, + withCredentials: false, onUploadProgress: (x: any) => { transfer.progress = x.loaded diff --git a/client/src/components/header.vue b/client/src/components/header.vue index 7db5e10..2f6506f 100644 --- a/client/src/components/header.vue +++ b/client/src/components/header.vue @@ -31,6 +31,19 @@ }" /> +
  • + +
  • @@ -169,26 +182,24 @@ return !this.side && this.readTexts != this.texts } + get fileTransfer() { + return this.$accessor.remote.fileTransfer + } + + toggleLock(resource: AdminLockResource) { + this.$accessor.toggleLock(resource) + } + + isLocked(resource: AdminLockResource): boolean { + return this.$accessor.isLocked(resource) + } + readTexts: number = 0 toggleMenu() { this.$accessor.client.toggleSide() this.readTexts = this.texts } - toggleLock(resource: AdminLockResource) { - if (!this.admin) return - - if (this.isLocked(resource)) { - this.$accessor.unlock(resource) - } else { - this.$accessor.lock(resource) - } - } - - isLocked(resource: AdminLockResource): boolean { - return resource in this.locked && this.locked[resource] - } - lockedTooltip(resource: AdminLockResource) { if (this.admin) { return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`)) diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index 2535cb2..e6c991b 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -44,20 +44,6 @@
  • -
  • - {{ $t('setting.file_transfer') }} - -
  • -
  • - {{ $t('setting.unpriv_file_transfer') }} - -
  • {{ $t('setting.broadcast_title') }} @@ -380,22 +366,6 @@ return this.$accessor.settings.keyboard_layout } - get file_transfer() { - return this.$accessor.settings.file_transfer - } - - set file_transfer(value: boolean) { - this.$accessor.settings.setRemoteFileTransferStatus({ admin: value, unpriv: false }) - } - - get unpriv_file_transfer() { - return this.$accessor.settings.unpriv_file_transfer - } - - set unpriv_file_transfer(value: boolean) { - this.$accessor.settings.setRemoteFileTransferStatus({ 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 7404a98..33fcfe2 100644 --- a/client/src/components/side.vue +++ b/client/src/components/side.vue @@ -95,10 +95,7 @@ }) export default class extends Vue { get filetransferAllowed() { - return ( - (this.$accessor.user.admin && this.$accessor.settings.file_transfer) || - this.$accessor.settings.unpriv_file_transfer - ) + return this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer')) } get tab() { @@ -106,6 +103,7 @@ } @Watch('tab', { immediate: true }) + @Watch('filetransferAllowed', { immediate: true }) onTabChange() { // do not show the files tab if file transfer is disabled if (this.tab === 'files' && !this.filetransferAllowed) { @@ -113,6 +111,13 @@ } } + @Watch('filetransferAllowed') + onFileTransferAllowedChange() { + if (this.filetransferAllowed) { + this.$accessor.files.refresh() + } + } + change(tab: string) { this.$accessor.client.setTab(tab) } diff --git a/client/src/locale/de-de.ts b/client/src/locale/de-de.ts index 7eac5b4..c163c6f 100644 --- a/client/src/locale/de-de.ts +++ b/client/src/locale/de-de.ts @@ -69,6 +69,14 @@ export const locks = { notif_locked: 'Raum gesperrt', notif_unlocked: 'Raum entsperrt', }, + file_transfer: { + lock: 'Dateiübertragung sperren (für Nutzer)', + unlock: 'Dateiübertragung entsperren (für Nutzer)', + locked: 'Dateiübertragung gesperrt (für Nutzer)', + unlocked: 'Dateiübertragung entsperrt (für Nutzer)', + notif_locked: 'Dateiübertragung gesperrt', + notif_unlocked: 'Dateiübertragung entsperrt', + }, } export const setting = { @@ -78,8 +86,6 @@ export const setting = { ignore_emotes: 'Emotes ignorieren', chat_sound: 'Chat-Sound abspielen', keyboard_layout: 'Tastaturbelegung', - file_transfer: 'Dateiübertragung', - unpriv_file_transfer: 'Übertragung von Benutzerdateien', broadcast_title: 'Live-Übertragung', } diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index db9dfe8..edc7f2b 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -71,6 +71,14 @@ export const locks = { notif_locked: 'locked the room', notif_unlocked: 'unlocked the room', }, + file_transfer: { + lock: 'Lock File Transfer (for users)', + unlock: 'Unlock File Transfer (for users)', + locked: 'File Transfer Locked (for users)', + unlocked: 'File Transfer Unlocked (for users)', + notif_locked: 'locked file transfer', + notif_unlocked: 'unlocked file transfer', + }, } export const setting = { @@ -80,8 +88,6 @@ export const setting = { ignore_emotes: 'Ignore Emotes', chat_sound: 'Play Chat Sound', keyboard_layout: 'Keyboard Layout', - file_transfer: 'File Transfer', - unpriv_file_transfer: 'Non-admin File Transfer', broadcast_title: 'Live Broadcast', } diff --git a/client/src/locale/es-sp.ts b/client/src/locale/es-sp.ts index 0aecc12..d8bf6b9 100644 --- a/client/src/locale/es-sp.ts +++ b/client/src/locale/es-sp.ts @@ -75,6 +75,15 @@ export const locks = { notif_locked: 'bloqueó la sala', notif_unlocked: 'desbloqueó la sala', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -84,8 +93,6 @@ export const setting = { ignore_emotes: 'Ignorar Emotes', chat_sound: 'Reproducir Sonidos Chat', keyboard_layout: 'Keyboard Layout', - file_transfer: 'Transferencia de archivos', - unpriv_file_transfer: 'Transferencia de archivos de usuario', // TODO //broadcast_title: 'Live Broadcast', } diff --git a/client/src/locale/fi-fi.ts b/client/src/locale/fi-fi.ts index a63eb90..105cbf2 100644 --- a/client/src/locale/fi-fi.ts +++ b/client/src/locale/fi-fi.ts @@ -71,6 +71,15 @@ export const locks = { notif_locked: 'lukittu huone', notif_unlocked: 'vapautettu huone', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -80,8 +89,6 @@ export const setting = { ignore_emotes: 'Estä emojit', chat_sound: 'Soita viesti ääni', keyboard_layout: 'Näppäimistöasettelu', - file_transfer: 'Tiedoston siirto', - unpriv_file_transfer: 'Käyttäjän tiedostojen siirto', broadcast_title: 'Suora Lähetys', } diff --git a/client/src/locale/fr-fr.ts b/client/src/locale/fr-fr.ts index 7481dc3..67d49ea 100644 --- a/client/src/locale/fr-fr.ts +++ b/client/src/locale/fr-fr.ts @@ -75,6 +75,15 @@ export const locks = { notif_locked: 'a vérouillé la salle', notif_unlocked: 'a dévérouillé la salle', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -84,8 +93,6 @@ export const setting = { ignore_emotes: 'Ignorer les Emotes', chat_sound: 'Jouer le son du tchat', keyboard_layout: 'Langue du clavier', - file_transfer: 'Transfert de fichiers', - unpriv_file_transfer: "Transfert de fichiers d'utilisateurs", // TODO //broadcast_title: 'Live Broadcast', } diff --git a/client/src/locale/ko-kr.ts b/client/src/locale/ko-kr.ts index 10e6a02..d4fd19f 100644 --- a/client/src/locale/ko-kr.ts +++ b/client/src/locale/ko-kr.ts @@ -69,6 +69,15 @@ export const locks = { notif_locked: '방이 잠겼습니다', notif_unlocked: '방 잠금이 해제됐습니다', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -78,8 +87,6 @@ export const setting = { ignore_emotes: '이모지 무시', chat_sound: '채팅 소리 재생', keyboard_layout: '키보드 레이아웃', - file_transfer: '파일 전송', - unpriv_file_transfer: '사용자 파일 전송', broadcast_title: '실시간 방송', } diff --git a/client/src/locale/nb-no.ts b/client/src/locale/nb-no.ts index 6366784..ec3c969 100644 --- a/client/src/locale/nb-no.ts +++ b/client/src/locale/nb-no.ts @@ -75,6 +75,15 @@ export const locks = { notif_locked: 'låste rommet', notif_unlocked: 'låste opp rommet', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -84,8 +93,6 @@ export const setting = { ignore_emotes: 'Ignorer smilefjes', chat_sound: 'Sludringslyd', keyboard_layout: 'Tastaturoppsett', - file_transfer: 'Filoverførsel', - unpriv_file_transfer: 'Overførsel af brugerfiler', // TODO //broadcast_title: 'Live Broadcast', } diff --git a/client/src/locale/ru-ru.ts b/client/src/locale/ru-ru.ts index faa8ea1..cfb3a63 100644 --- a/client/src/locale/ru-ru.ts +++ b/client/src/locale/ru-ru.ts @@ -71,6 +71,15 @@ export const locks = { notif_locked: 'комната закрыта', notif_unlocked: 'комната открыта', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -80,8 +89,6 @@ export const setting = { ignore_emotes: 'Игнорировать эмоции', chat_sound: 'Проигрывать звук чата', keyboard_layout: 'Раскладка клавиатуры', - file_transfer: 'Передача файлов', - unpriv_file_transfer: 'Передача файлов пользователей', broadcast_title: 'Прямой эфир', } diff --git a/client/src/locale/sk-sk.ts b/client/src/locale/sk-sk.ts index f5cf9f8..5176dae 100644 --- a/client/src/locale/sk-sk.ts +++ b/client/src/locale/sk-sk.ts @@ -74,6 +74,15 @@ export const locks = { notif_locked: 'miestnosť bola zamknutá', notif_unlocked: 'miestnosť bola odomknutá', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -83,8 +92,6 @@ export const setting = { ignore_emotes: 'Ignorovať smajlíky', chat_sound: 'Prehrávať zvuky chatu', keyboard_layout: 'Rozloženie klávesnice', - file_transfer: 'Prenos súborov', - unpriv_file_transfer: 'Prenos súborov používateľa', broadcast_title: 'Živé vysielanie', } diff --git a/client/src/locale/sv-se.ts b/client/src/locale/sv-se.ts index 3ea55e3..f65cd4d 100644 --- a/client/src/locale/sv-se.ts +++ b/client/src/locale/sv-se.ts @@ -75,6 +75,15 @@ export const locks = { notif_locked: 'låste rummet', notif_unlocked: 'låste upp rummet', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -84,8 +93,6 @@ export const setting = { ignore_emotes: 'Ignorera Emotes', chat_sound: 'Spela Chatt Ljud', keyboard_layout: 'Tangentbordslayout', - file_transfer: 'Överföring av filer', - unpriv_file_transfer: 'Överföring av användarfiler', // TODO //broadcast_title: 'Live Broadcast', } diff --git a/client/src/locale/zh-cn.ts b/client/src/locale/zh-cn.ts index b709d63..7737f1d 100644 --- a/client/src/locale/zh-cn.ts +++ b/client/src/locale/zh-cn.ts @@ -71,6 +71,15 @@ export const locks = { notif_locked: '锁上房间', notif_unlocked: '解锁房间', }, + // TODO + //file_transfer: { + // lock: 'Lock File Transfer (for users)', + // unlock: 'Unlock File Transfer (for users)', + // locked: 'File Transfer Locked (for users)', + // unlocked: 'File Transfer Unlocked (for users)', + // notif_locked: 'locked file transfer', + // notif_unlocked: 'unlocked file transfer', + //}, } export const setting = { @@ -80,8 +89,6 @@ export const setting = { ignore_emotes: '忽略表情符号', chat_sound: '播放聊天声音', keyboard_layout: '键盘布局', - file_transfer: '文件传输', - unpriv_file_transfer: '用户文件传输', broadcast_title: '现场流媒体', } diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index 8dfdbd8..5deb010 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -39,7 +39,6 @@ export const EVENT = { EMOTE: 'chat/emote', }, FILETRANSFER: { - STATUS: 'filetransfer/status', LIST: 'filetransfer/list', REFRESH: 'filetransfer/refresh', }, @@ -98,10 +97,7 @@ export type SignalEvents = export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE -export type FileTransferEvents = - | typeof EVENT.FILETRANSFER.STATUS - | typeof EVENT.FILETRANSFER.LIST - | typeof EVENT.FILETRANSFER.REFRESH +export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index eefa6d6..777520e 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -25,7 +25,6 @@ import { SystemInitPayload, AdminLockResource, FileTransferListPayload, - FileTransferStatusPayload, } from './messages' interface NekoEvents extends BaseEvents {} @@ -135,8 +134,9 @@ export class NekoClient extends BaseClient implements EventEmitter { ///////////////////////////// // System Events ///////////////////////////// - protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks }: SystemInitPayload) { + protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) { this.$accessor.remote.setImplicitHosting(implicit_hosting) + this.$accessor.remote.setFileTransfer(file_transfer) for (const resource in locks) { this[EVENT.ADMIN.LOCK]({ @@ -354,12 +354,8 @@ export class NekoClient extends BaseClient implements EventEmitter { } ///////////////////////////// - // Filetransfer Events + // File Transfer 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 4e179ac..54d1cb1 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -39,7 +39,6 @@ export type WebSocketPayloads = | ChatPayload | ChatSendPayload | EmojiSendPayload - | FileTransferStatusPayload | ScreenResolutionPayload | ScreenConfigurationsPayload | AdminPayload @@ -61,6 +60,7 @@ export interface SystemInit extends WebSocketMessage, SystemInitPayload { export interface SystemInitPayload { implicit_hosting: boolean locks: Record + file_transfer: boolean } // system/disconnect @@ -197,16 +197,6 @@ export interface EmojiSendPayload { /* FILE TRANSFER PAYLOADS */ -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 } @@ -272,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload { id: string } -export type AdminLockResource = 'login' | 'control' +export type AdminLockResource = 'login' | 'control' | 'file_transfer' export interface AdminLockPayload { resource: AdminLockResource diff --git a/client/src/store/index.ts b/client/src/store/index.ts index 11bafe2..662b005 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -1,6 +1,6 @@ import Vue from 'vue' import Vuex from 'vuex' -import { useAccessor, mutationTree, actionTree } from 'typed-vuex' +import { useAccessor, mutationTree, getterTree, actionTree } from 'typed-vuex' import { EVENT } from '~/neko/events' import { AdminLockResource } from '~/neko/messages' import { get, set } from '~/utils/localstorage' @@ -56,8 +56,12 @@ export const mutations = mutationTree(state, { }, }) +export const getters = getterTree(state, { + isLocked: (state) => (resource: AdminLockResource) => resource in state.locked && state.locked[resource], +}) + export const actions = actionTree( - { state, mutations }, + { state, getters, mutations }, { initialise(store) { accessor.emoji.initialise() @@ -80,6 +84,14 @@ export const actions = actionTree( $client.sendMessage(EVENT.ADMIN.UNLOCK, { resource }) }, + toggleLock(_, resource: AdminLockResource) { + if (accessor.isLocked(resource)) { + accessor.unlock(resource) + } else { + accessor.lock(resource) + } + }, + login({ state }, { displayname, password }: { displayname: string; password: string }) { accessor.setLogin({ displayname, password }) $client.login(password, displayname) @@ -98,6 +110,7 @@ export const storePattern = { state, mutations, actions, + getters, modules: { video, chat, files, user, remote, settings, client, emoji }, } diff --git a/client/src/store/remote.ts b/client/src/store/remote.ts index c6834f7..ff99e1c 100644 --- a/client/src/store/remote.ts +++ b/client/src/store/remote.ts @@ -13,6 +13,7 @@ export const state = () => ({ clipboard: '', locked: false, implicitHosting: true, + fileTransfer: true, keyboardModifierState: -1, }) @@ -53,6 +54,10 @@ export const mutations = mutationTree(state, { state.implicitHosting = val }, + setFileTransfer(state, val: boolean) { + state.fileTransfer = val + }, + reset(state) { state.id = '' state.clipboard = '' diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 8b41112..3d99fad 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -20,9 +20,6 @@ export const state = () => { keyboard_layouts_list: {} as KeyboardLayouts, - file_transfer: false, - unpriv_file_transfer: false, - broadcast_is_active: false, broadcast_url: '', } @@ -61,14 +58,6 @@ 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 }, @@ -90,22 +79,6 @@ 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') - } - }, - setRemoteFileTransferStatus({ getters }, { admin, unpriv }) { - $client.sendMessage(EVENT.FILETRANSFER.STATUS, { admin, unpriv }) - }, - broadcastStatus({ getters }, { url, isActive }) { accessor.settings.setBroadcastStatus({ url, isActive }) }, diff --git a/server/internal/config/websocket.go b/server/internal/config/websocket.go index 377e141..163d9ec 100644 --- a/server/internal/config/websocket.go +++ b/server/internal/config/websocket.go @@ -15,9 +15,8 @@ type WebSocket struct { ControlProtection bool - FileTransfer bool - UnprivFileTransfer bool - FileTransferPath string + FileTransferEnabled bool + FileTransferPath string } func (WebSocket) Init(cmd *cobra.Command) error { @@ -46,13 +45,10 @@ 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 - } + // File transfer - 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 { + cmd.PersistentFlags().Bool("file_transfer_enabled", true, "enable file transfer feature") + if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil { return err } @@ -72,8 +68,7 @@ func (s *WebSocket) Set() { s.ControlProtection = viper.GetBool("control_protection") - s.FileTransfer = viper.GetBool("file_transfer") - s.UnprivFileTransfer = viper.GetBool("unpriv_file_transfer") + s.FileTransferEnabled = viper.GetBool("file_transfer_enabled") s.FileTransferPath = viper.GetString("file_transfer_path") s.FileTransferPath = filepath.Clean(s.FileTransferPath) } diff --git a/server/internal/http/http.go b/server/internal/http/http.go index 9b8a237..bf75d10 100644 --- a/server/internal/http/http.go +++ b/server/internal/http/http.go @@ -105,70 +105,77 @@ 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() - - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) - io.Copy(w, f) - }) - - 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) - for _, formheader := range r.MultipartForm.File["files"] { - formfile, err := formheader.Open() + // allow downloading and uploading files + if webSocketHandler.FileTransferEnabled() { + router.Get("/file", func(w http.ResponseWriter, r *http.Request) { + password := r.URL.Query().Get("pwd") + isAuthorized, err := webSocketHandler.CanTransferFiles(password) if err != nil { - logger.Warn().Err(err).Msg("failed to open formdata file") - http.Error(w, "error writing file", http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusForbidden) return } - defer formfile.Close() - f, err := os.OpenFile(webSocketHandler.MakeFilePath(formheader.Filename), os.O_WRONLY|os.O_CREATE, 0644) + + 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 + } + + filePath := webSocketHandler.FileTransferPath(filename) + f, err := os.Open(filePath) if err != nil { - http.Error(w, "unable to open file for writing", http.StatusInternalServerError) + http.Error(w, "not found or unable to open", http.StatusNotFound) return } defer f.Close() - io.Copy(f, formfile) - } - }) + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) + io.Copy(w, f) + }) + + 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) + for _, formheader := range r.MultipartForm.File["files"] { + filePath := webSocketHandler.FileTransferPath(formheader.Filename) + + 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 + } + defer formfile.Close() + + f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + http.Error(w, "unable to open file for writing", http.StatusInternalServerError) + return + } + defer f.Close() + + io.Copy(f, formfile) + } + }) + } router.Get("/health", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("true")) diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go index 4b6b047..a091ab6 100644 --- a/server/internal/types/event/events.go +++ b/server/internal/types/event/events.go @@ -35,7 +35,6 @@ const ( ) const ( - FILETRANSFER_STATUS = "filetransfer/status" FILETRANSFER_LIST = "filetransfer/list" FILETRANSFER_REFRESH = "filetransfer/refresh" ) diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go index 27a83d6..89552b7 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/types/message/messages.go @@ -14,6 +14,7 @@ type SystemInit struct { Event string `json:"event"` ImplicitHosting bool `json:"implicit_hosting"` Locks map[string]string `json:"locks"` + FileTransfer bool `json:"file_transfer"` } type SystemMessage struct { @@ -47,8 +48,8 @@ type SignalCandidate struct { } type MembersList struct { - Event string `json:"event"` - Memebers []*types.Member `json:"members"` + Event string `json:"event"` + Members []*types.Member `json:"members"` } type Member struct { @@ -106,17 +107,7 @@ type EmoteSend struct { Emote string `json:"emote"` } -type FileTransferTarget struct { - Event string `json:"event"` -} - -type FileTransferStatus struct { - Event string `json:"event"` - Admin bool `json:"admin"` - Unpriv bool `json:"unpriv"` -} - -type FileList struct { +type FileTransferList struct { Event string `json:"event"` Cwd string `json:"cwd"` Files []types.FileListItem `json:"files"` diff --git a/server/internal/types/websocket.go b/server/internal/types/websocket.go index cb80fcc..15b684c 100644 --- a/server/internal/types/websocket.go +++ b/server/internal/types/websocket.go @@ -34,8 +34,11 @@ type WebSocketHandler interface { Stats() Stats IsLocked(resource string) bool IsAdmin(password string) (bool, error) + + // File Transfer CanTransferFiles(password string) (bool, error) - MakeFilePath(filename string) string + FileTransferPath(filename string) string + FileTransferEnabled() bool } type FileListItem struct { diff --git a/server/internal/websocket/handler/admin.go b/server/internal/websocket/handler/admin.go index 7b9f1ee..6fb1ede 100644 --- a/server/internal/websocket/handler/admin.go +++ b/server/internal/websocket/handler/admin.go @@ -19,7 +19,12 @@ func (h *MessageHandler) adminLock(id string, session types.Session, payload *me return nil } - if payload.Resource != "login" && payload.Resource != "control" { + // allow only known resources + switch payload.Resource { + case "login": + case "control": + case "file_transfer": + default: h.logger.Debug().Msg("unknown lock resource") return nil } diff --git a/server/internal/websocket/handler/files.go b/server/internal/websocket/handler/files.go deleted file mode 100644 index 4ac85f5..0000000 --- a/server/internal/websocket/handler/files.go +++ /dev/null @@ -1,62 +0,0 @@ -package handler - -import ( - "errors" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" - "m1k1o/neko/internal/utils" -) - -func (h *MessageHandler) setFileTransferStatus(session types.Session, payload *message.FileTransferStatus) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - 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") - } - - files, err := utils.ListFiles(h.state.FileTransferPath()) - if err != nil { - return err - } - - return session.Send( - message.FileList{ - Event: event.FILETRANSFER_LIST, - Cwd: h.state.FileTransferPath(), - Files: files, - }) -} diff --git a/server/internal/websocket/handler/filetransfer.go b/server/internal/websocket/handler/filetransfer.go new file mode 100644 index 0000000..3ceb4d2 --- /dev/null +++ b/server/internal/websocket/handler/filetransfer.go @@ -0,0 +1,42 @@ +package handler + +import ( + "m1k1o/neko/internal/types" + "m1k1o/neko/internal/types/event" + "m1k1o/neko/internal/types/message" + "m1k1o/neko/internal/utils" +) + +func (h *MessageHandler) FileTransferRefresh(session types.Session) error { + fileTransferPath := h.state.FileTransferPath("") // root + + // allow users only if file transfer is not locked + if session != nil && !(session.Admin() || !h.state.IsLocked("file_transfer")) { + h.logger.Debug().Msg("file transfer is locked for users") + return nil + } + + files, err := utils.ListFiles(fileTransferPath) + if err != nil { + return err + } + + message := message.FileTransferList{ + Event: event.FILETRANSFER_LIST, + Cwd: fileTransferPath, + Files: files, + } + + // send to just one user + if session != nil { + return session.Send(message) + } + + // broadcast to all admins + if h.state.IsLocked("file_transfer") { + return h.sessions.AdminBroadcast(message, nil) + } + + // broadcast to all users + return h.sessions.Broadcast(message, nil) +} diff --git a/server/internal/websocket/handler/handler.go b/server/internal/websocket/handler/handler.go index 985f027..039ba49 100644 --- a/server/internal/websocket/handler/handler.go +++ b/server/internal/websocket/handler/handler.go @@ -127,14 +127,8 @@ 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) + return errors.Wrapf(h.FileTransferRefresh(session), "%s failed", header.Event) // Screen Events case event.SCREEN_RESOLUTION: diff --git a/server/internal/websocket/handler/session.go b/server/internal/websocket/handler/session.go index 33a03a3..da2b1bd 100644 --- a/server/internal/websocket/handler/session.go +++ b/server/internal/websocket/handler/session.go @@ -17,6 +17,7 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error Event: event.SYSTEM_INIT, ImplicitHosting: h.webrtc.ImplicitControl(), Locks: h.state.AllLocked(), + FileTransfer: h.state.FileTransferEnabled(), }); err != nil { h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT) return err @@ -34,14 +35,21 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error } } + // send file list if file transfer is enabled + if h.state.FileTransferEnabled() && (session.Admin() || !h.state.IsLocked("file_transfer")) { + if err := h.FileTransferRefresh(session); err != nil { + return err + } + } + return nil } func (h *MessageHandler) SessionConnected(id string, session types.Session) error { // send list of members to session if err := session.Send(message.MembersList{ - Event: event.MEMBER_LIST, - Memebers: h.sessions.Members(), + Event: event.MEMBER_LIST, + Members: h.sessions.Members(), }); err != nil { h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST) return err diff --git a/server/internal/websocket/state/state.go b/server/internal/websocket/state/state.go index 3eba130..a348c19 100644 --- a/server/internal/websocket/state/state.go +++ b/server/internal/websocket/state/state.go @@ -1,22 +1,22 @@ package state +import "path/filepath" + type State struct { banned map[string]string // IP -> session ID (that banned it) locked map[string]string // resource name -> session ID (that locked it) - fileTransferEnabled bool // admins can transfer files - fileTransferUnprivEnabled bool // all users can transfer files - fileTransferPath string // path where files are located + fileTransferEnabled bool + fileTransferPath string // path where files are located } -func New(fileTransferEnabled bool, fileTransferUnprivEnabled bool, fileTransferPath string) *State { +func New(fileTransferEnabled bool, fileTransferPath string) *State { return &State{ banned: make(map[string]string), locked: make(map[string]string), - fileTransferEnabled: fileTransferEnabled, - fileTransferUnprivEnabled: fileTransferUnprivEnabled, - fileTransferPath: fileTransferPath, + fileTransferEnabled: fileTransferEnabled, + fileTransferPath: fileTransferPath, } } @@ -68,21 +68,17 @@ func (s *State) AllLocked() map[string]string { return s.locked } -// File Transfer +// File transfer + +func (s *State) FileTransferPath(filename string) string { + if filename == "" { + return s.fileTransferPath + } + + cleanPath := filepath.Clean(filename) + return filepath.Join(s.fileTransferPath, cleanPath) +} func (s *State) FileTransferEnabled() bool { return s.fileTransferEnabled } - -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 96d6cd6..6d28ab1 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "os" - "path/filepath" "sync" "sync/atomic" "time" @@ -28,7 +27,7 @@ const CONTROL_PROTECTION_SESSION = "by_control_protection" func New(sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { logger := log.With().Str("module", "websocket").Logger() - state := state.New(conf.FileTransfer, conf.UnprivFileTransfer, conf.FileTransferPath) + state := state.New(conf.FileTransferEnabled, conf.FileTransferPath) // if control protection is enabled if conf.ControlProtection { @@ -36,9 +35,12 @@ func New(sessions types.SessionManager, desktop types.DesktopManager, capture ty logger.Info().Msgf("control locked on behalf of control protection") } - if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) { - err = os.Mkdir(conf.FileTransferPath, os.ModePerm) - logger.Err(err).Msg("creating file transfer directory") + // create file transfer directory if not exists + if conf.FileTransferEnabled { + if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) { + err = os.Mkdir(conf.FileTransferPath, os.ModePerm) + logger.Err(err).Msg("creating file transfer directory") + } } // apply default locks @@ -132,32 +134,6 @@ func (ws *WebSocketHandler) Start() { } } - // send file list if necessary - if ws.state.FileTransferEnabled() && (session.Admin() || 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( - message.FileList{ - Event: event.FILETRANSFER_LIST, - Cwd: ws.conf.FileTransferPath, - Files: files, - }); err != nil { - ws.logger.Warn().Err(err).Msg("file list event has failed") - } - } - } - // remove outdated stats if session.Admin() { ws.lastAdminLeftAt = nil @@ -222,32 +198,35 @@ func (ws *WebSocketHandler) Start() { ws.logger.Err(err).Msg("sync clipboard") }) - // watch for file changes - 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 e, ok := <-watcher.Events: - if !ok { - ws.logger.Info().Msg("file transfer dir watcher closed") - return - } - if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) { - ws.sendFileTransferUpdate() - } - case err := <-watcher.Errors: - ws.logger.Err(err).Msg("error in file transfer dir watcher") - } + // watch for file changes and send file list if file transfer is enabled + if ws.conf.FileTransferEnabled { + watcher, err := fsnotify.NewWatcher() + if err != nil { + ws.logger.Err(err).Msg("unable to start file transfer dir watcher") + return } - }() - if err := watcher.Add(ws.conf.FileTransferPath); err != nil { - ws.logger.Err(err).Msg("unable to add file transfer path to watcher") + go func() { + for { + select { + case e, ok := <-watcher.Events: + if !ok { + ws.logger.Info().Msg("file transfer dir watcher closed") + return + } + if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) { + ws.logger.Debug().Str("event", e.String()).Msg("file transfer dir watcher event") + ws.handler.FileTransferRefresh(nil) + } + 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") + } } } @@ -376,52 +355,6 @@ func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) { return false, fmt.Errorf("invalid password") } -func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { - if !ws.state.FileTransferEnabled() { - return false, nil - } - - isAdmin, err := ws.IsAdmin(password) - if err != nil { - return false, err - } - - return isAdmin || ws.state.UnprivFileTransferEnabled(), nil -} - -func (ws *WebSocketHandler) MakeFilePath(filename string) string { - cleanPath := filepath.Clean(filename) - return filepath.Join(ws.conf.FileTransferPath, cleanPath) -} - -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") - return - } - - message := message.FileList{ - Event: event.FILETRANSFER_LIST, - Cwd: ws.conf.FileTransferPath, - Files: files, - } - - if ws.state.UnprivFileTransferEnabled() { - err = ws.sessions.Broadcast(message, nil) - } else { - err = ws.sessions.AdminBroadcast(message, nil) - } - - if err != nil { - ws.logger.Err(err).Msg("unable to broadcast file list") - } -} - func (ws *WebSocketHandler) authenticate(r *http.Request) (bool, error) { passwords, ok := r.URL.Query()["password"] if !ok || len(passwords[0]) < 1 { @@ -492,3 +425,28 @@ func (ws *WebSocketHandler) handle(connection *websocket.Conn, id string) { } } } + +// +// File transfer +// + +func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { + if !ws.conf.FileTransferEnabled { + return false, nil + } + + isAdmin, err := ws.IsAdmin(password) + if err != nil { + return false, err + } + + return isAdmin || !ws.state.IsLocked("file_transfer"), nil +} + +func (ws *WebSocketHandler) FileTransferPath(filename string) string { + return ws.state.FileTransferPath(filename) +} + +func (ws *WebSocketHandler) FileTransferEnabled() bool { + return ws.conf.FileTransferEnabled +} From 1666693c2574934d63f36493881e8cfe96575180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:33:03 +0100 Subject: [PATCH 21/25] add cors. --- client/src/neko/index.ts | 2 ++ server/go.mod | 3 ++- server/go.sum | 2 ++ server/internal/config/server.go | 19 +++++++++++++++++++ server/internal/http/http.go | 10 ++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index 777520e..f6b56d2 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -47,6 +47,8 @@ export class NekoClient extends BaseClient implements EventEmitter { this.$vue = vue this.$accessor = vue.$accessor this.url = url + // convert ws url to http url + this.$vue.$http.defaults.baseURL = url.replace(/^ws/, 'http').replace(/\/ws$/, '') } private cleanup() { diff --git a/server/go.mod b/server/go.mod index 027b970..a3c7c96 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,8 +3,9 @@ module m1k1o/neko go 1.18 require ( - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 github.com/go-chi/chi v4.1.2+incompatible + github.com/go-chi/cors v1.2.1 github.com/gorilla/websocket v1.5.0 github.com/kataras/go-events v0.0.3 github.com/pion/ice/v2 v2.2.11 // indirect diff --git a/server/go.sum b/server/go.sum index 024d6b2..8fdd49c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -65,6 +65,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/server/internal/config/server.go b/server/internal/config/server.go index 51f0465..d3cd24f 100644 --- a/server/internal/config/server.go +++ b/server/internal/config/server.go @@ -1,6 +1,8 @@ package config import ( + "m1k1o/neko/internal/utils" + "net/http" "path" "github.com/spf13/cobra" @@ -13,6 +15,7 @@ type Server struct { Bind string Static string PathPrefix string + CORS []string } func (Server) Init(cmd *cobra.Command) error { @@ -41,6 +44,11 @@ func (Server) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().StringSlice("cors", []string{"*"}, "list of allowed origins for CORS") + if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil { + return err + } + return nil } @@ -50,4 +58,15 @@ func (s *Server) Set() { s.Bind = viper.GetString("bind") s.Static = viper.GetString("static") s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix"))) + + s.CORS = viper.GetStringSlice("cors") + in, _ := utils.ArrayIn("*", s.CORS) + if len(s.CORS) == 0 || in { + s.CORS = []string{"*"} + } +} + +func (s *Server) AllowOrigin(r *http.Request, origin string) bool { + in, _ := utils.ArrayIn(origin, s.CORS) + return in || s.CORS[0] == "*" } diff --git a/server/internal/http/http.go b/server/internal/http/http.go index bf75d10..b71a806 100644 --- a/server/internal/http/http.go +++ b/server/internal/http/http.go @@ -13,6 +13,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -38,6 +39,15 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t router.Use(middleware.Recoverer) // Recover from panics without crashing server router.Use(middleware.Compress(5, "application/octet-stream")) + router.Use(cors.Handler(cors.Options{ + AllowOriginFunc: conf.AllowOrigin, + AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + if conf.PathPrefix != "/" { router.Use(func(h http.Handler) http.Handler { return http.StripPrefix(conf.PathPrefix, h) From e25e9c2b24cdc1df1dbfcd11b8533d5f726357c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:33:08 +0100 Subject: [PATCH 22/25] lint. --- client/src/components/side.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/side.vue b/client/src/components/side.vue index 33fcfe2..51d4970 100644 --- a/client/src/components/side.vue +++ b/client/src/components/side.vue @@ -95,7 +95,9 @@ }) export default class extends Vue { get filetransferAllowed() { - return this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer')) + return ( + this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer')) + ) } get tab() { From ac822a253141fb63b60bc33d4aa5b5316b688946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:45:38 +0100 Subject: [PATCH 23/25] update docs. --- docs/changelog.md | 2 ++ docs/getting-started/configuration.md | 19 ++++++++++--------- server/internal/config/server.go | 3 ++- server/internal/config/websocket.go | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 9f608a0..5b3653a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,11 +8,13 @@ - Added `NEKO_PATH_PREFIX`. - Added screenshot function `/screenshot.jpg?pwd=`, works only for unlocked rooms. - Added emoji support (by @yesBad). +- Added file transfer (by @prophetofxenu). ### Misc - Server: Split `remote` to `desktop` and `capture`. - Server: Refactored `xorg` - added `xevent` and clipboard is handled as event (no looped polling anymore). - Introduced `NEKO_AUDIO_CODEC=` and `NEKO_VIDEO_CODEC=` as a new way of setting codecs. +- Added CORS. ## [n.eko v2.6](https://github.com/m1k1o/neko/releases/tag/v2.6) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 26a8db2..84e378b 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -32,6 +32,7 @@ nat1to1: - Currently supported: - `control` - `login` + - `file_transfer` - e.g. `control` ### WebRTC @@ -125,18 +126,18 @@ nat1to1: #### `NEKO_PATH_PREFIX`: - Path prefix for HTTP requests. - e.g. `/neko/` +#### `NEKO_CORS`: + - Cross origin request sharing, whitespace separated list of allowed hosts, `*` for all. + - e.g. `127.0.0.1 neko.example.com` ### File Transfer -#### `NEKO_FILE_TRANSFER`: - - Enable file transfer for admins at start - - e.g. `1` -#### `NEKO_UNPRIV_FILE_TRANSFER`: - - Enable file transfer for all users at start. Ignored if NEKO_FILE_TRANSFER not enabled. - - e.g. `1` +#### `NEKO_FILE_TRANSFER_ENABLED`: + - Enable file transfer feature. + - e.g. `true` #### `NEKO_FILE_TRANSFER_PATH`: - Path where files will be transferred between the host and users. By default this is - /home/neko/Downloads. If the path doesn't exist, it will be created. + `/home/neko/Downloads`. If the path doesn't exist, it will be created. - e.g. `/home/neko/Desktop` ### Expert settings @@ -165,10 +166,11 @@ Flags: --broadcast_url string URL for broadcasting, setting this value will automatically enable broadcasting --cert string path to the SSL cert used to secure the neko server --control_protection control protection means, users can gain control only if at least one admin is in the room + --cors strings list of allowed origins for CORS (default [*]) --device string audio device to capture (default "auto_null.monitor") --display string XDisplay to capture (default ":99.0") --epr string limits the pool of ephemeral ports that ICE UDP connections can allocate from (default "59000-59100") - --file_transfer allow file transfer for admins + --file_transfer_enabled enable file transfer feature (default false) --file_transfer_path string path to use for file transfer (default "/home/neko/Downloads") --g722 DEPRECATED: use audio_codec --h264 DEPRECATED: use video_codec @@ -194,7 +196,6 @@ Flags: --static string path to neko client files to serve (default "./www") --tcpmux int single TCP mux port for all peers --udpmux int single UDP mux port for all peers - --unpriv_file_transfer allow file transfer for non admins --video string video codec parameters to use for streaming --video_bitrate int video bitrate in kbit/s (default 3072) --video_codec string video codec to be used (default "vp8") diff --git a/server/internal/config/server.go b/server/internal/config/server.go index d3cd24f..f39766e 100644 --- a/server/internal/config/server.go +++ b/server/internal/config/server.go @@ -1,12 +1,13 @@ package config import ( - "m1k1o/neko/internal/utils" "net/http" "path" "github.com/spf13/cobra" "github.com/spf13/viper" + + "m1k1o/neko/internal/utils" ) type Server struct { diff --git a/server/internal/config/websocket.go b/server/internal/config/websocket.go index 163d9ec..7e534fd 100644 --- a/server/internal/config/websocket.go +++ b/server/internal/config/websocket.go @@ -47,7 +47,7 @@ func (WebSocket) Init(cmd *cobra.Command) error { // File transfer - cmd.PersistentFlags().Bool("file_transfer_enabled", true, "enable file transfer feature") + cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil { return err } From ad5abb6054e6802a28d316c2b3b7ae34af736916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:53:27 +0100 Subject: [PATCH 24/25] css fix long file names. --- client/src/components/files.vue | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 58d4ddf..f4af0f8 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -7,7 +7,7 @@
    -

    {{ item.name }}

    +

    {{ item.name }}

    {{ fileSize(item.size) }}

    @@ -29,7 +29,7 @@ 'fa-warning': download.status === 'failed', }" > -

    {{ download.name }}

    +

    {{ download.name }}

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

    @@ -58,7 +58,7 @@ 'fa-warning': upload.status === 'failed', }" > -

    {{ upload.name }}

    +

    {{ upload.name }}

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

    @@ -164,10 +164,17 @@ margin-left: auto; } + .file-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .file-size { margin-left: auto; margin-right: 0.5em; color: rgba($color: #fff, $alpha: 0.4); + white-space: nowrap; } .refresh:hover, From 3df319e071d93161fe56d99a9c404b42c20b3f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 19 Nov 2022 20:55:50 +0100 Subject: [PATCH 25/25] line height. --- client/src/components/files.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/files.vue b/client/src/components/files.vue index f4af0f8..bce8512 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -136,6 +136,7 @@ border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1); display: flex; flex-direction: row; + line-height: 1.2; } .transfers-list-header {