move filetransfer to locks.

This commit is contained in:
Miroslav Šedivý 2022-11-19 20:26:45 +01:00
parent cdb9b185f2
commit d17a7e8d82
33 changed files with 377 additions and 405 deletions

View File

@ -9,7 +9,7 @@
<i :class="fileIcon(item)" /> <i :class="fileIcon(item)" />
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<p class="file-size">{{ fileSize(item.size) }}</p> <p class="file-size">{{ fileSize(item.size) }}</p>
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="() => download(item)" /> <i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
</div> </div>
</div> </div>
<div class="transfer-area"> <div class="transfer-area">
@ -31,7 +31,7 @@
></i> ></i>
<p>{{ download.name }}</p> <p>{{ download.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p> <p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="() => removeTransfer(download)"></i> <i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
</div> </div>
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div> <div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
<progress <progress
@ -60,7 +60,7 @@
></i> ></i>
<p>{{ upload.name }}</p> <p>{{ upload.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p> <p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="() => removeTransfer(upload)"></i> <i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
</div> </div>
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div> <div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
<progress <progress
@ -75,8 +75,8 @@
<div <div
class="upload-area" class="upload-area"
:class="{ 'upload-area-drag': uploadAreaDrag }" :class="{ 'upload-area-drag': uploadAreaDrag }"
@dragover.prevent="() => (uploadAreaDrag = true)" @dragover.prevent="uploadAreaDrag = true"
@dragleave.prevent="() => (uploadAreaDrag = false)" @dragleave.prevent="uploadAreaDrag = false"
@drop.prevent="(e) => upload(e.dataTransfer)" @drop.prevent="(e) => upload(e.dataTransfer)"
@click="openFileBrowser" @click="openFileBrowser"
> >
@ -322,6 +322,7 @@
.get(url, { .get(url, {
responseType: 'blob', responseType: 'blob',
signal: abortController.signal, signal: abortController.signal,
withCredentials: false,
onDownloadProgress: (x) => { onDownloadProgress: (x) => {
transfer.progress = x.loaded transfer.progress = x.loaded
@ -380,6 +381,7 @@
this.$http this.$http
.post(url, formdata, { .post(url, formdata, {
signal: abortController.signal, signal: abortController.signal,
withCredentials: false,
onUploadProgress: (x: any) => { onUploadProgress: (x: any) => {
transfer.progress = x.loaded transfer.progress = x.loaded

View File

@ -31,6 +31,19 @@
}" }"
/> />
</li> </li>
<li v-if="fileTransfer">
<i
:class="[{ disabled: !admin }, { locked: isLocked('file_transfer') }, 'fas', 'fa-file']"
@click="toggleLock('file_transfer')"
v-tooltip="{
content: lockedTooltip('file_transfer'),
placement: 'bottom',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</li>
<li> <li>
<span v-if="showBadge" class="badge">&bull;</span> <span v-if="showBadge" class="badge">&bull;</span>
<i class="fas fa-bars toggle" @click="toggleMenu" /> <i class="fas fa-bars toggle" @click="toggleMenu" />
@ -169,26 +182,24 @@
return !this.side && this.readTexts != this.texts 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 readTexts: number = 0
toggleMenu() { toggleMenu() {
this.$accessor.client.toggleSide() this.$accessor.client.toggleSide()
this.readTexts = this.texts 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) { lockedTooltip(resource: AdminLockResource) {
if (this.admin) { if (this.admin) {
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`)) return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))

View File

@ -44,20 +44,6 @@
<span /> <span />
</label> </label>
</li> </li>
<li v-if="admin">
<span>{{ $t('setting.file_transfer') }}</span>
<label class="switch">
<input type="checkbox" v-model="file_transfer" />
<span />
</label>
</li>
<li v-if="admin && file_transfer">
<span>{{ $t('setting.unpriv_file_transfer') }}</span>
<label class="switch">
<input type="checkbox" v-model="unpriv_file_transfer" />
<span />
</label>
</li>
<li class="broadcast" v-if="admin"> <li class="broadcast" v-if="admin">
<div> <div>
<span>{{ $t('setting.broadcast_title') }}</span> <span>{{ $t('setting.broadcast_title') }}</span>
@ -380,22 +366,6 @@
return this.$accessor.settings.keyboard_layout 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() { get broadcast_is_active() {
return this.$accessor.settings.broadcast_is_active return this.$accessor.settings.broadcast_is_active
} }

View File

@ -95,10 +95,7 @@
}) })
export default class extends Vue { export default class extends Vue {
get filetransferAllowed() { get filetransferAllowed() {
return ( return this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
(this.$accessor.user.admin && this.$accessor.settings.file_transfer) ||
this.$accessor.settings.unpriv_file_transfer
)
} }
get tab() { get tab() {
@ -106,6 +103,7 @@
} }
@Watch('tab', { immediate: true }) @Watch('tab', { immediate: true })
@Watch('filetransferAllowed', { immediate: true })
onTabChange() { onTabChange() {
// do not show the files tab if file transfer is disabled // do not show the files tab if file transfer is disabled
if (this.tab === 'files' && !this.filetransferAllowed) { if (this.tab === 'files' && !this.filetransferAllowed) {
@ -113,6 +111,13 @@
} }
} }
@Watch('filetransferAllowed')
onFileTransferAllowedChange() {
if (this.filetransferAllowed) {
this.$accessor.files.refresh()
}
}
change(tab: string) { change(tab: string) {
this.$accessor.client.setTab(tab) this.$accessor.client.setTab(tab)
} }

View File

@ -69,6 +69,14 @@ export const locks = {
notif_locked: 'Raum gesperrt', notif_locked: 'Raum gesperrt',
notif_unlocked: 'Raum entsperrt', 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 = { export const setting = {
@ -78,8 +86,6 @@ export const setting = {
ignore_emotes: 'Emotes ignorieren', ignore_emotes: 'Emotes ignorieren',
chat_sound: 'Chat-Sound abspielen', chat_sound: 'Chat-Sound abspielen',
keyboard_layout: 'Tastaturbelegung', keyboard_layout: 'Tastaturbelegung',
file_transfer: 'Dateiübertragung',
unpriv_file_transfer: 'Übertragung von Benutzerdateien',
broadcast_title: 'Live-Übertragung', broadcast_title: 'Live-Übertragung',
} }

View File

@ -71,6 +71,14 @@ export const locks = {
notif_locked: 'locked the room', notif_locked: 'locked the room',
notif_unlocked: 'unlocked 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 = { export const setting = {
@ -80,8 +88,6 @@ export const setting = {
ignore_emotes: 'Ignore Emotes', ignore_emotes: 'Ignore Emotes',
chat_sound: 'Play Chat Sound', chat_sound: 'Play Chat Sound',
keyboard_layout: 'Keyboard Layout', keyboard_layout: 'Keyboard Layout',
file_transfer: 'File Transfer',
unpriv_file_transfer: 'Non-admin File Transfer',
broadcast_title: 'Live Broadcast', broadcast_title: 'Live Broadcast',
} }

View File

@ -75,6 +75,15 @@ export const locks = {
notif_locked: 'bloqueó la sala', notif_locked: 'bloqueó la sala',
notif_unlocked: 'desbloqueó 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 = { export const setting = {
@ -84,8 +93,6 @@ export const setting = {
ignore_emotes: 'Ignorar Emotes', ignore_emotes: 'Ignorar Emotes',
chat_sound: 'Reproducir Sonidos Chat', chat_sound: 'Reproducir Sonidos Chat',
keyboard_layout: 'Keyboard Layout', keyboard_layout: 'Keyboard Layout',
file_transfer: 'Transferencia de archivos',
unpriv_file_transfer: 'Transferencia de archivos de usuario',
// TODO // TODO
//broadcast_title: 'Live Broadcast', //broadcast_title: 'Live Broadcast',
} }

View File

@ -71,6 +71,15 @@ export const locks = {
notif_locked: 'lukittu huone', notif_locked: 'lukittu huone',
notif_unlocked: 'vapautettu 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 = { export const setting = {
@ -80,8 +89,6 @@ export const setting = {
ignore_emotes: 'Estä emojit', ignore_emotes: 'Estä emojit',
chat_sound: 'Soita viesti ääni', chat_sound: 'Soita viesti ääni',
keyboard_layout: 'Näppäimistöasettelu', keyboard_layout: 'Näppäimistöasettelu',
file_transfer: 'Tiedoston siirto',
unpriv_file_transfer: 'Käyttäjän tiedostojen siirto',
broadcast_title: 'Suora Lähetys', broadcast_title: 'Suora Lähetys',
} }

View File

@ -75,6 +75,15 @@ export const locks = {
notif_locked: 'a vérouillé la salle', notif_locked: 'a vérouillé la salle',
notif_unlocked: 'a dé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 = { export const setting = {
@ -84,8 +93,6 @@ export const setting = {
ignore_emotes: 'Ignorer les Emotes', ignore_emotes: 'Ignorer les Emotes',
chat_sound: 'Jouer le son du tchat', chat_sound: 'Jouer le son du tchat',
keyboard_layout: 'Langue du clavier', keyboard_layout: 'Langue du clavier',
file_transfer: 'Transfert de fichiers',
unpriv_file_transfer: "Transfert de fichiers d'utilisateurs",
// TODO // TODO
//broadcast_title: 'Live Broadcast', //broadcast_title: 'Live Broadcast',
} }

View File

@ -69,6 +69,15 @@ export const locks = {
notif_locked: '방이 잠겼습니다', notif_locked: '방이 잠겼습니다',
notif_unlocked: '방 잠금이 해제됐습니다', 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 = { export const setting = {
@ -78,8 +87,6 @@ export const setting = {
ignore_emotes: '이모지 무시', ignore_emotes: '이모지 무시',
chat_sound: '채팅 소리 재생', chat_sound: '채팅 소리 재생',
keyboard_layout: '키보드 레이아웃', keyboard_layout: '키보드 레이아웃',
file_transfer: '파일 전송',
unpriv_file_transfer: '사용자 파일 전송',
broadcast_title: '실시간 방송', broadcast_title: '실시간 방송',
} }

View File

@ -75,6 +75,15 @@ export const locks = {
notif_locked: 'låste rommet', notif_locked: 'låste rommet',
notif_unlocked: 'låste opp 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 = { export const setting = {
@ -84,8 +93,6 @@ export const setting = {
ignore_emotes: 'Ignorer smilefjes', ignore_emotes: 'Ignorer smilefjes',
chat_sound: 'Sludringslyd', chat_sound: 'Sludringslyd',
keyboard_layout: 'Tastaturoppsett', keyboard_layout: 'Tastaturoppsett',
file_transfer: 'Filoverførsel',
unpriv_file_transfer: 'Overførsel af brugerfiler',
// TODO // TODO
//broadcast_title: 'Live Broadcast', //broadcast_title: 'Live Broadcast',
} }

View File

@ -71,6 +71,15 @@ export const locks = {
notif_locked: 'комната закрыта', notif_locked: 'комната закрыта',
notif_unlocked: 'комната открыта', 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 = { export const setting = {
@ -80,8 +89,6 @@ export const setting = {
ignore_emotes: 'Игнорировать эмоции', ignore_emotes: 'Игнорировать эмоции',
chat_sound: 'Проигрывать звук чата', chat_sound: 'Проигрывать звук чата',
keyboard_layout: 'Раскладка клавиатуры', keyboard_layout: 'Раскладка клавиатуры',
file_transfer: 'Передача файлов',
unpriv_file_transfer: 'Передача файлов пользователей',
broadcast_title: 'Прямой эфир', broadcast_title: 'Прямой эфир',
} }

View File

@ -74,6 +74,15 @@ export const locks = {
notif_locked: 'miestnosť bola zamknutá', notif_locked: 'miestnosť bola zamknutá',
notif_unlocked: 'miestnosť bola odomknutá', 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 = { export const setting = {
@ -83,8 +92,6 @@ export const setting = {
ignore_emotes: 'Ignorovať smajlíky', ignore_emotes: 'Ignorovať smajlíky',
chat_sound: 'Prehrávať zvuky chatu', chat_sound: 'Prehrávať zvuky chatu',
keyboard_layout: 'Rozloženie klávesnice', keyboard_layout: 'Rozloženie klávesnice',
file_transfer: 'Prenos súborov',
unpriv_file_transfer: 'Prenos súborov používateľa',
broadcast_title: 'Živé vysielanie', broadcast_title: 'Živé vysielanie',
} }

View File

@ -75,6 +75,15 @@ export const locks = {
notif_locked: 'låste rummet', notif_locked: 'låste rummet',
notif_unlocked: 'låste upp 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 = { export const setting = {
@ -84,8 +93,6 @@ export const setting = {
ignore_emotes: 'Ignorera Emotes', ignore_emotes: 'Ignorera Emotes',
chat_sound: 'Spela Chatt Ljud', chat_sound: 'Spela Chatt Ljud',
keyboard_layout: 'Tangentbordslayout', keyboard_layout: 'Tangentbordslayout',
file_transfer: 'Överföring av filer',
unpriv_file_transfer: 'Överföring av användarfiler',
// TODO // TODO
//broadcast_title: 'Live Broadcast', //broadcast_title: 'Live Broadcast',
} }

View File

@ -71,6 +71,15 @@ export const locks = {
notif_locked: '锁上房间', notif_locked: '锁上房间',
notif_unlocked: '解锁房间', 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 = { export const setting = {
@ -80,8 +89,6 @@ export const setting = {
ignore_emotes: '忽略表情符号', ignore_emotes: '忽略表情符号',
chat_sound: '播放聊天声音', chat_sound: '播放聊天声音',
keyboard_layout: '键盘布局', keyboard_layout: '键盘布局',
file_transfer: '文件传输',
unpriv_file_transfer: '用户文件传输',
broadcast_title: '现场流媒体', broadcast_title: '现场流媒体',
} }

View File

@ -39,7 +39,6 @@ export const EVENT = {
EMOTE: 'chat/emote', EMOTE: 'chat/emote',
}, },
FILETRANSFER: { FILETRANSFER: {
STATUS: 'filetransfer/status',
LIST: 'filetransfer/list', LIST: 'filetransfer/list',
REFRESH: 'filetransfer/refresh', REFRESH: 'filetransfer/refresh',
}, },
@ -98,10 +97,7 @@ export type SignalEvents =
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
export type FileTransferEvents = export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH
| typeof EVENT.FILETRANSFER.STATUS
| typeof EVENT.FILETRANSFER.LIST
| typeof EVENT.FILETRANSFER.REFRESH
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET

View File

@ -25,7 +25,6 @@ import {
SystemInitPayload, SystemInitPayload,
AdminLockResource, AdminLockResource,
FileTransferListPayload, FileTransferListPayload,
FileTransferStatusPayload,
} from './messages' } from './messages'
interface NekoEvents extends BaseEvents {} interface NekoEvents extends BaseEvents {}
@ -135,8 +134,9 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
///////////////////////////// /////////////////////////////
// System Events // 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.setImplicitHosting(implicit_hosting)
this.$accessor.remote.setFileTransfer(file_transfer)
for (const resource in locks) { for (const resource in locks) {
this[EVENT.ADMIN.LOCK]({ this[EVENT.ADMIN.LOCK]({
@ -354,12 +354,8 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
} }
///////////////////////////// /////////////////////////////
// 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) { protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
this.$accessor.files.setCwd(cwd) this.$accessor.files.setCwd(cwd)
this.$accessor.files.setFileList(files) this.$accessor.files.setFileList(files)

View File

@ -39,7 +39,6 @@ export type WebSocketPayloads =
| ChatPayload | ChatPayload
| ChatSendPayload | ChatSendPayload
| EmojiSendPayload | EmojiSendPayload
| FileTransferStatusPayload
| ScreenResolutionPayload | ScreenResolutionPayload
| ScreenConfigurationsPayload | ScreenConfigurationsPayload
| AdminPayload | AdminPayload
@ -61,6 +60,7 @@ export interface SystemInit extends WebSocketMessage, SystemInitPayload {
export interface SystemInitPayload { export interface SystemInitPayload {
implicit_hosting: boolean implicit_hosting: boolean
locks: Record<string, string> locks: Record<string, string>
file_transfer: boolean
} }
// system/disconnect // system/disconnect
@ -197,16 +197,6 @@ export interface EmojiSendPayload {
/* /*
FILE TRANSFER PAYLOADS 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 { export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
event: FileTransferEvents event: FileTransferEvents
} }
@ -272,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload {
id: string id: string
} }
export type AdminLockResource = 'login' | 'control' export type AdminLockResource = 'login' | 'control' | 'file_transfer'
export interface AdminLockPayload { export interface AdminLockPayload {
resource: AdminLockResource resource: AdminLockResource

View File

@ -1,6 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' 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 { EVENT } from '~/neko/events'
import { AdminLockResource } from '~/neko/messages' import { AdminLockResource } from '~/neko/messages'
import { get, set } from '~/utils/localstorage' 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( export const actions = actionTree(
{ state, mutations }, { state, getters, mutations },
{ {
initialise(store) { initialise(store) {
accessor.emoji.initialise() accessor.emoji.initialise()
@ -80,6 +84,14 @@ export const actions = actionTree(
$client.sendMessage(EVENT.ADMIN.UNLOCK, { resource }) $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 }) { login({ state }, { displayname, password }: { displayname: string; password: string }) {
accessor.setLogin({ displayname, password }) accessor.setLogin({ displayname, password })
$client.login(password, displayname) $client.login(password, displayname)
@ -98,6 +110,7 @@ export const storePattern = {
state, state,
mutations, mutations,
actions, actions,
getters,
modules: { video, chat, files, user, remote, settings, client, emoji }, modules: { video, chat, files, user, remote, settings, client, emoji },
} }

View File

@ -13,6 +13,7 @@ export const state = () => ({
clipboard: '', clipboard: '',
locked: false, locked: false,
implicitHosting: true, implicitHosting: true,
fileTransfer: true,
keyboardModifierState: -1, keyboardModifierState: -1,
}) })
@ -53,6 +54,10 @@ export const mutations = mutationTree(state, {
state.implicitHosting = val state.implicitHosting = val
}, },
setFileTransfer(state, val: boolean) {
state.fileTransfer = val
},
reset(state) { reset(state) {
state.id = '' state.id = ''
state.clipboard = '' state.clipboard = ''

View File

@ -20,9 +20,6 @@ export const state = () => {
keyboard_layouts_list: {} as KeyboardLayouts, keyboard_layouts_list: {} as KeyboardLayouts,
file_transfer: false,
unpriv_file_transfer: false,
broadcast_is_active: false, broadcast_is_active: false,
broadcast_url: '', broadcast_url: '',
} }
@ -61,14 +58,6 @@ export const mutations = mutationTree(state, {
set('keyboard_layout', value) 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) { setKeyboardLayoutsList(state, value: KeyboardLayouts) {
state.keyboard_layouts_list = value 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 }) { broadcastStatus({ getters }, { url, isActive }) {
accessor.settings.setBroadcastStatus({ url, isActive }) accessor.settings.setBroadcastStatus({ url, isActive })
}, },

View File

@ -15,9 +15,8 @@ type WebSocket struct {
ControlProtection bool ControlProtection bool
FileTransfer bool FileTransferEnabled bool
UnprivFileTransfer bool FileTransferPath string
FileTransferPath string
} }
func (WebSocket) Init(cmd *cobra.Command) error { func (WebSocket) Init(cmd *cobra.Command) error {
@ -46,13 +45,10 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().Bool("file_transfer", false, "allow file transfer for admins") // File transfer
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") cmd.PersistentFlags().Bool("file_transfer_enabled", true, "enable file transfer feature")
if err := viper.BindPFlag("unpriv_file_transfer", cmd.PersistentFlags().Lookup("unpriv_file_transfer")); err != nil { if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
return err return err
} }
@ -72,8 +68,7 @@ func (s *WebSocket) Set() {
s.ControlProtection = viper.GetBool("control_protection") s.ControlProtection = viper.GetBool("control_protection")
s.FileTransfer = viper.GetBool("file_transfer") s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
s.UnprivFileTransfer = viper.GetBool("unpriv_file_transfer")
s.FileTransferPath = viper.GetString("file_transfer_path") s.FileTransferPath = viper.GetString("file_transfer_path")
s.FileTransferPath = filepath.Clean(s.FileTransferPath) s.FileTransferPath = filepath.Clean(s.FileTransferPath)
} }

View File

@ -105,70 +105,77 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
} }
}) })
router.Get("/file", func(w http.ResponseWriter, r *http.Request) { // allow downloading and uploading files
password := r.URL.Query().Get("pwd") if webSocketHandler.FileTransferEnabled() {
isAuthorized, err := webSocketHandler.CanTransferFiles(password) router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
if err != nil { password := r.URL.Query().Get("pwd")
http.Error(w, err.Error(), http.StatusForbidden) isAuthorized, err := webSocketHandler.CanTransferFiles(password)
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()
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to open formdata file") http.Error(w, err.Error(), http.StatusForbidden)
http.Error(w, "error writing file", http.StatusInternalServerError)
return 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 { 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 return
} }
defer f.Close() 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) { router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("true")) _, _ = w.Write([]byte("true"))

View File

@ -35,7 +35,6 @@ const (
) )
const ( const (
FILETRANSFER_STATUS = "filetransfer/status"
FILETRANSFER_LIST = "filetransfer/list" FILETRANSFER_LIST = "filetransfer/list"
FILETRANSFER_REFRESH = "filetransfer/refresh" FILETRANSFER_REFRESH = "filetransfer/refresh"
) )

View File

@ -14,6 +14,7 @@ type SystemInit struct {
Event string `json:"event"` Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"` ImplicitHosting bool `json:"implicit_hosting"`
Locks map[string]string `json:"locks"` Locks map[string]string `json:"locks"`
FileTransfer bool `json:"file_transfer"`
} }
type SystemMessage struct { type SystemMessage struct {
@ -47,8 +48,8 @@ type SignalCandidate struct {
} }
type MembersList struct { type MembersList struct {
Event string `json:"event"` Event string `json:"event"`
Memebers []*types.Member `json:"members"` Members []*types.Member `json:"members"`
} }
type Member struct { type Member struct {
@ -106,17 +107,7 @@ type EmoteSend struct {
Emote string `json:"emote"` Emote string `json:"emote"`
} }
type FileTransferTarget struct { type FileTransferList struct {
Event string `json:"event"`
}
type FileTransferStatus struct {
Event string `json:"event"`
Admin bool `json:"admin"`
Unpriv bool `json:"unpriv"`
}
type FileList struct {
Event string `json:"event"` Event string `json:"event"`
Cwd string `json:"cwd"` Cwd string `json:"cwd"`
Files []types.FileListItem `json:"files"` Files []types.FileListItem `json:"files"`

View File

@ -34,8 +34,11 @@ type WebSocketHandler interface {
Stats() Stats Stats() Stats
IsLocked(resource string) bool IsLocked(resource string) bool
IsAdmin(password string) (bool, error) IsAdmin(password string) (bool, error)
// File Transfer
CanTransferFiles(password string) (bool, error) CanTransferFiles(password string) (bool, error)
MakeFilePath(filename string) string FileTransferPath(filename string) string
FileTransferEnabled() bool
} }
type FileListItem struct { type FileListItem struct {

View File

@ -19,7 +19,12 @@ func (h *MessageHandler) adminLock(id string, session types.Session, payload *me
return nil 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") h.logger.Debug().Msg("unknown lock resource")
return nil return nil
} }

View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -127,14 +127,8 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
}), "%s failed", header.Event) }), "%s failed", header.Event)
// File Transfer Events // 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: 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 // Screen Events
case event.SCREEN_RESOLUTION: case event.SCREEN_RESOLUTION:

View File

@ -17,6 +17,7 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
Event: event.SYSTEM_INIT, Event: event.SYSTEM_INIT,
ImplicitHosting: h.webrtc.ImplicitControl(), ImplicitHosting: h.webrtc.ImplicitControl(),
Locks: h.state.AllLocked(), Locks: h.state.AllLocked(),
FileTransfer: h.state.FileTransferEnabled(),
}); err != nil { }); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT) h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
return err 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 return nil
} }
func (h *MessageHandler) SessionConnected(id string, session types.Session) error { func (h *MessageHandler) SessionConnected(id string, session types.Session) error {
// send list of members to session // send list of members to session
if err := session.Send(message.MembersList{ if err := session.Send(message.MembersList{
Event: event.MEMBER_LIST, Event: event.MEMBER_LIST,
Memebers: h.sessions.Members(), Members: h.sessions.Members(),
}); err != nil { }); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST) h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST)
return err return err

View File

@ -1,22 +1,22 @@
package state package state
import "path/filepath"
type State struct { type State struct {
banned map[string]string // IP -> session ID (that banned it) banned map[string]string // IP -> session ID (that banned it)
locked map[string]string // resource name -> session ID (that locked it) locked map[string]string // resource name -> session ID (that locked it)
fileTransferEnabled bool // admins can transfer files fileTransferEnabled bool
fileTransferUnprivEnabled bool // all users can transfer files fileTransferPath string // path where files are located
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{ return &State{
banned: make(map[string]string), banned: make(map[string]string),
locked: make(map[string]string), locked: make(map[string]string),
fileTransferEnabled: fileTransferEnabled, fileTransferEnabled: fileTransferEnabled,
fileTransferUnprivEnabled: fileTransferUnprivEnabled, fileTransferPath: fileTransferPath,
fileTransferPath: fileTransferPath,
} }
} }
@ -68,21 +68,17 @@ func (s *State) AllLocked() map[string]string {
return s.locked 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 { func (s *State) FileTransferEnabled() bool {
return s.fileTransferEnabled 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
}

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "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 { 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() 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 control protection is enabled
if conf.ControlProtection { 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") logger.Info().Msgf("control locked on behalf of control protection")
} }
if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) { // create file transfer directory if not exists
err = os.Mkdir(conf.FileTransferPath, os.ModePerm) if conf.FileTransferEnabled {
logger.Err(err).Msg("creating 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 // 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 // remove outdated stats
if session.Admin() { if session.Admin() {
ws.lastAdminLeftAt = nil ws.lastAdminLeftAt = nil
@ -222,32 +198,35 @@ func (ws *WebSocketHandler) Start() {
ws.logger.Err(err).Msg("sync clipboard") ws.logger.Err(err).Msg("sync clipboard")
}) })
// watch for file changes // watch for file changes and send file list if file transfer is enabled
watcher, err := fsnotify.NewWatcher() if ws.conf.FileTransferEnabled {
if err != nil { watcher, err := fsnotify.NewWatcher()
ws.logger.Err(err).Msg("unable to start file transfer dir watcher") if err != nil {
return 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")
}
} }
}()
if err := watcher.Add(ws.conf.FileTransferPath); err != nil { go func() {
ws.logger.Err(err).Msg("unable to add file transfer path to watcher") 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") 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) { func (ws *WebSocketHandler) authenticate(r *http.Request) (bool, error) {
passwords, ok := r.URL.Query()["password"] passwords, ok := r.URL.Query()["password"]
if !ok || len(passwords[0]) < 1 { 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
}