mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22e35dd525 | |||
| 23d8a2718e | |||
| 92f99168b8 | |||
| 97d737683f | |||
| 71216a30ca | |||
| f399ac8df5 | |||
| 020def0305 | |||
| 8100f0f46b | |||
| 5f40eb9388 | |||
| 21ad64423f | |||
| 3191dbb058 | |||
| ff6dd395c9 |
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: webpage/package-lock.json
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ RUN set -eux; apt-get update; \
|
||||
#
|
||||
# create a profile directory
|
||||
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \
|
||||
chown -R neko:neko /home/neko/.mozilla/firefox/profile.default; \
|
||||
chown -R neko:neko /home/neko/.mozilla; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN set -eux; apt-get update; \
|
||||
#
|
||||
# create a profile directory
|
||||
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \
|
||||
chown -R neko:neko /home/neko/.mozilla/firefox/profile.default; \
|
||||
chown -R neko:neko /home/neko/.mozilla; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils; \
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN set -eux; apt-get update; \
|
||||
#
|
||||
# create a profile directory
|
||||
mkdir -p /home/neko/.waterfox/profile.default/extensions; \
|
||||
chown -R neko:neko /home/neko/.waterfox/profile.default; \
|
||||
chown -R neko:neko /home/neko/.waterfox; \
|
||||
#
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils bzip2; \
|
||||
|
||||
@@ -323,6 +323,6 @@ docker run --rm -i \
|
||||
-e "RUNTIME_DOCKERFILE=$RUNTIME_DOCKERFILE" \
|
||||
--workdir /src \
|
||||
--entrypoint go \
|
||||
golang:1.25-trixie \
|
||||
golang:1.26-trixie \
|
||||
run utils/docker/main.go \
|
||||
-i Dockerfile.tmpl -client "$CLIENT_DIST" | build_image $BASE_IMAGE -f - .
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
# neko client
|
||||
|
||||
The web client for [neko](https://github.com/m1k1o/neko): React 19 + TypeScript, built with Vite.
|
||||
It speaks only the v3 API (`/api/ws` + REST), so it works with `NEKO_LEGACY=false`.
|
||||
It speaks only the v3 API (`/api/ws` + REST).
|
||||
|
||||
```
|
||||
client/
|
||||
@@ -248,10 +248,10 @@ NEKO_URL=http://localhost:3000 npm run dev
|
||||
|
||||
`e2e.mjs` drives two (or more) headless Chromium users through login, video, chat, control, files,
|
||||
admin actions, reconnects and URL parameters. It needs a disposable server (it takes control, types
|
||||
into the desktop and kicks users) with the legacy API off and the multiuser provider:
|
||||
into the desktop and kicks users) with the multiuser provider:
|
||||
|
||||
```sh
|
||||
docker run -d -p 8080:8080 -p 52000:52000/udp -e NEKO_LEGACY=false \
|
||||
docker run -d -p 8080:8080 -p 52000:52000/udp \
|
||||
-e NEKO_MEMBER_PROVIDER=multiuser -e NEKO_MEMBER_MULTIUSER_USER_PASSWORD=neko \
|
||||
-e NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD=admin -e NEKO_FILETRANSFER_ENABLED=true \
|
||||
-e NEKO_WEBRTC_UDPMUX=52000 \
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { chromium } from 'playwright'
|
||||
// Two-user smoke test against a running v3 server (NEKO_LEGACY=false, multiuser provider):
|
||||
// Two-user smoke test against a running v3 server with the multiuser provider:
|
||||
// NEKO_URL=https://host/ NEKO_ADMIN_PASSWORD=admin NEKO_USER_PASSWORD=neko npm run test:e2e
|
||||
// Screenshots land in ./e2e-out. Needs a KDE/X desktop that opens KRunner on Alt+F2 for the typing step.
|
||||
import fs from 'node:fs'
|
||||
|
||||
@@ -82,12 +82,14 @@ RUN set -eux; \
|
||||
# copy runtime configs
|
||||
COPY --chown=neko:neko .Xresources /home/$USERNAME/.Xresources
|
||||
COPY dbus /usr/bin/dbus
|
||||
COPY intel/neko-serve /usr/bin/neko-serve
|
||||
COPY default.pa /etc/pulse/default.pa
|
||||
COPY supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY supervisord.dbus.conf /etc/neko/supervisord.dbus.conf
|
||||
COPY intel/supervisord.neko.conf /etc/neko/supervisord/neko.conf
|
||||
COPY xorg.conf /etc/neko/xorg.conf
|
||||
COPY intel/add-render-group.sh /usr/bin/add-render-group.sh
|
||||
COPY intel/supervisord.rendergroup.conf /etc/neko/supervisord/supervisord.rendergroup.conf
|
||||
COPY intel/entrypoint.sh /usr/bin/entrypoint.sh
|
||||
|
||||
#
|
||||
# copy runtime folders
|
||||
@@ -106,6 +108,10 @@ ENV NEKO_PLUGINS_ENABLED=true
|
||||
ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
|
||||
ENV NEKO_HWENC=VAAPI
|
||||
ENV RENDER_GID=
|
||||
# libva does not initialize the X11 display without an explicitly selected
|
||||
# driver here ('vaGetDriverNames() failed'), and the GStreamer VA plugin scan
|
||||
# uses X11. This image only ships the iHD driver (intel-media-va-driver-non-free).
|
||||
ENV LIBVA_DRIVER_NAME=iHD
|
||||
|
||||
#
|
||||
# add healthcheck
|
||||
@@ -116,4 +122,5 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
|
||||
#
|
||||
# run neko
|
||||
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# supervisord reads /etc/group to determine the supplementary groups of every
|
||||
# program it spawns, so the render group has to be assigned before supervisord
|
||||
# starts. Doing it from a program spawned by supervisord (as the previous
|
||||
# 'rendergroup-init' did) races with the neko program: neko is usually spawned
|
||||
# before 'usermod' has finished and is left without access to /dev/dri/render*,
|
||||
# which in turn makes the VA-API encoders fail to register.
|
||||
#
|
||||
# Assign the render group to the neko user first, then hand over to supervisord.
|
||||
if ! /usr/bin/add-render-group.sh; then
|
||||
echo "entrypoint: could not assign the render group, VA-API may not work (is /dev/dri passed to the container?)" >&2
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
# GStreamer only registers the VA-API elements (e.g. vah264enc) if a usable VA
|
||||
# display is found while the plugins are scanned for the first time, and the
|
||||
# result of that scan is cached in the user's registry. supervisord starts the
|
||||
# X server and neko in the same pass, so without waiting here neko can scan the
|
||||
# plugins before X (and therefore VA-API) is usable. The cache then lacks the
|
||||
# encoder elements and every client connect fails with
|
||||
# '(pipeline error) no element "vah264enc"'.
|
||||
#
|
||||
# Wait until VA is usable through X, then drop any stale registry, so that the
|
||||
# first scan of this process happens with a working VA display.
|
||||
i=0
|
||||
while [ "$i" -lt 30 ]; do
|
||||
if vainfo >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [ "$i" -ge 30 ]; then
|
||||
echo "neko-serve: VA-API was not usable after 30s, starting neko anyway" >&2
|
||||
fi
|
||||
|
||||
rm -f "$HOME/.cache/gstreamer-1.0/registry."*
|
||||
|
||||
exec /usr/bin/neko serve "$@"
|
||||
@@ -0,0 +1,16 @@
|
||||
# supervisord reads the files matched by [include] after the main config, and
|
||||
# the last definition of a program wins, so this overrides the [program:neko]
|
||||
# block from /etc/neko/supervisord.conf. Only the intel image ships
|
||||
# /usr/bin/neko-serve. Keep the two blocks in sync.
|
||||
[program:neko]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/neko-serve --server.static "/var/www"
|
||||
stopsignal=INT
|
||||
stopwaitsecs=3
|
||||
autorestart=true
|
||||
priority=800
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/var/log/neko/neko.log
|
||||
stdout_logfile_maxbytes=100MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
@@ -1,12 +0,0 @@
|
||||
[program:rendergroup-init]
|
||||
environment=RENDER_GID="%(ENV_RENDER_GID)s",USER="%(ENV_USER)s"
|
||||
command=/usr/bin/add-render-group.sh
|
||||
startsecs=0
|
||||
startretries=0
|
||||
autorestart=false
|
||||
priority=10
|
||||
user=root
|
||||
stdout_logfile=/var/log/neko/rendergroup.log
|
||||
stdout_logfile_maxbytes=1MB
|
||||
stdout_logfile_backups=10
|
||||
redirect_stderr=true
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE=golang:1.25-trixie
|
||||
ARG BASE_IMAGE=golang:1.26-trixie
|
||||
FROM $BASE_IMAGE AS server
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
+3
-9
@@ -81,10 +81,7 @@ func init() {
|
||||
// set root config values
|
||||
rootConfig.Set()
|
||||
|
||||
// legacy if explicitly enabled or if unspecified and legacy config is found
|
||||
if viper.GetBool("legacy") || !viper.IsSet("legacy") {
|
||||
rootConfig.SetV2()
|
||||
}
|
||||
rootConfig.SetV2()
|
||||
|
||||
//////
|
||||
// logs
|
||||
@@ -166,11 +163,8 @@ func init() {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
// legacy if explicitly enabled or if unspecified and legacy config is found
|
||||
if viper.GetBool("legacy") || !viper.IsSet("legacy") {
|
||||
if err := rootConfig.InitV2(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
if err := rootConfig.InitV2(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko.Version.Details())
|
||||
|
||||
+23
-30
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/api"
|
||||
"github.com/m1k1o/neko/server/internal/capture"
|
||||
@@ -88,26 +87,23 @@ func (c *serve) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// legacy if explicitly enabled or if unspecified and legacy config is found
|
||||
if viper.GetBool("legacy") || !viper.IsSet("legacy") {
|
||||
if err := c.configs.Desktop.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Desktop.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.InitV2(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,15 +120,12 @@ func (c *serve) PreRun(cmd *cobra.Command, args []string) {
|
||||
c.configs.Plugins.Set()
|
||||
c.configs.Server.Set()
|
||||
|
||||
// legacy if explicitly enabled or if unspecified and legacy config is found
|
||||
if viper.GetBool("legacy") || !viper.IsSet("legacy") {
|
||||
c.configs.Desktop.SetV2()
|
||||
c.configs.Capture.SetV2()
|
||||
c.configs.WebRTC.SetV2()
|
||||
c.configs.Member.SetV2()
|
||||
c.configs.Session.SetV2()
|
||||
c.configs.Server.SetV2()
|
||||
}
|
||||
c.configs.Desktop.SetV2()
|
||||
c.configs.Capture.SetV2()
|
||||
c.configs.WebRTC.SetV2()
|
||||
c.configs.Member.SetV2()
|
||||
c.configs.Session.SetV2()
|
||||
c.configs.Server.SetV2()
|
||||
}
|
||||
|
||||
func (c *serve) Start(cmd *cobra.Command) {
|
||||
|
||||
+33
-36
@@ -1,22 +1,22 @@
|
||||
module github.com/m1k1o/neko/server
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/PaesslerAG/gval v1.2.4
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.5.0
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/kataras/go-events v0.0.3
|
||||
github.com/pion/ice/v4 v4.2.2
|
||||
github.com/pion/interceptor v0.1.44
|
||||
github.com/pion/ice/v4 v4.4.8
|
||||
github.com/pion/interceptor v0.1.49
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/rtcp v1.2.16
|
||||
github.com/pion/webrtc/v4 v4.2.11
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/pion/rtcp v1.2.19
|
||||
github.com/pion/webrtc/v4 v4.2.24
|
||||
github.com/prometheus/client_golang v1.25.0
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
)
|
||||
@@ -24,41 +24,38 @@ require (
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-colorable v0.1.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pion/datachannel v1.6.3 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.15 // indirect
|
||||
github.com/pion/mdns/v2 v2.2.2 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtp v1.10.1 // indirect
|
||||
github.com/pion/sctp v1.9.4 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.18 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.1 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/pion/rtp v1.10.5 // indirect
|
||||
github.com/pion/sctp v1.12.0 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.20 // indirect
|
||||
github.com/pion/srtp/v3 v3.1.3 // indirect
|
||||
github.com/pion/stun/v4 v4.0.1 // indirect
|
||||
github.com/pion/transport/v5 v5.1.1 // indirect
|
||||
github.com/pion/turn/v5 v5.1.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shopspring/decimal v1.6.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.58.0 // indirect
|
||||
golang.org/x/net v0.61.0 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
||||
+70
-79
@@ -7,14 +7,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/go-chi/chi/v5 v5.5.0 h1:xdKQ1dJmVJsmgahrXSKhUx8NyVPzpwdlyrrQFpMPJgg=
|
||||
github.com/go-chi/chi/v5 v5.5.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
@@ -29,76 +27,72 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4=
|
||||
github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A=
|
||||
github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/ice/v4 v4.2.2 h1:dQJzzcgTFHDYyV3BoCfjPeX+JEtr58BWPi4PGyo6Vjg=
|
||||
github.com/pion/ice/v4 v4.2.2/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c=
|
||||
github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=
|
||||
github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/datachannel v1.6.3 h1:HZAYLpunVI0xi057ZiANl558d0uSpomcWw4UEPz3qXs=
|
||||
github.com/pion/datachannel v1.6.3/go.mod h1:oRmkXVVXxkUbqa8MatgdNHW29kvtQY3HUdjJQsfv2GE=
|
||||
github.com/pion/dtls/v3 v3.1.15 h1:okWk5LNwU21DfhvG8z4wm45knHVSPEdkihwp6BxfQcc=
|
||||
github.com/pion/dtls/v3 v3.1.15/go.mod h1:iKFQNYrjsN2TiA2YKKMqB9MOZaFpjFULBI/A4sW0eyc=
|
||||
github.com/pion/ice/v4 v4.4.8 h1:NB22Ze5WrgMjcf9L+O+1B29bl6anq+sbyvddImDLdhc=
|
||||
github.com/pion/ice/v4 v4.4.8/go.mod h1:zCRRg4Mkpdbx9B+75fd4z2APvIXeIraeUOB7IMPt8mU=
|
||||
github.com/pion/interceptor v0.1.49 h1:iyBsNHRoLNNXZiJA/DdGvJgdyfS8YWDCzxIAlVjt5nY=
|
||||
github.com/pion/interceptor v0.1.49/go.mod h1:MZ6PJkja/TCo350HAnBrs/rUIyad9mWjpcvytrf3ViQ=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/mdns/v2 v2.2.2 h1:qq9Kj3PnjF0mfSwd8Icu0CXy5aoYdQKnNUIsGPeUpgk=
|
||||
github.com/pion/mdns/v2 v2.2.2/go.mod h1:ZX5f0AAH1D6TOjdjvcBORZZHaZuG0t9+br78lHEwiJ4=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=
|
||||
github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.9.4 h1:cMxEu0F5tbP4qH07bKf1Zjf4rUih9LIo0qQt424e258=
|
||||
github.com/pion/sctp v1.9.4/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
|
||||
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
|
||||
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
|
||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||
github.com/pion/webrtc/v4 v4.2.11 h1:QUX1QZKlNIn4O7U5JxLPGP0sV5RTncZkzu9SPR3jVNU=
|
||||
github.com/pion/webrtc/v4 v4.2.11/go.mod h1:s/rAiyy77GyRFrZMx+Ls6aua26dIBPudH8/ZHYbIRWY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/pion/rtcp v1.2.19 h1:T79lR2Tfd/7zj+41nsUrftrUEee+Z03NQr9GSPSQ/WU=
|
||||
github.com/pion/rtcp v1.2.19/go.mod h1:vX7Es6skWYm4oVvOSC5+4WHLYUTZHcbGNmk5wjzMGH4=
|
||||
github.com/pion/rtp v1.10.5 h1:ip0HhO/wYZqQ4bKS+R99KnZh/GRCmIT0jDXikub7vlE=
|
||||
github.com/pion/rtp v1.10.5/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
|
||||
github.com/pion/sctp v1.12.0 h1:AzUdVkVBNyGY1W4crWO1/aOWzMaW3zsVHGrNuAfFQiI=
|
||||
github.com/pion/sctp v1.12.0/go.mod h1:6k1zrpkrxSwbisitn/AlzDQaa15DR49MFsrMochG/yc=
|
||||
github.com/pion/sdp/v3 v3.0.20 h1:TS6DViqcmp+49f0+mjw9anbr9xY3vJtsZewxAvlMCRQ=
|
||||
github.com/pion/sdp/v3 v3.0.20/go.mod h1:slIMXDK5OKj0nhISwjfeN18AzTBCt2LYZq9uPw0cU5Q=
|
||||
github.com/pion/srtp/v3 v3.1.3 h1:O3sfbfWfq/OyEaXRbX02Z0SNQRijS9nDdc/DEb8jxlU=
|
||||
github.com/pion/srtp/v3 v3.1.3/go.mod h1:v3uQtrCz9LH3aahau0jtzRDsWGwKkh4a0BiBwzzxhtQ=
|
||||
github.com/pion/stun/v4 v4.0.1 h1:S2ggK4hsUJJKoZfpL90uIqIRTj6xJ0NOIEkXgcpQif0=
|
||||
github.com/pion/stun/v4 v4.0.1/go.mod h1:byktbPA8U7HjJ2H8w3moP7nzq0Q+kYNulvMLPaubQL4=
|
||||
github.com/pion/transport/v5 v5.1.1 h1:Sauq8YJqRopgRfIgnpfKpdpKG14g0N4jWlRgpWsEfN8=
|
||||
github.com/pion/transport/v5 v5.1.1/go.mod h1:Qxw6fCEjFWQkRDZOhS4Vf+neJBcihauvA3uyEa1J1F0=
|
||||
github.com/pion/turn/v5 v5.1.2 h1:acEOO+D5txJ52vKfcilmSMItfP23Q9K6pJ1LM5P2VV0=
|
||||
github.com/pion/turn/v5 v5.1.2/go.mod h1:57Oadc8fHnac15VFyCd6nEaBTVZBxQe2POUMD4lsHRA=
|
||||
github.com/pion/webrtc/v4 v4.2.24 h1:Ove9nRpDAfK39+QSw3/Ru17VDiECjbiLaM73mRG4Yms=
|
||||
github.com/pion/webrtc/v4 v4.2.24/go.mod h1:JxD/FgjMDKCWFukjknTdG5a9rR4PUxkg0yBX1/JBInM=
|
||||
github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.1 h1:ZAiQed/nk8IIcsdzb2TlJu23NEUDyDLQ9TXTN2WRK6Y=
|
||||
github.com/prometheus/common v0.72.1/go.mod h1:1IoBY4IRaJnhs2ESjNzjilP1cJ/P8pimGoomq7zRXrA=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shopspring/decimal v1.6.0 h1:DcTwjCj4tJPLuGZYsR9W0hyX9pQwMIkdo26Ig1T9xX4=
|
||||
github.com/shopspring/decimal v1.6.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -110,8 +104,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
@@ -120,22 +114,19 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -86,7 +86,6 @@ func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Remove when legacy mode is removed as all sessions must be synced with their providers.
|
||||
func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
|
||||
@@ -378,14 +378,6 @@ func (s *Capture) Set() {
|
||||
},
|
||||
}
|
||||
s.VideoIDs = []string{"main"}
|
||||
|
||||
if viper.GetBool("legacy") {
|
||||
legacyPipeline := s.VideoPipelines["main"]
|
||||
// Hacky way to enable pointer for legacy pipeline.
|
||||
legacyPipeline.GstPipeline = strings.Replace(legacyPipeline.GstPipeline, "show-pointer=false", "show-pointer=true", 1)
|
||||
s.VideoPipelines["legacy"] = legacyPipeline
|
||||
// we do not add legacy to VideoIDs so that its ignored by bandwidth estimator
|
||||
}
|
||||
} else {
|
||||
log.Warn().Msgf("no video pipelines specified, using default")
|
||||
|
||||
@@ -414,13 +406,6 @@ func (s *Capture) Set() {
|
||||
},
|
||||
}
|
||||
s.VideoIDs = []string{"main"}
|
||||
|
||||
if viper.GetBool("legacy") {
|
||||
legacyPipeline := s.VideoPipelines["main"]
|
||||
legacyPipeline.ShowPointer = true
|
||||
s.VideoPipelines["legacy"] = legacyPipeline
|
||||
// we do not add legacy to VideoIDs so that its ignored by bandwidth estimator
|
||||
}
|
||||
}
|
||||
} else if videoPipeline != "" {
|
||||
log.Warn().Msg("you are setting both single video pipeline and multiple video pipelines, ignoring single video pipeline")
|
||||
@@ -485,51 +470,63 @@ func (s *Capture) Set() {
|
||||
}
|
||||
|
||||
func (s *Capture) SetV2() {
|
||||
enableLegacy := false
|
||||
|
||||
var ok bool
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
//
|
||||
// video
|
||||
//
|
||||
|
||||
if display := viper.GetString("display"); display != "" {
|
||||
s.Display = display
|
||||
if useV2Fallback("capture.video.display") {
|
||||
s.Display = display
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_DISPLAY' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_DISPLAY' and/or 'NEKO_DESKTOP_DISPLAY' instead, also consider using 'DISPLAY' env variable if both should be the same")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
modifiedVideoCodec := false
|
||||
if videoCodec := viper.GetString("video_codec"); videoCodec != "" {
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
|
||||
videoCodecValue, valid := codec.ParseStr(videoCodec)
|
||||
if !valid || videoCodecValue.Type != webrtc.RTPCodecTypeVideo {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
} else if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = videoCodecValue
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedVideoCodec = true
|
||||
}
|
||||
|
||||
if viper.GetBool("vp8") {
|
||||
s.VideoCodec = codec.VP8()
|
||||
if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedVideoCodec = true
|
||||
} else if viper.GetBool("vp9") {
|
||||
s.VideoCodec = codec.VP9()
|
||||
if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = codec.VP9()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedVideoCodec = true
|
||||
} else if viper.GetBool("h264") {
|
||||
s.VideoCodec = codec.H264()
|
||||
if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = codec.H264()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedVideoCodec = true
|
||||
} else if viper.GetBool("av1") {
|
||||
s.VideoCodec = codec.AV1()
|
||||
if useV2Fallback("capture.video.codec") {
|
||||
s.VideoCodec = codec.AV1()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedVideoCodec = true
|
||||
}
|
||||
|
||||
@@ -566,12 +563,10 @@ func (s *Capture) SetV2() {
|
||||
// Hacky way to disable pointer.
|
||||
GstPipeline: strings.Replace(pipeline, "show-pointer=true", "show-pointer=false", 1),
|
||||
},
|
||||
"legacy": {
|
||||
GstPipeline: pipeline,
|
||||
},
|
||||
}
|
||||
// we do not add legacy to VideoIDs so that its ignored by bandwidth estimator
|
||||
s.VideoIDs = []string{"main"}
|
||||
if useV2Fallback("capture.video.ids") {
|
||||
s.VideoIDs = []string{"main"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +575,7 @@ func (s *Capture) SetV2() {
|
||||
}
|
||||
|
||||
// TODO: add deprecated warning and proper alternative for HW enc, bitrate and max fps
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
//
|
||||
@@ -588,42 +583,56 @@ func (s *Capture) SetV2() {
|
||||
//
|
||||
|
||||
if audioDevice := viper.GetString("device"); audioDevice != "" {
|
||||
s.AudioDevice = audioDevice
|
||||
if useV2Fallback("capture.audio.device") {
|
||||
s.AudioDevice = audioDevice
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_DEVICE' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_DEVICE' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
modifiedAudioCodec := false
|
||||
if audioCodec := viper.GetString("audio_codec"); audioCodec != "" {
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
|
||||
audioCodecValue, valid := codec.ParseStr(audioCodec)
|
||||
if !valid || audioCodecValue.Type != webrtc.RTPCodecTypeAudio {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
} else if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = audioCodecValue
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedAudioCodec = true
|
||||
}
|
||||
|
||||
if viper.GetBool("opus") {
|
||||
s.AudioCodec = codec.Opus()
|
||||
if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedAudioCodec = true
|
||||
} else if viper.GetBool("g722") {
|
||||
s.AudioCodec = codec.G722()
|
||||
if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = codec.G722()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedAudioCodec = true
|
||||
} else if viper.GetBool("pcmu") {
|
||||
s.AudioCodec = codec.PCMU()
|
||||
if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = codec.PCMU()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedAudioCodec = true
|
||||
} else if viper.GetBool("pcma") {
|
||||
s.AudioCodec = codec.PCMA()
|
||||
if useV2Fallback("capture.audio.codec") {
|
||||
s.AudioCodec = codec.PCMA()
|
||||
}
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
modifiedAudioCodec = true
|
||||
}
|
||||
|
||||
@@ -632,11 +641,15 @@ func (s *Capture) SetV2() {
|
||||
|
||||
// audio pipeline
|
||||
if modifiedAudioCodec || audioBitrate != 0 || audioPipeline != "" {
|
||||
pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("unable to create audio pipeline, using default")
|
||||
if useV2Fallback("capture.audio.pipeline") {
|
||||
pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("unable to create audio pipeline, using default")
|
||||
} else {
|
||||
s.AudioPipeline = pipeline
|
||||
}
|
||||
} else {
|
||||
s.AudioPipeline = pipeline
|
||||
log.Warn().Msg("ignoring legacy audio pipeline settings because NEKO_CAPTURE_AUDIO_PIPELINE is already set")
|
||||
}
|
||||
|
||||
if audioPipeline != "" {
|
||||
@@ -644,7 +657,7 @@ func (s *Capture) SetV2() {
|
||||
}
|
||||
|
||||
// TODO: add deprecated warning and proper alternative for audio bitrate
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
//
|
||||
@@ -652,24 +665,28 @@ func (s *Capture) SetV2() {
|
||||
//
|
||||
|
||||
if viper.IsSet("broadcast_pipeline") {
|
||||
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
|
||||
if useV2Fallback("capture.broadcast.pipeline") {
|
||||
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_PIPELINE' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_PIPELINE' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("broadcast_url") {
|
||||
s.BroadcastUrl = viper.GetString("broadcast_url")
|
||||
if useV2Fallback("capture.broadcast.url") {
|
||||
s.BroadcastUrl = viper.GetString("broadcast_url")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_URL' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_URL' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("broadcast_autostart") {
|
||||
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
|
||||
if useV2Fallback("capture.broadcast.autostart") {
|
||||
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_AUTOSTART' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_AUTOSTART' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *Desktop) Set() {
|
||||
}
|
||||
|
||||
func (s *Desktop) SetV2() {
|
||||
enableLegacy := false
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
if viper.IsSet("screen") {
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
@@ -121,19 +121,17 @@ func (s *Desktop) SetV2() {
|
||||
height, err2 := strconv.ParseInt(res[2], 10, 64)
|
||||
rate, err3 := strconv.ParseInt(res[3], 10, 64)
|
||||
|
||||
if err1 == nil && err2 == nil && err3 == nil {
|
||||
if err1 == nil && err2 == nil && err3 == nil && useV2Fallback("desktop.screen") {
|
||||
s.ScreenSize.Width = int(width)
|
||||
s.ScreenSize.Height = int(height)
|
||||
s.ScreenSize.Rate = int16(rate)
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_SCREEN' which is deprecated, please use 'NEKO_DESKTOP_SCREEN' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,27 +266,31 @@ func (s *Member) Set() {
|
||||
}
|
||||
|
||||
func (s *Member) SetV2() {
|
||||
enableLegacy := false
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
if viper.IsSet("password") || viper.IsSet("password_admin") {
|
||||
s.Provider = "multiuser"
|
||||
if userPassword := viper.GetString("password"); userPassword != "" {
|
||||
s.Multiuser.UserPassword = userPassword
|
||||
} else {
|
||||
s.Multiuser.UserPassword = "neko"
|
||||
if useV2Fallback("member.provider") {
|
||||
s.Provider = "multiuser"
|
||||
}
|
||||
if adminPassword := viper.GetString("password_admin"); adminPassword != "" {
|
||||
s.Multiuser.AdminPassword = adminPassword
|
||||
} else {
|
||||
s.Multiuser.AdminPassword = "admin"
|
||||
if viper.IsSet("password") && useV2Fallback("member.multiuser.user_password") {
|
||||
if userPassword := viper.GetString("password"); userPassword != "" {
|
||||
s.Multiuser.UserPassword = userPassword
|
||||
} else {
|
||||
s.Multiuser.UserPassword = "neko"
|
||||
}
|
||||
}
|
||||
if viper.IsSet("password_admin") && useV2Fallback("member.multiuser.admin_password") {
|
||||
if adminPassword := viper.GetString("password_admin"); adminPassword != "" {
|
||||
s.Multiuser.AdminPassword = adminPassword
|
||||
} else {
|
||||
s.Multiuser.AdminPassword = "admin"
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PASSWORD' and 'NEKO_PASSWORD_ADMIN' which are deprecated, please use 'NEKO_MEMBER_MULTIUSER_USER_PASSWORD' and 'NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD' with 'NEKO_MEMBER_PROVIDER=multiuser' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -11,9 +12,12 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func useV2Fallback(v3Keys ...string) bool {
|
||||
return !slices.ContainsFunc(v3Keys, viper.IsSet)
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Config string
|
||||
Legacy bool
|
||||
|
||||
LogLevel zerolog.Level
|
||||
LogTime string
|
||||
@@ -34,14 +38,6 @@ func (Root) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// whether legacy configs and api should be enabled.
|
||||
// - if not specified, it will be automatically enabled if at least one legacy config entry is found.
|
||||
// - if it is specified, it will be enabled/disabled regardless of the presence of legacy config entries.
|
||||
cmd.PersistentFlags().Bool("legacy", true, "enable legacy mode")
|
||||
if err := viper.BindPFlag("legacy", cmd.PersistentFlags().Lookup("legacy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.level", "info", "set log level (trace, debug, info, warn, error, fatal, panic, disabled)")
|
||||
if err := viper.BindPFlag("log.level", cmd.PersistentFlags().Lookup("log.level")); err != nil {
|
||||
return err
|
||||
@@ -81,10 +77,6 @@ func (Root) InitV2(cmd *cobra.Command) error {
|
||||
|
||||
func (s *Root) Set() {
|
||||
s.Config = viper.GetString("config")
|
||||
s.Legacy = viper.GetBool("legacy")
|
||||
if s.Legacy {
|
||||
log.Info().Msg("legacy configuration is enabled")
|
||||
}
|
||||
|
||||
logLevel := viper.GetString("log.level")
|
||||
level, err := zerolog.ParseLevel(logLevel)
|
||||
@@ -121,25 +113,18 @@ func (s *Root) Set() {
|
||||
}
|
||||
|
||||
func (s *Root) SetV2() {
|
||||
enableLegacy := false
|
||||
|
||||
if viper.IsSet("logs") {
|
||||
if viper.GetBool("logs") {
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
if useV2Fallback("log.dir") {
|
||||
if viper.GetBool("logs") {
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
}
|
||||
s.LogDir = logs
|
||||
} else {
|
||||
s.LogDir = ""
|
||||
}
|
||||
s.LogDir = logs
|
||||
} else {
|
||||
s.LogDir = ""
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_LOGS' which is deprecated, please use 'NEKO_LOG_DIR=/path/to/logs' instead")
|
||||
enableLegacy = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (Server) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed")
|
||||
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS and WebSocket connections, if empty only same-origin WebSocket connections are accepted, if '*' is present all origins are allowed")
|
||||
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,52 +127,64 @@ func (s *Server) Set() {
|
||||
}
|
||||
|
||||
func (s *Server) SetV2() {
|
||||
enableLegacy := false
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
if viper.IsSet("cert") {
|
||||
s.Cert = viper.GetString("cert")
|
||||
if useV2Fallback("server.cert") {
|
||||
s.Cert = viper.GetString("cert")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CERT' which is deprecated, please use 'NEKO_SERVER_CERT' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("key") {
|
||||
s.Key = viper.GetString("key")
|
||||
if useV2Fallback("server.key") {
|
||||
s.Key = viper.GetString("key")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_KEY' which is deprecated, please use 'NEKO_SERVER_KEY' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("bind") {
|
||||
s.Bind = viper.GetString("bind")
|
||||
if useV2Fallback("server.bind") {
|
||||
s.Bind = viper.GetString("bind")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_BIND' which is deprecated, please use 'NEKO_SERVER_BIND' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("proxy") {
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
if useV2Fallback("server.proxy") {
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PROXY' which is deprecated, please use 'NEKO_SERVER_PROXY' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("static") {
|
||||
s.Static = viper.GetString("static")
|
||||
if useV2Fallback("server.static") {
|
||||
s.Static = viper.GetString("static")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_STATIC' which is deprecated, please use 'NEKO_SERVER_STATIC' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("path_prefix") {
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
if useV2Fallback("server.path_prefix") {
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_PATH_PREFIX' which is deprecated, please use 'NEKO_SERVER_PATH_PREFIX' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("cors") {
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
if useV2Fallback("server.cors") {
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
}
|
||||
if slices.Contains(s.CORS, "*") {
|
||||
s.CORS = []string{"*"}
|
||||
log.Warn().Msg("CORS is enabled for all origins, this is not recommended for production environments")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CORS' which is deprecated, please use 'NEKO_SERVER_CORS' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,9 +193,8 @@ func (s *Server) HasCors() bool {
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(origin string) bool {
|
||||
// if CORS is not configured, or a wildcard is present,
|
||||
// allow WebSocket connections from any origin
|
||||
if len(s.CORS) == 0 || s.CORS[0] == "*" {
|
||||
// only an explicit wildcard allows any origin
|
||||
if len(s.CORS) > 0 && s.CORS[0] == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ func (s *Session) Set() {
|
||||
}
|
||||
|
||||
func (s *Session) SetV2() {
|
||||
enableLegacy := false
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
if viper.IsSet("locks") {
|
||||
locks := viper.GetStringSlice("locks")
|
||||
@@ -179,34 +179,42 @@ func (s *Session) SetV2() {
|
||||
switch lock {
|
||||
// TODO: file_transfer
|
||||
case "control":
|
||||
s.LockedControls = true
|
||||
if useV2Fallback("session.locked_controls") {
|
||||
s.LockedControls = true
|
||||
}
|
||||
case "login":
|
||||
s.LockedLogins = true
|
||||
if useV2Fallback("session.locked_logins") {
|
||||
s.LockedLogins = true
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_LOCKS' which is deprecated, please use 'NEKO_SESSION_LOCKED_CONTROLS' and 'NEKO_SESSION_LOCKED_LOGINS' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
if viper.IsSet("implicit_control") {
|
||||
s.ImplicitHosting = viper.GetBool("implicit_control")
|
||||
if useV2Fallback("session.implicit_hosting") {
|
||||
s.ImplicitHosting = viper.GetBool("implicit_control")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_IMPLICIT_CONTROL' which is deprecated, please use 'NEKO_SESSION_IMPLICIT_HOSTING' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("control_protection") {
|
||||
s.ControlProtection = viper.GetBool("control_protection")
|
||||
if useV2Fallback("session.control_protection") {
|
||||
s.ControlProtection = viper.GetBool("control_protection")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CONTROL_PROTECTION' which is deprecated, please use 'NEKO_SESSION_CONTROL_PROTECTION' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("heartbeat_interval") {
|
||||
s.HeartbeatInterval = viper.GetInt("heartbeat_interval")
|
||||
if useV2Fallback("session.heartbeat_interval") {
|
||||
s.HeartbeatInterval = viper.GetInt("heartbeat_interval")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_HEARTBEAT_INTERVAL' which is deprecated, please use 'NEKO_SESSION_HEARTBEAT_INTERVAL' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,27 +318,35 @@ func (s *WebRTC) Set() {
|
||||
}
|
||||
|
||||
func (s *WebRTC) SetV2() {
|
||||
enableLegacy := false
|
||||
deprecatedV2ConfigUsed := false
|
||||
|
||||
if viper.IsSet("nat1to1") {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
if useV2Fallback("webrtc.nat1to1") {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_NAT1TO1' which is deprecated, please use 'NEKO_WEBRTC_NAT1TO1' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("tcpmux") {
|
||||
s.TCPMux = viper.GetInt("tcpmux")
|
||||
if useV2Fallback("webrtc.tcpmux") {
|
||||
s.TCPMux = viper.GetInt("tcpmux")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_TCPMUX' which is deprecated, please use 'NEKO_WEBRTC_TCPMUX' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("udpmux") {
|
||||
s.UDPMux = viper.GetInt("udpmux")
|
||||
if useV2Fallback("webrtc.udpmux") {
|
||||
s.UDPMux = viper.GetInt("udpmux")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_UDPMUX' which is deprecated, please use 'NEKO_WEBRTC_UDPMUX' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
if viper.IsSet("icelite") {
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
if useV2Fallback("webrtc.icelite") {
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICELITE' which is deprecated, please use 'NEKO_WEBRTC_ICELITE' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
if viper.IsSet("iceservers") {
|
||||
@@ -350,24 +358,32 @@ func (s *WebRTC) SetV2() {
|
||||
log.Panic().Err(err).Msg("failed to process iceservers")
|
||||
}
|
||||
}
|
||||
s.ICEServersFrontend = iceServers
|
||||
s.ICEServersBackend = iceServers
|
||||
if useV2Fallback("webrtc.iceservers.frontend", "webrtc.iceservers") {
|
||||
s.ICEServersFrontend = iceServers
|
||||
}
|
||||
if useV2Fallback("webrtc.iceservers.backend", "webrtc.iceservers") {
|
||||
s.ICEServersBackend = iceServers
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVERS' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
if viper.IsSet("iceserver") {
|
||||
iceServerSlice := viper.GetStringSlice("iceserver")
|
||||
if len(iceServerSlice) > 0 {
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, types.ICEServer{URLs: iceServerSlice})
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, types.ICEServer{URLs: iceServerSlice})
|
||||
if len(iceServerSlice) > 0 && (useV2Fallback("webrtc.iceservers.frontend", "webrtc.iceservers") || useV2Fallback("webrtc.iceservers.backend", "webrtc.iceservers")) {
|
||||
if useV2Fallback("webrtc.iceservers.frontend", "webrtc.iceservers") {
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, types.ICEServer{URLs: iceServerSlice})
|
||||
}
|
||||
if useV2Fallback("webrtc.iceservers.backend", "webrtc.iceservers") {
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, types.ICEServer{URLs: iceServerSlice})
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVER' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
if viper.IsSet("ipfetch") {
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
if len(s.NAT1To1IPs) == 0 && useV2Fallback("webrtc.nat1to1", "webrtc.ip_retrieval_url") {
|
||||
ipfetch := viper.GetString("ipfetch")
|
||||
ip, err := utils.HttpRequestGET(ipfetch)
|
||||
if err != nil {
|
||||
@@ -376,7 +392,7 @@ func (s *WebRTC) SetV2() {
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_IPFETCH' which is deprecated, please use 'NEKO_WEBRTC_IP_RETRIEVAL_URL' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
if viper.IsSet("epr") {
|
||||
@@ -396,20 +412,20 @@ func (s *WebRTC) SetV2() {
|
||||
}
|
||||
}
|
||||
|
||||
if min > max {
|
||||
s.EphemeralMin = max
|
||||
s.EphemeralMax = min
|
||||
} else {
|
||||
s.EphemeralMin = min
|
||||
s.EphemeralMax = max
|
||||
if useV2Fallback("webrtc.epr") {
|
||||
if min > max {
|
||||
s.EphemeralMin = max
|
||||
s.EphemeralMax = min
|
||||
} else {
|
||||
s.EphemeralMin = min
|
||||
s.EphemeralMax = max
|
||||
}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_EPR' which is deprecated, please use 'NEKO_WEBRTC_EPR' instead")
|
||||
enableLegacy = true
|
||||
deprecatedV2ConfigUsed = true
|
||||
}
|
||||
|
||||
// set legacy flag if any V2 configuration was used
|
||||
if !viper.IsSet("legacy") && enableLegacy {
|
||||
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
|
||||
viper.Set("legacy", true)
|
||||
if deprecatedV2ConfigUsed {
|
||||
log.Error().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package event
|
||||
|
||||
const (
|
||||
SYSTEM_INIT = "system/init"
|
||||
SYSTEM_DISCONNECT = "system/disconnect"
|
||||
SYSTEM_ERROR = "system/error"
|
||||
)
|
||||
|
||||
const (
|
||||
CLIENT_HEARTBEAT = "client/heartbeat"
|
||||
)
|
||||
|
||||
const (
|
||||
SIGNAL_OFFER = "signal/offer"
|
||||
SIGNAL_ANSWER = "signal/answer"
|
||||
SIGNAL_PROVIDE = "signal/provide"
|
||||
SIGNAL_CANDIDATE = "signal/candidate"
|
||||
)
|
||||
|
||||
const (
|
||||
MEMBER_LIST = "member/list"
|
||||
MEMBER_CONNECTED = "member/connected"
|
||||
MEMBER_DISCONNECTED = "member/disconnected"
|
||||
)
|
||||
|
||||
const (
|
||||
CONTROL_LOCKED = "control/locked"
|
||||
CONTROL_RELEASE = "control/release"
|
||||
CONTROL_REQUEST = "control/request"
|
||||
CONTROL_REQUESTING = "control/requesting"
|
||||
CONTROL_GIVE = "control/give"
|
||||
CONTROL_CLIPBOARD = "control/clipboard"
|
||||
CONTROL_KEYBOARD = "control/keyboard"
|
||||
)
|
||||
|
||||
const (
|
||||
CHAT_MESSAGE = "chat/message"
|
||||
CHAT_EMOTE = "chat/emote"
|
||||
)
|
||||
|
||||
const (
|
||||
FILETRANSFER_LIST = "filetransfer/list"
|
||||
FILETRANSFER_REFRESH = "filetransfer/refresh"
|
||||
)
|
||||
|
||||
const (
|
||||
SCREEN_CONFIGURATIONS = "screen/configurations"
|
||||
SCREEN_RESOLUTION = "screen/resolution"
|
||||
SCREEN_SET = "screen/set"
|
||||
)
|
||||
|
||||
const (
|
||||
BROADCAST_STATUS = "broadcast/status"
|
||||
BROADCAST_CREATE = "broadcast/create"
|
||||
BROADCAST_DESTROY = "broadcast/destroy"
|
||||
)
|
||||
|
||||
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"
|
||||
ADMIN_GIVE = "admin/give"
|
||||
)
|
||||
@@ -1,463 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/api"
|
||||
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
|
||||
oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message"
|
||||
oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types"
|
||||
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
"github.com/m1k1o/neko/server/pkg/types/event"
|
||||
"github.com/m1k1o/neko/server/pkg/types/message"
|
||||
"github.com/m1k1o/neko/server/pkg/utils"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultUpgrader specifies the parameters for upgrading an HTTP
|
||||
// connection to a WebSocket connection.
|
||||
DefaultUpgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type LegacyHandler struct {
|
||||
logger zerolog.Logger
|
||||
serverAddr string
|
||||
pathPrefix string
|
||||
bannedIPs map[string]struct{}
|
||||
sessionIPs map[string]string
|
||||
wsDialer *websocket.Dialer
|
||||
}
|
||||
|
||||
func New(serverAddr, pathPrefix string) *LegacyHandler {
|
||||
// Init
|
||||
|
||||
return &LegacyHandler{
|
||||
logger: log.With().Str("module", "legacy").Logger(),
|
||||
serverAddr: serverAddr,
|
||||
pathPrefix: pathPrefix,
|
||||
bannedIPs: make(map[string]struct{}),
|
||||
sessionIPs: make(map[string]string),
|
||||
wsDialer: &websocket.Dialer{
|
||||
Proxy: nil, // disable proxy for local requests
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LegacyHandler) Route(r types.Router) {
|
||||
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error {
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new websocket connection
|
||||
connClient, err := DefaultUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return utils.HttpError(http.StatusInternalServerError).
|
||||
WithInternalErr(err).
|
||||
Msg("couldn't upgrade connection to websocket")
|
||||
}
|
||||
defer connClient.Close()
|
||||
s.connClient = connClient
|
||||
|
||||
if h.isBanned(r) {
|
||||
s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_DISCONNECT,
|
||||
Title: "banned ip",
|
||||
Message: "you are banned",
|
||||
})
|
||||
}
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("username")
|
||||
password := r.URL.Query().Get("password")
|
||||
err = s.create(username, password)
|
||||
if err != nil {
|
||||
h.logger.Error().Err(err).Msg("couldn't create a new session")
|
||||
|
||||
s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_DISCONNECT,
|
||||
Title: "couldn't create a new session",
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
// we can't return HTTP error here because the connection is already upgraded
|
||||
return nil
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
// dial to the remote backend
|
||||
backendURL := "ws://" + h.serverAddr + path.Join(s.pathPrefix, "/api/ws")
|
||||
backendHeaders := make(http.Header)
|
||||
if s.token != "" {
|
||||
backendURL += "?token=" + url.QueryEscape(s.token)
|
||||
} else {
|
||||
backendHeaders.Set("Cookie", s.cookie)
|
||||
}
|
||||
connBackend, _, err := h.wsDialer.Dial(backendURL, backendHeaders)
|
||||
if err != nil {
|
||||
h.logger.Error().Err(err).Msg("couldn't dial to the remote backend")
|
||||
|
||||
s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_DISCONNECT,
|
||||
Title: "couldn't dial to the remote backend",
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
// we can't return HTTP error here because the connection is already upgraded
|
||||
return nil
|
||||
}
|
||||
defer connBackend.Close()
|
||||
s.connBackend = connBackend
|
||||
|
||||
// request signal
|
||||
videoAuto := true
|
||||
if err = s.toBackend(event.SIGNAL_REQUEST, message.SignalRequest{
|
||||
Video: types.PeerVideoRequest{
|
||||
Auto: &videoAuto,
|
||||
},
|
||||
}); err != nil {
|
||||
h.logger.Error().Err(err).Msg("couldn't request signal")
|
||||
s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_DISCONNECT,
|
||||
Title: "couldn't request signal",
|
||||
Message: err.Error(),
|
||||
})
|
||||
|
||||
// we can't return HTTP error here because the connection is already upgraded
|
||||
return nil
|
||||
}
|
||||
|
||||
// copy messages between the client and the backend
|
||||
errClient := make(chan error, 1)
|
||||
errBackend := make(chan error, 1)
|
||||
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error, rewriteTextMessage func([]byte) error) {
|
||||
for {
|
||||
msgType, msg, err := src.ReadMessage()
|
||||
if err != nil {
|
||||
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
|
||||
if e, ok := err.(*websocket.CloseError); ok {
|
||||
if e.Code != websocket.CloseNoStatusReceived {
|
||||
m = websocket.FormatCloseMessage(e.Code, e.Text)
|
||||
}
|
||||
}
|
||||
errc <- fmt.Errorf("src read message error: %w", err)
|
||||
dst.WriteMessage(websocket.CloseMessage, m)
|
||||
break
|
||||
}
|
||||
|
||||
// handle text messages
|
||||
if msgType == websocket.TextMessage {
|
||||
err = rewriteTextMessage(msg)
|
||||
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrBackendRespone) {
|
||||
h.logger.Error().Err(err).Msg("backend response error")
|
||||
|
||||
s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_ERROR,
|
||||
Title: "backend response error",
|
||||
Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrWebsocketSend) {
|
||||
errc <- fmt.Errorf("dst write message error: %w", err)
|
||||
break
|
||||
}
|
||||
|
||||
h.logger.Error().Err(err).Msg("couldn't rewrite text message")
|
||||
continue
|
||||
}
|
||||
|
||||
// forward ping pong messages
|
||||
if msgType == websocket.PingMessage ||
|
||||
msgType == websocket.PongMessage {
|
||||
err = dst.WriteMessage(msgType, msg)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// backend -> client
|
||||
go replicateWebsocketConn(connClient, connBackend, errClient, s.wsToClient)
|
||||
|
||||
// client -> backend
|
||||
go replicateWebsocketConn(connBackend, connClient, errBackend, s.wsToBackend)
|
||||
|
||||
var message string
|
||||
select {
|
||||
case err = <-errClient:
|
||||
message = "websocketproxy: Error when copying from backend to client"
|
||||
case err = <-errBackend:
|
||||
message = "websocketproxy: Error when copying from client to backend"
|
||||
}
|
||||
|
||||
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
|
||||
h.logger.Error().Err(err).Msg(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error {
|
||||
if h.isBanned(r) {
|
||||
return utils.HttpForbidden("banned ip")
|
||||
}
|
||||
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("usr")
|
||||
password := r.URL.Query().Get("pwd")
|
||||
err := s.create(username, password)
|
||||
if err != nil {
|
||||
return utils.HttpForbidden(err.Error())
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
if !s.isAdmin {
|
||||
return utils.HttpUnauthorized().Msg("bad authorization")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// get all sessions
|
||||
sessions := []api.SessionDataPayload{}
|
||||
err = s.apiReq(http.MethodGet, "/api/sessions", nil, &sessions)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// get stats
|
||||
newStats := types.Stats{}
|
||||
err = s.apiReq(http.MethodGet, "/api/stats", nil, &newStats)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// get settings
|
||||
settings := types.Settings{}
|
||||
err = s.apiReq(http.MethodGet, "/api/room/settings", nil, &settings)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
var stats oldTypes.Stats
|
||||
|
||||
// create empty array so that it's not null in json
|
||||
stats.Members = []*oldTypes.Member{}
|
||||
|
||||
for _, session := range sessions {
|
||||
if session.State.IsConnected {
|
||||
stats.Connections++
|
||||
member, err := profileToMember(session.ID, session.Profile)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
// append members
|
||||
stats.Members = append(stats.Members, member)
|
||||
}
|
||||
}
|
||||
|
||||
locks, err := s.settingsToLocks(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats.Host = newStats.HostId
|
||||
// TODO: stats.Banned, not implemented yet
|
||||
stats.Locked = locks
|
||||
stats.ServerStartedAt = newStats.ServerStartedAt
|
||||
stats.LastAdminLeftAt = newStats.LastAdminLeftAt
|
||||
stats.LastUserLeftAt = newStats.LastUserLeftAt
|
||||
stats.ControlProtection = settings.ControlProtection
|
||||
stats.ImplicitControl = settings.ImplicitHosting
|
||||
|
||||
return json.NewEncoder(w).Encode(stats)
|
||||
})
|
||||
|
||||
r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error {
|
||||
if h.isBanned(r) {
|
||||
return utils.HttpForbidden("banned ip")
|
||||
}
|
||||
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("usr")
|
||||
password := r.URL.Query().Get("pwd")
|
||||
err := s.create(username, password)
|
||||
if err != nil {
|
||||
return utils.HttpForbidden(err.Error())
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
if !s.isAdmin {
|
||||
return utils.HttpUnauthorized().Msg("bad authorization")
|
||||
}
|
||||
|
||||
quality := r.URL.Query().Get("quality")
|
||||
|
||||
// get the screenshot
|
||||
body, headers, err := s.req(http.MethodGet, "/api/room/screen/shot.jpg?quality="+url.QueryEscape(quality), nil, nil)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// copy headers
|
||||
w.Header().Set("Content-Length", headers.Get("Content-Length"))
|
||||
w.Header().Set("Content-Type", headers.Get("Content-Type"))
|
||||
|
||||
// copy the body to the response writer
|
||||
_, err = io.Copy(w, body)
|
||||
return err
|
||||
})
|
||||
|
||||
// allow downloading and uploading files
|
||||
r.Get("/file", func(w http.ResponseWriter, r *http.Request) error {
|
||||
if h.isBanned(r) {
|
||||
return utils.HttpForbidden("banned ip")
|
||||
}
|
||||
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("usr")
|
||||
password := r.URL.Query().Get("pwd")
|
||||
err := s.create(username, password)
|
||||
if err != nil {
|
||||
return utils.HttpForbidden(err.Error())
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
|
||||
body, headers, err := s.req(http.MethodGet, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// copy headers
|
||||
w.Header().Set("Content-Length", headers.Get("Content-Length"))
|
||||
w.Header().Set("Content-Type", headers.Get("Content-Type"))
|
||||
|
||||
// copy the body to the response writer
|
||||
_, err = io.Copy(w, body)
|
||||
return err
|
||||
})
|
||||
|
||||
r.Post("/file", func(w http.ResponseWriter, r *http.Request) error {
|
||||
if h.isBanned(r) {
|
||||
return utils.HttpForbidden("banned ip")
|
||||
}
|
||||
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("usr")
|
||||
password := r.URL.Query().Get("pwd")
|
||||
err := s.create(username, password)
|
||||
if err != nil {
|
||||
return utils.HttpForbidden(err.Error())
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
body, _, err := s.req(http.MethodPost, "/api/filetransfer", r.Header, r.Body)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// copy the body to the response writer
|
||||
_, err = io.Copy(w, body)
|
||||
return err
|
||||
})
|
||||
|
||||
r.Delete("/file", func(w http.ResponseWriter, r *http.Request) error {
|
||||
if h.isBanned(r) {
|
||||
return utils.HttpForbidden("banned ip")
|
||||
}
|
||||
|
||||
s := h.newSession(r)
|
||||
|
||||
// create a new session
|
||||
username := r.URL.Query().Get("usr")
|
||||
password := r.URL.Query().Get("pwd")
|
||||
err := s.create(username, password)
|
||||
if err != nil {
|
||||
return utils.HttpForbidden(err.Error())
|
||||
}
|
||||
defer s.destroy()
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
|
||||
body, _, err := s.req(http.MethodDelete, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
// copy the body to the response writer
|
||||
_, err = io.Copy(w, body)
|
||||
return err
|
||||
})
|
||||
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := w.Write([]byte("true"))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (h *LegacyHandler) ban(sessionId string) error {
|
||||
// find session by id
|
||||
ip, ok := h.sessionIPs[sessionId]
|
||||
if !ok {
|
||||
return fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
h.bannedIPs[ip] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LegacyHandler) isBanned(r *http.Request) bool {
|
||||
ip := getIp(r)
|
||||
_, ok := h.bannedIPs[ip]
|
||||
return ok
|
||||
}
|
||||
|
||||
func getIp(r *http.Request) string {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
if e, ok := err.(*net.AddrError); ok && e.Err == "missing port in address" {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"github.com/m1k1o/neko/server/internal/http/legacy/types"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
type SystemInit struct {
|
||||
Event string `json:"event"`
|
||||
Locks map[string]string `json:"locks"`
|
||||
ImplicitHosting bool `json:"implicit_hosting"`
|
||||
FileTransfer bool `json:"file_transfer"`
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
}
|
||||
|
||||
type SystemMessage struct {
|
||||
Event string `json:"event"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SignalProvide struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
SDP string `json:"sdp"`
|
||||
Lite bool `json:"lite"`
|
||||
ICE []webrtc.ICEServer `json:"ice"`
|
||||
}
|
||||
|
||||
type SignalOffer struct {
|
||||
Event string `json:"event"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
type SignalAnswer struct {
|
||||
Event string `json:"event"`
|
||||
DisplayName string `json:"displayname"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
type SignalCandidate struct {
|
||||
Event string `json:"event"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type MembersList struct {
|
||||
Event string `json:"event"`
|
||||
Members []*types.Member `json:"members"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
Event string `json:"event"`
|
||||
*types.Member
|
||||
}
|
||||
type MemberDisconnected struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type Clipboard struct {
|
||||
Event string `json:"event"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Keyboard struct {
|
||||
Event string `json:"event"`
|
||||
Layout *string `json:"layout,omitempty"`
|
||||
CapsLock *bool `json:"capsLock,omitempty"`
|
||||
NumLock *bool `json:"numLock,omitempty"`
|
||||
ScrollLock *bool `json:"scrollLock,omitempty"` // TODO: ScrollLock is deprecated.
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type ControlTarget struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type ChatReceive struct {
|
||||
Event string `json:"event"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatSend struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type EmoteReceive struct {
|
||||
Event string `json:"event"`
|
||||
Emote string `json:"emote"`
|
||||
}
|
||||
|
||||
type EmoteSend struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Emote string `json:"emote"`
|
||||
}
|
||||
|
||||
type FileTransferList struct {
|
||||
Event string `json:"event"`
|
||||
Cwd string `json:"cwd"`
|
||||
UserDownload bool `json:"user_download"`
|
||||
UserUpload bool `json:"user_upload"`
|
||||
UserDelete bool `json:"user_delete"`
|
||||
Files []types.FileListItem `json:"files"`
|
||||
}
|
||||
|
||||
type Admin struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type AdminTarget struct {
|
||||
Event string `json:"event"`
|
||||
Target string `json:"target"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type AdminLock struct {
|
||||
Event string `json:"event"`
|
||||
Resource string `json:"resource"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type ScreenResolution struct {
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rate int16 `json:"rate"`
|
||||
}
|
||||
|
||||
type ScreenConfigurations struct {
|
||||
Event string `json:"event"`
|
||||
Configurations map[int]types.ScreenConfiguration `json:"configurations"`
|
||||
}
|
||||
|
||||
type BroadcastStatus struct {
|
||||
Event string `json:"event"`
|
||||
URL string `json:"url"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type BroadcastCreate struct {
|
||||
Event string `json:"event"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/api"
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWebsocketSend = fmt.Errorf("failed to send message to websocket")
|
||||
ErrBackendRespone = fmt.Errorf("error response from backend")
|
||||
)
|
||||
|
||||
type memberStruct struct {
|
||||
member *oldTypes.Member
|
||||
connected bool
|
||||
sent bool
|
||||
}
|
||||
|
||||
type session struct {
|
||||
r *http.Request
|
||||
h *LegacyHandler
|
||||
|
||||
logger zerolog.Logger
|
||||
serverAddr string
|
||||
pathPrefix string
|
||||
|
||||
id, ip string
|
||||
token string
|
||||
cookie string
|
||||
name string
|
||||
isAdmin bool
|
||||
client *http.Client
|
||||
|
||||
lastHostID string
|
||||
lockedControls bool
|
||||
lockedLogins bool
|
||||
lockedFileTransfer bool
|
||||
sessions map[string]*memberStruct
|
||||
|
||||
muClient sync.Mutex
|
||||
connClient *websocket.Conn
|
||||
muBackend sync.Mutex
|
||||
connBackend *websocket.Conn
|
||||
}
|
||||
|
||||
func (h *LegacyHandler) newSession(r *http.Request) *session {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil // disable proxy for local requests
|
||||
|
||||
return &session{
|
||||
r: r,
|
||||
h: h,
|
||||
logger: h.logger,
|
||||
serverAddr: h.serverAddr,
|
||||
pathPrefix: h.pathPrefix,
|
||||
client: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
sessions: make(map[string]*memberStruct),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) req(method, reqPath string, headers http.Header, request io.Reader) (io.ReadCloser, http.Header, error) {
|
||||
req, err := http.NewRequest(method, "http://"+s.serverAddr+path.Join(s.pathPrefix, reqPath), request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
if s.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+s.token)
|
||||
} else if s.cookie != "" {
|
||||
req.Header.Set("Cookie", s.cookie)
|
||||
}
|
||||
|
||||
res, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
// try to unmarsal as json error message
|
||||
var apiErr struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &apiErr); err == nil {
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrBackendRespone, apiErr.Message)
|
||||
}
|
||||
// return raw body if failed to unmarshal
|
||||
return nil, nil, fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
return res.Body, res.Header, nil
|
||||
}
|
||||
|
||||
func (s *session) apiReq(method, path string, request, response any) error {
|
||||
return s.apiReqWithHeaders(method, path, request, response, nil)
|
||||
}
|
||||
|
||||
func (s *session) apiReqWithHeaders(method, path string, request, response any, headers http.Header) error {
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if headers == nil {
|
||||
headers = make(http.Header)
|
||||
}
|
||||
headers.Set("Content-Type", "application/json")
|
||||
|
||||
resBody, _, err := s.req(method, path, headers, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resBody.Close()
|
||||
|
||||
if resBody == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
io.Copy(io.Discard, resBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.NewDecoder(resBody).Decode(response)
|
||||
}
|
||||
|
||||
// send message to client (in old format)
|
||||
func (s *session) toClient(payload any) error {
|
||||
s.muClient.Lock()
|
||||
defer s.muClient.Unlock()
|
||||
|
||||
err := s.connClient.WriteJSON(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrWebsocketSend, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// send message to backend (in new format)
|
||||
func (s *session) toBackend(event string, payload any) error {
|
||||
s.muBackend.Lock()
|
||||
defer s.muBackend.Unlock()
|
||||
|
||||
rawPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.connBackend.WriteJSON(types.WebSocketMessage{
|
||||
Event: event,
|
||||
Payload: rawPayload,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrWebsocketSend, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) create(username, password string) error {
|
||||
data := api.SessionDataPayload{}
|
||||
if cookie := s.r.Header.Get("Cookie"); cookie != "" {
|
||||
err := s.apiReqWithHeaders(http.MethodGet, "/api/whoami", nil, &data, http.Header{
|
||||
"Cookie": []string{cookie},
|
||||
})
|
||||
if err == nil {
|
||||
s.cookie = cookie
|
||||
s.setSessionData(data)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := s.apiReq(http.MethodPost, "/api/login", api.SessionLoginPayload{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.setSessionData(data)
|
||||
|
||||
if s.token == "" {
|
||||
return fmt.Errorf("token not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) setSessionData(data api.SessionDataPayload) {
|
||||
s.id, s.ip = data.ID, getIp(s.r)
|
||||
s.h.sessionIPs[s.id] = s.ip // save session ip by id
|
||||
s.token = data.Token
|
||||
s.name = data.Profile.Name
|
||||
s.isAdmin = data.Profile.IsAdmin
|
||||
}
|
||||
|
||||
func (s *session) destroy() {
|
||||
defer s.client.CloseIdleConnections()
|
||||
|
||||
// logout session
|
||||
err := s.apiReq(http.MethodPost, "/api/logout", nil, nil)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to logout")
|
||||
}
|
||||
|
||||
// remove session id from ip map
|
||||
delete(s.h.sessionIPs, s.id)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
)
|
||||
|
||||
func TestCreateUsesExistingSessionCookie(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/whoami":
|
||||
if r.Method != http.MethodGet {
|
||||
t.Fatalf("method = %s, want GET", r.Method)
|
||||
}
|
||||
if got := r.Header.Get("Cookie"); got != "NEKO_SESSION=session-token" {
|
||||
t.Fatalf("cookie = %q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "oauth:user-123",
|
||||
"profile": types.MemberProfile{
|
||||
Name: "Ada Lovelace",
|
||||
IsAdmin: true,
|
||||
},
|
||||
})
|
||||
case "/api/protected":
|
||||
if got := r.Header.Get("Cookie"); got != "NEKO_SESSION=session-token" {
|
||||
t.Fatalf("authenticated request cookie = %q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(true)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "http://neko.test/ws", nil)
|
||||
request.Header.Set("Cookie", "NEKO_SESSION=session-token")
|
||||
handler := &LegacyHandler{sessionIPs: make(map[string]string)}
|
||||
session := &session{
|
||||
r: request,
|
||||
h: handler,
|
||||
serverAddr: strings.TrimPrefix(backend.URL, "http://"),
|
||||
client: backend.Client(),
|
||||
}
|
||||
|
||||
if err := session.create("", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if session.id != "oauth:user-123" || session.name != "Ada Lovelace" || !session.isAdmin {
|
||||
t.Fatalf("session = %#v", session)
|
||||
}
|
||||
if session.cookie != "NEKO_SESSION=session-token" || session.token != "" {
|
||||
t.Fatalf("authentication state = cookie %q token %q", session.cookie, session.token)
|
||||
}
|
||||
if err := session.apiReq(http.MethodGet, "/api/protected", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type Stats struct {
|
||||
Connections uint32 `json:"connections"`
|
||||
Host string `json:"host"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"displayname"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Admin bool `json:"admin"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type FileListItem struct {
|
||||
Filename string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type ScreenConfiguration struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rates map[int]int16 `json:"rates"`
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
|
||||
oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/api/room"
|
||||
"github.com/m1k1o/neko/server/internal/plugins/chat"
|
||||
"github.com/m1k1o/neko/server/internal/plugins/filetransfer"
|
||||
"github.com/m1k1o/neko/server/internal/plugins/openinapp"
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
"github.com/m1k1o/neko/server/pkg/types/event"
|
||||
"github.com/m1k1o/neko/server/pkg/types/message"
|
||||
)
|
||||
|
||||
func (s *session) wsToBackend(msg []byte) error {
|
||||
header := oldMessage.Message{}
|
||||
err := json.Unmarshal(msg, &header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch header.Event {
|
||||
// Client Events
|
||||
case oldEvent.CLIENT_HEARTBEAT:
|
||||
// do nothing
|
||||
return nil
|
||||
|
||||
// Signal Events
|
||||
case oldEvent.SIGNAL_OFFER:
|
||||
request := &oldMessage.SignalOffer{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(event.SIGNAL_OFFER, &message.SignalDescription{
|
||||
SDP: request.SDP,
|
||||
})
|
||||
|
||||
case oldEvent.SIGNAL_ANSWER:
|
||||
request := &oldMessage.SignalAnswer{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.DisplayName != "" {
|
||||
// Update display name if it is not set
|
||||
if s.name == "" {
|
||||
err = s.apiReq(http.MethodPost, "/api/profile", map[string]any{
|
||||
"name": request.DisplayName,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.name = request.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
// try to set legacy video stream, if it fails, it will be ignored
|
||||
if err := s.toBackend(event.SIGNAL_VIDEO, &message.SignalVideo{
|
||||
PeerVideoRequest: types.PeerVideoRequest{
|
||||
Selector: &types.StreamSelector{
|
||||
Type: types.StreamSelectorTypeExact,
|
||||
ID: "legacy",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(event.SIGNAL_ANSWER, &message.SignalDescription{
|
||||
SDP: request.SDP,
|
||||
})
|
||||
|
||||
case oldEvent.SIGNAL_CANDIDATE:
|
||||
request := &oldMessage.SignalCandidate{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var candidate webrtc.ICECandidateInit
|
||||
err = json.Unmarshal([]byte(request.Data), &candidate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(event.SIGNAL_CANDIDATE, &message.SignalCandidate{
|
||||
ICECandidateInit: candidate,
|
||||
})
|
||||
|
||||
// Control Events
|
||||
case oldEvent.CONTROL_RELEASE:
|
||||
return s.toBackend(event.CONTROL_RELEASE, nil)
|
||||
|
||||
case oldEvent.CONTROL_REQUEST:
|
||||
return s.toBackend(event.CONTROL_REQUEST, nil)
|
||||
|
||||
case oldEvent.CONTROL_GIVE:
|
||||
request := &oldMessage.Control{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/room/control/give/"+request.ID, nil, nil)
|
||||
|
||||
case oldEvent.CONTROL_CLIPBOARD:
|
||||
request := &oldMessage.Clipboard{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(event.CLIPBOARD_SET, &message.ClipboardData{
|
||||
Text: request.Text,
|
||||
})
|
||||
|
||||
case oldEvent.CONTROL_KEYBOARD:
|
||||
request := &oldMessage.Keyboard{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Layout != nil {
|
||||
err = s.toBackend(event.KEYBOARD_MAP, &message.KeyboardMap{
|
||||
KeyboardMap: types.KeyboardMap{
|
||||
Layout: *request.Layout,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if request.CapsLock != nil || request.NumLock != nil || request.ScrollLock != nil {
|
||||
err = s.toBackend(event.KEYBOARD_MODIFIERS, &message.KeyboardModifiers{
|
||||
KeyboardModifiers: types.KeyboardModifiers{
|
||||
CapsLock: request.CapsLock,
|
||||
NumLock: request.NumLock,
|
||||
// ScrollLock: request.ScrollLock, // ScrollLock is deprecated.
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// Chat Events
|
||||
case oldEvent.CHAT_MESSAGE:
|
||||
request := &oldMessage.ChatReceive{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(chat.CHAT_MESSAGE, &chat.Content{
|
||||
Text: request.Content,
|
||||
})
|
||||
|
||||
case oldEvent.CHAT_EMOTE:
|
||||
request := &oldMessage.EmoteReceive{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// loopback emote
|
||||
err = s.toClient(&oldMessage.EmoteSend{
|
||||
Event: oldEvent.CHAT_EMOTE,
|
||||
ID: s.id,
|
||||
Emote: request.Emote,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// broadcast emote to other users
|
||||
return s.toBackend(event.SEND_BROADCAST, &message.SendBroadcast{
|
||||
Sender: s.id,
|
||||
Subject: "emote",
|
||||
Body: request.Emote,
|
||||
})
|
||||
|
||||
// File Transfer Events
|
||||
case oldEvent.FILETRANSFER_REFRESH:
|
||||
return s.toBackend(filetransfer.FILETRANSFER_UPDATE, nil)
|
||||
|
||||
// Open In App Events
|
||||
case openinapp.OPENINAPP_OPENLINK:
|
||||
request := &openinapp.Url{}
|
||||
if err := json.Unmarshal(msg, request); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.apiReq(http.MethodPost, "/api/openinapp/openlink", request, nil)
|
||||
|
||||
// Screen Events
|
||||
case oldEvent.SCREEN_RESOLUTION:
|
||||
response := &types.ScreenSize{}
|
||||
err := s.apiReq(http.MethodGet, "/api/room/screen", nil, response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.ScreenResolution{
|
||||
Event: oldEvent.SCREEN_RESOLUTION,
|
||||
Width: response.Width,
|
||||
Height: response.Height,
|
||||
Rate: response.Rate,
|
||||
})
|
||||
|
||||
case oldEvent.SCREEN_CONFIGURATIONS:
|
||||
response := &[]types.ScreenSize{}
|
||||
err := s.apiReq(http.MethodGet, "/api/room/screen/configurations", nil, response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.ScreenConfigurations{
|
||||
Event: oldEvent.SCREEN_CONFIGURATIONS,
|
||||
Configurations: screenConfigurations(*response),
|
||||
})
|
||||
|
||||
case oldEvent.SCREEN_SET:
|
||||
request := &oldMessage.ScreenResolution{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toBackend(event.SCREEN_SET, &message.ScreenSize{
|
||||
ScreenSize: types.ScreenSize{
|
||||
Width: request.Width,
|
||||
Height: request.Height,
|
||||
Rate: request.Rate,
|
||||
},
|
||||
})
|
||||
|
||||
// Broadcast Events
|
||||
case oldEvent.BROADCAST_CREATE:
|
||||
request := &oldMessage.BroadcastCreate{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/room/broadcast/start", room.BroadcastStatusPayload{
|
||||
URL: request.URL,
|
||||
IsActive: true,
|
||||
}, nil)
|
||||
|
||||
case oldEvent.BROADCAST_DESTROY:
|
||||
return s.apiReq(http.MethodPost, "/api/room/broadcast/stop", nil, nil)
|
||||
|
||||
// Admin Events
|
||||
case oldEvent.ADMIN_LOCK:
|
||||
request := &oldMessage.AdminLock{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]any{}
|
||||
|
||||
switch request.Resource {
|
||||
case "login":
|
||||
data["locked_logins"] = true
|
||||
case "control":
|
||||
data["locked_controls"] = true
|
||||
case "file_transfer":
|
||||
data["plugins"] = map[string]any{
|
||||
"filetransfer.enabled": false,
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown resource: %s", request.Resource)
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/room/settings", data, nil)
|
||||
|
||||
case oldEvent.ADMIN_UNLOCK:
|
||||
request := &oldMessage.AdminLock{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]any{}
|
||||
|
||||
switch request.Resource {
|
||||
case "login":
|
||||
data["locked_logins"] = false
|
||||
case "control":
|
||||
data["locked_controls"] = false
|
||||
case "file_transfer":
|
||||
data["plugins"] = map[string]any{
|
||||
"filetransfer.enabled": true,
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown resource: %s", request.Resource)
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/room/settings", data, nil)
|
||||
|
||||
case oldEvent.ADMIN_CONTROL:
|
||||
return s.apiReq(http.MethodPost, "/api/room/control/take", nil, nil)
|
||||
|
||||
case oldEvent.ADMIN_RELEASE:
|
||||
return s.apiReq(http.MethodPost, "/api/room/control/reset", nil, nil)
|
||||
|
||||
case oldEvent.ADMIN_GIVE:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.apiReq(http.MethodPost, "/api/room/control/give/"+request.ID, nil, nil)
|
||||
|
||||
case oldEvent.ADMIN_BAN:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.h.ban(request.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fallthrough // continue to kick
|
||||
|
||||
case oldEvent.ADMIN_KICK:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{
|
||||
"can_login": false,
|
||||
}, nil)
|
||||
|
||||
case oldEvent.ADMIN_MUTE:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{
|
||||
"plugins": map[string]any{
|
||||
"chat.can_send": false,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
case oldEvent.ADMIN_UNMUTE:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{
|
||||
"plugins": map[string]any{
|
||||
"chat.can_send": true,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown event type: %s", header.Event)
|
||||
}
|
||||
}
|
||||
@@ -1,778 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
|
||||
oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message"
|
||||
oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/plugins/chat"
|
||||
"github.com/m1k1o/neko/server/internal/plugins/filetransfer"
|
||||
"github.com/m1k1o/neko/server/internal/plugins/openinapp"
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
"github.com/m1k1o/neko/server/pkg/types/event"
|
||||
"github.com/m1k1o/neko/server/pkg/types/message"
|
||||
)
|
||||
|
||||
func profileToMember(id string, profile types.MemberProfile) (*oldTypes.Member, error) {
|
||||
settings := chat.Settings{
|
||||
CanSend: true, // defaults to true
|
||||
CanReceive: true, // defaults to true
|
||||
}
|
||||
|
||||
err := profile.Plugins.Unmarshal(chat.PluginName, &settings)
|
||||
if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) {
|
||||
return nil, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", chat.PluginName, err)
|
||||
}
|
||||
|
||||
return &oldTypes.Member{
|
||||
ID: id,
|
||||
Name: profile.Name,
|
||||
Avatar: profile.Avatar,
|
||||
Admin: profile.IsAdmin,
|
||||
Muted: !settings.CanSend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func screenConfigurations(screenSizes []types.ScreenSize) map[int]oldTypes.ScreenConfiguration {
|
||||
rates := map[string]map[int16]struct{}{}
|
||||
for _, size := range screenSizes {
|
||||
key := fmt.Sprintf("%dx%d", size.Width, size.Height)
|
||||
if _, ok := rates[key]; !ok {
|
||||
rates[key] = map[int16]struct{}{}
|
||||
}
|
||||
rates[key][size.Rate] = struct{}{}
|
||||
}
|
||||
|
||||
usedScreenSizes := map[string]struct{}{}
|
||||
screenSizesList := map[int]oldTypes.ScreenConfiguration{}
|
||||
for i, size := range screenSizes {
|
||||
key := fmt.Sprintf("%dx%d", size.Width, size.Height)
|
||||
if _, ok := usedScreenSizes[key]; ok {
|
||||
continue
|
||||
}
|
||||
usedScreenSizes[key] = struct{}{}
|
||||
|
||||
c, ratesMap := 0, map[int]int16{}
|
||||
for rate := range rates[key] {
|
||||
ratesMap[c] = rate
|
||||
c++
|
||||
}
|
||||
|
||||
screenSizesList[i] = oldTypes.ScreenConfiguration{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rates: ratesMap,
|
||||
}
|
||||
}
|
||||
|
||||
return screenSizesList
|
||||
}
|
||||
|
||||
func (s *session) settingsToLocks(settings types.Settings) (map[string]string, error) {
|
||||
//
|
||||
// FileTransfer
|
||||
//
|
||||
|
||||
filetransferSettings := filetransfer.Settings{
|
||||
Enabled: true, // defaults to true
|
||||
}
|
||||
|
||||
err := settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings)
|
||||
if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) {
|
||||
return nil, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err)
|
||||
}
|
||||
|
||||
//
|
||||
// Locks
|
||||
//
|
||||
|
||||
locks := map[string]string{}
|
||||
if settings.LockedLogins {
|
||||
locks["login"] = "" // TODO: We don't know who locked the login.
|
||||
s.lockedLogins = true
|
||||
}
|
||||
if settings.LockedControls {
|
||||
locks["control"] = "" // TODO: We don't know who locked the control.
|
||||
s.lockedControls = true
|
||||
}
|
||||
if !filetransferSettings.Enabled {
|
||||
locks["file_transfer"] = "" // TODO: We don't know who locked the file transfer.
|
||||
s.lockedFileTransfer = true
|
||||
}
|
||||
|
||||
return locks, nil
|
||||
}
|
||||
|
||||
func (s *session) sendControlHost(request message.ControlHost) error {
|
||||
lastHostID := s.lastHostID
|
||||
|
||||
if request.HasHost {
|
||||
s.lastHostID = request.ID
|
||||
|
||||
if request.ID == request.HostID {
|
||||
if request.ID == lastHostID || lastHostID == "" {
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.CONTROL_LOCKED,
|
||||
ID: request.HostID,
|
||||
})
|
||||
} else {
|
||||
return s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_CONTROL,
|
||||
ID: request.ID,
|
||||
Target: lastHostID,
|
||||
})
|
||||
}
|
||||
} else if request.ID == "" {
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.CONTROL_LOCKED,
|
||||
ID: request.HostID,
|
||||
})
|
||||
} else {
|
||||
return s.toClient(&oldMessage.ControlTarget{
|
||||
Event: oldEvent.CONTROL_GIVE,
|
||||
ID: request.ID,
|
||||
Target: request.HostID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if request.ID != "" {
|
||||
s.lastHostID = ""
|
||||
|
||||
if request.ID == lastHostID {
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.CONTROL_RELEASE,
|
||||
ID: request.ID,
|
||||
})
|
||||
} else {
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.ADMIN_RELEASE,
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) wsToClient(msg []byte) error {
|
||||
data := types.WebSocketMessage{}
|
||||
err := json.Unmarshal(msg, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch data.Event {
|
||||
// System Events
|
||||
case event.SYSTEM_DISCONNECT:
|
||||
request := &message.SystemDisconnect{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if profile change is the reason for disconnect, it's a kick event
|
||||
if request.Message == "profile changed" {
|
||||
request.Message = "kicked"
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SystemMessage{
|
||||
Event: oldEvent.SYSTEM_DISCONNECT,
|
||||
Message: request.Message,
|
||||
})
|
||||
|
||||
case event.SYSTEM_INIT:
|
||||
request := &message.SystemInit{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// MembersList
|
||||
//
|
||||
|
||||
membersList := []*oldTypes.Member{}
|
||||
s.sessions = map[string]*memberStruct{}
|
||||
for id, session := range request.Sessions {
|
||||
if !session.State.IsConnected {
|
||||
continue
|
||||
}
|
||||
member, err := profileToMember(id, session.Profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
membersList = append(membersList, member)
|
||||
s.sessions[id] = &memberStruct{
|
||||
sent: member.Name != "",
|
||||
connected: true,
|
||||
member: member,
|
||||
}
|
||||
}
|
||||
|
||||
err = s.toClient(&oldMessage.MembersList{
|
||||
Event: oldEvent.MEMBER_LIST,
|
||||
Members: membersList,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// ScreenSize
|
||||
//
|
||||
|
||||
err = s.toClient(&oldMessage.ScreenResolution{
|
||||
Event: oldEvent.SCREEN_RESOLUTION,
|
||||
Width: request.ScreenSize.Width,
|
||||
Height: request.ScreenSize.Height,
|
||||
Rate: request.ScreenSize.Rate,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// actually its already set when we create the session
|
||||
s.id = request.SessionId
|
||||
|
||||
//
|
||||
// ControlHost
|
||||
//
|
||||
|
||||
err = s.sendControlHost(request.ControlHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locks, err := s.settingsToLocks(request.Settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SystemInit{
|
||||
Event: oldEvent.SYSTEM_INIT,
|
||||
ImplicitHosting: request.Settings.ImplicitHosting,
|
||||
Locks: locks,
|
||||
// TODO: hack - we don't know if file transfer is enabled, we would need to check the global config.
|
||||
FileTransfer: viper.GetBool("filetransfer.enabled") || (viper.GetBool("legacy") && viper.GetBool("file_transfer_enabled")),
|
||||
HeartbeatInterval: request.Settings.HeartbeatInterval,
|
||||
})
|
||||
|
||||
case event.SYSTEM_ADMIN:
|
||||
request := &message.SystemAdmin{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// ScreenSizesList
|
||||
//
|
||||
|
||||
err = s.toClient(&oldMessage.ScreenConfigurations{
|
||||
Event: oldEvent.SCREEN_CONFIGURATIONS,
|
||||
Configurations: screenConfigurations(request.ScreenSizesList),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// BroadcastStatus
|
||||
//
|
||||
|
||||
return s.toClient(&oldMessage.BroadcastStatus{
|
||||
Event: oldEvent.BROADCAST_STATUS,
|
||||
URL: request.BroadcastStatus.URL,
|
||||
IsActive: request.BroadcastStatus.IsActive,
|
||||
})
|
||||
|
||||
// Member Events
|
||||
|
||||
case event.SESSION_CREATED:
|
||||
request := &message.SessionData{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
member, err := profileToMember(request.ID, request.Profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only save session - will be notified on connect
|
||||
s.sessions[request.ID] = &memberStruct{
|
||||
member: member,
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case event.SESSION_DELETED:
|
||||
request := &message.SessionID{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only continue if session is in the list - should have been already removed
|
||||
if _, ok := s.sessions[request.ID]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
delete(s.sessions, request.ID)
|
||||
|
||||
return s.toClient(&oldMessage.MemberDisconnected{
|
||||
Event: oldEvent.MEMBER_DISCONNECTED,
|
||||
ID: request.ID,
|
||||
})
|
||||
|
||||
case event.SESSION_PROFILE:
|
||||
request := &message.MemberProfile{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// session profile is expected to change when updating a name after connecting
|
||||
m, ok := s.sessions[request.ID]
|
||||
if !ok || m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// update member profile
|
||||
member, err := profileToMember(request.ID, request.MemberProfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mutedChanged := m.member.Muted != member.Muted
|
||||
m.member = member
|
||||
|
||||
if m.connected && !m.sent && member.Name != "" {
|
||||
m.sent = true
|
||||
|
||||
// oldEvent.MEMBER_CONNECTED if not sent already
|
||||
err = s.toClient(&oldMessage.Member{
|
||||
Event: oldEvent.MEMBER_CONNECTED,
|
||||
Member: member,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if mutedChanged && member.Muted {
|
||||
return s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_MUTE,
|
||||
ID: "", // TODO: We don't know who (un)muted the user.
|
||||
Target: request.ID,
|
||||
})
|
||||
} else if mutedChanged && !member.Muted {
|
||||
return s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_UNMUTE,
|
||||
ID: "", // TODO: We don't know who (un)muted the user.
|
||||
Target: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case event.SESSION_STATE:
|
||||
request := &message.SessionState{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, ok := s.sessions[request.ID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if request.IsConnected {
|
||||
m.connected = true
|
||||
|
||||
if m.member.Muted {
|
||||
err = s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_MUTE,
|
||||
ID: "", // TODO: We don't know who (un)muted the user.
|
||||
Target: request.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !m.sent && m.member.Name != "" {
|
||||
m.sent = true
|
||||
|
||||
// oldEvent.MEMBER_CONNECTED if not sent already
|
||||
return s.toClient(&oldMessage.Member{
|
||||
Event: oldEvent.MEMBER_CONNECTED,
|
||||
Member: m.member,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !request.IsConnected {
|
||||
delete(s.sessions, request.ID)
|
||||
|
||||
// oldEvent.MEMBER_DISCONNECTED if nor sent already
|
||||
return s.toClient(&oldMessage.MemberDisconnected{
|
||||
Event: oldEvent.MEMBER_DISCONNECTED,
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// Signal Events
|
||||
case event.SIGNAL_VIDEO, event.SIGNAL_AUDIO:
|
||||
// ignore video and audio signals, because they are not part of the legacy protocol
|
||||
return nil
|
||||
|
||||
case event.SIGNAL_OFFER:
|
||||
request := &message.SignalDescription{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SignalOffer{
|
||||
Event: oldEvent.SIGNAL_OFFER,
|
||||
SDP: request.SDP,
|
||||
})
|
||||
|
||||
case event.SIGNAL_ANSWER:
|
||||
request := &message.SignalDescription{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SignalAnswer{
|
||||
Event: oldEvent.SIGNAL_ANSWER,
|
||||
DisplayName: s.name,
|
||||
SDP: request.SDP,
|
||||
})
|
||||
|
||||
case event.SIGNAL_CANDIDATE:
|
||||
request := &message.SignalCandidate{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
json, err := json.Marshal(request.ICECandidateInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SignalCandidate{
|
||||
Event: oldEvent.SIGNAL_CANDIDATE,
|
||||
Data: string(json),
|
||||
})
|
||||
|
||||
case event.SIGNAL_PROVIDE:
|
||||
request := &message.SignalProvide{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iceServers := []webrtc.ICEServer{}
|
||||
for _, ice := range request.ICEServers {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: ice.URLs,
|
||||
Username: ice.Username,
|
||||
Credential: ice.Credential,
|
||||
CredentialType: webrtc.ICECredentialTypePassword,
|
||||
})
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.SignalProvide{
|
||||
Event: oldEvent.SIGNAL_PROVIDE,
|
||||
ID: s.id, // SessionId
|
||||
SDP: request.SDP,
|
||||
Lite: len(iceServers) == 0, // if no ICE servers are provided, it's a lite offer
|
||||
ICE: iceServers,
|
||||
})
|
||||
|
||||
// Control Events
|
||||
case event.CLIPBOARD_UPDATED:
|
||||
request := &message.ClipboardData{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.Clipboard{
|
||||
Event: oldEvent.CONTROL_CLIPBOARD,
|
||||
Text: request.Text,
|
||||
})
|
||||
|
||||
case event.CONTROL_HOST:
|
||||
request := &message.ControlHost{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.sendControlHost(*request)
|
||||
|
||||
case event.CONTROL_REQUEST:
|
||||
request := &message.SessionID{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.id == request.ID {
|
||||
// if i am the one that is requesting, send CONTROL_REQUEST to me
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.CONTROL_REQUEST,
|
||||
ID: request.ID,
|
||||
})
|
||||
} else {
|
||||
// if not, let me know someone else is requesting
|
||||
return s.toClient(&oldMessage.Control{
|
||||
Event: oldEvent.CONTROL_REQUESTING,
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Chat Events
|
||||
case chat.CHAT_INIT:
|
||||
// ignore chat init, because it is not part of the legacy protocol
|
||||
return nil
|
||||
|
||||
case chat.CHAT_MESSAGE:
|
||||
request := &chat.Message{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.ChatSend{
|
||||
Event: oldEvent.CHAT_MESSAGE,
|
||||
ID: request.ID,
|
||||
Content: request.Content.Text,
|
||||
})
|
||||
|
||||
case event.SEND_BROADCAST:
|
||||
request := &message.SendBroadcast{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Subject == "emote" {
|
||||
return s.toClient(&oldMessage.EmoteSend{
|
||||
Event: oldEvent.CHAT_EMOTE,
|
||||
ID: request.Sender,
|
||||
Emote: request.Body.(string),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// File Transfer Events
|
||||
case filetransfer.FILETRANSFER_UPDATE:
|
||||
request := &filetransfer.Message{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files := []oldTypes.FileListItem{}
|
||||
for _, file := range request.Files {
|
||||
var itemType string
|
||||
switch file.Type {
|
||||
case filetransfer.ItemTypeFile:
|
||||
itemType = "file"
|
||||
case filetransfer.ItemTypeDir:
|
||||
itemType = "dir"
|
||||
}
|
||||
files = append(files, oldTypes.FileListItem{
|
||||
Filename: file.Name,
|
||||
Type: itemType,
|
||||
Size: file.Size,
|
||||
})
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.FileTransferList{
|
||||
Event: oldEvent.FILETRANSFER_LIST,
|
||||
Cwd: request.RootDir,
|
||||
UserDownload: request.UserDownload,
|
||||
UserUpload: request.UserUpload,
|
||||
UserDelete: request.UserDelete,
|
||||
Files: files,
|
||||
})
|
||||
|
||||
// Open In App Events
|
||||
case openinapp.OPENINAPP_INIT:
|
||||
request := &openinapp.Init{}
|
||||
if err := json.Unmarshal(data.Payload, request); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.toClient(&struct {
|
||||
Event string `json:"event"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Event: openinapp.OPENINAPP_INIT,
|
||||
Enabled: request.Enabled,
|
||||
})
|
||||
|
||||
// Screen Events
|
||||
case event.SCREEN_UPDATED:
|
||||
request := &message.ScreenSizeUpdate{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.ScreenResolution{
|
||||
Event: oldEvent.SCREEN_RESOLUTION,
|
||||
ID: request.ID,
|
||||
Width: request.ScreenSize.Width,
|
||||
Height: request.ScreenSize.Height,
|
||||
Rate: request.ScreenSize.Rate,
|
||||
})
|
||||
|
||||
// Broadcast Events
|
||||
case event.BROADCAST_STATUS:
|
||||
request := &message.BroadcastStatus{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.toClient(&oldMessage.BroadcastStatus{
|
||||
Event: oldEvent.BROADCAST_STATUS,
|
||||
URL: request.URL,
|
||||
IsActive: request.IsActive,
|
||||
})
|
||||
|
||||
// Admin Events
|
||||
case event.SYSTEM_SETTINGS:
|
||||
request := &message.SystemSettingsUpdate{}
|
||||
err := json.Unmarshal(data.Payload, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.lockedControls != request.LockedControls {
|
||||
s.lockedControls = request.LockedControls
|
||||
|
||||
if request.LockedControls {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_LOCK,
|
||||
Resource: "control",
|
||||
ID: request.ID,
|
||||
})
|
||||
} else {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_UNLOCK,
|
||||
Resource: "control",
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.lockedLogins != request.LockedLogins {
|
||||
s.lockedLogins = request.LockedLogins
|
||||
|
||||
if request.LockedLogins {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_LOCK,
|
||||
Resource: "login",
|
||||
ID: request.ID,
|
||||
})
|
||||
} else {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_UNLOCK,
|
||||
Resource: "login",
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// FileTransfer
|
||||
//
|
||||
|
||||
filetransferSettings := filetransfer.Settings{
|
||||
Enabled: true, // defaults to true
|
||||
}
|
||||
|
||||
err = request.Settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings)
|
||||
if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) {
|
||||
return fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err)
|
||||
}
|
||||
|
||||
if s.lockedFileTransfer != !filetransferSettings.Enabled {
|
||||
s.lockedFileTransfer = !filetransferSettings.Enabled
|
||||
|
||||
if !filetransferSettings.Enabled {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_LOCK,
|
||||
Resource: "file_transfer",
|
||||
ID: request.ID,
|
||||
})
|
||||
} else {
|
||||
err = s.toClient(&oldMessage.AdminLock{
|
||||
Event: oldEvent.ADMIN_UNLOCK,
|
||||
Resource: "file_transfer",
|
||||
ID: request.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
/*
|
||||
case:
|
||||
s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_BAN,
|
||||
})
|
||||
case:
|
||||
s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_KICK,
|
||||
})
|
||||
case:
|
||||
s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_MUTE,
|
||||
})
|
||||
case:
|
||||
s.toClient(&oldMessage.AdminTarget{
|
||||
Event: oldEvent.ADMIN_UNMUTE,
|
||||
})
|
||||
*/
|
||||
|
||||
case event.SYSTEM_HEARTBEAT:
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown event type: %s", data.Event)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
)
|
||||
|
||||
func TestProfileToMemberPreservesAvatar(t *testing.T) {
|
||||
member, err := profileToMember("oauth:user-123", types.MemberProfile{
|
||||
Name: "Ada Lovelace",
|
||||
Avatar: "https://example.test/ada.png",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if member.Name != "Ada Lovelace" || member.Avatar != "https://example.test/ada.png" {
|
||||
t.Fatalf("member = %#v", member)
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,17 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/config"
|
||||
"github.com/m1k1o/neko/server/internal/http/legacy"
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
)
|
||||
|
||||
@@ -56,7 +55,13 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
|
||||
|
||||
router.Get("/api/ws", WebSocketManager.Upgrade(func(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
return origin == "" || config.AllowOrigin(origin)
|
||||
if origin == "" || config.AllowOrigin(origin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// by default, only same-origin browser connections are accepted
|
||||
u, err := url.Parse(origin)
|
||||
return err == nil && u.Host != "" && strings.EqualFold(u.Host, r.Host)
|
||||
}))
|
||||
|
||||
batch := batchHandler{
|
||||
@@ -139,24 +144,6 @@ func (manager *HttpManagerCtx) Start() {
|
||||
}
|
||||
}()
|
||||
manager.logger.Info().Msgf("https listening on %s", manager.http.Addr)
|
||||
|
||||
// if we have legacy mode, we need to start local http server too
|
||||
if viper.GetBool("legacy") {
|
||||
// create a listener for the API server with a random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to start legacy http proxy")
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(listener, manager.router); err != http.ErrServerClosed {
|
||||
manager.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
manager.logger.Info().Msgf("legacy proxy listening on %s", listener.Addr().String())
|
||||
|
||||
legacy.New(listener.Addr().String(), manager.config.PathPrefix).Route(manager.router)
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if err := manager.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
@@ -164,11 +151,6 @@ func (manager *HttpManagerCtx) Start() {
|
||||
}
|
||||
}()
|
||||
manager.logger.Info().Msgf("http listening on %s", manager.http.Addr)
|
||||
|
||||
// start legacy proxy if enabled
|
||||
if viper.GetBool("legacy") {
|
||||
legacy.New(manager.http.Addr, manager.config.PathPrefix).Route(manager.router)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,3 +109,24 @@ func TestWebSocketOriginAllowsInternalDialWithoutOrigin(t *testing.T) {
|
||||
t.Fatal("unconfigured browser Origin was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketOriginDefaultsToSameOrigin(t *testing.T) {
|
||||
webSocketManager := &originCapturingWebSocketManager{}
|
||||
serverConfig := &config.Server{}
|
||||
memberConfig := &config.Member{}
|
||||
sessionManager := session.New(&config.Session{})
|
||||
apiManager := api.New(sessionManager, member.New(sessionManager, memberConfig), nil, nil, memberConfig, serverConfig)
|
||||
New(webSocketManager, apiManager, serverConfig, memberConfig)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "http://neko.example.test/api/ws", nil)
|
||||
|
||||
request.Header.Set("Origin", "http://neko.example.test")
|
||||
if !webSocketManager.checkOrigin(request) {
|
||||
t.Fatal("same Origin was rejected")
|
||||
}
|
||||
|
||||
request.Header.Set("Origin", "https://attacker.example")
|
||||
if webSocketManager.checkOrigin(request) {
|
||||
t.Fatal("cross-site Origin was accepted with empty CORS")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,12 @@ func (service *Service) Complete(ctx context.Context, stateToken, code string) (
|
||||
email = claim(idClaims, "email")
|
||||
}
|
||||
|
||||
// Providers that omit email_verified are trusted; only an explicit false is rejected.
|
||||
if email != "" && (claimFalse(claims, "email_verified") || claimFalse(idClaims, "email_verified")) {
|
||||
log.Warn().Msg("OAuth provider reported the email as unverified; ignoring it for admin_emails/user_emails")
|
||||
email = ""
|
||||
}
|
||||
|
||||
if email == "" && (len(config.AdminEmails) > 0 || len(config.UserEmails) > 0) {
|
||||
log.Warn().Msg("OAuth admin_emails/user_emails are configured but the provider did not return an 'email' claim; request the 'email' scope")
|
||||
}
|
||||
|
||||
@@ -70,6 +70,21 @@ func boolClaim(claims map[string]any, field string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// claimFalse reports whether the claim is present and explicitly false.
|
||||
func claimFalse(claims map[string]any, field string) bool {
|
||||
value, ok := claims[field]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch value := value.(type) {
|
||||
case bool:
|
||||
return !value
|
||||
case string:
|
||||
return strings.EqualFold(value, "false")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func administrator(email string, isAdmin bool, emails []string) bool {
|
||||
if isAdmin {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package oauth
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClaimFalse(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
claims map[string]any
|
||||
want bool
|
||||
}{
|
||||
{"absent", map[string]any{}, false},
|
||||
{"true", map[string]any{"email_verified": true}, false},
|
||||
{"false", map[string]any{"email_verified": false}, true},
|
||||
{"string true", map[string]any{"email_verified": "true"}, false},
|
||||
{"string false", map[string]any{"email_verified": "False"}, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := claimFalse(tc.claims, "email_verified"); got != tc.want {
|
||||
t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,16 +47,11 @@ func (session *SessionCtx) profileChanged() {
|
||||
}
|
||||
|
||||
if (!session.profile.CanConnect || !session.profile.CanLogin || !session.profile.CanWatch) && session.state.IsWatching {
|
||||
// TODO: Needed for legacy implementation. Websocket must die before webrtc and deliver signal close message
|
||||
// otherwise webrtc destroy would trigger websocket reconnect. In case of kick event, webrtc destroy is called
|
||||
// before websocket destroy that delivers the information about the kick.
|
||||
time.AfterFunc(time.Second, func() {
|
||||
// The peer may have been removed if the user disconnected while
|
||||
// waiting for the delayed teardown.
|
||||
if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil {
|
||||
webrtcPeer.Destroy()
|
||||
}
|
||||
})
|
||||
// The peer may have been removed if the user disconnected while
|
||||
// waiting for the delayed teardown.
|
||||
if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil {
|
||||
webrtcPeer.Destroy()
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected {
|
||||
@@ -77,18 +72,6 @@ func (session *SessionCtx) IsHost() bool {
|
||||
return session.manager.isHost(session)
|
||||
}
|
||||
|
||||
// only needed for legacy webrtc handler
|
||||
func (session *SessionCtx) LegacyIsHost() bool {
|
||||
settings := session.manager.Settings()
|
||||
if !session.profile.CanHost || session.PrivateModeEnabled() {
|
||||
return false
|
||||
}
|
||||
if settings.LockedControls && !session.profile.IsAdmin {
|
||||
return false
|
||||
}
|
||||
return settings.ImplicitHosting || session.manager.isHost(session)
|
||||
}
|
||||
|
||||
func (session *SessionCtx) SetAsHost() {
|
||||
session.manager.setHost(session, session)
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
|
||||
"github.com/m1k1o/neko/server/pkg/types"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
OP_MOVE = 0x01
|
||||
OP_SCROLL = 0x02
|
||||
OP_KEY_DOWN = 0x03
|
||||
OP_KEY_UP = 0x04
|
||||
OP_KEY_CLK = 0x05
|
||||
)
|
||||
|
||||
type PayloadHeader struct {
|
||||
Event uint8
|
||||
Length uint16
|
||||
}
|
||||
|
||||
type PayloadMove struct {
|
||||
PayloadHeader
|
||||
X uint16
|
||||
Y uint16
|
||||
}
|
||||
|
||||
type PayloadScroll struct {
|
||||
PayloadHeader
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
type PayloadKey struct {
|
||||
PayloadHeader
|
||||
Key uint64 // TODO: uint32
|
||||
}
|
||||
|
||||
func (manager *WebRTCManagerCtx) handleLegacy(
|
||||
logger zerolog.Logger, data []byte,
|
||||
session types.Session,
|
||||
) error {
|
||||
// continue only if session is host
|
||||
if !session.LegacyIsHost() {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(data)
|
||||
header := &PayloadHeader{}
|
||||
hbytes := make([]byte, 3)
|
||||
|
||||
if _, err := buffer.Read(hbytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Read(bytes.NewBuffer(hbytes), binary.LittleEndian, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = bytes.NewBuffer(data)
|
||||
|
||||
switch header.Event {
|
||||
case OP_MOVE:
|
||||
payload := &PayloadMove{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.desktop.Move(int(payload.X), int(payload.Y))
|
||||
case OP_SCROLL:
|
||||
payload := &PayloadScroll{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.
|
||||
Trace().
|
||||
Str("x", strconv.Itoa(int(payload.X))).
|
||||
Str("y", strconv.Itoa(int(payload.Y))).
|
||||
Msg("scroll")
|
||||
|
||||
manager.desktop.Scroll(int(payload.X), int(payload.Y), false)
|
||||
case OP_KEY_DOWN:
|
||||
payload := &PayloadKey{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
err := manager.desktop.ButtonDown(uint32(payload.Key))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("button down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Trace().Msgf("button down %d", payload.Key)
|
||||
} else {
|
||||
err := manager.desktop.KeyDown(uint32(payload.Key))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("key down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Trace().Msgf("key down %d", payload.Key)
|
||||
}
|
||||
case OP_KEY_UP:
|
||||
payload := &PayloadKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
err := manager.desktop.ButtonUp(uint32(payload.Key))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("button up failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Trace().Msgf("button up %d", payload.Key)
|
||||
} else {
|
||||
err := manager.desktop.KeyUp(uint32(payload.Key))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("key up failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Trace().Msgf("key up %d", payload.Key)
|
||||
}
|
||||
case OP_KEY_CLK:
|
||||
// unused
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/m1k1o/neko/server/internal/config"
|
||||
"github.com/m1k1o/neko/server/internal/webrtc/cursor"
|
||||
@@ -485,23 +484,6 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session) (*webrtc.Sess
|
||||
|
||||
connection.OnDataChannel(func(dc *webrtc.DataChannel) {
|
||||
logger.Info().Interface("data_channel", dc).Msg("got remote data channel")
|
||||
|
||||
//
|
||||
// old implementation created a new data channel on client side
|
||||
// new implementation creates a new data channel on server side
|
||||
//
|
||||
|
||||
if viper.GetBool("legacy") {
|
||||
// handle legacy data channel
|
||||
dc.OnMessage(func(message webrtc.DataChannelMessage) {
|
||||
if err := manager.handleLegacy(logger, message.Data, session); err != nil {
|
||||
logger.Err(err).Msg("data handle failed")
|
||||
}
|
||||
})
|
||||
|
||||
// handle legacy data channel
|
||||
peer.dataChannel = dc
|
||||
}
|
||||
})
|
||||
|
||||
var once sync.Once
|
||||
|
||||
@@ -69,7 +69,6 @@ type Session interface {
|
||||
Profile() MemberProfile
|
||||
State() SessionState
|
||||
IsHost() bool
|
||||
LegacyIsHost() bool
|
||||
SetAsHost()
|
||||
SetAsHostBy(session Session)
|
||||
ClearHost()
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
module github.com/m1k1o/neko/utils/docker
|
||||
|
||||
go 1.25.1
|
||||
go 1.26.0
|
||||
|
||||
|
||||
@@ -234,6 +234,10 @@ The sign-in endpoint is `GET /api/oauth/login`; the callback endpoint is `GET /a
|
||||
`admin_emails` and `user_emails` are matched against the `email` claim. Most providers only include that claim if the `email` scope is requested, so `scopes` must include `"email"` (in addition to `"openid"`) or the claim will be empty and these lists will never match.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
If the provider returns `email_verified: false`, Neko ignores the email, so it cannot match `admin_emails` or `user_emails`. If the claim is missing, the email is trusted, because many providers (including generic OAuth2 providers such as GitHub) do not return it at all. In that case `admin_emails` and `user_emails` rely on the identity provider having verified the address. If users can register or edit their email without verification on a provider that does not send `email_verified`, anyone could claim an address listed in `admin_emails` and obtain an administrator session. Require email verification at the provider or do not use these lists.
|
||||
:::
|
||||
|
||||
For OAuth sessions, `GET /api/whoami` includes `extra_data`: the merged userinfo and ID-token claims, with userinfo values taking precedence. It is returned only to the authenticated session owner and is intended for profile-field troubleshooting. Do not place secrets in provider profile claims.
|
||||
|
||||
For non-OIDC providers, set `authorization_url`, `token_url`, `userinfo_url`, and optionally `redirect_url` directly. For GitHub, for example, use `read:user` as a scope and set `subject_field: id`, `username_field: login`, and `avatar_field: avatar_url`.
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
--plugins.required if true, neko will exit if there is an error when loading a plugin
|
||||
--server.bind string address/port/socket to serve neko (default "127.0.0.1:8080")
|
||||
--server.cert string path to the SSL cert used to secure the neko server
|
||||
--server.cors strings list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed
|
||||
--server.cors strings list of allowed origins for CORS and WebSocket connections, if empty only same-origin WebSocket connections are accepted, if '*' is present all origins are allowed
|
||||
--server.key string path to the SSL key used to secure the neko server
|
||||
--server.metrics enable prometheus metrics available at /metrics (default true)
|
||||
--server.path_prefix string path prefix for HTTP requests (default "/")
|
||||
|
||||
@@ -20,7 +20,7 @@ The production build is written to `client/dist/`.
|
||||
|
||||
## Backend
|
||||
|
||||
**Requires:** [Go](https://golang.org/) 1.25+ and the following system libraries (Debian/Ubuntu):
|
||||
**Requires:** [Go](https://golang.org/) 1.26+ and the following system libraries (Debian/Ubuntu):
|
||||
|
||||
```bash
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -58,7 +58,7 @@ The repository uses a [Dockerfile template](https://github.com/m1k1o/neko/blob/m
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Go](https://golang.org/) 1.25+ (for the template pre-processor)
|
||||
- [Go](https://golang.org/) 1.26+ (for the template pre-processor)
|
||||
- [Node.js](https://nodejs.org/) 24 LTS (22.18 or newer; for the frontend, unless you supply a pre-built `client/dist/`)
|
||||
- [Docker](https://www.docker.com/) with BuildKit enabled
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
# Migration from V2
|
||||
|
||||
Currently, Neko is in compatibility mode, meaning that as soon as a single V2 configuration option is set, the legacy mode is enabled. This approach allows for a smooth transition from V2 to V3, where it does not expose the V2 API for new users but still allows existing users who use the old configuration to continue using it as before.
|
||||
|
||||
The legacy mode includes a compatibility layer that allows V2 clients to connect to V3. Currently, the **client is not compatible with V3**, so the legacy mode is enabled by default. It can be explicitly enabled or disabled by setting the `NEKO_LEGACY` environment variable to `true` or `false`.
|
||||
|
||||
:::warning
|
||||
The legacy mode is **still used by the client**. It is recommended to migrate to the new configuration options, but do not disable the legacy mode unless you are using a new client that is compatible with V3 (e.g., [demodesk/neko-client](https://github.com/demodesk/neko-client)). Once the new client is released, the legacy mode will be automatically removed from the server.
|
||||
:::
|
||||
|
||||
If you set both V3 and V2 configuration options, the V2 configuration options will take precedence over the V3 configuration options. This is to ensure that the legacy mode works as expected and does not break existing configurations.
|
||||
Neko v3 accepts many V2 configuration options as deprecated aliases, which are mapped to their V3 equivalents. When both forms are set, the V3 option takes precedence. The V2 API and client compatibility layer have been removed; clients must use the V3 API.
|
||||
|
||||
## Docker Images {#docker}
|
||||
|
||||
@@ -20,7 +12,7 @@ All applications available in the V2 images are also available in the V3 images.
|
||||
|
||||
## Configuration {#config}
|
||||
|
||||
V3 is compatible with V2 configuration options when legacy support is enabled. You should be able to run V3 with the V2 configuration without any issues.
|
||||
V3 continues to accept the V2 configuration aliases listed below. This does not enable or restore the V2 API.
|
||||
|
||||
The configuration in Neko V3 has been structured differently compared to V2. The V3 configuration is more modular and allows for more flexibility. The V3 configuration is split into multiple sections, each section is responsible for a specific part of the application. This allows for better organization and easier management of the configuration.
|
||||
|
||||
@@ -63,9 +55,7 @@ See the V3 configuration options for the [WebRTC Video](/docs/v3/configuration/c
|
||||
| `NEKO_MAX_FPS` | **removed**, use [custom pipeline](/docs/v3/configuration/capture#video.gst_pipeline) instead |
|
||||
|
||||
|
||||
:::warning Limitation
|
||||
V2 did not have client-side cursor support, the cursor was always part of the video stream. In V3, the cursor is sent separately from the video stream. Therefore, when using legacy configuration, there will be two video streams created, one with the cursor (for V2 clients) and one without the cursor (for V3 clients). Please consider using new configuration options if this is not the desired behavior.
|
||||
:::
|
||||
V2 video settings map to a single V3 video pipeline. The V3 client renders the cursor separately from the video stream.
|
||||
|
||||
### WebRTC Audio {#config.audio}
|
||||
|
||||
@@ -109,7 +99,7 @@ See the V3 configuration options for the [Authentication](/docs/v3/configuration
|
||||
| `NEKO_PASSWORD` | `NEKO_MEMBER_MULTIUSER_USER_PASSWORD` with `NEKO_MEMBER_PROVIDER=multiuser` |
|
||||
| `NEKO_PASSWORD_ADMIN` | `NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD` with `NEKO_MEMBER_PROVIDER=multiuser` |
|
||||
|
||||
In order for the legacy authentication to work, you need to set [Multi-user](/docs/v3/configuration/authentication#member.multiuser).
|
||||
The V2 password settings map to the [Multi-user](/docs/v3/configuration/authentication#member.multiuser) provider.
|
||||
|
||||
:::warning Limitation
|
||||
V2 clients might not be compatible with any other authentication provider than the `multiuser`.
|
||||
@@ -131,7 +121,7 @@ See the V3 configuration options for the [WebRTC](/docs/v3/configuration/webrtc)
|
||||
|
||||
### Full V2 Configuration Reference {#config.full}
|
||||
|
||||
Here is a full list of all the configuration options available in Neko V2 that are still available in Neko V3 with legacy support enabled.
|
||||
Here is a full list of V2 configuration options that remain available as deprecated aliases in Neko V3.
|
||||
|
||||
import { ConfigurationTab } from '@site/src/components/Configuration';
|
||||
import configOptions from './help.json';
|
||||
@@ -142,95 +132,10 @@ See the full [V3 configuration reference](/docs/v3/configuration/#full) for more
|
||||
|
||||
## API {#api}
|
||||
|
||||
V3 is compatible with the V2 API when legacy support is enabled. There was specifically created a compatibility layer (legacy API) that allows V2 clients to connect to V3. The legacy API is enabled by default, but it can be disabled if needed. In later versions, the legacy API will be removed.
|
||||
|
||||
### Authentication {#api.auth}
|
||||
|
||||
In V2 there was only one authentication provider available, as in V3 called the [`multiuser`](/docs/v3/configuration/authentication#member.multiuser) provider. The API knew based on the provided password (as `?pwd=` query string) if the user is an admin or not.
|
||||
|
||||
Since V3 handles authentication differently (see [API documentation](/docs/v3/api#authentication)), there has been added `?usr=` query string to the API to specify the username. The password is still provided as `?pwd=` query string. The `?usr=` query string is still optional, if not provided, the API will generate a random username.
|
||||
|
||||
:::warning Limitation
|
||||
For every request in the legacy API, a new user session is created based on the `?usr=&pwd=` query string. The session is destroyed after the API request is completed. So for HTTP API requests, the sessions are short-lived but for WebSocket API requests, the session is kept alive until the WebSocket connection is closed.
|
||||
:::
|
||||
|
||||
Only the [`multiuser`](/docs/v3/configuration/authentication#member.multiuser) provider (or the [`noauth`](/docs/v3/configuration/authentication#member.noauth) provider) is supported without specifying the `?usr=` query string.
|
||||
|
||||
### WebSocket Messages {#api.ws}
|
||||
|
||||
Since WebSocket messages are not user-facing API, there exists no migration guide for them. When the legacy API is enabled, the user connects to the `/ws` endpoint and is handled by the compatibility layer V2 API. The V3 API is available at the `/api/ws` endpoint.
|
||||
|
||||
V2 used to send WebSocket ping messages every 60 seconds, whereas V3 sends them every 10 seconds and additionally uses a heartbeat mechanism to verify if the connection is still active.
|
||||
|
||||
### WebRTC API {#api.webrtc}
|
||||
|
||||
Since the WebRTC API is not user-facing API, there exists no migration guide for it. It has been changed to Big Endian format (previously Little Endian) to allow easier manipulation on the client side.
|
||||
V2 created a new data channel on the client side, V3 creates a new data channel on the server side. That means, the server just listens for a new data channel from the client and accepts it with the legacy API handler. It overwrites the existing V3 data channel with the legacy one.
|
||||
|
||||
### HTTP API {#api.http}
|
||||
|
||||
The V2 version had a very limited HTTP API, the V3 API is much more powerful and flexible. See the [API documentation](/docs/v3/api) for more details.
|
||||
|
||||
#### GET `/health` {#api.http.health}
|
||||
|
||||
Migrated to the [Health](/docs/v3/api/healthcheck) endpoint for server health checks.
|
||||
|
||||
Returns `200 OK` if the server is running.
|
||||
|
||||
#### GET `/stats` {#api.http.stats}
|
||||
|
||||
Migrated to the [Stats](/docs/v3/api/stats) endpoint for server statistics and the [List Sessions](/docs/v3/api/sessions-get) endpoint for the session list.
|
||||
|
||||
Returns a JSON object with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
// How many connections are currently active
|
||||
"connections": 0,
|
||||
// Who is currently having a session (empty if no one)
|
||||
"host": "<session_id>",
|
||||
// List of currently connected users
|
||||
"members": [
|
||||
{
|
||||
"session_id": "<session_id>",
|
||||
"displayname": "Name",
|
||||
"admin": true,
|
||||
"muted": false,
|
||||
}
|
||||
],
|
||||
// List of banned IPs and who banned them as a session_id
|
||||
"banned": {
|
||||
"<ip>": "<session_id>"
|
||||
},
|
||||
// List of locked resources and who locked them as a session_id
|
||||
"locked": {
|
||||
"<resource>": "<session_id>"
|
||||
},
|
||||
// Server uptime
|
||||
"server_started_at": "2021-01-01T00:00:00Z",
|
||||
// When was the last admin or user left the session
|
||||
"last_admin_left_at": "2021-01-01T00:00:00Z",
|
||||
"last_user_left_at": "2021-01-01T00:00:00Z",
|
||||
// Whether the control protection or implicit control is enabled
|
||||
"control_protection": false,
|
||||
"implicit_control": false,
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/screenshot.jpg` {#api.http.screenshot}
|
||||
|
||||
Migrated to the [Screenshot](/docs/v3/api/screen-shot-image) endpoint for taking screenshots.
|
||||
|
||||
Returns a screenshot of the desktop as a JPEG image.
|
||||
|
||||
#### GET `/file` {#api.http.file}
|
||||
|
||||
The whole functionality of file transfer has been moved to a [File Transfer Plugin](/docs/v3/configuration/plugins#filetransfer).
|
||||
The V2 API compatibility layer has been removed. Migrate clients to the [V3 API](/docs/v3/api); the V2 `/ws` endpoint and V2 HTTP endpoints are no longer available.
|
||||
|
||||
## Limitations {#limitations}
|
||||
|
||||
In v2, locks and muted users were managed using a simple map that tracked who set the lock and what was locked. In v3, locks are now implemented as setting options and no longer store the `session_id` of the user who applied the lock. As a result, if a client refreshes the page or reconnects, the lock information is lost, and the user who set the lock is displayed as `Somebody`.
|
||||
|
||||
Additionally, when using the legacy API with a v2 client, API calls occur in a different order than expected. The client first retrieves the session list before registering the user, meaning the current `session_id` is not known when the session list is fetched. That means, the current user appears as `Somebody` in the session list.
|
||||
|
||||
Currently, the v3 has no native support for pipeline generation, meaning that the user has to manually specify the pipeline for video and audio if he wants to customize it. The v2 had a simple built-in support for setting the video bitrate, fps, audio bitrate, and hardware encoding. Since the introduction of multiple video pipelines in v3, this feature has been removed.
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
[
|
||||
{
|
||||
"key": [
|
||||
"legacy"
|
||||
],
|
||||
"type": "boolean",
|
||||
"defaultValue": "true",
|
||||
"description": "enable legacy mode"
|
||||
},
|
||||
{
|
||||
"key": [
|
||||
"logs"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
--legacy enable legacy mode (default true)
|
||||
|
||||
--logs save logs to file
|
||||
|
||||
--cert string path to the SSL cert used to secure the neko server
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# Release Notes
|
||||
|
||||
## master {#master}
|
||||
## [n.eko v3.2.0](https://github.com/m1k1o/neko/releases/tag/v3.2.0) {#v3.2.0}
|
||||
|
||||
### New Features {#master-feats}
|
||||
- Rewrote the web client in React with a framework-free core library (`client/core`, published as `@m1k1o/neko`) that uses only the v3 API, so it works with `NEKO_LEGACY=false`. The look and features of the Vue 2 client are kept: chat with markdown, emoji and open-in-app links, emotes, file transfer, member admin actions, locks, broadcast, microphone, all translations and URL parameters.
|
||||
- Kick now removes the session (the user may log in again).
|
||||
- The client reconnects by itself when its websocket goes silent (dropped networks often never close it), and asks for a new media stream when the server drops it.
|
||||
- Ctrl+V pastes what was copied most recently on either side: local clipboard text is sent to the remote first when it is newer than the remote's. Text entered through an IME or an on-screen keyboard is typed as key events.
|
||||
### New Features {#v3.2.0-feats}
|
||||
- Replaced the Vue 2 web client with a React 19 and TypeScript client built with Vite. It keeps the familiar layout and workflows: login and invite links, OAuth, video controls, fullscreen and Picture-in-Picture, clipboard, control handoff, microphone, emotes, chat, file transfer, settings, broadcast, member actions, and all 15 translations and URL parameters ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Added a framework-free TypeScript client core, `client/core` (`@m1k1o/neko`), with authentication, room state and events, a pluggable stream transport, WebRTC, typed input channels, and the keyboard/mouse/touch overlay. It can be used independently of the React GUI or embedded in another framework ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- The client detects silent websocket connections, reconnects with backoff, and requests a replacement media peer when WebRTC fails. The stream can continue through a signalling outage, and playback resumes after reconnection ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Added binary WebRTC data-channel input with websocket control-event fallback for transports without their own input channel. The client also handles IME and on-screen keyboard text as key events; Ctrl+V uses whichever clipboard was changed most recently ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Chat now renders CommonMark and GitHub Flavored Markdown, including tables, lists, and nested quotes. Links are limited to HTTP, HTTPS, and mailto; HTML and images are not rendered. The new renderer replaces the hand-written parser and keeps hostile or oversized messages from stalling the chat ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
|
||||
### Fixes {#master-fixes}
|
||||
- Fixed video timestamps running slow when the encoder falls behind: frames captured late or dropped before the encoder made the stream's clock lag real time, so browsers buffered up to seconds of video and lost lip sync. RTP timestamps now follow the time between captured frames ([#712](https://github.com/m1k1o/neko/pull/712)).
|
||||
### Fixes {#v3.2.0-fixes}
|
||||
- Bound each OAuth authorization state to a short-lived, HttpOnly browser cookie, preventing a callback URL from creating a session in a browser that did not start the login. Concurrent login flows and explicit reverse-proxy redirects are supported ([#708](https://github.com/m1k1o/neko/pull/708)).
|
||||
- Fixed session and member IDs containing URL path characters. The `multiuser` and `noauth` providers now use sanitized IDs while preserving the display name; unsafe usernames submitted through `POST /api/members` are rejected ([#712](https://github.com/m1k1o/neko/pull/712)).
|
||||
- Fixed a server panic when video pipelines were configured without explicit video IDs, and when no video stream was available ([#711](https://github.com/m1k1o/neko/pull/711)).
|
||||
- Fixed slow WebRTC video timestamps under load, which could make browser buffers grow and audio/video drift. Generated pipelines also keep only the newest raw frame when an encoder falls behind, reducing input latency instead of accumulating stale frames ([#712](https://github.com/m1k1o/neko/pull/712)).
|
||||
- Fixed the client leaving a user at the disconnected screen when the server rejects a banned session, and hiding the microphone control when the browser does not support media capture ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Internal websocket connections without an `Origin` header are now accepted, allowing trusted server-side websocket dials ([#710](https://github.com/m1k1o/neko/pull/710)).
|
||||
|
||||
### Changes {#master-changes}
|
||||
- Generated video pipelines keep at most one raw frame queued, so a slow encoder drops frames instead of adding latency.
|
||||
- New metric `neko_capture_streamsink_frames`: video frames produced per pipeline; a rate below the configured fps means the encoder falls behind.
|
||||
- Session ids on the `multiuser` and `noauth` providers are built from a sanitised login name (the name itself is shown as typed); `POST /api/members` rejects usernames that would not be safe in a URL path.
|
||||
- Chat messages are limited to `chat.max_length` characters (default 512, the web client's input limit); longer ones are rejected over the API and dropped over the websocket.
|
||||
- The default video pipeline no longer draws the mouse pointer into the stream; the client draws it, so it was shown twice (once with the stream's delay). Set `capture.video.show_pointer` to `true` to get the old behaviour.
|
||||
- The client no longer uses the legacy (v2) API. IP bans, which only existed in the legacy layer, are not available.
|
||||
- Scroll sensitivity uses the v3 scale (-5 to 5); the `?scroll` query param is mapped onto it.
|
||||
- File downloads are streamed by the browser instead of being buffered in memory.
|
||||
- The microphone switches itself off when the connection is re-established.
|
||||
- Opening the client in a second tab or window takes over the session; the first tab says so.
|
||||
- Chat markdown follows CommonMark/GFM (headings, lists, tables, nested quotes); the Discord-style `>>>` block quote is gone, images are not rendered, and messages longer than 512 characters are shown as plain text.
|
||||
- The client uses the system font stack instead of Whitney, and shows emoji as native characters instead of an image sheet.
|
||||
### Changes {#v3.2.0-changes}
|
||||
- The React client uses the v3 API only and works with `NEKO_LEGACY=false`. It no longer supports legacy v2-only behavior, including IP bans; kick removes the session, and the user may log in again ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Added `chat.max_length` (default 512 characters; `0` disables the limit). Longer messages receive HTTP 400 through REST and are dropped over the websocket. Incoming websocket frames are limited to 1 MiB ([#712](https://github.com/m1k1o/neko/pull/712)).
|
||||
- Added the `neko_capture_streamsink_frames` metric to show when a video pipeline produces fewer frames than its configured frame rate ([#712](https://github.com/m1k1o/neko/pull/712)).
|
||||
- The default video pipeline no longer draws the mouse pointer into the stream; the client draws it without the stream delay. Set `capture.video.show_pointer` to `true` to restore the old behavior ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- Scroll sensitivity uses the v3 scale (-5 to 5), with the `?scroll` URL parameter mapped to it. File downloads are streamed instead of buffered in memory. The microphone turns off when the connection is re-established, and opening a second tab takes over the session ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
- The client uses the system font stack instead of Whitney and renders emoji as native characters rather than a sprite sheet ([#713](https://github.com/m1k1o/neko/pull/713)).
|
||||
|
||||
## [n.eko v3.1.6](https://github.com/m1k1o/neko/releases/tag/v3.1.6) {#v3.1.6}
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ Bear in mind that your Neko server does not have to run on the same computer as
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule /ws(.*) "ws://localhost:8080/ws$1" [P,L]
|
||||
# forward every websocket upgrade to the same path (the client connects to /api/ws)
|
||||
RewriteRule ^/(.*) "ws://localhost:8080/$1" [P,L]
|
||||
|
||||
# Available log levels: trace8, ..., trace1, debug, info, notice, warn,
|
||||
# error, crit, alert, emerg.
|
||||
|
||||
@@ -28,7 +28,12 @@ export default async function createConfig(): Promise<Config> {
|
||||
trailingSlash: false,
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
Generated
+4659
-6521
File diff suppressed because it is too large
Load Diff
+15
-7
@@ -18,21 +18,21 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@docusaurus/core": "3.10.2",
|
||||
"@docusaurus/preset-classic": "3.10.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"docusaurus-plugin-openapi-docs": "^4.3.4",
|
||||
"docusaurus-theme-openapi-docs": "^4.3.4",
|
||||
"docusaurus-plugin-openapi-docs": "^5.2.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.2.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"remark-code-import": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/tsconfig": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/module-type-aliases": "3.10.2",
|
||||
"@docusaurus/tsconfig": "3.10.2",
|
||||
"@docusaurus/types": "3.10.2",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -49,5 +49,13 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"overrides": {
|
||||
"js-yaml": "^4.3.2",
|
||||
"postcss-selector-parser": "^7.1.6",
|
||||
"serialize-javascript": "^7.1.2",
|
||||
"tinypool": "^2.2.0",
|
||||
"uuid": "^14.0.3",
|
||||
"yaml": "^2.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user