3 Commits

Author SHA1 Message Date
7cc53a0429 remove locks. 2024-04-21 20:37:58 +02:00
014b68e1fb remove control protection. 2024-04-21 20:16:09 +02:00
5c683fb1b8 remove filetransfer. 2024-04-20 11:08:17 +02:00
38 changed files with 19 additions and 1706 deletions

View File

@ -1,6 +1,6 @@
<template>
<ul>
<li v-if="!implicitHosting && (!controlLocked || hosting)">
<li v-if="!implicitHosting && hosting">
<i
:class="[
!disabeld && shakeKbd ? 'shake' : '',
@ -22,9 +22,9 @@
</li>
<li class="no-pointer" v-if="implicitHosting">
<i
:class="[controlLocked ? 'disabled' : '', 'fas', 'fa-mouse-pointer']"
:class="['fas', 'fa-mouse-pointer']"
v-tooltip="{
content: controlLocked ? $t('controls.hasnot') : $t('controls.has'),
content: $t('controls.has'),
placement: 'top',
offset: 5,
boundariesElement: 'body',
@ -32,7 +32,7 @@
}"
/>
</li>
<li v-if="implicitHosting || (!implicitHosting && (!controlLocked || hosting))">
<li v-if="implicitHosting || (!implicitHosting && hosting)">
<label
class="switch"
v-tooltip="{
@ -43,7 +43,7 @@
delay: { show: 300, hide: 100 },
}"
>
<input type="checkbox" v-model="locked" :disabled="!hosting || (implicitHosting && controlLocked)" />
<input type="checkbox" v-model="locked" :disabled="!hosting || implicitHosting" />
<span />
</label>
</li>
@ -258,10 +258,6 @@
export default class extends Vue {
@Prop(Boolean) readonly shakeKbd!: boolean
get controlLocked() {
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
}
get disabeld() {
return this.$accessor.remote.hosted
}

View File

@ -1,520 +0,0 @@
<template>
<div class="files">
<div class="files-cwd">
<p>{{ cwd }}</p>
<i class="fas fa-rotate-right refresh" @click="refresh" />
</div>
<div class="files-list">
<div v-for="item in files" :key="item.name" class="files-list-item">
<i :class="fileIcon(item)" />
<p class="file-name" :title="item.name">{{ item.name }}</p>
<p class="file-size">{{ fileSize(item.size) }}</p>
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
</div>
</div>
<div class="transfer-area">
<div class="transfers" v-if="transfers.length > 0">
<p v-if="downloads.length > 0" class="transfers-list-header">
<span>{{ $t('files.downloads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:class="{
'fa-clock': download.status === 'pending',
'fa-arrows-rotate': download.status === 'inprogress',
'fa-check': download.status === 'completed',
'fa-warning': download.status === 'failed',
}"
></i>
<p class="file-name" :title="download.name">{{ download.name }}</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>
</div>
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="download.name + ' progress'"
:value="download.progress"
:max="download.size"
></progress>
</div>
<p v-if="uploads.length > 0" class="transfers-list-header">
<span>{{ $t('files.uploads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:title="upload.status"
:class="{
'fa-clock': upload.status === 'pending',
'fa-arrows-rotate': upload.status === 'inprogress',
'fa-check': upload.status === 'completed',
'fa-warning': upload.status === 'failed',
}"
></i>
<p class="file-name" :title="upload.name">{{ upload.name }}</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>
</div>
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="upload.name + ' progress'"
:value="upload.progress"
:max="upload.size"
></progress>
</div>
</div>
<div
class="upload-area"
:class="{ 'upload-area-drag': uploadAreaDrag }"
@dragover.prevent="uploadAreaDrag = true"
@dragleave.prevent="uploadAreaDrag = false"
@drop.prevent="(e) => upload(e.dataTransfer)"
@click="openFileBrowser"
>
<i class="fas fa-file-arrow-up" />
<p>{{ $t('files.upload_here') }}</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.files {
flex: 1;
flex-direction: column;
display: flex;
max-width: 100%;
.files-cwd {
display: flex;
flex-direction: row;
margin: 10px 10px 0px 10px;
padding: 0.5em;
font-weight: 600;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.files-list {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
overflow-y: scroll;
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;
}
}
.files-list-item {
padding: 0.5em;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
display: flex;
flex-direction: row;
line-height: 1.2;
}
.transfers-list-header {
display: flex;
justify-content: space-between;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
}
.file-icon,
.transfer-status {
width: 14px;
margin-right: 0.5em;
}
.transfer-error {
border: 1px solid $style-error;
border-radius: 5px;
padding: 10px;
}
.files-list-item:last-child {
border-bottom: 0px;
}
.refresh {
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,
.download:hover,
.remove-transfer:hover {
cursor: pointer;
}
.transfer-area {
margin-top: auto;
}
.transfers {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
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 {
padding: 10px;
font-weight: 600;
}
.transfer-info {
display: flex;
flex-direction: row;
max-width: 100%;
padding: 10px;
}
.transfer-progress {
margin: 0px 10px 10px 10px;
width: 95%;
}
.upload-area {
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.upload-area:hover {
cursor: pointer;
}
.upload-area-drag,
.upload-area:hover {
background-color: rgba($color: #fff, $alpha: 0.1);
}
.upload-area > i {
font-size: 4em;
margin: 10px 10px 10px 10px;
}
.upload-area > p {
margin: 0px 10px 10px 10px;
}
}
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Markdown from './markdown'
import Content from './context.vue'
import { FileTransfer, FileListItem } from '~/neko/types'
@Component({
name: 'neko-files',
components: {
'neko-markdown': Markdown,
'neko-context': Content,
},
})
export default class extends Vue {
public uploadAreaDrag: boolean = false
get cwd() {
return this.$accessor.files.cwd
}
get files() {
return this.$accessor.files.files
}
get transfers() {
return this.$accessor.files.transfers
}
get downloads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'download')
}
get uploads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'upload')
}
refresh() {
this.$accessor.files.refresh()
}
download(item: FileListItem) {
if (this.downloads.map((t) => t.name).includes(item.name)) {
return
}
const url =
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
const abortController = new AbortController()
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: item.name,
direction: 'download',
// this may be smaller than the actual transfer amount, but for large files the
// content length is not sent (chunked transfer)
size: item.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.get(url, {
responseType: 'blob',
signal: abortController.signal,
withCredentials: false,
onDownloadProgress: (x) => {
transfer.progress = x.loaded
if (x.total && transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.then((res) => {
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', item.name)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
transfer.progress = transfer.size
transfer.status = 'completed'
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
upload(dt: DataTransfer) {
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password)
this.uploadAreaDrag = false
for (const file of dt.files) {
const abortController = new AbortController()
const formdata = new FormData()
formdata.append('files', file, file.name)
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: file.name,
direction: 'upload',
size: file.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.post(url, formdata, {
signal: abortController.signal,
withCredentials: false,
onUploadProgress: (x: any) => {
transfer.progress = x.loaded
if (transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
}
openFileBrowser() {
const input = document.createElement('input')
input.type = 'file'
input.setAttribute('multiple', 'true')
input.onchange = (e: Event) => {
if (e === null) return
const dt = new DataTransfer()
const target = e.target as HTMLInputElement
if (target.files === null) return
for (const f of target.files) {
dt.items.add(f)
}
this.upload(dt)
}
input.click()
}
removeTransfer(transfer: FileTransfer) {
if (transfer.status !== 'completed') {
transfer.abortController?.abort()
}
this.$accessor.files.removeTransfer(transfer)
}
fileIcon(file: FileListItem) {
let className = 'file-icon fas '
// if is directory
if (file.type === 'dir') {
className += 'fa-folder'
return className
}
// try to get file extension
const ext = file.name.split('.').pop()
if (ext === undefined) {
className += 'fa-file'
return className
}
// try to find icon
switch (ext.toLowerCase()) {
case 'txt':
case 'md':
className += 'fa-file-text'
break
case 'pdf':
className += 'fa-file-pdf'
break
case 'zip':
case 'rar':
case '7z':
case 'gz':
className += 'fa-archive'
break
case 'aac':
case 'flac':
case 'midi':
case 'mp3':
case 'ogg':
case 'wav':
className += 'fa-music'
break
case 'avi':
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'
}
return className
}
fileSize(size: number) {
if (size < 1024) {
return size + ' B'
}
if (size < 1024 * 1024) {
return Math.round(size / 1024) + ' KB'
}
if (size < 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024)) + ' MB'
}
if (size < 1024 * 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024 * 1024)) + ' GB'
}
return Math.round(size / (1024 * 1024 * 1024 * 1024)) + ' TB'
}
}
</script>

View File

@ -5,45 +5,6 @@
<span><b>n</b>.eko</span>
</a>
<ul class="menu">
<li>
<i
:class="[{ disabled: !admin }, { locked: isLocked('control') }, 'fas', 'fa-mouse']"
@click="toggleLock('control')"
v-tooltip="{
content: lockedTooltip('control'),
placement: 'bottom',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</li>
<li>
<i
:class="[{ disabled: !admin }, { locked: isLocked('login') }, locked ? 'fa-lock' : 'fa-lock-open', 'fas']"
@click="toggleLock('login')"
v-tooltip="{
content: lockedTooltip('login'),
placement: 'bottom',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</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>
<span v-if="showBadge" class="badge">&bull;</span>
<i class="fas fa-bars toggle" @click="toggleMenu" />
@ -162,14 +123,6 @@
@Component({ name: 'neko-settings' })
export default class extends Vue {
get admin() {
return this.$accessor.user.admin
}
get locked() {
return this.$accessor.locked
}
get side() {
return this.$accessor.client.side
}
@ -182,30 +135,10 @@
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
}
lockedTooltip(resource: AdminLockResource) {
if (this.admin) {
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))
}
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `locked` : `unlocked`))
}
}
</script>

View File

@ -6,10 +6,6 @@
<i class="fas fa-comment-alt" />
<span>{{ $t('side.chat') }}</span>
</li>
<li v-if="filetransferAllowed" :class="{ active: tab === 'files' }" @click.stop.prevent="change('files')">
<i class="fas fa-file" />
<span>{{ $t('side.files') }}</span>
</li>
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
<i class="fas fa-sliders-h" />
<span>{{ $t('side.settings') }}</span>
@ -79,47 +75,23 @@
</style>
<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator'
import { Vue, Component } from 'vue-property-decorator'
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 {
get filetransferAllowed() {
return (
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
)
}
get tab() {
return this.$accessor.client.tab
}
@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) {
this.change('chat')
}
}
@Watch('filetransferAllowed')
onFileTransferAllowedChange() {
if (this.filetransferAllowed) {
this.$accessor.files.refresh()
}
}
change(tab: string) {
this.$accessor.client.setTab(tab)
}

View File

@ -52,33 +52,6 @@ export const controls = {
unlock: 'Steuerung entsperren',
}
export const locks = {
control: {
lock: 'Steuerung sperren (für Nutzer)',
unlock: 'Steuerung entsperren (für Nutzer)',
locked: 'Steuerung gesperrt (für Nutzer)',
unlocked: 'Steuerung entsperrt (für Nutzer)',
notif_locked: 'Steuerung sperren für Nutzer',
notif_unlocked: 'Steuerung entsperren für Nutzer',
},
login: {
lock: 'Raum sperren (für Nutzer)',
unlock: 'Raum entsperren (für Nutzer)',
locked: 'Raum gesperrt (für Nutzer)',
unlocked: 'Raum entsperrt (für Nutzer)',
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 = {
scroll: 'Scroll-Empfindlichkeit',
scroll_invert: 'Bildlauf umkehren',

View File

@ -54,33 +54,6 @@ export const controls = {
hasnot: 'You do not have control',
}
export const locks = {
control: {
lock: 'Lock Controls (for users)',
unlock: 'Unlock Controls (for users)',
locked: 'Controls Locked (for users)',
unlocked: 'Controls Unlocked (for users)',
notif_locked: 'locked controls for users',
notif_unlocked: 'unlocked controls for users',
},
login: {
lock: 'Lock Room (for users)',
unlock: 'Unlock Room (for users)',
locked: 'Room Locked (for users)',
unlocked: 'Room Unlocked (for users)',
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 = {
scroll: 'Scroll Sensitivity',
scroll_invert: 'Invert Scroll',

View File

@ -57,35 +57,6 @@ export const controls = {
//hasnot: 'You do not have control',
}
export const locks = {
// TODO
//control: {
// lock: 'Lock Controls (for users)',
// unlock: 'Unlock Controls (for users)',
// locked: 'Controls Locked (for users)',
// unlocked: 'Controls Unlocked (for users)',
// notif_locked: 'locked controls for users',
// notif_unlocked: 'unlocked controls for users',
//},
login: {
lock: 'Bloquear sala (para usuarios)',
unlock: 'Desbloquear sala (para usuarios)',
locked: 'Sala bloqueada (para usuarios)',
unlocked: 'Sala desbloqueada (para usuarios)',
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 = {
scroll: 'Sensibilidad del Scroll',
scroll_invert: 'Invertir Scroll',

View File

@ -54,34 +54,6 @@ export const controls = {
hasnot: 'Sinulle ei ole kontrolleja',
}
export const locks = {
control: {
lock: 'Lukitse kontrollit (käyttäjiltä)',
unlock: 'Vapauta kontrollit (käyttäjiltä)',
locked: 'Kontrollit lukittu (käyttäjiltä)',
unlocked: 'Kontrollit vapautettu (käyttäjiltä)',
notif_locked: 'kontrollit on lukittu käyttäjiltä',
notif_unlocked: 'kontrollit on vapautettu käyttäjille',
},
login: {
lock: 'Lukitse huone (käyttäjiltä)',
unlock: 'Vapauta huone (käyttäjiltä)',
locked: 'Huone lukittu (käyttäjiltä)',
unlocked: 'Huone vapautettu (käyttäjiltä)',
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 = {
scroll: 'Scrollin herkkyys',
scroll_invert: 'Käänteinen Scroll',

View File

@ -57,35 +57,6 @@ export const controls = {
// hasnot: 'You do not have control',
}
export const locks = {
// TODO
//control: {
// lock: 'Lock Controls (for users)',
// unlock: 'Unlock Controls (for users)',
// locked: 'Controls Locked (for users)',
// unlocked: 'Controls Unlocked (for users)',
// notif_locked: 'locked controls for users',
// notif_unlocked: 'unlocked controls for users',
//},
login: {
lock: 'Vérouiller la salle (pour les utilisateurs)',
unlock: 'Dévérouiller la salle (pour les utilisateurs)',
locked: 'Salle vérouillée (pour les utilisateurs)',
unlocked: 'Salle dévérouillée (pour les utilisateurs)',
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 = {
scroll: 'Sensibilité de défilement (scroll)',
scroll_invert: 'Inverser le défilement (scroll)',

View File

@ -52,34 +52,6 @@ export const controls = {
unlock: '조작 잠금 해제하기',
}
export const locks = {
control: {
lock: '조작 잠그기 (사용자)',
unlock: '조작 잠금 해제하기 (사용자)',
locked: '조작이 잠겼습니다 (사용자)',
unlocked: '조작 잠금이 해제됐습니다 (사용자)',
notif_locked: '사용자의 조작을 잠궜습니다',
notif_unlocked: '사용자의 조작 잠금을 해제했습니다',
},
login: {
lock: '방 잠그기 (사용자)',
unlock: '방 잠금 해제하기 (사용자)',
locked: '방이 잠겼습니다 (사용자)',
unlocked: '방 잠금이 해제됐습니다 (사용자)',
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 = {
scroll: '스크롤 감도',
scroll_invert: '스크롤 반전',

View File

@ -57,35 +57,6 @@ export const controls = {
//hasnot: 'You do not have control',
}
export const locks = {
// TODO
//control: {
// lock: 'Lock Controls (for users)',
// unlock: 'Unlock Controls (for users)',
// locked: 'Controls Locked (for users)',
// unlocked: 'Controls Unlocked (for users)',
// notif_locked: 'locked controls for users',
// notif_unlocked: 'unlocked controls for users',
//},
login: {
lock: 'Lås rommet (for brukere)',
unlock: 'Lås opp rommet (for brukere)',
locked: 'Rom låst (for brukere)',
unlocked: 'Rom opplåst (for brukere)',
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 = {
scroll: 'Rullingssensitivitet',
scroll_invert: 'Inverter rulling',

View File

@ -54,34 +54,6 @@ export const controls = {
hasnot: 'Вы не управляете',
}
export const locks = {
control: {
lock: 'Закрепить управление (для пользователей)',
unlock: 'Открепить управление (для пользователей)',
locked: 'Управление закреплено (для пользователей)',
unlocked: 'Управление откреплено (для пользователей)',
notif_locked: 'закреплено управление для пользователей',
notif_unlocked: 'откреплено управление для пользователей',
},
login: {
lock: 'Закрыть комнату (для пользователей)',
unlock: 'Открыть комнату (для пользователей)',
locked: 'Комната закрыта (для пользователей)',
unlocked: 'Комната открыта (для пользователей)',
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 = {
scroll: 'Чувствительность прокрутки',
scroll_invert: 'Инвертировать прокрутку',

View File

@ -57,34 +57,6 @@ export const controls = {
//hasnot: 'You do not have control',
}
export const locks = {
control: {
lock: 'Zakázať ovládanie (pre používateľov)',
unlock: 'Povoliť ovládanie (pre používateľov)',
locked: 'Ovládanie je zakázané (pre používateľov)',
unlocked: 'Ovládanie je povolené (pre používateľov)',
notif_locked: 'zakázal/a ovládanie pre používateľov',
notif_unlocked: 'povolil/a ovládanie pre používateľov',
},
login: {
lock: 'Zamknúť miestnosť (pre používateľov)',
unlock: 'Odomknúť miestnosť (pre používateľov)',
locked: 'Miestnosť je zamknutá (pre používateľov)',
unlocked: 'Miestnosť odomknutá (pre používateľov)',
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 = {
scroll: 'Citlivosť kolieska myši',
scroll_invert: 'Invertovať koliesko myši',

View File

@ -57,35 +57,6 @@ export const controls = {
//hasnot: 'You do not have control',
}
export const locks = {
// TODO
//control: {
// lock: 'Lock Controls (for users)',
// unlock: 'Unlock Controls (for users)',
// locked: 'Controls Locked (for users)',
// unlocked: 'Controls Unlocked (for users)',
// notif_locked: 'locked controls for users',
// notif_unlocked: 'unlocked controls for users',
//},
login: {
lock: 'Lås rum (för användare)',
unlock: 'Lås upp rummet (för användare)',
locked: 'Rum låst (för användare)',
unlocked: 'Rum upplåst (för användare)',
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 = {
scroll: 'Scrollkänslighet',
scroll_invert: 'Vänd Scrollen',

View File

@ -54,34 +54,6 @@ export const controls = {
hasnot: '你没有控制',
}
export const locks = {
control: {
lock: '对所有用户进行锁定控制',
unlock: '对所有用户进行解锁控制',
locked: '锁定的控制装置',
unlocked: '解锁的控制装置',
notif_locked: '为用户锁定控制',
notif_unlocked: '为用户解锁控制',
},
login: {
lock: '所有用户的锁定室',
unlock: '所有用户的解锁室',
locked: '为所有用户锁定的房间',
unlocked: '为所有用户解锁的房间',
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 = {
scroll: '滚动敏感度',
scroll_invert: '反转滚动敏感度',

View File

@ -54,33 +54,6 @@ export const controls = {
hasnot: '您沒有控制權',
}
export const locks = {
control: {
lock: '鎖定控制(對使用者)',
unlock: '解鎖控制(對使用者)',
locked: '已鎖定控制(對使用者)',
unlocked: '已解鎖控制(對使用者)',
notif_locked: '已鎖定使用者控制',
notif_unlocked: '已解鎖使用者控制',
},
login: {
lock: '鎖定房間(對使用者)',
unlock: '解鎖房間(對使用者)',
locked: '房間已鎖定(對使用者)',
unlocked: '房間已解鎖(對使用者)',
notif_locked: '已鎖定房間',
notif_unlocked: '已解鎖房間',
},
file_transfer: {
lock: '鎖定檔案傳輸(對使用者)',
unlock: '解鎖檔案傳輸(對使用者)',
locked: '檔案傳輸已鎖定(對使用者)',
unlocked: '檔案傳輸已解鎖(對使用者)',
notif_locked: '已鎖定檔案傳輸',
notif_unlocked: '已解鎖檔案傳輸',
},
}
export const setting = {
scroll: '滾動靈敏度',
scroll_invert: '反向滾動',

View File

@ -38,10 +38,6 @@ export const EVENT = {
MESSAGE: 'chat/message',
EMOTE: 'chat/emote',
},
FILETRANSFER: {
LIST: 'filetransfer/list',
REFRESH: 'filetransfer/refresh',
},
SCREEN: {
CONFIGURATIONS: 'screen/configurations',
RESOLUTION: 'screen/resolution',
@ -55,8 +51,6 @@ export const EVENT = {
ADMIN: {
BAN: 'admin/ban',
KICK: 'admin/kick',
LOCK: 'admin/lock',
UNLOCK: 'admin/unlock',
MUTE: 'admin/mute',
UNMUTE: 'admin/unmute',
CONTROL: 'admin/control',
@ -73,7 +67,6 @@ export type WebSocketEvents =
| MemberEvents
| SignalEvents
| ChatEvents
| FileTransferEvents
| ScreenEvents
| BroadcastEvents
| AdminEvents
@ -97,8 +90,6 @@ export type SignalEvents =
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
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
export type BroadcastEvents =
@ -109,8 +100,6 @@ export type BroadcastEvents =
export type AdminEvents =
| typeof EVENT.ADMIN.BAN
| typeof EVENT.ADMIN.KICK
| typeof EVENT.ADMIN.LOCK
| typeof EVENT.ADMIN.UNLOCK
| typeof EVENT.ADMIN.MUTE
| typeof EVENT.ADMIN.UNMUTE
| typeof EVENT.ADMIN.CONTROL

View File

@ -22,7 +22,6 @@ import {
AdminLockMessage,
SystemInitPayload,
AdminLockResource,
FileTransferListPayload,
} from './messages'
interface NekoEvents extends BaseEvents {}
@ -134,17 +133,9 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
/////////////////////////////
// System Events
/////////////////////////////
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) {
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, file_transfer }: SystemInitPayload) {
this.$accessor.remote.setImplicitHosting(implicit_hosting)
this.$accessor.remote.setFileTransfer(file_transfer)
for (const resource in locks) {
this[EVENT.ADMIN.LOCK]({
event: EVENT.ADMIN.LOCK,
resource: resource as AdminLockResource,
id: locks[resource],
})
}
}
protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) {
@ -353,14 +344,6 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.chat.newEmote({ type: emote })
}
/////////////////////////////
// File Transfer Events
/////////////////////////////
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
this.$accessor.files.setCwd(cwd)
this.$accessor.files.setFileList(files)
}
/////////////////////////////
// Screen Events
/////////////////////////////
@ -486,28 +469,6 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
})
}
protected [EVENT.ADMIN.LOCK]({ id, resource }: AdminLockMessage) {
this.$accessor.setLocked(resource)
this.$accessor.chat.newMessage({
id,
content: this.$vue.$t(`locks.${resource}.notif_locked`) as string,
type: 'event',
created: new Date(),
})
}
protected [EVENT.ADMIN.UNLOCK]({ id, resource }: AdminLockMessage) {
this.$accessor.setUnlocked(resource)
this.$accessor.chat.newMessage({
id,
content: this.$vue.$t(`locks.${resource}.notif_unlocked`) as string,
type: 'event',
created: new Date(),
})
}
protected [EVENT.ADMIN.CONTROL]({ id, target }: AdminTargetPayload) {
this.$accessor.remote.setHost(id)
this.$accessor.remote.changeKeyboard()

View File

@ -8,9 +8,8 @@ import {
ChatEvents,
ScreenEvents,
AdminEvents,
FileTransferEvents,
} from './events'
import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
import { Member, ScreenConfigurations, ScreenResolution } from './types'
export type WebSocketMessages =
| WebSocketMessage
@ -194,18 +193,6 @@ export interface EmojiSendPayload {
emote: string
}
/*
FILE TRANSFER PAYLOADS
*/
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
event: FileTransferEvents
}
export interface FileTransferListPayload {
cwd: string
files: FileListItem[]
}
/*
SCREEN PAYLOADS
*/

View File

@ -22,20 +22,3 @@ export interface ScreenResolution {
height: number
rate: number
}
export interface FileListItem {
name: string
type: 'file' | 'dir'
size: number
}
export interface FileTransfer {
id: number
name: string
direction: 'upload' | 'download'
size: number
progress: number
status: 'pending' | 'inprogress' | 'completed' | 'failed'
error?: string
abortController?: AbortController
}

View File

@ -1,72 +0,0 @@
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
import { FileListItem, FileTransfer } from '~/neko/types'
import { EVENT } from '~/neko/events'
import { accessor } from '~/store'
export const state = () => ({
cwd: '',
files: [] as FileListItem[],
transfers: [] as FileTransfer[],
})
export const getters = getterTree(state, {
//
})
export const mutations = mutationTree(state, {
_setCwd(state, cwd: string) {
state.cwd = cwd
},
_setFileList(state, files: FileListItem[]) {
state.files = files
},
_addTransfer(state, transfer: FileTransfer) {
state.transfers = [...state.transfers, transfer]
},
_removeTransfer(state, transfer: FileTransfer) {
state.transfers = state.transfers.filter((t) => t.id !== transfer.id)
},
})
export const actions = actionTree(
{ state, getters, mutations },
{
setCwd(store, cwd: string) {
accessor.files._setCwd(cwd)
},
setFileList(store, files: FileListItem[]) {
accessor.files._setFileList(files)
},
addTransfer(store, transfer: FileTransfer) {
if (transfer.status !== 'pending') {
return
}
accessor.files._addTransfer(transfer)
},
removeTransfer(store, transfer: FileTransfer) {
accessor.files._removeTransfer(transfer)
},
cancelAllTransfers() {
for (const t of accessor.files.transfers) {
if (t.status !== 'completed') {
t.abortController?.abort()
}
accessor.files.removeTransfer(t)
}
},
refresh() {
if (!accessor.connected) {
return
}
$client.sendMessage(EVENT.FILETRANSFER.REFRESH)
},
},
)

View File

@ -7,7 +7,6 @@ import { get, set } from '~/utils/localstorage'
import * as video from './video'
import * as chat from './chat'
import * as files from './files'
import * as remote from './remote'
import * as user from './user'
import * as settings from './settings'
@ -20,7 +19,6 @@ export const state = () => ({
active: false,
connecting: false,
connected: false,
locked: {} as Record<string, boolean>,
})
export const mutations = mutationTree(state, {
@ -33,14 +31,6 @@ export const mutations = mutationTree(state, {
state.password = password
},
setLocked(state, resource: string) {
Vue.set(state.locked, resource, true)
},
setUnlocked(state, resource: string) {
Vue.set(state.locked, resource, false)
},
setConnnecting(state) {
state.connected = false
state.connecting = true
@ -56,9 +46,7 @@ export const mutations = mutationTree(state, {
},
})
export const getters = getterTree(state, {
isLocked: (state) => (resource: AdminLockResource) => resource in state.locked && state.locked[resource],
})
export const getters = getterTree(state, {})
export const actions = actionTree(
{ state, getters, mutations },
@ -68,30 +56,6 @@ export const actions = actionTree(
accessor.settings.initialise()
},
lock(_, resource: AdminLockResource) {
if (!accessor.connected || !accessor.user.admin) {
return
}
$client.sendMessage(EVENT.ADMIN.LOCK, { resource })
},
unlock(_, resource: AdminLockResource) {
if (!accessor.connected || !accessor.user.admin) {
return
}
$client.sendMessage(EVENT.ADMIN.UNLOCK, { resource })
},
toggleLock(_, resource: AdminLockResource) {
if (accessor.isLocked(resource)) {
accessor.unlock(resource)
} else {
accessor.lock(resource)
}
},
login(store, { displayname, password }: { displayname: string; password: string }) {
accessor.setLogin({ displayname, password })
$client.login(password, displayname)
@ -111,7 +75,7 @@ export const storePattern = {
mutations,
actions,
getters,
modules: { video, chat, files, user, remote, settings, client, emoji },
modules: { video, chat, user, remote, settings, client, emoji },
}
Vue.use(Vuex)

View File

@ -1,8 +1,6 @@
package config
import (
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -10,12 +8,6 @@ import (
type WebSocket struct {
Password string
AdminPassword string
Locks []string
ControlProtection bool
FileTransferEnabled bool
FileTransferPath string
}
func (WebSocket) Init(cmd *cobra.Command) error {
@ -29,39 +21,10 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)")
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
return err
}
cmd.PersistentFlags().Bool("control_protection", false, "control protection means, users can gain control only if at least one admin is in the room")
if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil {
return err
}
// File transfer
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
}
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
}
func (s *WebSocket) Set() {
s.Password = viper.GetString("password")
s.AdminPassword = viper.GetString("password_admin")
s.Locks = viper.GetStringSlice("locks")
s.ControlProtection = viper.GetBool("control_protection")
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
s.FileTransferPath = viper.GetString("file_transfer_path")
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
}

View File

@ -3,12 +3,9 @@ package http
import (
"context"
"encoding/json"
"fmt"
"image/jpeg"
"io"
"net/http"
"os"
"regexp"
"strconv"
"github.com/go-chi/chi/v5"
@ -21,8 +18,6 @@ import (
"m1k1o/neko/internal/types"
)
const FILE_UPLOAD_BUF_SIZE = 65000
type Server struct {
logger zerolog.Logger
router *chi.Mux
@ -98,11 +93,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
return
}
if webSocketHandler.IsLocked("login") {
http.Error(w, "room is locked", http.StatusLocked)
return
}
quality, err := strconv.Atoi(r.URL.Query().Get("quality"))
if err != nil {
quality = 90
@ -118,89 +108,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
}
})
// 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 {
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
}
filePath := webSocketHandler.FileTransferPath(filename)
f, err := os.Open(filePath)
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=%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
}
err = r.ParseMultipartForm(32 << 20)
if err != nil || r.MultipartForm == nil {
logger.Warn().Err(err).Msg("failed to parse multipart form")
http.Error(w, "error parsing form", http.StatusBadRequest)
return
}
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)
}
err = r.MultipartForm.RemoveAll()
if err != nil {
logger.Warn().Err(err).Msg("failed to remove multipart form")
}
})
}
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("true"))
})

View File

@ -28,8 +28,6 @@ type SessionManager struct {
capture types.CaptureManager
members map[string]*Session
eventsChannel chan types.SessionEvent
// TODO: Handle locks in sessions as flags.
controlLocked bool
}
func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket) types.Session {
@ -118,16 +116,6 @@ func (manager *SessionManager) Get(id string) (types.Session, bool) {
return session, ok
}
// TODO: Handle locks in sessions as flags.
func (manager *SessionManager) SetControlLocked(locked bool) {
manager.controlLocked = locked
}
func (manager *SessionManager) CanControl(id string) bool {
session, ok := manager.Get(id)
return ok && (!manager.controlLocked || session.Admin())
}
func (manager *SessionManager) Admins() []*types.Member {
manager.mu.Lock()
defer manager.mu.Unlock()

View File

@ -34,11 +34,6 @@ const (
CHAT_EMOTE = "chat/emote"
)
const (
FILETRANSFER_LIST = "filetransfer/list"
FILETRANSFER_REFRESH = "filetransfer/refresh"
)
const (
SCREEN_CONFIGURATIONS = "screen/configurations"
SCREEN_RESOLUTION = "screen/resolution"
@ -54,9 +49,7 @@ const (
const (
ADMIN_BAN = "admin/ban"
ADMIN_KICK = "admin/kick"
ADMIN_LOCK = "admin/lock"
ADMIN_MUTE = "admin/mute"
ADMIN_UNLOCK = "admin/unlock"
ADMIN_UNMUTE = "admin/unmute"
ADMIN_CONTROL = "admin/control"
ADMIN_RELEASE = "admin/release"

View File

@ -11,10 +11,8 @@ type Message struct {
}
type SystemInit struct {
Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"`
Locks map[string]string `json:"locks"`
FileTransfer bool `json:"file_transfer"`
Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"`
}
type SystemMessage struct {
@ -107,12 +105,6 @@ type EmoteSend struct {
Emote string `json:"emote"`
}
type FileTransferList struct {
Event string `json:"event"`
Cwd string `json:"cwd"`
Files []types.FileListItem `json:"files"`
}
type Admin struct {
Event string `json:"event"`
ID string `json:"id"`

View File

@ -55,8 +55,6 @@ type SessionManager interface {
ClearHost()
Has(id string) bool
Get(id string) (Session, bool)
SetControlLocked(locked bool)
CanControl(id string) bool
Members() []*Member
Admins() []*Member
Destroy(id string)

View File

@ -11,14 +11,12 @@ type Stats struct {
Members []*Member `json:"members"`
Banned map[string]string `json:"banned"` // IP -> session ID (that banned it)
Locked map[string]string `json:"locked"` // resource name -> session ID (that locked it)
ServerStartedAt time.Time `json:"server_started_at"`
LastAdminLeftAt *time.Time `json:"last_admin_left_at"`
LastUserLeftAt *time.Time `json:"last_user_left_at"`
ControlProtection bool `json:"control_protection"`
ImplicitControl bool `json:"implicit_control"`
ImplicitControl bool `json:"implicit_control"`
}
type WebSocket interface {
@ -32,17 +30,5 @@ type WebSocketHandler interface {
Shutdown() error
Upgrade(w http.ResponseWriter, r *http.Request) error
Stats() Stats
IsLocked(resource string) bool
IsAdmin(password string) (bool, error)
// File Transfer
CanTransferFiles(password string) (bool, error)
FileTransferPath(filename string) string
FileTransferEnabled() bool
}
type FileListItem struct {
Filename string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
}

View File

@ -1,36 +0,0 @@
package utils
import (
"os"
"m1k1o/neko/internal/types"
)
func ListFiles(path string) ([]types.FileListItem, error) {
items, err := os.ReadDir(path)
if err != nil {
return nil, err
}
out := make([]types.FileListItem, len(items))
for i, item := range items {
var itemType string = ""
var size int64 = 0
if item.IsDir() {
itemType = "dir"
} else {
itemType = "file"
info, err := item.Info()
if err == nil {
size = info.Size()
}
}
out[i] = types.FileListItem{
Filename: item.Name(),
Type: itemType,
Size: size,
}
}
return out, nil
}

View File

@ -39,7 +39,7 @@ type PayloadKey struct {
}
func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error {
if (!manager.config.ImplicitControl && !manager.sessions.IsHost(id)) || (manager.config.ImplicitControl && !manager.sessions.CanControl(id)) {
if (!manager.config.ImplicitControl && !manager.sessions.IsHost(id)) || manager.config.ImplicitControl {
return nil
}

View File

@ -8,78 +8,6 @@ import (
"m1k1o/neko/internal/types/message"
)
func (h *MessageHandler) adminLock(id string, session types.Session, payload *message.AdminLock) error {
if !session.Admin() {
h.logger.Debug().Msg("user not admin")
return nil
}
if h.state.IsLocked(payload.Resource) {
h.logger.Debug().Str("resource", payload.Resource).Msg("resource already locked...")
return nil
}
// allow only known resources
switch payload.Resource {
case "login":
case "control":
case "file_transfer":
default:
h.logger.Debug().Msg("unknown lock resource")
return nil
}
// TODO: Handle locks in sessions as flags.
if payload.Resource == "control" {
h.sessions.SetControlLocked(true)
}
h.state.Lock(payload.Resource, id)
if err := h.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_LOCK,
ID: id,
Resource: payload.Resource,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK)
return err
}
return nil
}
func (h *MessageHandler) adminUnlock(id string, session types.Session, payload *message.AdminLock) error {
if !session.Admin() {
h.logger.Debug().Msg("user not admin")
return nil
}
if !h.state.IsLocked(payload.Resource) {
h.logger.Debug().Str("resource", payload.Resource).Msg("resource not locked...")
return nil
}
// TODO: Handle locks in sessions as flags.
if payload.Resource == "control" {
h.sessions.SetControlLocked(false)
}
h.state.Unlock(payload.Resource)
if err := h.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_UNLOCK,
ID: id,
Resource: payload.Resource,
}, nil); err != nil {
h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK)
return err
}
return nil
}
func (h *MessageHandler) adminControl(id string, session types.Session) error {
if !session.Admin() {
h.logger.Debug().Msg("user not admin")

View File

@ -33,12 +33,6 @@ func (h *MessageHandler) controlRelease(id string, session types.Session) error
func (h *MessageHandler) controlRequest(id string, session types.Session) error {
// check for host
if !h.sessions.HasHost() {
// check if control is locked or user is admin
if h.state.IsLocked("control") && !session.Admin() {
h.logger.Debug().Msg("control is locked")
return nil
}
// set host
err := h.sessions.SetHost(id)
if err != nil {
@ -96,12 +90,6 @@ func (h *MessageHandler) controlGive(id string, session types.Session, payload *
return nil
}
// check if control is locked or giver is admin
if h.state.IsLocked("control") && !session.Admin() {
h.logger.Debug().Msg("control is locked")
return nil
}
// set host
err := h.sessions.SetHost(payload.ID)
if err != nil {
@ -124,7 +112,7 @@ func (h *MessageHandler) controlGive(id string, session types.Session, payload *
func (h *MessageHandler) controlClipboard(id string, session types.Session, payload *message.Clipboard) error {
// check if session can access clipboard
if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) {
if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || h.webrtc.ImplicitControl() {
h.logger.Debug().Str("id", id).Msg("cannot access clipboard")
return nil
}
@ -135,7 +123,7 @@ func (h *MessageHandler) controlClipboard(id string, session types.Session, payl
func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error {
// check if session can control keyboard
if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) {
if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || h.webrtc.ImplicitControl() {
h.logger.Debug().Str("id", id).Msg("cannot control keyboard")
return nil
}

View File

@ -1,47 +0,0 @@
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 {
if !h.state.FileTransferEnabled() {
return nil
}
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
}
// TODO: keep list of files in memory and update it on file changes
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

@ -50,11 +50,6 @@ func (h *MessageHandler) Connected(admin bool, address string) (bool, string) {
}
}
if h.state.IsLocked("login") && !admin {
h.logger.Debug().Msg("server locked")
return false, "locked"
}
return true, ""
}
@ -132,10 +127,6 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
return h.chatEmote(id, session, payload)
}), "%s failed", header.Event)
// File Transfer Events
case event.FILETRANSFER_REFRESH:
return errors.Wrapf(h.FileTransferRefresh(session), "%s failed", header.Event)
// Screen Events
case event.SCREEN_RESOLUTION:
return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event)
@ -159,18 +150,6 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
return errors.Wrapf(h.boradcastDestroy(session), "%s failed", header.Event)
// Admin Events
case event.ADMIN_LOCK:
payload := &message.AdminLock{}
return errors.Wrapf(
utils.Unmarshal(payload, raw, func() error {
return h.adminLock(id, session, payload)
}), "%s failed", header.Event)
case event.ADMIN_UNLOCK:
payload := &message.AdminLock{}
return errors.Wrapf(
utils.Unmarshal(payload, raw, func() error {
return h.adminUnlock(id, session, payload)
}), "%s failed", header.Event)
case event.ADMIN_CONTROL:
return errors.Wrapf(h.adminControl(id, session), "%s failed", header.Event)
case event.ADMIN_RELEASE:

View File

@ -16,8 +16,6 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
if err := session.Send(message.SystemInit{
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
@ -35,13 +33,6 @@ 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
}

View File

@ -1,22 +1,14 @@
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
fileTransferPath string // path where files are located
}
func New(fileTransferEnabled bool, fileTransferPath string) *State {
func New() *State {
return &State{
banned: make(map[string]string),
locked: make(map[string]string),
fileTransferEnabled: fileTransferEnabled,
fileTransferPath: fileTransferPath,
}
}
@ -43,42 +35,3 @@ func (s *State) GetBanned(ip string) (string, bool) {
func (s *State) AllBanned() map[string]string {
return s.banned
}
// Lock
func (s *State) Lock(resource, id string) {
s.locked[resource] = id
}
func (s *State) Unlock(resource string) {
delete(s.locked, resource)
}
func (s *State) IsLocked(resource string) bool {
_, ok := s.locked[resource]
return ok
}
func (s *State) GetLocked(resource string) (string, bool) {
id, ok := s.locked[resource]
return id, ok
}
func (s *State) AllLocked() map[string]string {
return s.locked
}
// 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
}

View File

@ -3,12 +3,10 @@ package websocket
import (
"fmt"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -22,35 +20,10 @@ import (
"m1k1o/neko/internal/websocket/state"
)
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.FileTransferEnabled, conf.FileTransferPath)
// if control protection is enabled
if conf.ControlProtection {
state.Lock("control", CONTROL_PROTECTION_SESSION)
logger.Info().Msgf("control locked on behalf of control protection")
}
// 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
for _, lock := range conf.Locks {
state.Lock(lock, "") // empty session ID
}
if len(conf.Locks) > 0 {
logger.Info().Msgf("locked resources: %+v", conf.Locks)
}
state := state.New()
handler := handler.New(
sessions,
@ -123,24 +96,6 @@ func (ws *WebSocketHandler) Start() {
ws.logger.Debug().Str("id", e.Id).Msg("session connected")
}
// if control protection is enabled and at least one admin
// and if room was locked on behalf control protection, unlock
sess, ok := ws.state.GetLocked("control")
if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 {
ws.state.Unlock("control")
ws.sessions.SetControlLocked(false) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control unlocked on behalf of control protection")
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_UNLOCK,
ID: e.Id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK)
}
}
// remove outdated stats
if e.Session.Admin() {
ws.lastAdminLeftAt = nil
@ -157,25 +112,6 @@ func (ws *WebSocketHandler) Start() {
membersCount := len(ws.sessions.Members())
adminCount := len(ws.sessions.Admins())
// if control protection is enabled and no admin
// and room is not locked, lock
ok := ws.state.IsLocked("control")
if !ok && ws.conf.ControlProtection && adminCount == 0 {
ws.state.Lock("control", CONTROL_PROTECTION_SESSION)
ws.sessions.SetControlLocked(true) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control locked and released on behalf of control protection")
ws.handler.AdminRelease(e.Id, e.Session)
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_LOCK,
ID: e.Id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK)
}
}
// if this was the last admin
if e.Session.Admin() && adminCount == 0 {
now := time.Now()
@ -216,37 +152,6 @@ func (ws *WebSocketHandler) Start() {
ws.logger.Err(err).Msg("sync clipboard")
}
}()
// 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
}
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")
}
}
}
func (ws *WebSocketHandler) Shutdown() error {
@ -347,21 +252,15 @@ func (ws *WebSocketHandler) Stats() types.Stats {
Members: ws.sessions.Members(),
Banned: ws.state.AllBanned(),
Locked: ws.state.AllLocked(),
ServerStartedAt: ws.serverStartedAt,
LastAdminLeftAt: ws.lastAdminLeftAt,
LastUserLeftAt: ws.lastUserLeftAt,
ControlProtection: ws.conf.ControlProtection,
ImplicitControl: ws.webrtc.ImplicitControl(),
ImplicitControl: ws.webrtc.ImplicitControl(),
}
}
func (ws *WebSocketHandler) IsLocked(resource string) bool {
return ws.state.IsLocked(resource)
}
func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) {
if password == ws.conf.AdminPassword {
return true, nil
@ -444,28 +343,3 @@ 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
}