diff --git a/client/src/components/files.vue b/client/src/components/files.vue index 84b34c83..58d4ddff 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 7db5e10c..2f6506fb 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 2535cb27..e6c991bd 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 7404a98c..33fcfe2e 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 7eac5b4e..c163c6fa 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 db9dfe8f..edc7f2bb 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 0aecc123..d8bf6b96 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 a63eb90a..105cbf2d 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 7481dc36..67d49eaa 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 10e6a023..d4fd19f4 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 63667847..ec3c9699 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 faa8ea17..cfb3a634 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 f5cf9f80..5176daee 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 3ea55e39..f65cd4d5 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 b709d639..7737f1d3 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 8dfdbd85..5deb010a 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 eefa6d68..777520e2 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 4e179acd..54d1cb11 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 11bafe2c..662b0050 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 c6834f7c..ff99e1c7 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 8b41112c..3d99fad3 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 377e141e..163d9ec7 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 9b8a2370..bf75d103 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 4b6b047c..a091ab67 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 27a83d6d..89552b71 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 cb80fccd..15b684c9 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 7b9f1eeb..6fb1edeb 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 4ac85f5c..00000000 --- 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 00000000..3ceb4d2d --- /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 985f0278..039ba492 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 33a03a31..da2b1bde 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 3eba130d..a348c19c 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 96d6cd6a..6d28ab1a 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 +}