Merge branch 'demodesk-v3' into v3
This commit is contained in:
commit
356a566bc6
248 changed files with 78237 additions and 4508 deletions
148
.devcontainer/Dockerfile
Normal file
148
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#
|
||||
# Stage 0: Build xorg dependencies.
|
||||
#
|
||||
FROM debian:bullseye-slim as xorg-deps
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y \
|
||||
git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
WORKDIR /xorg
|
||||
|
||||
COPY xorg/ /xorg/
|
||||
|
||||
# build xserver-xorg-video-dummy 0.3.8-2 with RandR support.
|
||||
RUN set -eux; \
|
||||
cd xf86-video-dummy; \
|
||||
git clone --depth 1 --branch xserver-xorg-video-dummy-1_0.3.8-2 https://salsa.debian.org/xorg-team/driver/xserver-xorg-video-dummy; \
|
||||
cd xserver-xorg-video-dummy; \
|
||||
patch -p1 < ../xdummy-randr.patch; \
|
||||
./autogen.sh; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# build custom input driver
|
||||
RUN set -eux; \
|
||||
cd xf86-input-neko; \
|
||||
./autogen.sh --prefix=/usr; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.166.0/containers/go/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Go version: 1, 1.16, 1.15
|
||||
ARG VARIANT="1"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# build dependencies
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
libx11-dev libxrandr-dev libxtst-dev libgtk-3-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; \
|
||||
# install libxcvt-dev (not available in base image)
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt-dev_0.1.2-1_amd64.deb; \
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt0_0.1.2-1_amd64.deb; \
|
||||
apt-get install --no-install-recommends ./libxcvt0_0.1.2-1_amd64.deb ./libxcvt-dev_0.1.2-1_amd64.deb;
|
||||
|
||||
# runtime dependencies
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
wget ca-certificates supervisor \
|
||||
pulseaudio dbus-x11 xserver-xorg-video-dummy \
|
||||
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6 \
|
||||
#
|
||||
# needed for profile upload preStop hook
|
||||
zip curl \
|
||||
#
|
||||
# file chooser handler, clipboard, drop
|
||||
xdotool xclip libgtk-3-0 \
|
||||
#
|
||||
# gst
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
|
||||
gstreamer1.0-pulseaudio;
|
||||
# libxcvt already installed
|
||||
|
||||
# dev runtime dependencies
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
xfce4 xfce4-terminal firefox-esr sudo;
|
||||
|
||||
# configure runtime
|
||||
ARG USERNAME=neko
|
||||
ARG USER_UID=1001
|
||||
ARG USER_GID=$USER_UID
|
||||
RUN set -eux; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
|
||||
adduser $USERNAME audio; \
|
||||
adduser $USERNAME video; \
|
||||
adduser $USERNAME pulse; \
|
||||
#
|
||||
# add sudo support
|
||||
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME; \
|
||||
chmod 0440 /etc/sudoers.d/$USERNAME; \
|
||||
#
|
||||
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
|
||||
mkdir /tmp/.X11-unix; \
|
||||
chmod 1777 /tmp/.X11-unix; \
|
||||
chown $USERNAME /tmp/.X11-unix/; \
|
||||
#
|
||||
# make directories for neko
|
||||
mkdir -p /etc/neko /var/www; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Chinese fonts
|
||||
fonts-arphic-ukai fonts-arphic-uming \
|
||||
# Japanese fonts
|
||||
fonts-ipafont-mincho fonts-ipafont-gothic \
|
||||
# Korean fonts
|
||||
fonts-unfonts-core \
|
||||
# Indian fonts
|
||||
fonts-indic;
|
||||
|
||||
# copy dependencies from previous stage
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
# copy runtime files
|
||||
COPY runtime/dbus /usr/bin/dbus
|
||||
COPY runtime/default.pa /etc/pulse/default.pa
|
||||
COPY runtime/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY runtime/xorg.conf /etc/neko/xorg.conf
|
||||
COPY runtime/icon-theme /home/$USERNAME/.icons/default
|
||||
|
||||
# copy dev runtime files
|
||||
COPY dev/runtime/config.yml /etc/neko/neko.yml
|
||||
COPY dev/runtime/supervisord.conf /etc/neko/supervisord/dev.conf
|
||||
|
||||
# customized scripts
|
||||
RUN chmod +x /usr/bin/dbus;\
|
||||
echo '#!/bin/sh\nsleep infinity' > /usr/bin/neko; \
|
||||
chmod +x /usr/bin/neko; \
|
||||
echo '#!/bin/sh\nsudo sh -c "export USER='$USERNAME'\nexport HOME=/home/'$USERNAME'\n/usr/bin/supervisord -c /etc/neko/supervisord.conf"' > /usr/bin/deps; \
|
||||
chmod +x /usr/bin/deps; \
|
||||
touch .env.development;
|
||||
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_SERVER_BIND=:3000
|
||||
ENV NEKO_WEBRTC_EPR=3001-3004
|
||||
20
.devcontainer/README.md
Normal file
20
.devcontainer/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# dev container
|
||||
|
||||
You need to run all dependencies with `deps` command before you start debugging.
|
||||
|
||||
Create `.env.development` in repository root. Make sure your local IP is correct.
|
||||
|
||||
```sh
|
||||
NEKO_WEBRTC_NAT1TO1=10.0.0.8
|
||||
```
|
||||
|
||||
# without container
|
||||
|
||||
- Make sure `pulseaudio` contains correct configuration.
|
||||
- Specify `DISPLAY` that is being used by xorg.
|
||||
|
||||
```sh
|
||||
DISPLAY=:0
|
||||
NEKO_WEBRTC_NAT1TO1=10.0.0.8
|
||||
NEKO_SERVER_BIND=:3000
|
||||
```
|
||||
44
.devcontainer/devcontainer.json
Normal file
44
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.166.0/containers/go
|
||||
{
|
||||
"name": "Go",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "../server/",
|
||||
"args": {
|
||||
// Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.15
|
||||
"VARIANT": "1.20",
|
||||
// Options
|
||||
"INSTALL_NODE": "false",
|
||||
"NODE_VERSION": "lts/*"
|
||||
}
|
||||
},
|
||||
"runArgs": [ "--cap-add=SYS_PTRACE", "--cap-add=SYS_ADMIN", "--shm-size=2G", "--security-opt", "seccomp=unconfined" ],
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"go.goroot": "/usr/local/go"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"golang.Go"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"appPort": ["3000:3000", "3001:3001/udp", "3002:3002/udp", "3003:3003/udp", "3004:3004/udp"],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "go version",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "neko"
|
||||
}
|
||||
44
.github/workflows/server_build.yml
vendored
Normal file
44
.github/workflows/server_build.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Create and publish a Docker image
|
||||
|
||||
on: workflow_dispatch
|
||||
# push:
|
||||
# branches:
|
||||
# - 'master'
|
||||
# tags:
|
||||
# - 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: ./server/
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
53
.github/workflows/server_build_variants.yml
vendored
Normal file
53
.github/workflows/server_build_variants.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Create and publish a Docker image variant
|
||||
|
||||
on: workflow_dispatch
|
||||
# push:
|
||||
# tags:
|
||||
# - 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image-variant:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- variant: bookworm
|
||||
dockerfile: Dockerfile.bookworm
|
||||
- variant: nvidia
|
||||
dockerfile: Dockerfile.nvidia
|
||||
- variant: nvidia_bookworm
|
||||
dockerfile: Dockerfile.nvidia.bookworm
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.variant }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: ./server/
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
21
.github/workflows/server_pull_requests.yml
vendored
Normal file
21
.github/workflows/server_pull_requests.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: Build a Docker image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: ./server/
|
||||
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "launch",
|
||||
"type": "go",
|
||||
"debugAdapter": "dlv-dap",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/server/cmd/neko",
|
||||
"output": "${workspaceFolder}/server/bin/debug/neko",
|
||||
"cwd": "${workspaceFolder}/server/",
|
||||
"args": ["serve", "-d", "-c", "dev/runtime/config.yml"],
|
||||
"env": {
|
||||
"DISPLAY": ":99.0",
|
||||
"PION_LOG_TRACE": "all",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
|
|
@ -1 +1,13 @@
|
|||
{}
|
||||
{
|
||||
"go.inferGopath": false,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.delveConfig": {
|
||||
"useApiV1": false
|
||||
},
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
LICENSE
3
LICENSE
|
|
@ -187,7 +187,8 @@
|
|||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2020 Nurdism <nurdism.io@gmail.com>
|
||||
Copyright (C) 2020-2023 m1k1o
|
||||
Copyright (C) 2020-2024 m1k1o & Demodesk GmbH
|
||||
Copyright (C) 2024- m1k1o
|
||||
All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
DISPLAY=:99.0
|
||||
PION_LOG_TRACE=all
|
||||
12
server/.gitignore
vendored
Normal file
12
server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
bin/
|
||||
.idea
|
||||
.env.development
|
||||
|
||||
runtime/fonts/*
|
||||
!runtime/fonts/.gitkeep
|
||||
|
||||
runtime/icon-theme/*
|
||||
!runtime/icon-theme/.gitkeep
|
||||
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
||||
16
server/.vscode/launch.json
vendored
16
server/.vscode/launch.json
vendored
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "launch",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/cmd/neko",
|
||||
"envFile": "${workspaceFolder}/.env.development",
|
||||
"output": "${workspaceFolder}/bin/debug/neko",
|
||||
"cwd": "${workspaceFolder}/",
|
||||
"args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "neko", "--password_admin", "admin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
22
server/.vscode/settings.json
vendored
22
server/.vscode/settings.json
vendored
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
{
|
||||
"go.formatTool": "goformat",
|
||||
"go.inferGopath": false,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.delveConfig": {
|
||||
"useApiV1": false,
|
||||
"dlvLoadConfig": {
|
||||
"followPointers": true,
|
||||
"maxVariableRecurse": 3,
|
||||
"maxStringLen": 400,
|
||||
"maxArrayValues": 400,
|
||||
"maxStructFields": -1
|
||||
}
|
||||
},
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
182
server/Dockerfile
Normal file
182
server/Dockerfile
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
ARG BASE_IMAGE=debian:bullseye-slim
|
||||
ARG BUILD_IMAGE=golang:1.21-bullseye
|
||||
|
||||
#
|
||||
# Stage 0: Build xorg dependencies.
|
||||
#
|
||||
FROM $BASE_IMAGE as xorg-deps
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y \
|
||||
git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
WORKDIR /xorg
|
||||
|
||||
COPY xorg/ /xorg/
|
||||
|
||||
# build xf86-video-dummy v0.3.8 with RandR support
|
||||
RUN set -eux; \
|
||||
cd xf86-video-dummy/v0.3.8; \
|
||||
patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \
|
||||
autoreconf -v --install; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# build custom input driver
|
||||
RUN set -eux; \
|
||||
cd xf86-input-neko; \
|
||||
./autogen.sh --prefix=/usr; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
#
|
||||
# Stage 1: Build.
|
||||
#
|
||||
FROM $BUILD_IMAGE as build
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libx11-dev libxrandr-dev libxtst-dev libgtk-3-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; \
|
||||
# install libxcvt-dev (not available in debian:bullseye)
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt-dev_0.1.2-1_amd64.deb; \
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt0_0.1.2-1_amd64.deb; \
|
||||
apt-get install --no-install-recommends ./libxcvt0_0.1.2-1_amd64.deb ./libxcvt-dev_0.1.2-1_amd64.deb; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
ARG GIT_TAG
|
||||
|
||||
#
|
||||
# build server
|
||||
COPY . .
|
||||
RUN ./build
|
||||
|
||||
#
|
||||
# Stage 2: Runtime.
|
||||
#
|
||||
FROM $BASE_IMAGE as runtime
|
||||
|
||||
#
|
||||
# set custom user
|
||||
ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates supervisor \
|
||||
pulseaudio dbus-x11 xserver-xorg-video-dummy \
|
||||
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6 \
|
||||
#
|
||||
# needed for profile upload preStop hook
|
||||
zip curl \
|
||||
#
|
||||
# file chooser handler, clipboard, drop
|
||||
xdotool xclip libgtk-3-0 \
|
||||
#
|
||||
# gst
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
|
||||
gstreamer1.0-pulseaudio; \
|
||||
# install libxcvt0 (not available in debian:bullseye)
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt0_0.1.2-1_amd64.deb; \
|
||||
apt-get install --no-install-recommends ./libxcvt0_0.1.2-1_amd64.deb; \
|
||||
rm ./libxcvt0_0.1.2-1_amd64.deb; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
|
||||
adduser $USERNAME audio; \
|
||||
adduser $USERNAME video; \
|
||||
adduser $USERNAME pulse; \
|
||||
#
|
||||
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
|
||||
mkdir /tmp/.X11-unix; \
|
||||
chmod 1777 /tmp/.X11-unix; \
|
||||
chown $USERNAME /tmp/.X11-unix/; \
|
||||
#
|
||||
# make directories for neko
|
||||
mkdir -p /etc/neko /var/www; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Chinese fonts
|
||||
fonts-arphic-ukai fonts-arphic-uming \
|
||||
# Japanese fonts
|
||||
fonts-ipafont-mincho fonts-ipafont-gothic \
|
||||
# Korean fonts
|
||||
fonts-unfonts-core \
|
||||
# Indian fonts
|
||||
fonts-indic; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
# copy dependencies from previous stage
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
#
|
||||
# copy runtime configs
|
||||
COPY --chown=neko:neko runtime/.Xresources /home/$USERNAME/.Xresources
|
||||
COPY runtime/dbus /usr/bin/dbus
|
||||
COPY runtime/default.pa /etc/pulse/default.pa
|
||||
COPY runtime/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY runtime/supervisord.dbus.conf /etc/neko/supervisord.dbus.conf
|
||||
COPY runtime/xorg.conf /etc/neko/xorg.conf
|
||||
|
||||
#
|
||||
# copy runtime folders
|
||||
COPY --chown=neko:neko runtime/icon-theme /home/$USERNAME/.icons/default
|
||||
COPY runtime/fontconfig/* /etc/fonts/conf.d/
|
||||
COPY runtime/fonts /usr/local/share/fonts
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_SERVER_BIND=:8080
|
||||
ENV NEKO_PLUGINS_ENABLED=true
|
||||
ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
|
||||
|
||||
#
|
||||
# copy plugins from previous stage
|
||||
COPY --from=build /src/bin/plugins/ $NEKO_PLUGINS_DIR
|
||||
|
||||
#
|
||||
# copy executable from previous stage
|
||||
COPY --from=build /src/bin/neko /usr/bin/neko
|
||||
|
||||
#
|
||||
# add healthcheck
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1
|
||||
|
||||
#
|
||||
# run neko
|
||||
CMD ["/usr/bin/supervisord", "-s", "-c", "/etc/neko/supervisord.conf"]
|
||||
172
server/Dockerfile.bookworm
Normal file
172
server/Dockerfile.bookworm
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG BUILD_IMAGE=golang:1.21-bookworm
|
||||
|
||||
#
|
||||
# Stage 0: Build xorg dependencies.
|
||||
#
|
||||
FROM $BASE_IMAGE as xorg-deps
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y \
|
||||
git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
WORKDIR /xorg
|
||||
|
||||
COPY xorg/ /xorg/
|
||||
|
||||
# build xf86-video-dummy v0.3.8 with RandR support
|
||||
RUN set -eux; \
|
||||
cd xf86-video-dummy/v0.3.8; \
|
||||
patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \
|
||||
autoreconf -v --install; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# build custom input driver
|
||||
RUN set -eux; \
|
||||
cd xf86-input-neko; \
|
||||
./autogen.sh --prefix=/usr; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
#
|
||||
# Stage 1: Build.
|
||||
#
|
||||
FROM $BUILD_IMAGE as build
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libx11-dev libxrandr-dev libxtst-dev libgtk-3-dev libxcvt-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
ARG GIT_TAG
|
||||
|
||||
#
|
||||
# build server
|
||||
COPY . .
|
||||
RUN ./build
|
||||
|
||||
#
|
||||
# Stage 2: Runtime.
|
||||
#
|
||||
FROM $BASE_IMAGE as runtime
|
||||
|
||||
#
|
||||
# set custom user
|
||||
ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates supervisor \
|
||||
pulseaudio xserver-xorg-video-dummy \
|
||||
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 libxcvt0 \
|
||||
#
|
||||
# needed for profile upload preStop hook
|
||||
zip curl \
|
||||
#
|
||||
# file chooser handler, clipboard, drop
|
||||
xdotool xclip libgtk-3-0 \
|
||||
#
|
||||
# gst
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
|
||||
gstreamer1.0-pulseaudio; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
|
||||
adduser $USERNAME audio; \
|
||||
adduser $USERNAME video; \
|
||||
adduser $USERNAME pulse; \
|
||||
#
|
||||
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
|
||||
mkdir /tmp/.X11-unix; \
|
||||
chmod 1777 /tmp/.X11-unix; \
|
||||
chown $USERNAME /tmp/.X11-unix/; \
|
||||
#
|
||||
# make directories for neko
|
||||
mkdir -p /etc/neko /var/www; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Chinese fonts
|
||||
fonts-arphic-ukai fonts-arphic-uming \
|
||||
# Japanese fonts
|
||||
fonts-ipafont-mincho fonts-ipafont-gothic \
|
||||
# Korean fonts
|
||||
fonts-unfonts-core \
|
||||
# Indian fonts
|
||||
fonts-indic; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
# copy dependencies from previous stage
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
#
|
||||
# copy runtime configs
|
||||
COPY --chown=neko:neko runtime/.Xresources /home/$USERNAME/.Xresources
|
||||
COPY runtime/default.pa /etc/pulse/default.pa
|
||||
COPY runtime/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY runtime/xorg.conf /etc/neko/xorg.conf
|
||||
|
||||
#
|
||||
# copy runtime folders
|
||||
COPY --chown=neko:neko runtime/icon-theme /home/$USERNAME/.icons/default
|
||||
COPY runtime/fontconfig/* /etc/fonts/conf.d/
|
||||
COPY runtime/fonts /usr/local/share/fonts
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_SERVER_BIND=:8080
|
||||
ENV NEKO_PLUGINS_ENABLED=true
|
||||
ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
|
||||
|
||||
#
|
||||
# copy plugins from previous stage
|
||||
COPY --from=build /src/bin/plugins/ $NEKO_PLUGINS_DIR
|
||||
|
||||
#
|
||||
# copy executable from previous stage
|
||||
COPY --from=build /src/bin/neko /usr/bin/neko
|
||||
|
||||
#
|
||||
# add healthcheck
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1
|
||||
|
||||
#
|
||||
# run neko
|
||||
CMD ["/usr/bin/supervisord", "-s", "-c", "/etc/neko/supervisord.conf"]
|
||||
335
server/Dockerfile.nvidia
Normal file
335
server/Dockerfile.nvidia
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
ARG UBUNTU_RELEASE=20.04
|
||||
ARG CUDA_VERSION=11.4.3
|
||||
ARG VIRTUALGL_VERSION=3.1
|
||||
ARG GSTREAMER_VERSION=1.20
|
||||
|
||||
#
|
||||
# Stage 0: Build gstreamer with nvidia plugins.
|
||||
#
|
||||
FROM ubuntu:${UBUNTU_RELEASE} AS gstreamer
|
||||
ARG GSTREAMER_VERSION
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Install essentials
|
||||
curl build-essential ca-certificates git \
|
||||
# Install pip and ninja
|
||||
python3-pip python-gi-dev ninja-build \
|
||||
# Install build deps
|
||||
autopoint autoconf automake autotools-dev libtool gettext bison flex gtk-doc-tools \
|
||||
# Install libraries
|
||||
librtmp-dev \
|
||||
libvo-aacenc-dev \
|
||||
libtool-bin \
|
||||
libgtk2.0-dev \
|
||||
libgl1-mesa-dev \
|
||||
libopus-dev \
|
||||
libpulse-dev \
|
||||
libssl-dev \
|
||||
libx264-dev \
|
||||
libvpx-dev; \
|
||||
# Install meson
|
||||
pip3 install meson; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# build gstreamer
|
||||
RUN set -eux; \
|
||||
git clone --depth 1 --branch $GSTREAMER_VERSION https://gitlab.freedesktop.org/gstreamer/gstreamer.git /gstreamer/src; \
|
||||
cd /gstreamer/src; \
|
||||
mkdir -p /usr/share/gstreamer; \
|
||||
meson --prefix /usr/share/gstreamer \
|
||||
-Dgpl=enabled \
|
||||
-Dugly=enabled \
|
||||
-Dgst-plugins-ugly:x264=enabled \
|
||||
build; \
|
||||
ninja -C build; \
|
||||
meson install -C build;
|
||||
|
||||
#
|
||||
# Stage 0: Build xorg dependencies.
|
||||
#
|
||||
FROM debian:bullseye-slim as xorg-deps
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y \
|
||||
git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
WORKDIR /xorg
|
||||
|
||||
COPY xorg/ /xorg/
|
||||
|
||||
# build xf86-video-dummy v0.3.8 with RandR support
|
||||
RUN set -eux; \
|
||||
cd xf86-video-dummy/v0.3.8; \
|
||||
patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \
|
||||
autoreconf -v --install; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# build custom input driver
|
||||
RUN set -eux; \
|
||||
cd xf86-input-neko; \
|
||||
./autogen.sh --prefix=/usr; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
#
|
||||
# Stage 1: Build.
|
||||
#
|
||||
FROM golang:1.21-bullseye as build
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libx11-dev libxrandr-dev libxtst-dev libgtk-3-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; \
|
||||
# install libxcvt-dev (not available in debian:bullseye)
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt-dev_0.1.2-1_amd64.deb; \
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt0_0.1.2-1_amd64.deb; \
|
||||
apt-get install --no-install-recommends ./libxcvt0_0.1.2-1_amd64.deb ./libxcvt-dev_0.1.2-1_amd64.deb; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
ARG GIT_TAG
|
||||
|
||||
#
|
||||
# build server
|
||||
COPY . .
|
||||
RUN ./build
|
||||
|
||||
#
|
||||
# Stage 2: Runtime.
|
||||
#
|
||||
FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
|
||||
ARG UBUNTU_RELEASE
|
||||
ARG VIRTUALGL_VERSION
|
||||
|
||||
# Make all NVIDIA GPUs visible by default
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES all
|
||||
|
||||
#
|
||||
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
|
||||
ENV VGL_DISPLAY egl
|
||||
|
||||
#
|
||||
# set custom user
|
||||
ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
#
|
||||
# install hardware accleration dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
dpkg --add-architecture i386; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# opengl base: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/base/Dockerfile
|
||||
libxau6 libxau6:i386 \
|
||||
libxdmcp6 libxdmcp6:i386 \
|
||||
libxcb1 libxcb1:i386 \
|
||||
libxext6 libxext6:i386 \
|
||||
libx11-6 libx11-6:i386 \
|
||||
# opengl runtime: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/glvnd/runtime/Dockerfile
|
||||
libglvnd0 libglvnd0:i386 \
|
||||
libgl1 libgl1:i386 \
|
||||
libglx0 libglx0:i386 \
|
||||
libegl1 libegl1:i386 \
|
||||
libgles2 libgles2:i386 \
|
||||
# hardware accleration utilities
|
||||
libglu1 libglu1:i386 \
|
||||
libvulkan-dev libvulkan-dev:i386 \
|
||||
mesa-utils mesa-utils-extra \
|
||||
mesa-va-drivers mesa-vulkan-drivers \
|
||||
vainfo vdpauinfo; \
|
||||
#
|
||||
# install vulkan-utils or vulkan-tools depending on ubuntu release
|
||||
if [ "${UBUNTU_RELEASE}" = "18.04" ]; then \
|
||||
apt-get install -y --no-install-recommends vulkan-utils; \
|
||||
else \
|
||||
apt-get install -y --no-install-recommends vulkan-tools; \
|
||||
fi; \
|
||||
#
|
||||
# create symlink for libnvrtc.so (needed for cudaconvert)
|
||||
find /usr/local/cuda/lib64/ -maxdepth 1 -type l -name "*libnvrtc.so.*" -exec sh -c 'ln -sf {} /usr/local/cuda/lib64/libnvrtc.so' \;; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# add cuda to ld path, for gstreamer cuda plugins
|
||||
ENV LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}:/usr/local/cuda/lib:/usr/local/cuda/lib64"
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates supervisor \
|
||||
pulseaudio dbus-x11 xserver-xorg-video-dummy \
|
||||
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6 libx264-155 libvo-aacenc0 librtmp1 \
|
||||
libgtk-3-bin software-properties-common cabextract aptitude vim curl \
|
||||
#
|
||||
# needed for profile upload preStop hook
|
||||
zip curl \
|
||||
#
|
||||
# file chooser handler, clipboard, drop
|
||||
xdotool xclip libgtk-3-0; \
|
||||
# install libxcvt0 (not available in debian:bullseye)
|
||||
wget http://ftp.de.debian.org/debian/pool/main/libx/libxcvt/libxcvt0_0.1.2-1_amd64.deb; \
|
||||
apt-get install --no-install-recommends ./libxcvt0_0.1.2-1_amd64.deb; \
|
||||
rm ./libxcvt0_0.1.2-1_amd64.deb; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
|
||||
adduser $USERNAME audio; \
|
||||
adduser $USERNAME video; \
|
||||
adduser $USERNAME pulse; \
|
||||
#
|
||||
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
|
||||
mkdir /tmp/.X11-unix; \
|
||||
chmod 1777 /tmp/.X11-unix; \
|
||||
chown $USERNAME /tmp/.X11-unix/; \
|
||||
#
|
||||
# make directories for neko
|
||||
mkdir -p /etc/neko /var/www; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Chinese fonts
|
||||
fonts-arphic-ukai fonts-arphic-uming \
|
||||
# Japanese fonts
|
||||
fonts-ipafont-mincho fonts-ipafont-gothic \
|
||||
# Korean fonts
|
||||
fonts-unfonts-core \
|
||||
# Indian fonts
|
||||
fonts-indic; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
# copy dependencies from previous stage
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
#
|
||||
# configure EGL and Vulkan manually
|
||||
RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)') && \
|
||||
# Configure EGL manually
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libEGL_nvidia.so.0\"\n\
|
||||
}\n\
|
||||
}" > /usr/share/glvnd/egl_vendor.d/10_nvidia.json && \
|
||||
# Configure Vulkan manually
|
||||
mkdir -p /etc/vulkan/icd.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libGLX_nvidia.so.0\",\n\
|
||||
\"api_version\" : \"${VULKAN_API_VERSION}\"\n\
|
||||
}\n\
|
||||
}" > /etc/vulkan/icd.d/nvidia_icd.json
|
||||
|
||||
#
|
||||
# install VirtualGL and make libraries available for preload
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
|
||||
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
chmod u+s /usr/lib/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/libdlfaker.so; \
|
||||
chmod u+s /usr/lib32/libvglfaker.so; \
|
||||
chmod u+s /usr/lib32/libdlfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*;
|
||||
|
||||
#
|
||||
# copy runtime configs
|
||||
COPY --chown=neko:neko runtime/.Xresources /home/$USERNAME/.Xresources
|
||||
COPY runtime/dbus /usr/bin/dbus
|
||||
COPY runtime/default.pa /etc/pulse/default.pa
|
||||
COPY runtime/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY runtime/supervisord.dbus.conf /etc/neko/supervisord.dbus.conf
|
||||
COPY runtime/xorg.conf /etc/neko/xorg.conf
|
||||
|
||||
#
|
||||
# copy runtime folders
|
||||
COPY --chown=neko:neko runtime/icon-theme /home/$USERNAME/.icons/default
|
||||
COPY runtime/fontconfig/* /etc/fonts/conf.d/
|
||||
COPY runtime/fonts /usr/local/share/fonts
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_SERVER_BIND=:8080
|
||||
ENV NEKO_PLUGINS_ENABLED=true
|
||||
ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
|
||||
|
||||
#
|
||||
# set gstreamer envs
|
||||
ENV PATH="/usr/share/gstreamer/bin:${PATH}"
|
||||
ENV LD_LIBRARY_PATH="/usr/share/gstreamer/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
ENV PKG_CONFIG_PATH="/usr/share/gstreamer/lib/x86_64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
|
||||
|
||||
#
|
||||
# copy gstreamer from previous stage
|
||||
COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
|
||||
|
||||
#
|
||||
# copy plugins from previous stage
|
||||
COPY --from=build /src/bin/plugins/ $NEKO_PLUGINS_DIR
|
||||
|
||||
#
|
||||
# copy executable from previous stage
|
||||
COPY --from=build /src/bin/neko /usr/bin/neko
|
||||
|
||||
#
|
||||
# add healthcheck
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1
|
||||
|
||||
#
|
||||
# run neko
|
||||
CMD ["/usr/bin/supervisord", "-s", "-c", "/etc/neko/supervisord.conf"]
|
||||
325
server/Dockerfile.nvidia.bookworm
Normal file
325
server/Dockerfile.nvidia.bookworm
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
ARG UBUNTU_RELEASE=22.04
|
||||
ARG CUDA_VERSION=12.2.0
|
||||
ARG VIRTUALGL_VERSION=3.1
|
||||
ARG GSTREAMER_VERSION=1.22
|
||||
|
||||
#
|
||||
# Stage 0: Build gstreamer with nvidia plugins.
|
||||
#
|
||||
FROM ubuntu:${UBUNTU_RELEASE} AS gstreamer
|
||||
ARG GSTREAMER_VERSION
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Install essentials
|
||||
curl build-essential ca-certificates git \
|
||||
# Install pip and ninja
|
||||
python3-pip python-gi-dev ninja-build \
|
||||
# Install build deps
|
||||
autopoint autoconf automake autotools-dev libtool gettext bison flex gtk-doc-tools \
|
||||
# Install libraries
|
||||
librtmp-dev \
|
||||
libvo-aacenc-dev \
|
||||
libtool-bin \
|
||||
libgtk2.0-dev \
|
||||
libgl1-mesa-dev \
|
||||
libopus-dev \
|
||||
libpulse-dev \
|
||||
libssl-dev \
|
||||
libx264-dev \
|
||||
libvpx-dev; \
|
||||
# Install meson
|
||||
pip3 install meson; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# build gstreamer
|
||||
RUN set -eux; \
|
||||
git clone --depth 1 --branch $GSTREAMER_VERSION https://gitlab.freedesktop.org/gstreamer/gstreamer.git /gstreamer/src; \
|
||||
cd /gstreamer/src; \
|
||||
mkdir -p /usr/share/gstreamer; \
|
||||
meson --prefix /usr/share/gstreamer \
|
||||
-Dgpl=enabled \
|
||||
-Dugly=enabled \
|
||||
-Dgst-plugins-ugly:x264=enabled \
|
||||
build; \
|
||||
ninja -C build; \
|
||||
meson install -C build;
|
||||
|
||||
#
|
||||
# Stage 0: Build xorg dependencies.
|
||||
#
|
||||
FROM debian:bookworm-slim as xorg-deps
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y \
|
||||
git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
WORKDIR /xorg
|
||||
|
||||
COPY xorg/ /xorg/
|
||||
|
||||
# build xf86-video-dummy v0.3.8 with RandR support
|
||||
RUN set -eux; \
|
||||
cd xf86-video-dummy/v0.3.8; \
|
||||
patch -p1 < ../01_v0.3.8_xdummy-randr.patch; \
|
||||
autoreconf -v --install; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
# build custom input driver
|
||||
RUN set -eux; \
|
||||
cd xf86-input-neko; \
|
||||
./autogen.sh --prefix=/usr; \
|
||||
./configure; \
|
||||
make -j$(nproc); \
|
||||
make install;
|
||||
|
||||
#
|
||||
# Stage 1: Build.
|
||||
#
|
||||
FROM golang:1.21-bookworm as build
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libx11-dev libxrandr-dev libxtst-dev libgtk-3-dev libxcvt-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
ARG GIT_TAG
|
||||
|
||||
#
|
||||
# build server
|
||||
COPY . .
|
||||
RUN ./build
|
||||
|
||||
#
|
||||
# Stage 2: Runtime.
|
||||
#
|
||||
FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
|
||||
ARG UBUNTU_RELEASE
|
||||
ARG VIRTUALGL_VERSION
|
||||
|
||||
# Make all NVIDIA GPUs visible by default
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES all
|
||||
|
||||
#
|
||||
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
|
||||
ENV VGL_DISPLAY egl
|
||||
|
||||
#
|
||||
# set custom user
|
||||
ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
#
|
||||
# install hardware accleration dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
dpkg --add-architecture i386; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# opengl base: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/base/Dockerfile
|
||||
libxau6 libxau6:i386 \
|
||||
libxdmcp6 libxdmcp6:i386 \
|
||||
libxcb1 libxcb1:i386 \
|
||||
libxext6 libxext6:i386 \
|
||||
libx11-6 libx11-6:i386 \
|
||||
# opengl runtime: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/glvnd/runtime/Dockerfile
|
||||
libglvnd0 libglvnd0:i386 \
|
||||
libgl1 libgl1:i386 \
|
||||
libglx0 libglx0:i386 \
|
||||
libegl1 libegl1:i386 \
|
||||
libgles2 libgles2:i386 \
|
||||
# hardware accleration utilities
|
||||
libglu1 libglu1:i386 \
|
||||
libvulkan-dev libvulkan-dev:i386 \
|
||||
mesa-utils mesa-utils-extra \
|
||||
mesa-va-drivers mesa-vulkan-drivers \
|
||||
vainfo vdpauinfo; \
|
||||
#
|
||||
# install vulkan-utils or vulkan-tools depending on ubuntu release
|
||||
if [ "${UBUNTU_RELEASE}" = "18.04" ]; then \
|
||||
apt-get install -y --no-install-recommends vulkan-utils; \
|
||||
else \
|
||||
apt-get install -y --no-install-recommends vulkan-tools; \
|
||||
fi; \
|
||||
#
|
||||
# create symlink for libnvrtc.so (needed for cudaconvert)
|
||||
find /usr/local/cuda/lib64/ -maxdepth 1 -type l -name "*libnvrtc.so.*" -exec sh -c 'ln -sf {} /usr/local/cuda/lib64/libnvrtc.so' \;; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# add cuda to ld path, for gstreamer cuda plugins
|
||||
ENV LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}:/usr/local/cuda/lib:/usr/local/cuda/lib64"
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates supervisor \
|
||||
pulseaudio xserver-xorg-video-dummy \
|
||||
libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 libx264-163 libvo-aacenc0 librtmp1 libxcvt0 \
|
||||
libgtk-3-bin software-properties-common cabextract aptitude vim curl \
|
||||
#
|
||||
# needed for profile upload preStop hook
|
||||
zip curl \
|
||||
#
|
||||
# file chooser handler, clipboard, drop
|
||||
xdotool xclip libgtk-3-0; \
|
||||
#
|
||||
# create a non-root user
|
||||
groupadd --gid $USER_GID $USERNAME; \
|
||||
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
|
||||
adduser $USERNAME audio; \
|
||||
adduser $USERNAME video; \
|
||||
adduser $USERNAME pulse; \
|
||||
#
|
||||
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
|
||||
mkdir /tmp/.X11-unix; \
|
||||
chmod 1777 /tmp/.X11-unix; \
|
||||
chown $USERNAME /tmp/.X11-unix/; \
|
||||
#
|
||||
# make directories for neko
|
||||
mkdir -p /etc/neko /var/www; \
|
||||
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Emojis
|
||||
fonts-noto-color-emoji \
|
||||
# Chinese fonts
|
||||
fonts-arphic-ukai fonts-arphic-uming \
|
||||
# Japanese fonts
|
||||
fonts-ipafont-mincho fonts-ipafont-gothic \
|
||||
# Korean fonts
|
||||
fonts-unfonts-core \
|
||||
# Indian fonts
|
||||
fonts-indic; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
# copy dependencies from previous stage
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
|
||||
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
#
|
||||
# configure EGL and Vulkan manually
|
||||
RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)') && \
|
||||
# Configure EGL manually
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libEGL_nvidia.so.0\"\n\
|
||||
}\n\
|
||||
}" > /usr/share/glvnd/egl_vendor.d/10_nvidia.json && \
|
||||
# Configure Vulkan manually
|
||||
mkdir -p /etc/vulkan/icd.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libGLX_nvidia.so.0\",\n\
|
||||
\"api_version\" : \"${VULKAN_API_VERSION}\"\n\
|
||||
}\n\
|
||||
}" > /etc/vulkan/icd.d/nvidia_icd.json
|
||||
|
||||
#
|
||||
# install VirtualGL and make libraries available for preload
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
|
||||
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
|
||||
chmod u+s /usr/lib/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/libdlfaker.so; \
|
||||
chmod u+s /usr/lib32/libvglfaker.so; \
|
||||
chmod u+s /usr/lib32/libdlfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
|
||||
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*;
|
||||
|
||||
#
|
||||
# copy runtime configs
|
||||
COPY --chown=neko:neko runtime/.Xresources /home/$USERNAME/.Xresources
|
||||
COPY runtime/default.pa /etc/pulse/default.pa
|
||||
COPY runtime/supervisord.conf /etc/neko/supervisord.conf
|
||||
COPY runtime/xorg.conf /etc/neko/xorg.conf
|
||||
|
||||
#
|
||||
# copy runtime folders
|
||||
COPY --chown=neko:neko runtime/icon-theme /home/$USERNAME/.icons/default
|
||||
COPY runtime/fontconfig/* /etc/fonts/conf.d/
|
||||
COPY runtime/fonts /usr/local/share/fonts
|
||||
|
||||
#
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_SERVER_BIND=:8080
|
||||
ENV NEKO_PLUGINS_ENABLED=true
|
||||
ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
|
||||
|
||||
#
|
||||
# set gstreamer envs
|
||||
ENV PATH="/usr/share/gstreamer/bin:${PATH}"
|
||||
ENV LD_LIBRARY_PATH="/usr/share/gstreamer/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
ENV PKG_CONFIG_PATH="/usr/share/gstreamer/lib/x86_64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
|
||||
|
||||
#
|
||||
# copy gstreamer from previous stage
|
||||
COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
|
||||
|
||||
#
|
||||
# copy plugins from previous stage
|
||||
COPY --from=build /src/bin/plugins/ $NEKO_PLUGINS_DIR
|
||||
|
||||
#
|
||||
# copy executable from previous stage
|
||||
COPY --from=build /src/bin/neko /usr/bin/neko
|
||||
|
||||
#
|
||||
# add healthcheck
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
|
||||
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1
|
||||
|
||||
#
|
||||
# run neko
|
||||
CMD ["/usr/bin/supervisord", "-s", "-c", "/etc/neko/supervisord.conf"]
|
||||
62
server/build
62
server/build
|
|
@ -4,7 +4,12 @@
|
|||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
|
||||
#
|
||||
# do not build plugins when passing "core" as first argument
|
||||
if [ "$1" = "core" ];
|
||||
then
|
||||
skip_plugins="true"
|
||||
fi
|
||||
|
||||
#
|
||||
# set git build variables if git exists
|
||||
|
|
@ -13,12 +18,10 @@ then
|
|||
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||
GIT_TAG=`git tag --points-at $GIT_COMMIT | head -n 1`
|
||||
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
|
||||
GIT_COMMIT="${GIT_DIRTY}${GIT_COMMIT}"
|
||||
fi
|
||||
|
||||
#
|
||||
# load dependencies
|
||||
# load server dependencies
|
||||
go get -v -t -d .
|
||||
|
||||
#
|
||||
|
|
@ -27,9 +30,58 @@ go build \
|
|||
-o bin/neko \
|
||||
-ldflags "
|
||||
-s -w
|
||||
-X 'm1k1o/neko.buildDate=${BUILD_TIME}'
|
||||
-X 'm1k1o/neko.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'`'
|
||||
-X 'm1k1o/neko.gitCommit=${GIT_COMMIT}'
|
||||
-X 'm1k1o/neko.gitBranch=${GIT_BRANCH}'
|
||||
-X 'm1k1o/neko.gitTag=${GIT_TAG}'
|
||||
" \
|
||||
cmd/neko/main.go;
|
||||
|
||||
#
|
||||
# ensure plugins folder exists
|
||||
mkdir -p bin/plugins
|
||||
|
||||
#
|
||||
# if plugins are ignored
|
||||
if [ "$skip_plugins" = "true" ];
|
||||
then
|
||||
echo "Not building plugins..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# if plugins directory does not exist
|
||||
if [ ! -d "./plugins" ];
|
||||
then
|
||||
echo "No plugins directory found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#
|
||||
# remove old plugins
|
||||
rm -f bin/plugins/*
|
||||
|
||||
#
|
||||
# build plugins
|
||||
for plugPath in ./plugins/*; do
|
||||
if [ ! -d $plugPath ];
|
||||
then
|
||||
continue
|
||||
fi
|
||||
|
||||
pushd $plugPath
|
||||
|
||||
echo "Building plugin: $plugPath"
|
||||
|
||||
if [ ! -f "go.plug.mod" ];
|
||||
then
|
||||
echo "go.plug.mod not found, skipping..."
|
||||
popd
|
||||
continue
|
||||
fi
|
||||
|
||||
# build plugin
|
||||
go build -modfile=go.plug.mod -buildmode=plugin -buildvcs=false -o "../../bin/plugins/${plugPath##*/}.so"
|
||||
|
||||
popd
|
||||
done
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
|
||||
"m1k1o/neko"
|
||||
"m1k1o/neko/cmd"
|
||||
"m1k1o/neko/internal/utils"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
|
|
|
|||
50
server/cmd/plugins.go
Normal file
50
server/cmd/plugins.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/plugins"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := &cobra.Command{
|
||||
Use: "plugins [directory]",
|
||||
Short: "load, verify and list plugins",
|
||||
Long: `load, verify and list plugins`,
|
||||
Run: pluginsCmd,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
||||
root.AddCommand(command)
|
||||
}
|
||||
|
||||
func pluginsCmd(cmd *cobra.Command, args []string) {
|
||||
pluginDir := "/etc/neko/plugins"
|
||||
if len(args) > 0 {
|
||||
pluginDir = args[0]
|
||||
}
|
||||
log.Info().Str("dir", pluginDir).Msg("plugins directory")
|
||||
|
||||
plugs := plugins.New(&config.Plugins{
|
||||
Enabled: true,
|
||||
Required: true,
|
||||
Dir: pluginDir,
|
||||
})
|
||||
|
||||
meta := plugs.Metadata()
|
||||
if len(meta) == 0 {
|
||||
log.Fatal().Msg("no plugins found")
|
||||
}
|
||||
|
||||
// marshal indent to stdout
|
||||
dec := json.NewEncoder(os.Stdout)
|
||||
dec.SetIndent("", " ")
|
||||
err := dec.Encode(meta)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("unable to marshal metadata")
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -15,9 +16,18 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko"
|
||||
"m1k1o/neko/internal/config"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
// properly log unhandled panics
|
||||
defer func() {
|
||||
panicVal := recover()
|
||||
if panicVal != nil {
|
||||
log.Panic().Msgf("%v", panicVal)
|
||||
}
|
||||
}()
|
||||
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
|
|
@ -25,58 +35,24 @@ var root = &cobra.Command{
|
|||
Use: "neko",
|
||||
Short: "neko streaming server",
|
||||
Long: `neko streaming server`,
|
||||
Version: neko.Service.Version.String(),
|
||||
Version: neko.Version.String(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootConfig := config.Root{}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
console := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
|
||||
if !viper.GetBool("logs") {
|
||||
log.Logger = log.Output(console)
|
||||
} else {
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(logs); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(logs, os.ModePerm)
|
||||
}
|
||||
|
||||
latest := filepath.Join(logs, "neko-latest.log")
|
||||
_, err := os.Stat(latest)
|
||||
if err == nil {
|
||||
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to create log file")
|
||||
}
|
||||
|
||||
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("logger dropped %d messages", missed)
|
||||
})
|
||||
|
||||
log.Logger = log.Output(io.MultiWriter(console, logger))
|
||||
}
|
||||
|
||||
//////
|
||||
// configs
|
||||
//////
|
||||
config := viper.GetString("config")
|
||||
|
||||
config := viper.GetString("config") // Use config file from the flag.
|
||||
if config == "" {
|
||||
config = os.Getenv("NEKO_CONFIG") // Use config file from the environment variable.
|
||||
}
|
||||
|
||||
if config != "" {
|
||||
viper.SetConfigFile(config) // Use config file from the flag.
|
||||
viper.SetConfigFile(config)
|
||||
} else {
|
||||
if runtime.GOOS == "linux" {
|
||||
viper.AddConfigPath("/etc/neko/")
|
||||
|
|
@ -87,41 +63,103 @@ func init() {
|
|||
}
|
||||
|
||||
viper.SetEnvPrefix("NEKO")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
if config != "" {
|
||||
log.Error().Err(err)
|
||||
// read config values
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
_, notFound := err.(viper.ConfigFileNotFoundError)
|
||||
if !notFound {
|
||||
log.Fatal().Err(err).Msg("unable to read config file")
|
||||
}
|
||||
}
|
||||
|
||||
debug := viper.GetBool("debug")
|
||||
if debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
// get full config file path
|
||||
config = viper.ConfigFileUsed()
|
||||
|
||||
// set root config values
|
||||
rootConfig.Set()
|
||||
|
||||
//////
|
||||
// logs
|
||||
//////
|
||||
var logWriter io.Writer
|
||||
|
||||
// log to a directory instead of stderr
|
||||
if rootConfig.LogDir != "" {
|
||||
if _, err := os.Stat(rootConfig.LogDir); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(rootConfig.LogDir, os.ModePerm)
|
||||
}
|
||||
|
||||
latest := filepath.Join(rootConfig.LogDir, "neko-latest.log")
|
||||
if _, err := os.Stat(latest); err == nil {
|
||||
err = os.Rename(latest, filepath.Join(rootConfig.LogDir, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to open log file")
|
||||
}
|
||||
|
||||
logWriter = diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("logger dropped %d messages", missed)
|
||||
})
|
||||
} else {
|
||||
logWriter = os.Stderr
|
||||
}
|
||||
|
||||
// log console output instead of json
|
||||
if !rootConfig.LogJson {
|
||||
logWriter = zerolog.ConsoleWriter{
|
||||
Out: logWriter,
|
||||
NoColor: rootConfig.LogNocolor,
|
||||
}
|
||||
}
|
||||
|
||||
// save new logger output
|
||||
log.Logger = log.Output(logWriter)
|
||||
|
||||
// set custom log level
|
||||
if rootConfig.LogLevel != zerolog.NoLevel {
|
||||
zerolog.SetGlobalLevel(rootConfig.LogLevel)
|
||||
}
|
||||
|
||||
// set custom log tiem format
|
||||
if rootConfig.LogTime != "" {
|
||||
zerolog.TimeFieldFormat = rootConfig.LogTime
|
||||
}
|
||||
|
||||
timeFormat := rootConfig.LogTime
|
||||
if rootConfig.LogTime == zerolog.TimeFormatUnix {
|
||||
timeFormat = "UNIX"
|
||||
}
|
||||
|
||||
file := viper.ConfigFileUsed()
|
||||
logger := log.With().
|
||||
Bool("debug", debug).
|
||||
Str("logging", viper.GetString("logs")).
|
||||
Str("config", file).
|
||||
Str("config", config).
|
||||
Str("log-level", zerolog.GlobalLevel().String()).
|
||||
Bool("log-json", rootConfig.LogJson).
|
||||
Str("log-time", timeFormat).
|
||||
Str("log-dir", rootConfig.LogDir).
|
||||
Logger()
|
||||
|
||||
if file == "" {
|
||||
if config == "" {
|
||||
logger.Warn().Msg("preflight complete without config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete")
|
||||
if _, err := os.Stat(config); os.IsNotExist(err) {
|
||||
logger.Err(err).Msg("preflight complete with nonexistent config file")
|
||||
} else {
|
||||
logger.Info().Msg("preflight complete with config file")
|
||||
}
|
||||
}
|
||||
|
||||
neko.Service.Root.Set()
|
||||
})
|
||||
|
||||
if err := neko.Service.Root.Init(root); err != nil {
|
||||
if err := rootConfig.Init(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko.Service.Version.Details())
|
||||
root.SetVersionTemplate(neko.Version.Details())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,240 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"m1k1o/neko"
|
||||
"m1k1o/neko/internal/api"
|
||||
"m1k1o/neko/internal/capture"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/desktop"
|
||||
"m1k1o/neko/internal/http"
|
||||
"m1k1o/neko/internal/member"
|
||||
"m1k1o/neko/internal/plugins"
|
||||
"m1k1o/neko/internal/session"
|
||||
"m1k1o/neko/internal/webrtc"
|
||||
"m1k1o/neko/internal/websocket"
|
||||
)
|
||||
|
||||
func init() {
|
||||
service := serve{}
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
Run: neko.Service.ServeCommand,
|
||||
Use: "serve",
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
PreRun: service.PreRun,
|
||||
Run: service.Run,
|
||||
}
|
||||
|
||||
configs := []config.Config{
|
||||
neko.Service.Server,
|
||||
neko.Service.WebRTC,
|
||||
neko.Service.Capture,
|
||||
neko.Service.Desktop,
|
||||
neko.Service.WebSocket,
|
||||
}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
for _, cfg := range configs {
|
||||
cfg.Set()
|
||||
}
|
||||
neko.Service.Preflight()
|
||||
})
|
||||
|
||||
for _, cfg := range configs {
|
||||
if err := cfg.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run serve command")
|
||||
}
|
||||
if err := service.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to initialize configuration")
|
||||
}
|
||||
|
||||
root.AddCommand(command)
|
||||
}
|
||||
|
||||
type serve struct {
|
||||
logger zerolog.Logger
|
||||
|
||||
configs struct {
|
||||
Desktop config.Desktop
|
||||
Capture config.Capture
|
||||
WebRTC config.WebRTC
|
||||
Member config.Member
|
||||
Session config.Session
|
||||
Plugins config.Plugins
|
||||
Server config.Server
|
||||
}
|
||||
|
||||
managers struct {
|
||||
desktop *desktop.DesktopManagerCtx
|
||||
capture *capture.CaptureManagerCtx
|
||||
webRTC *webrtc.WebRTCManagerCtx
|
||||
member *member.MemberManagerCtx
|
||||
session *session.SessionManagerCtx
|
||||
webSocket *websocket.WebSocketManagerCtx
|
||||
plugins *plugins.ManagerCtx
|
||||
api *api.ApiManagerCtx
|
||||
http *http.HttpManagerCtx
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serve) Init(cmd *cobra.Command) error {
|
||||
if err := c.configs.Desktop.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Capture.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.WebRTC.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Member.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Session.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Plugins.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.configs.Server.Init(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// V2 configuration
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *serve) PreRun(cmd *cobra.Command, args []string) {
|
||||
c.logger = log.With().Str("service", "neko").Logger()
|
||||
|
||||
c.configs.Desktop.Set()
|
||||
c.configs.Capture.Set()
|
||||
c.configs.WebRTC.Set()
|
||||
c.configs.Member.Set()
|
||||
c.configs.Session.Set()
|
||||
c.configs.Plugins.Set()
|
||||
c.configs.Server.Set()
|
||||
|
||||
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) {
|
||||
c.managers.session = session.New(
|
||||
&c.configs.Session,
|
||||
)
|
||||
|
||||
c.managers.member = member.New(
|
||||
c.managers.session,
|
||||
&c.configs.Member,
|
||||
)
|
||||
|
||||
if err := c.managers.member.Connect(); err != nil {
|
||||
c.logger.Panic().Err(err).Msg("unable to connect to member manager")
|
||||
}
|
||||
|
||||
c.managers.desktop = desktop.New(
|
||||
&c.configs.Desktop,
|
||||
)
|
||||
c.managers.desktop.Start()
|
||||
|
||||
c.managers.capture = capture.New(
|
||||
c.managers.desktop,
|
||||
&c.configs.Capture,
|
||||
)
|
||||
c.managers.capture.Start()
|
||||
|
||||
c.managers.webRTC = webrtc.New(
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
&c.configs.WebRTC,
|
||||
)
|
||||
c.managers.webRTC.Start()
|
||||
|
||||
c.managers.webSocket = websocket.New(
|
||||
c.managers.session,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
c.managers.webRTC,
|
||||
)
|
||||
c.managers.webSocket.Start()
|
||||
|
||||
c.managers.api = api.New(
|
||||
c.managers.session,
|
||||
c.managers.member,
|
||||
c.managers.desktop,
|
||||
c.managers.capture,
|
||||
)
|
||||
|
||||
c.managers.plugins = plugins.New(
|
||||
&c.configs.Plugins,
|
||||
)
|
||||
|
||||
// init and set configuration now
|
||||
// this means it won't be in --help
|
||||
c.managers.plugins.InitConfigs(cmd)
|
||||
c.managers.plugins.SetConfigs()
|
||||
|
||||
c.managers.plugins.Start(
|
||||
c.managers.session,
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
)
|
||||
|
||||
c.managers.http = http.New(
|
||||
c.managers.webSocket,
|
||||
c.managers.api,
|
||||
&c.configs.Server,
|
||||
)
|
||||
c.managers.http.Start()
|
||||
}
|
||||
|
||||
func (c *serve) Shutdown() {
|
||||
var err error
|
||||
|
||||
err = c.managers.http.Shutdown()
|
||||
c.logger.Err(err).Msg("http manager shutdown")
|
||||
|
||||
err = c.managers.plugins.Shutdown()
|
||||
c.logger.Err(err).Msg("plugins manager shutdown")
|
||||
|
||||
err = c.managers.webSocket.Shutdown()
|
||||
c.logger.Err(err).Msg("websocket manager shutdown")
|
||||
|
||||
err = c.managers.webRTC.Shutdown()
|
||||
c.logger.Err(err).Msg("webrtc manager shutdown")
|
||||
|
||||
err = c.managers.capture.Shutdown()
|
||||
c.logger.Err(err).Msg("capture manager shutdown")
|
||||
|
||||
err = c.managers.desktop.Shutdown()
|
||||
c.logger.Err(err).Msg("desktop manager shutdown")
|
||||
|
||||
err = c.managers.member.Disconnect()
|
||||
c.logger.Err(err).Msg("member manager disconnect")
|
||||
}
|
||||
|
||||
func (c *serve) Run(cmd *cobra.Command, args []string) {
|
||||
c.logger.Info().Msg("starting neko server")
|
||||
c.Start(cmd)
|
||||
c.logger.Info().Msg("neko ready")
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
c.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig)
|
||||
c.Shutdown()
|
||||
c.logger.Info().Msg("shutdown complete")
|
||||
}
|
||||
|
|
|
|||
23
server/dev/build
Executable file
23
server/dev/build
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
#
|
||||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||
|
||||
# if first argument is nvidia, use nvidia dockerfile
|
||||
if [ "$1" = "nvidia" ]; then
|
||||
echo "Building nvidia docker image"
|
||||
DOCKERFILE="Dockerfile.nvidia"
|
||||
else
|
||||
echo "Building default docker image"
|
||||
DOCKERFILE="Dockerfile"
|
||||
fi
|
||||
|
||||
docker build -t neko_server_build --target build --build-arg "GIT_COMMIT=$GIT_COMMIT" --build-arg "GIT_BRANCH=$GIT_BRANCH" -f ../$DOCKERFILE ..
|
||||
docker build -t neko_server_runtime --target runtime --build-arg "GIT_COMMIT=$GIT_COMMIT" --build-arg "GIT_BRANCH=$GIT_BRANCH" -f ../$DOCKERFILE ..
|
||||
|
||||
docker build -t neko_server_app --build-arg "BASE_IMAGE=neko_server_runtime" -f ./runtime/Dockerfile ./runtime
|
||||
3
server/dev/exec
Executable file
3
server/dev/exec
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker exec -it neko_server_dev /bin/bash
|
||||
12
server/dev/fmt
Executable file
12
server/dev/fmt
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run -it --rm \
|
||||
--entrypoint="go" \
|
||||
-v "${PWD}/../:/src" \
|
||||
neko_server_build fmt ./...
|
||||
25
server/dev/go
Executable file
25
server/dev/go
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run -it \
|
||||
--name "neko_server_go" \
|
||||
--entrypoint="go" \
|
||||
-v "${PWD}/../:/src" \
|
||||
neko_server_build "$@";
|
||||
#
|
||||
# copy package files
|
||||
docker cp neko_server_go:/src/go.mod "../go.mod"
|
||||
docker cp neko_server_go:/src/go.sum "../go.sum"
|
||||
|
||||
#
|
||||
# commit changes to image
|
||||
docker commit "neko_server_go" "neko_server_build"
|
||||
|
||||
#
|
||||
# remove contianer
|
||||
docker rm "neko_server_go"
|
||||
14
server/dev/lint
Executable file
14
server/dev/lint
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then
|
||||
echo "Image 'neko_server_build' not found. Run ./build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# build server
|
||||
docker run --rm -it \
|
||||
-v "${PWD}/../:/src" \
|
||||
--entrypoint="/bin/bash" \
|
||||
neko_server_build -c '[ -f ./bin/golangci-lint ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.31.0;./bin/golangci-lint run';
|
||||
32
server/dev/rebuild
Executable file
32
server/dev/rebuild
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
#
|
||||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
#
|
||||
# build server
|
||||
docker run --rm -it \
|
||||
-v "${PWD}/../:/src" \
|
||||
--entrypoint="/bin/bash" \
|
||||
neko_server_build "./build" "$@";
|
||||
|
||||
#
|
||||
# remove old plugins
|
||||
docker exec neko_server_dev rm -rf /etc/neko/plugins
|
||||
|
||||
#
|
||||
# replace server binary in container
|
||||
docker cp "${PWD}/../bin/neko" neko_server_dev:/usr/bin/neko
|
||||
|
||||
#
|
||||
# replace plugin binaries in container
|
||||
if [ -d "${PWD}/../bin/plugins" ];
|
||||
then
|
||||
docker cp "${PWD}/../bin/plugins" neko_server_dev:/etc/neko/plugins
|
||||
fi
|
||||
|
||||
#
|
||||
# restart server
|
||||
docker exec neko_server_dev supervisorctl -c /etc/neko/supervisord.conf restart neko
|
||||
32
server/dev/rebuild.input
Executable file
32
server/dev/rebuild.input
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
cd ../xorg/xf86-input-neko
|
||||
|
||||
#
|
||||
# aborting if any command returns a non-zero value
|
||||
set -e
|
||||
|
||||
#
|
||||
# check if docker image exists
|
||||
if [ -z "$(docker images -q xf86-input-neko)" ]; then
|
||||
echo "Docker image not found, building it"
|
||||
docker build -t xf86-input-neko .
|
||||
fi
|
||||
|
||||
#
|
||||
# if there is no ./configure script, run autogen.sh and configure
|
||||
if [ ! -f ./configure ]; then
|
||||
docker run -v $PWD/:/app --rm xf86-input-neko bash -c './autogen.sh && ./configure'
|
||||
fi
|
||||
|
||||
#
|
||||
# make install
|
||||
docker run -v $PWD/:/app --rm xf86-input-neko bash -c 'make && make install DESTDIR=/app/build'
|
||||
|
||||
#
|
||||
# replace input driver in container
|
||||
docker cp "${PWD}/build/usr/local/lib/xorg/modules/input/neko_drv.so" neko_server_dev:/usr/lib/xorg/modules/input/neko_drv.so
|
||||
|
||||
#
|
||||
# restart server
|
||||
docker exec neko_server_dev supervisorctl -c /etc/neko/supervisord.conf restart x-server
|
||||
31
server/dev/runtime/Dockerfile
Normal file
31
server/dev/runtime/Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
ARG BASE_IMAGE=neko_server_runtime:latest
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"
|
||||
|
||||
#
|
||||
# install xfce and firefox
|
||||
RUN set -eux; apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
dbus-x11 xfce4 xfce4-terminal sudo \
|
||||
xz-utils bzip2 libgtk-3-0 libdbus-glib-1-2; \
|
||||
#
|
||||
# fetch latest firefox release
|
||||
wget -O /tmp/firefox-setup.tar.bz2 "${SRC_URL}"; \
|
||||
mkdir /usr/lib/firefox; \
|
||||
tar -xjf /tmp/firefox-setup.tar.bz2 -C /usr/lib; \
|
||||
rm -f /tmp/firefox-setup.tar.bz2; \
|
||||
ln -s /usr/lib/firefox/firefox /usr/bin/firefox; \
|
||||
#
|
||||
# add user to sudoers
|
||||
usermod -aG sudo neko; \
|
||||
echo "neko:neko" | chpasswd; \
|
||||
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
|
||||
# clean up
|
||||
apt-get --purge autoremove -y xz-utils bzip2; \
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# copy configuation files
|
||||
COPY supervisord.conf /etc/neko/supervisord/xfce.conf
|
||||
122
server/dev/runtime/config.nvidia.yml
Normal file
122
server/dev/runtime/config.nvidia.yml
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
capture:
|
||||
video:
|
||||
codec: h264
|
||||
ids:
|
||||
- nvh264enc
|
||||
- x264enc
|
||||
pipelines:
|
||||
nvh264enc:
|
||||
fps: 25
|
||||
bitrate: 2
|
||||
#gst_prefix: "! cudaupload ! cudaconvert ! video/x-raw(memory:CUDAMemory),format=NV12"
|
||||
gst_prefix: "! video/x-raw,format=NV12"
|
||||
gst_encoder: "nvh264enc"
|
||||
gst_params:
|
||||
bitrate: 3000
|
||||
rc-mode: 5 # Low-Delay CBR, High Quality
|
||||
preset: 5 # Low Latency, High Performance
|
||||
zerolatency: true
|
||||
gop-size: 25
|
||||
gst_suffix: "! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"
|
||||
x264enc:
|
||||
fps: 25
|
||||
bitrate: 1
|
||||
gst_prefix: "! video/x-raw,format=I420"
|
||||
gst_encoder: "x264enc"
|
||||
gst_params:
|
||||
threads: 4
|
||||
bitrate: 4096
|
||||
key-int-max: 25
|
||||
byte-stream: true
|
||||
tune: zerolatency
|
||||
speed-preset: veryfast
|
||||
gst_suffix: "! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"
|
||||
screencast:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
pprof: true
|
||||
|
||||
desktop:
|
||||
screen: "1920x1080@60"
|
||||
|
||||
member:
|
||||
# provider: "object"
|
||||
# object:
|
||||
# users:
|
||||
# - username: "admin"
|
||||
# password: "admin"
|
||||
# profile:
|
||||
# name: "Administrator"
|
||||
# is_admin: true
|
||||
# can_login: true
|
||||
# can_connect: true
|
||||
# can_watch: true
|
||||
# can_host: true
|
||||
# can_share_media: true
|
||||
# can_access_clipboard: true
|
||||
# sends_inactive_cursor: true
|
||||
# can_see_inactive_cursors: true
|
||||
# - username: "user"
|
||||
# password: "neko"
|
||||
# profile:
|
||||
# name: "User"
|
||||
# is_admin: false
|
||||
# can_login: true
|
||||
# can_connect: true
|
||||
# can_watch: true
|
||||
# can_host: true
|
||||
# can_share_media: true
|
||||
# can_access_clipboard: true
|
||||
# sends_inactive_cursor: true
|
||||
# can_see_inactive_cursors: false
|
||||
# provider: "file"
|
||||
# file:
|
||||
# path: "/home/neko/members.json"
|
||||
provider: "multiuser"
|
||||
multiuser:
|
||||
admin_password: "admin"
|
||||
user_password: "neko"
|
||||
# admin_profile: # optional
|
||||
# user_profile: # optional
|
||||
# provider: "noauth"
|
||||
|
||||
session:
|
||||
# Allows reconnecting the websocket even if the previous
|
||||
# connection was not closed. Can lead to session hijacking.
|
||||
merciful_reconnect: true
|
||||
# Show inactive cursors on the screen. Can lead to multiple
|
||||
# data sent via WebSockets and additonal rendering cost on
|
||||
# the clients.
|
||||
implicit_hosting: false
|
||||
inactive_cursors: true
|
||||
api_token: "neko123"
|
||||
cookie:
|
||||
# Disabling cookies will result to use Bearer Authentication.
|
||||
# This is less secure, because access token will be sent to
|
||||
# client in playload and accessible via JS app.
|
||||
enabled: false
|
||||
secure: false
|
||||
|
||||
webrtc:
|
||||
icelite: true
|
||||
iceservers:
|
||||
# Backend servers are ignored if icelite is true.
|
||||
backend:
|
||||
- urls: [ stun:stun.l.google.com:19302 ]
|
||||
frontend:
|
||||
- urls: [ stun:stun.l.google.com:19305 ]
|
||||
#username: foo
|
||||
#credential: bar
|
||||
# estimator:
|
||||
# enabled: true
|
||||
# passive: false
|
||||
# debug: true
|
||||
# initial_bitrate: 1000000
|
||||
# read_interval: 1s
|
||||
# stable_duration: 10s
|
||||
# unstable_duration: 5s
|
||||
# stalled_duration: 20s
|
||||
# downgrade_backoff: 10s
|
||||
# upgrade_backoff: 30s
|
||||
# diff_threshold: 0.5
|
||||
144
server/dev/runtime/config.yml
Normal file
144
server/dev/runtime/config.yml
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
capture:
|
||||
video:
|
||||
codec: vp8
|
||||
ids: [ hq, lq ]
|
||||
pipelines:
|
||||
hq:
|
||||
fps: 25
|
||||
gst_encoder: vp8enc
|
||||
gst_params:
|
||||
target-bitrate: round(3072 * 650)
|
||||
cpu-used: 4
|
||||
end-usage: cbr
|
||||
threads: 4
|
||||
deadline: 1
|
||||
undershoot: 95
|
||||
buffer-size: (3072 * 4)
|
||||
buffer-initial-size: (3072 * 2)
|
||||
buffer-optimal-size: (3072 * 3)
|
||||
keyframe-max-dist: 25
|
||||
min-quantizer: 4
|
||||
max-quantizer: 20
|
||||
lq:
|
||||
fps: 25
|
||||
gst_encoder: vp8enc
|
||||
gst_params:
|
||||
target-bitrate: round(1024 * 650)
|
||||
cpu-used: 4
|
||||
end-usage: cbr
|
||||
threads: 4
|
||||
deadline: 1
|
||||
undershoot: 95
|
||||
buffer-size: (1024 * 4)
|
||||
buffer-initial-size: (1024 * 2)
|
||||
buffer-optimal-size: (1024 * 3)
|
||||
keyframe-max-dist: 25
|
||||
min-quantizer: 4
|
||||
max-quantizer: 20
|
||||
# video:
|
||||
# codec: h264
|
||||
# ids: [ main ]
|
||||
# pipelines:
|
||||
# main:
|
||||
# width: (width / 3) * 2
|
||||
# height: (height / 3) * 2
|
||||
# fps: 20
|
||||
# gst_prefix: "! video/x-raw,format=I420"
|
||||
# gst_encoder: "x264enc"
|
||||
# gst_params:
|
||||
# threads: 4
|
||||
# bitrate: 4096
|
||||
# key-int-max: 15
|
||||
# byte-stream: true
|
||||
# tune: zerolatency
|
||||
# speed-preset: veryfast
|
||||
# gst_suffix: "! video/x-h264,stream-format=byte-stream"
|
||||
screencast:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
pprof: true
|
||||
|
||||
desktop:
|
||||
screen: "1920x1080@60"
|
||||
|
||||
member:
|
||||
# provider: "object"
|
||||
# object:
|
||||
# users:
|
||||
# - username: "admin"
|
||||
# password: "admin"
|
||||
# profile:
|
||||
# name: "Administrator"
|
||||
# is_admin: true
|
||||
# can_login: true
|
||||
# can_connect: true
|
||||
# can_watch: true
|
||||
# can_host: true
|
||||
# can_share_media: true
|
||||
# can_access_clipboard: true
|
||||
# sends_inactive_cursor: true
|
||||
# can_see_inactive_cursors: true
|
||||
# - username: "user"
|
||||
# password: "neko"
|
||||
# profile:
|
||||
# name: "User"
|
||||
# is_admin: false
|
||||
# can_login: true
|
||||
# can_connect: true
|
||||
# can_watch: true
|
||||
# can_host: true
|
||||
# can_share_media: true
|
||||
# can_access_clipboard: true
|
||||
# sends_inactive_cursor: true
|
||||
# can_see_inactive_cursors: false
|
||||
# provider: "file"
|
||||
# file:
|
||||
# path: "/home/neko/members.json"
|
||||
provider: "multiuser"
|
||||
multiuser:
|
||||
admin_password: "admin"
|
||||
user_password: "neko"
|
||||
# admin_profile: # optional
|
||||
# user_profile: # optional
|
||||
# provider: "noauth"
|
||||
|
||||
session:
|
||||
# Allows reconnecting the websocket even if the previous
|
||||
# connection was not closed. Can lead to session hijacking.
|
||||
merciful_reconnect: true
|
||||
# Show inactive cursors on the screen. Can lead to multiple
|
||||
# data sent via WebSockets and additonal rendering cost on
|
||||
# the clients.
|
||||
implicit_hosting: false
|
||||
inactive_cursors: true
|
||||
api_token: "neko123"
|
||||
cookie:
|
||||
# Disabling cookies will result to use Bearer Authentication.
|
||||
# This is less secure, because access token will be sent to
|
||||
# client in playload and accessible via JS app.
|
||||
enabled: false
|
||||
secure: false
|
||||
|
||||
webrtc:
|
||||
icelite: true
|
||||
iceservers:
|
||||
# Backend servers are ignored if icelite is true.
|
||||
backend:
|
||||
- urls: [ stun:stun.l.google.com:19302 ]
|
||||
frontend:
|
||||
- urls: [ stun:stun.l.google.com:19305 ]
|
||||
#username: foo
|
||||
#credential: bar
|
||||
# estimator:
|
||||
# enabled: true
|
||||
# passive: false
|
||||
# debug: true
|
||||
# initial_bitrate: 1000000
|
||||
# read_interval: 1s
|
||||
# stable_duration: 10s
|
||||
# unstable_duration: 5s
|
||||
# stalled_duration: 20s
|
||||
# downgrade_backoff: 10s
|
||||
# upgrade_backoff: 30s
|
||||
# diff_threshold: 0.5
|
||||
10
server/dev/runtime/supervisord.conf
Normal file
10
server/dev/runtime/supervisord.conf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[program:xfce]
|
||||
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
|
||||
command=/usr/bin/startxfce4
|
||||
stopsignal=INT
|
||||
autorestart=true
|
||||
priority=500
|
||||
user=%(ENV_USER)s
|
||||
stdout_logfile=/dev/stderr
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
64
server/dev/start
Executable file
64
server/dev/start
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ -z "$(docker images -q neko_server_app 2> /dev/null)" ]; then
|
||||
echo "Image 'neko_server_app' not found. Running ./build first."
|
||||
./build
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_PORT ]; then
|
||||
NEKO_PORT="3000"
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_MUX ]; then
|
||||
NEKO_MUX="52100"
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_NAT1TO1 ]; then
|
||||
for i in $(ifconfig -l 2>/dev/null); do
|
||||
NEKO_NAT1TO1=$(ipconfig getifaddr $i)
|
||||
if [ ! -z $NEKO_NAT1TO1 ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z $NEKO_NAT1TO1 ]; then
|
||||
NEKO_NAT1TO1=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_NAT1TO1 ]; then
|
||||
NEKO_NAT1TO1=$(hostname -i 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
# if first argument is nvidia, start with nvidia runtime
|
||||
if [ "$1" = "nvidia" ]; then
|
||||
echo "Starting nvidia docker image"
|
||||
EXTRAOPTS="--gpus all"
|
||||
CONFIG="config.nvidia.yml"
|
||||
else
|
||||
echo "Starting default docker image"
|
||||
EXTRAOPTS=""
|
||||
CONFIG="config.yml"
|
||||
fi
|
||||
|
||||
echo "Using app port: ${NEKO_PORT}"
|
||||
echo "Using mux port: ${NEKO_MUX}"
|
||||
echo "Using IP address: ${NEKO_NAT1TO1}"
|
||||
|
||||
# start server
|
||||
docker run --rm -it \
|
||||
--name "neko_server_dev" \
|
||||
-p "${NEKO_PORT}:8080" \
|
||||
-p "${NEKO_MUX}:${NEKO_MUX}/tcp" \
|
||||
-p "${NEKO_MUX}:${NEKO_MUX}/udp" \
|
||||
-e "NEKO_WEBRTC_UDPMUX=${NEKO_MUX}" \
|
||||
-e "NEKO_WEBRTC_TCPMUX=${NEKO_MUX}" \
|
||||
-e "NEKO_WEBRTC_NAT1TO1=${NEKO_NAT1TO1}" \
|
||||
-e "NEKO_SESSION_FILE=/home/neko/sessions.txt" \
|
||||
-v "${PWD}/runtime/$CONFIG:/etc/neko/neko.yml" \
|
||||
-e "NEKO_DEBUG=1" \
|
||||
--shm-size=2G \
|
||||
--security-opt seccomp=unconfined \
|
||||
$EXTRAOPTS \
|
||||
neko_server_app:latest;
|
||||
|
|
@ -1,55 +1,68 @@
|
|||
module m1k1o/neko
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/PaesslerAG/gval v1.2.2
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/pion/ice/v2 v2.3.0
|
||||
github.com/pion/interceptor v0.1.12
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kataras/go-events v0.0.3
|
||||
github.com/pion/ice/v2 v2.3.12
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.12 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.55
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/pion/rtcp v1.2.13
|
||||
github.com/pion/webrtc/v3 v3.2.24
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.6 // indirect
|
||||
github.com/pion/mdns v0.0.7 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/sctp v1.8.6 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun v0.4.0 // indirect
|
||||
github.com/pion/transport/v2 v2.0.2 // indirect
|
||||
github.com/pion/turn/v2 v2.1.0 // indirect
|
||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
||||
github.com/spf13/afero v1.9.4 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.46.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
611
server/go.sum
611
server/go.sum
|
|
@ -1,155 +1,57 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E=
|
||||
github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
|
@ -158,13 +60,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
|
|
@ -176,425 +77,221 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
|
|||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
|
||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
||||
github.com/pion/ice/v2 v2.3.0 h1:G+ysriabk1p9wbySDpdsnlD+6ZspLlDLagRduRfzJPk=
|
||||
github.com/pion/ice/v2 v2.3.0/go.mod h1:+xO/cXVnnVUr6D2ZJcCT5g9LngucUkkTvfnTMqUxKRM=
|
||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
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.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
|
||||
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
|
||||
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
|
||||
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
|
||||
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.0.1/go.mod h1:93OYg91+mrGxKW+Jrgzmqr80kgXqD7J0yybOrdr7w0Y=
|
||||
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
|
||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
||||
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
|
||||
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
||||
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
|
||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
||||
github.com/pion/webrtc/v3 v3.1.55 h1:jQt98hZ8DUi/l/s/rtogthBdsKKvKekFgZCX9hMEqRo=
|
||||
github.com/pion/webrtc/v3 v3.1.55/go.mod h1:M1gU5mnvvo4e1nnLvF23esYz0nZAFOtbU/wq44MSfbc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
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/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
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.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
|
||||
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
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/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
|
||||
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
|
@ -606,13 +303,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
|||
84
server/internal/api/members/bluk.go
Normal file
84
server/internal/api/members/bluk.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package members
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type MemberBulkUpdatePayload struct {
|
||||
IDs []string `json:"ids"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersBulkUpdate(w http.ResponseWriter, r *http.Request) error {
|
||||
bytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to read post body").WithInternalErr(err)
|
||||
}
|
||||
|
||||
header := &MemberBulkUpdatePayload{}
|
||||
if err := json.Unmarshal(bytes, &header); err != nil {
|
||||
return utils.HttpBadRequest("unable to unmarshal payload").WithInternalErr(err)
|
||||
}
|
||||
|
||||
for _, memberId := range header.IDs {
|
||||
// TODO: Bulk select?
|
||||
profile, err := h.members.Select(memberId)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to select member profile").
|
||||
Msgf("failed to update member %s", memberId)
|
||||
}
|
||||
|
||||
body := &MemberBulkUpdatePayload{
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(bytes, &body); err != nil {
|
||||
return utils.HttpBadRequest().
|
||||
WithInternalErr(err).
|
||||
Msgf("unable to unmarshal payload for member %s", memberId)
|
||||
}
|
||||
|
||||
if err := h.members.UpdateProfile(memberId, body.Profile); err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to update member profile").
|
||||
Msgf("failed to update member %s", memberId)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
type MemberBulkDeletePayload struct {
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersBulkDelete(w http.ResponseWriter, r *http.Request) error {
|
||||
bytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to read post body").WithInternalErr(err)
|
||||
}
|
||||
|
||||
data := &MemberBulkDeletePayload{}
|
||||
if err := json.Unmarshal(bytes, &data); err != nil {
|
||||
return utils.HttpBadRequest("unable to unmarshal payload").WithInternalErr(err)
|
||||
}
|
||||
|
||||
for _, memberId := range data.IDs {
|
||||
if err := h.members.Delete(memberId); err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to delete member").
|
||||
Msgf("failed to delete member %s", memberId)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
144
server/internal/api/members/controler.go
Normal file
144
server/internal/api/members/controler.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package members
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type MemberDataPayload struct {
|
||||
ID string `json:"id"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
}
|
||||
|
||||
type MemberCreatePayload struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
}
|
||||
|
||||
type MemberPasswordPayload struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersList(w http.ResponseWriter, r *http.Request) error {
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
// TODO: Default zero.
|
||||
limit = 0
|
||||
}
|
||||
|
||||
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if err != nil {
|
||||
// TODO: Default zero.
|
||||
offset = 0
|
||||
}
|
||||
|
||||
entries, err := h.members.SelectAll(limit, offset)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
members := []MemberDataPayload{}
|
||||
for id, profile := range entries {
|
||||
members = append(members, MemberDataPayload{
|
||||
ID: id,
|
||||
Profile: profile,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, members)
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersCreate(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &MemberCreatePayload{
|
||||
// default values
|
||||
Profile: types.MemberProfile{
|
||||
IsAdmin: false,
|
||||
CanLogin: true,
|
||||
CanConnect: true,
|
||||
CanWatch: true,
|
||||
CanHost: true,
|
||||
CanShareMedia: true,
|
||||
CanAccessClipboard: true,
|
||||
SendsInactiveCursor: true,
|
||||
CanSeeInactiveCursors: true,
|
||||
},
|
||||
}
|
||||
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.Username == "" {
|
||||
return utils.HttpBadRequest("username cannot be empty")
|
||||
}
|
||||
|
||||
if data.Password == "" {
|
||||
return utils.HttpBadRequest("password cannot be empty")
|
||||
}
|
||||
|
||||
id, err := h.members.Insert(data.Username, data.Password, data.Profile)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrMemberAlreadyExists) {
|
||||
return utils.HttpUnprocessableEntity("member already exists")
|
||||
}
|
||||
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, MemberDataPayload{
|
||||
ID: id,
|
||||
Profile: data.Profile,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersRead(w http.ResponseWriter, r *http.Request) error {
|
||||
member := GetMember(r)
|
||||
profile := member.Profile
|
||||
|
||||
return utils.HttpSuccess(w, profile)
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersUpdateProfile(w http.ResponseWriter, r *http.Request) error {
|
||||
member := GetMember(r)
|
||||
data := &member.Profile
|
||||
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.members.UpdateProfile(member.ID, *data); err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersUpdatePassword(w http.ResponseWriter, r *http.Request) error {
|
||||
member := GetMember(r)
|
||||
data := &MemberPasswordPayload{}
|
||||
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.members.UpdatePassword(member.ID, data.Password); err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *MembersHandler) membersDelete(w http.ResponseWriter, r *http.Request) error {
|
||||
member := GetMember(r)
|
||||
|
||||
if err := h.members.Delete(member.ID); err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
83
server/internal/api/members/handler.go
Normal file
83
server/internal/api/members/handler.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package members
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const keyMemberCtx key = iota
|
||||
|
||||
type MembersHandler struct {
|
||||
members types.MemberManager
|
||||
}
|
||||
|
||||
func New(
|
||||
members types.MemberManager,
|
||||
) *MembersHandler {
|
||||
// Init
|
||||
|
||||
return &MembersHandler{
|
||||
members: members,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MembersHandler) Route(r types.Router) {
|
||||
r.Get("/", h.membersList)
|
||||
|
||||
r.With(auth.AdminsOnly).Group(func(r types.Router) {
|
||||
r.Post("/", h.membersCreate)
|
||||
r.With(h.ExtractMember).Route("/{memberId}", func(r types.Router) {
|
||||
r.Get("/", h.membersRead)
|
||||
r.Post("/", h.membersUpdateProfile)
|
||||
r.Post("/password", h.membersUpdatePassword)
|
||||
r.Delete("/", h.membersDelete)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MembersHandler) RouteBulk(r types.Router) {
|
||||
r.With(auth.AdminsOnly).Group(func(r types.Router) {
|
||||
r.Post("/update", h.membersBulkUpdate)
|
||||
r.Post("/delete", h.membersBulkDelete)
|
||||
})
|
||||
}
|
||||
|
||||
type MemberData struct {
|
||||
ID string
|
||||
Profile types.MemberProfile
|
||||
}
|
||||
|
||||
func SetMember(r *http.Request, session MemberData) context.Context {
|
||||
return context.WithValue(r.Context(), keyMemberCtx, session)
|
||||
}
|
||||
|
||||
func GetMember(r *http.Request) MemberData {
|
||||
return r.Context().Value(keyMemberCtx).(MemberData)
|
||||
}
|
||||
|
||||
func (h *MembersHandler) ExtractMember(w http.ResponseWriter, r *http.Request) (context.Context, error) {
|
||||
memberId := chi.URLParam(r, "memberId")
|
||||
|
||||
profile, err := h.members.Select(memberId)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrMemberDoesNotExist) {
|
||||
return nil, utils.HttpNotFound("member not found")
|
||||
}
|
||||
|
||||
return nil, utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return SetMember(r, MemberData{
|
||||
ID: memberId,
|
||||
Profile: profile,
|
||||
}), nil
|
||||
}
|
||||
70
server/internal/api/room/broadcast.go
Normal file
70
server/internal/api/room/broadcast.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/pkg/types/message"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type BroadcastStatusPayload struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (h *RoomHandler) broadcastStatus(w http.ResponseWriter, r *http.Request) error {
|
||||
broadcast := h.capture.Broadcast()
|
||||
|
||||
return utils.HttpSuccess(w, BroadcastStatusPayload{
|
||||
IsActive: broadcast.Started(),
|
||||
URL: broadcast.Url(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *RoomHandler) broadcastStart(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &BroadcastStatusPayload{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.URL == "" {
|
||||
return utils.HttpBadRequest("missing broadcast URL")
|
||||
}
|
||||
|
||||
broadcast := h.capture.Broadcast()
|
||||
if broadcast.Started() {
|
||||
return utils.HttpUnprocessableEntity("server is already broadcasting")
|
||||
}
|
||||
|
||||
if err := broadcast.Start(data.URL); err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
h.sessions.AdminBroadcast(
|
||||
event.BROADCAST_STATUS,
|
||||
message.BroadcastStatus{
|
||||
IsActive: broadcast.Started(),
|
||||
URL: broadcast.Url(),
|
||||
})
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) broadcastStop(w http.ResponseWriter, r *http.Request) error {
|
||||
broadcast := h.capture.Broadcast()
|
||||
if !broadcast.Started() {
|
||||
return utils.HttpUnprocessableEntity("server is not broadcasting")
|
||||
}
|
||||
|
||||
broadcast.Stop()
|
||||
|
||||
h.sessions.AdminBroadcast(
|
||||
event.BROADCAST_STATUS,
|
||||
message.BroadcastStatus{
|
||||
IsActive: broadcast.Started(),
|
||||
URL: broadcast.Url(),
|
||||
})
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
107
server/internal/api/room/clipboard.go
Normal file
107
server/internal/api/room/clipboard.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
// TODO: Unused now.
|
||||
//"bytes"
|
||||
//"strings"
|
||||
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type ClipboardPayload struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
}
|
||||
|
||||
func (h *RoomHandler) clipboardGetText(w http.ResponseWriter, r *http.Request) error {
|
||||
data, err := h.desktop.ClipboardGetText()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, ClipboardPayload{
|
||||
Text: data.Text,
|
||||
HTML: data.HTML,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *RoomHandler) clipboardSetText(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &ClipboardPayload{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := h.desktop.ClipboardSetText(types.ClipboardText{
|
||||
Text: data.Text,
|
||||
HTML: data.HTML,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) clipboardGetImage(w http.ResponseWriter, r *http.Request) error {
|
||||
bytes, err := h.desktop.ClipboardGetBinary("image/png")
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
|
||||
_, err = w.Write(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
/* TODO: Unused now.
|
||||
func (h *RoomHandler) clipboardSetImage(w http.ResponseWriter, r *http.Request) error {
|
||||
err := r.ParseMultipartForm(MAX_UPLOAD_SIZE)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("failed to parse multipart form").WithInternalErr(err)
|
||||
}
|
||||
|
||||
//nolint
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("no file received").WithInternalErr(err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
mime := header.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(mime, "image/") {
|
||||
return utils.HttpBadRequest("file must be image")
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
_, err = buffer.ReadFrom(file)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err).WithInternalMsg("unable to read from uploaded file")
|
||||
}
|
||||
|
||||
err = h.desktop.ClipboardSetBinary("image/png", buffer.Bytes())
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err).WithInternalMsg("unable set image to clipboard")
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) clipboardGetTargets(w http.ResponseWriter, r *http.Request) error {
|
||||
targets, err := h.desktop.ClipboardGetTargets()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, targets)
|
||||
}
|
||||
|
||||
*/
|
||||
109
server/internal/api/room/control.go
Normal file
109
server/internal/api/room/control.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/pkg/types/message"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type ControlStatusPayload struct {
|
||||
HasHost bool `json:"has_host"`
|
||||
HostId string `json:"host_id,omitempty"`
|
||||
}
|
||||
|
||||
type ControlTargetPayload struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlStatus(w http.ResponseWriter, r *http.Request) error {
|
||||
host, hasHost := h.sessions.GetHost()
|
||||
|
||||
var hostId string
|
||||
if hasHost {
|
||||
hostId = host.ID()
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, ControlStatusPayload{
|
||||
HasHost: hasHost,
|
||||
HostId: hostId,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
host, hasHost := h.sessions.GetHost()
|
||||
if hasHost {
|
||||
// TODO: Some throttling mechanism to prevent spamming.
|
||||
|
||||
// let host know that someone wants to take control
|
||||
host.Send(
|
||||
event.CONTROL_REQUEST,
|
||||
message.SessionID{
|
||||
ID: session.ID(),
|
||||
})
|
||||
|
||||
return utils.HttpError(http.StatusAccepted, "control request sent")
|
||||
}
|
||||
|
||||
if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin {
|
||||
return utils.HttpForbidden("controls are locked")
|
||||
}
|
||||
|
||||
session.SetAsHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlRelease(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
if !session.IsHost() {
|
||||
return utils.HttpUnprocessableEntity("session is not the host")
|
||||
}
|
||||
|
||||
h.desktop.ResetKeys()
|
||||
session.ClearHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlTake(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
session.SetAsHost()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlGive(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
target, ok := h.sessions.Get(sessionId)
|
||||
if !ok {
|
||||
return utils.HttpNotFound("target session was not found")
|
||||
}
|
||||
|
||||
if !target.Profile().CanHost {
|
||||
return utils.HttpBadRequest("target session is not allowed to host")
|
||||
}
|
||||
|
||||
target.SetAsHostBy(session)
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) controlReset(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
_, hasHost := h.sessions.GetHost()
|
||||
|
||||
if hasHost {
|
||||
h.desktop.ResetKeys()
|
||||
session.ClearHost()
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
126
server/internal/api/room/handler.go
Normal file
126
server/internal/api/room/handler.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type RoomHandler struct {
|
||||
sessions types.SessionManager
|
||||
desktop types.DesktopManager
|
||||
capture types.CaptureManager
|
||||
|
||||
privateModeImage []byte
|
||||
}
|
||||
|
||||
func New(
|
||||
sessions types.SessionManager,
|
||||
desktop types.DesktopManager,
|
||||
capture types.CaptureManager,
|
||||
) *RoomHandler {
|
||||
h := &RoomHandler{
|
||||
sessions: sessions,
|
||||
desktop: desktop,
|
||||
capture: capture,
|
||||
}
|
||||
|
||||
// generate fallback image for private mode when needed
|
||||
sessions.OnSettingsChanged(func(session types.Session, new, old types.Settings) {
|
||||
if old.PrivateMode && !new.PrivateMode {
|
||||
log.Debug().Msg("clearing private mode fallback image")
|
||||
h.privateModeImage = nil
|
||||
return
|
||||
}
|
||||
|
||||
if !old.PrivateMode && new.PrivateMode {
|
||||
img := h.desktop.GetScreenshotImage()
|
||||
bytes, err := utils.CreateJPGImage(img, 90)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("could not generate private mode fallback image")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msg("using private mode fallback image")
|
||||
h.privateModeImage = bytes
|
||||
}
|
||||
})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *RoomHandler) Route(r types.Router) {
|
||||
r.With(auth.AdminsOnly).Route("/settings", func(r types.Router) {
|
||||
r.Post("/", h.settingsSet)
|
||||
r.Get("/", h.settingsGet)
|
||||
})
|
||||
|
||||
r.With(auth.AdminsOnly).Route("/broadcast", func(r types.Router) {
|
||||
r.Get("/", h.broadcastStatus)
|
||||
r.Post("/start", h.broadcastStart)
|
||||
r.Post("/stop", h.broadcastStop)
|
||||
})
|
||||
|
||||
r.With(auth.CanAccessClipboardOnly).With(auth.HostsOnly).Route("/clipboard", func(r types.Router) {
|
||||
r.Get("/", h.clipboardGetText)
|
||||
r.Post("/", h.clipboardSetText)
|
||||
r.Get("/image.png", h.clipboardGetImage)
|
||||
|
||||
// TODO: Refactor. xclip is failing to set propper target type
|
||||
// and this content is sent back to client as text in another
|
||||
// clipboard update. Therefore endpoint is not usable!
|
||||
//r.Post("/image", h.clipboardSetImage)
|
||||
|
||||
// TODO: Refactor. If there would be implemented custom target
|
||||
// retrieval, this endpoint would be useful.
|
||||
//r.Get("/targets", h.clipboardGetTargets)
|
||||
})
|
||||
|
||||
r.With(auth.CanHostOnly).Route("/keyboard", func(r types.Router) {
|
||||
r.Get("/map", h.keyboardMapGet)
|
||||
r.With(auth.HostsOnly).Post("/map", h.keyboardMapSet)
|
||||
|
||||
r.Get("/modifiers", h.keyboardModifiersGet)
|
||||
r.With(auth.HostsOnly).Post("/modifiers", h.keyboardModifiersSet)
|
||||
})
|
||||
|
||||
r.With(auth.CanHostOnly).Route("/control", func(r types.Router) {
|
||||
r.Get("/", h.controlStatus)
|
||||
r.Post("/request", h.controlRequest)
|
||||
r.Post("/release", h.controlRelease)
|
||||
|
||||
r.With(auth.AdminsOnly).Post("/take", h.controlTake)
|
||||
r.With(auth.HostsOrAdminsOnly).Post("/give/{sessionId}", h.controlGive)
|
||||
r.With(auth.AdminsOnly).Post("/reset", h.controlReset)
|
||||
})
|
||||
|
||||
r.With(auth.CanWatchOnly).Route("/screen", func(r types.Router) {
|
||||
r.Get("/", h.screenConfiguration)
|
||||
r.With(auth.AdminsOnly).Post("/", h.screenConfigurationChange)
|
||||
r.With(auth.AdminsOnly).Get("/configurations", h.screenConfigurationsList)
|
||||
|
||||
r.Get("/cast.jpg", h.screenCastGet)
|
||||
r.With(auth.AdminsOnly).Get("/shot.jpg", h.screenShotGet)
|
||||
})
|
||||
|
||||
r.With(h.uploadMiddleware).Route("/upload", func(r types.Router) {
|
||||
r.Post("/drop", h.uploadDrop)
|
||||
r.Post("/dialog", h.uploadDialogPost)
|
||||
r.Delete("/dialog", h.uploadDialogClose)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (h *RoomHandler) uploadMiddleware(w http.ResponseWriter, r *http.Request) (context.Context, error) {
|
||||
session, ok := auth.GetSession(r)
|
||||
if !ok || (!session.IsHost() && (!session.Profile().CanHost || !h.sessions.Settings().ImplicitHosting)) {
|
||||
return nil, utils.HttpForbidden("without implicit hosting, only host can upload files")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
47
server/internal/api/room/keyboard.go
Normal file
47
server/internal/api/room/keyboard.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func (h *RoomHandler) keyboardMapSet(w http.ResponseWriter, r *http.Request) error {
|
||||
keyboardMap := types.KeyboardMap{}
|
||||
if err := utils.HttpJsonRequest(w, r, &keyboardMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := h.desktop.SetKeyboardMap(keyboardMap)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardMapGet(w http.ResponseWriter, r *http.Request) error {
|
||||
keyboardMap, err := h.desktop.GetKeyboardMap()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, keyboardMap)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardModifiersSet(w http.ResponseWriter, r *http.Request) error {
|
||||
keyboardModifiers := types.KeyboardModifiers{}
|
||||
if err := utils.HttpJsonRequest(w, r, &keyboardModifiers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.desktop.SetKeyboardModifiers(keyboardModifiers)
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) keyboardModifiersGet(w http.ResponseWriter, r *http.Request) error {
|
||||
keyboardModifiers := h.desktop.GetKeyboardModifiers()
|
||||
|
||||
return utils.HttpSuccess(w, keyboardModifiers)
|
||||
}
|
||||
101
server/internal/api/room/screen.go
Normal file
101
server/internal/api/room/screen.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/pkg/types/message"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func (h *RoomHandler) screenConfiguration(w http.ResponseWriter, r *http.Request) error {
|
||||
screenSize := h.desktop.GetScreenSize()
|
||||
|
||||
return utils.HttpSuccess(w, screenSize)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.Request) error {
|
||||
auth, _ := auth.GetSession(r)
|
||||
|
||||
data := &types.ScreenSize{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
size, err := h.desktop.SetScreenSize(types.ScreenSize{
|
||||
Width: data.Width,
|
||||
Height: data.Height,
|
||||
Rate: data.Rate,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return utils.HttpUnprocessableEntity("cannot set screen size").WithInternalErr(err)
|
||||
}
|
||||
|
||||
h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSizeUpdate{
|
||||
ID: auth.ID(),
|
||||
ScreenSize: size,
|
||||
})
|
||||
|
||||
return utils.HttpSuccess(w, data)
|
||||
}
|
||||
|
||||
// TODO: remove.
|
||||
func (h *RoomHandler) screenConfigurationsList(w http.ResponseWriter, r *http.Request) error {
|
||||
configurations := h.desktop.ScreenConfigurations()
|
||||
|
||||
return utils.HttpSuccess(w, configurations)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenShotGet(w http.ResponseWriter, r *http.Request) error {
|
||||
quality, err := strconv.Atoi(r.URL.Query().Get("quality"))
|
||||
if err != nil {
|
||||
quality = 90
|
||||
}
|
||||
|
||||
img := h.desktop.GetScreenshotImage()
|
||||
bytes, err := utils.CreateJPGImage(img, quality)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
_, err = w.Write(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *RoomHandler) screenCastGet(w http.ResponseWriter, r *http.Request) error {
|
||||
// display fallback image when private mode is enabled even if screencast is not
|
||||
if session, ok := auth.GetSession(r); ok && session.PrivateModeEnabled() {
|
||||
if h.privateModeImage != nil {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
_, err := w.Write(h.privateModeImage)
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.HttpBadRequest("private mode is enabled but no fallback image available")
|
||||
}
|
||||
|
||||
screencast := h.capture.Screencast()
|
||||
if !screencast.Enabled() {
|
||||
return utils.HttpBadRequest("screencast pipeline is not enabled")
|
||||
}
|
||||
|
||||
bytes, err := screencast.Image()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
_, err = w.Write(bytes)
|
||||
return err
|
||||
}
|
||||
38
server/internal/api/room/settings.go
Normal file
38
server/internal/api/room/settings.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func (h *RoomHandler) settingsGet(w http.ResponseWriter, r *http.Request) error {
|
||||
settings := h.sessions.Settings()
|
||||
return utils.HttpSuccess(w, settings)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) settingsSet(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
// We read the request body first and unmashal it inside the UpdateSettingsFunc
|
||||
// to ensure atomicity of the operation.
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to read request body").WithInternalErr(err)
|
||||
}
|
||||
|
||||
h.sessions.UpdateSettingsFunc(session, func(settings *types.Settings) bool {
|
||||
err = json.Unmarshal(body, settings)
|
||||
return err == nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("unable to parse provided data").WithInternalErr(err)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
172
server/internal/api/room/upload.go
Normal file
172
server/internal/api/room/upload.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
// TODO: Extract file uploading to custom utility.
|
||||
|
||||
// maximum upload size of 32 MB
|
||||
const maxUploadSize = 32 << 20
|
||||
|
||||
func (h *RoomHandler) uploadDrop(w http.ResponseWriter, r *http.Request) error {
|
||||
if !h.desktop.IsUploadDropEnabled() {
|
||||
return utils.HttpBadRequest("upload drop is disabled")
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(maxUploadSize)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("failed to parse multipart form").WithInternalErr(err)
|
||||
}
|
||||
|
||||
//nolint
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
|
||||
X, err := strconv.Atoi(r.FormValue("x"))
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("no X coordinate received").WithInternalErr(err)
|
||||
}
|
||||
|
||||
Y, err := strconv.Atoi(r.FormValue("y"))
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("no Y coordinate received").WithInternalErr(err)
|
||||
}
|
||||
|
||||
req_files := r.MultipartForm.File["files"]
|
||||
if len(req_files) == 0 {
|
||||
return utils.HttpBadRequest("no files received")
|
||||
}
|
||||
|
||||
dir, err := os.MkdirTemp("", "neko-drop-*")
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to create temporary directory")
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
for _, req_file := range req_files {
|
||||
path := path.Join(dir, req_file.Filename)
|
||||
|
||||
srcFile, err := req_file.Open()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to open uploaded file")
|
||||
}
|
||||
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to open destination file")
|
||||
}
|
||||
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to copy uploaded file to destination file")
|
||||
}
|
||||
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
if !h.desktop.DropFiles(X, Y, files) {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalMsg("unable to drop files")
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) uploadDialogPost(w http.ResponseWriter, r *http.Request) error {
|
||||
if !h.desktop.IsFileChooserDialogEnabled() {
|
||||
return utils.HttpBadRequest("file chooser dialog is disabled")
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(maxUploadSize)
|
||||
if err != nil {
|
||||
return utils.HttpBadRequest("failed to parse multipart form").WithInternalErr(err)
|
||||
}
|
||||
|
||||
//nolint
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
|
||||
req_files := r.MultipartForm.File["files"]
|
||||
if len(req_files) == 0 {
|
||||
return utils.HttpBadRequest("no files received")
|
||||
}
|
||||
|
||||
if !h.desktop.IsFileChooserDialogOpened() {
|
||||
return utils.HttpUnprocessableEntity("file chooser dialog is not open")
|
||||
}
|
||||
|
||||
dir, err := os.MkdirTemp("", "neko-dialog-*")
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to create temporary directory")
|
||||
}
|
||||
|
||||
for _, req_file := range req_files {
|
||||
path := path.Join(dir, req_file.Filename)
|
||||
|
||||
srcFile, err := req_file.Open()
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to open uploaded file")
|
||||
}
|
||||
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to open destination file")
|
||||
}
|
||||
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to copy uploaded file to destination file")
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.desktop.HandleFileChooserDialog(dir); err != nil {
|
||||
return utils.HttpInternalServerError().
|
||||
WithInternalErr(err).
|
||||
WithInternalMsg("unable to handle file chooser dialog")
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *RoomHandler) uploadDialogClose(w http.ResponseWriter, r *http.Request) error {
|
||||
if !h.desktop.IsFileChooserDialogEnabled() {
|
||||
return utils.HttpBadRequest("file chooser dialog is disabled")
|
||||
}
|
||||
|
||||
if !h.desktop.IsFileChooserDialogOpened() {
|
||||
return utils.HttpUnprocessableEntity("file chooser dialog is not open")
|
||||
}
|
||||
|
||||
h.desktop.CloseFileChooserDialog()
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
86
server/internal/api/router.go
Normal file
86
server/internal/api/router.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/internal/api/members"
|
||||
"m1k1o/neko/internal/api/room"
|
||||
"m1k1o/neko/internal/api/sessions"
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type ApiManagerCtx struct {
|
||||
sessions types.SessionManager
|
||||
members types.MemberManager
|
||||
desktop types.DesktopManager
|
||||
capture types.CaptureManager
|
||||
routers map[string]func(types.Router)
|
||||
}
|
||||
|
||||
func New(
|
||||
sessions types.SessionManager,
|
||||
members types.MemberManager,
|
||||
desktop types.DesktopManager,
|
||||
capture types.CaptureManager,
|
||||
) *ApiManagerCtx {
|
||||
|
||||
return &ApiManagerCtx{
|
||||
sessions: sessions,
|
||||
members: members,
|
||||
desktop: desktop,
|
||||
capture: capture,
|
||||
routers: make(map[string]func(types.Router)),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Route(r types.Router) {
|
||||
r.Post("/login", api.Login)
|
||||
|
||||
// Authenticated area
|
||||
r.Group(func(r types.Router) {
|
||||
r.Use(api.Authenticate)
|
||||
|
||||
r.Post("/logout", api.Logout)
|
||||
r.Get("/whoami", api.Whoami)
|
||||
r.Post("/profile", api.UpdateProfile)
|
||||
|
||||
sessionsHandler := sessions.New(api.sessions)
|
||||
r.Route("/sessions", sessionsHandler.Route)
|
||||
|
||||
membersHandler := members.New(api.members)
|
||||
r.Route("/members", membersHandler.Route)
|
||||
r.Route("/members_bulk", membersHandler.RouteBulk)
|
||||
|
||||
roomHandler := room.New(api.sessions, api.desktop, api.capture)
|
||||
r.Route("/room", roomHandler.Route)
|
||||
|
||||
for path, router := range api.routers {
|
||||
r.Route(path, router)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) {
|
||||
session, err := api.sessions.Authenticate(r)
|
||||
if err != nil {
|
||||
if api.sessions.CookieEnabled() {
|
||||
api.sessions.CookieClearToken(w, r)
|
||||
}
|
||||
|
||||
if errors.Is(err, types.ErrSessionLoginDisabled) {
|
||||
return nil, utils.HttpForbidden("login is disabled for this session")
|
||||
}
|
||||
|
||||
return nil, utils.HttpUnauthorized().WithInternalErr(err)
|
||||
}
|
||||
|
||||
return auth.SetSession(r, session), nil
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) AddRouter(path string, router func(types.Router)) {
|
||||
api.routers[path] = router
|
||||
}
|
||||
105
server/internal/api/session.go
Normal file
105
server/internal/api/session.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type SessionLoginPayload struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SessionDataPayload struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
State types.SessionState `json:"state"`
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Login(w http.ResponseWriter, r *http.Request) error {
|
||||
data := &SessionLoginPayload{}
|
||||
if err := utils.HttpJsonRequest(w, r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, token, err := api.members.Login(data.Username, data.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionAlreadyConnected) {
|
||||
return utils.HttpUnprocessableEntity("session already connected")
|
||||
} else if errors.Is(err, types.ErrMemberDoesNotExist) || errors.Is(err, types.ErrMemberInvalidPassword) {
|
||||
return utils.HttpUnauthorized().WithInternalErr(err)
|
||||
} else if errors.Is(err, types.ErrSessionLoginsLocked) {
|
||||
return utils.HttpForbidden("logins are locked").WithInternalErr(err)
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
sessionData := SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
}
|
||||
|
||||
if api.sessions.CookieEnabled() {
|
||||
api.sessions.CookieSetToken(w, token)
|
||||
} else {
|
||||
sessionData.Token = token
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, sessionData)
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
err := api.members.Logout(session.ID())
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session is not logged in")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
if api.sessions.CookieEnabled() {
|
||||
api.sessions.CookieClearToken(w, r)
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, true)
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
return utils.HttpSuccess(w, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
data := session.Profile()
|
||||
if err := utils.HttpJsonRequest(w, r, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := api.sessions.Update(session.ID(), data)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session does not exist")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, true)
|
||||
}
|
||||
81
server/internal/api/sessions/controller.go
Normal file
81
server/internal/api/sessions/controller.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type SessionDataPayload struct {
|
||||
ID string `json:"id"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
State types.SessionState `json:"state"`
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsList(w http.ResponseWriter, r *http.Request) error {
|
||||
sessions := []SessionDataPayload{}
|
||||
for _, session := range h.sessions.List() {
|
||||
sessions = append(sessions, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, sessions)
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsRead(w http.ResponseWriter, r *http.Request) error {
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
session, ok := h.sessions.Get(sessionId)
|
||||
if !ok {
|
||||
return utils.HttpNotFound("session not found")
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, SessionDataPayload{
|
||||
ID: session.ID(),
|
||||
Profile: session.Profile(),
|
||||
State: session.State(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsDelete(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := auth.GetSession(r)
|
||||
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
if sessionId == session.ID() {
|
||||
return utils.HttpBadRequest("cannot delete own session")
|
||||
}
|
||||
|
||||
err := h.sessions.Delete(sessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session not found")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) sessionsDisconnect(w http.ResponseWriter, r *http.Request) error {
|
||||
sessionId := chi.URLParam(r, "sessionId")
|
||||
|
||||
err := h.sessions.Disconnect(sessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrSessionNotFound) {
|
||||
return utils.HttpBadRequest("session not found")
|
||||
} else {
|
||||
return utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w)
|
||||
}
|
||||
30
server/internal/api/sessions/handler.go
Normal file
30
server/internal/api/sessions/handler.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
type SessionsHandler struct {
|
||||
sessions types.SessionManager
|
||||
}
|
||||
|
||||
func New(
|
||||
sessions types.SessionManager,
|
||||
) *SessionsHandler {
|
||||
// Init
|
||||
|
||||
return &SessionsHandler{
|
||||
sessions: sessions,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SessionsHandler) Route(r types.Router) {
|
||||
r.Get("/", h.sessionsList)
|
||||
|
||||
r.With(auth.AdminsOnly).Route("/{sessionId}", func(r types.Router) {
|
||||
r.Get("/", h.sessionsRead)
|
||||
r.Delete("/", h.sessionsDelete)
|
||||
r.Post("/disconnect", h.sessionsDisconnect)
|
||||
})
|
||||
}
|
||||
|
|
@ -3,26 +3,32 @@ package capture
|
|||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/pkg/gst"
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
type BroacastManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
|
||||
pipeline *gst.Pipeline
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func(url string) (string, error)
|
||||
|
||||
url string
|
||||
started bool
|
||||
|
||||
// metrics
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func broadcastNew(pipelineFn func(url string) (string, error), url string, started bool) *BroacastManagerCtx {
|
||||
func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string, autostart bool) *BroacastManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "broadcast").
|
||||
|
|
@ -31,8 +37,34 @@ func broadcastNew(pipelineFn func(url string) (string, error), url string, start
|
|||
return &BroacastManagerCtx{
|
||||
logger: logger,
|
||||
pipelineFn: pipelineFn,
|
||||
url: url,
|
||||
started: started && url != "",
|
||||
url: defaultUrl,
|
||||
started: defaultUrl != "" && autostart,
|
||||
|
||||
// metrics
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "broadcast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +118,6 @@ func (manager *BroacastManagerCtx) createPipeline() error {
|
|||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
var err error
|
||||
pipelineStr, err := manager.pipelineFn(manager.url)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -103,6 +134,8 @@ func (manager *BroacastManagerCtx) createPipeline() error {
|
|||
}
|
||||
|
||||
manager.pipeline.Play()
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -118,4 +151,6 @@ func (manager *BroacastManagerCtx) destroyPipeline() {
|
|||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.pipelinesActive.Set(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,48 +2,189 @@ package capture
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
type CaptureManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
desktop types.DesktopManager
|
||||
config *config.Capture
|
||||
|
||||
// sinks
|
||||
broadcast *BroacastManagerCtx
|
||||
audio *StreamSinkManagerCtx
|
||||
video *StreamSinkManagerCtx
|
||||
broadcast *BroacastManagerCtx
|
||||
screencast *ScreencastManagerCtx
|
||||
audio *StreamSinkManagerCtx
|
||||
video *StreamSelectorManagerCtx
|
||||
|
||||
// sources
|
||||
webcam *StreamSrcManagerCtx
|
||||
microphone *StreamSrcManagerCtx
|
||||
}
|
||||
|
||||
func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx {
|
||||
logger := log.With().Str("module", "capture").Logger()
|
||||
|
||||
videos := map[string]types.StreamSinkManager{}
|
||||
for video_id, cnf := range config.VideoPipelines {
|
||||
pipelineConf := cnf
|
||||
|
||||
createPipeline := func() (string, error) {
|
||||
if pipelineConf.GstPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1), nil
|
||||
}
|
||||
|
||||
screen := desktop.GetScreenSize()
|
||||
pipeline, err := pipelineConf.GetPipeline(screen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=false use-damage=false "+
|
||||
"%s ! appsink name=appsink", config.Display, pipeline,
|
||||
), nil
|
||||
}
|
||||
|
||||
// trigger function to catch evaluation errors at startup
|
||||
pipeline, err := createPipeline()
|
||||
if err != nil {
|
||||
logger.Panic().Err(err).
|
||||
Str("video_id", video_id).
|
||||
Msg("failed to create video pipeline")
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("video_id", video_id).
|
||||
Str("pipeline", pipeline).
|
||||
Msg("syntax check for video stream pipeline passed")
|
||||
|
||||
// append to videos
|
||||
videos[video_id] = streamSinkNew(config.VideoCodec, createPipeline, video_id)
|
||||
}
|
||||
|
||||
return &CaptureManagerCtx{
|
||||
logger: logger,
|
||||
desktop: desktop,
|
||||
config: config,
|
||||
|
||||
// sinks
|
||||
broadcast: broadcastNew(func(url string) (string, error) {
|
||||
return NewBroadcastPipeline(config.AudioDevice, config.Display, config.BroadcastPipeline, url)
|
||||
}, config.BroadcastUrl, config.BroadcastAutostart),
|
||||
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
|
||||
return NewAudioPipeline(config.AudioCodec, config.AudioDevice, config.AudioPipeline, config.AudioBitrate)
|
||||
}, "audio"),
|
||||
video: streamSinkNew(config.VideoCodec, func() (string, error) {
|
||||
// use screen fps as default
|
||||
fps := desktop.GetScreenSize().Rate
|
||||
// if max fps is set, cap it to that value
|
||||
if config.VideoMaxFPS > 0 && config.VideoMaxFPS < fps {
|
||||
fps = config.VideoMaxFPS
|
||||
if config.BroadcastPipeline != "" {
|
||||
var pipeline = config.BroadcastPipeline
|
||||
// replace {display} with valid display
|
||||
pipeline = strings.Replace(pipeline, "{display}", config.Display, 1)
|
||||
// replace {device} with valid device
|
||||
pipeline = strings.Replace(pipeline, "{device}", config.AudioDevice, 1)
|
||||
// replace {url} with valid URL
|
||||
return strings.Replace(pipeline, "{url}", url, 1), nil
|
||||
}
|
||||
return NewVideoPipeline(config.VideoCodec, config.Display, config.VideoPipeline, fps, config.VideoBitrate, config.VideoHWEnc)
|
||||
}, "video"),
|
||||
|
||||
return fmt.Sprintf(
|
||||
"flvmux name=mux ! rtmpsink location='%s live=1' "+
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! voaacenc bitrate=%d "+
|
||||
"! mux. "+
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! x264enc threads=4 bitrate=%d key-int-max=15 byte-stream=true tune=zerolatency speed-preset=%s "+
|
||||
"! mux.", url, config.AudioDevice, config.BroadcastAudioBitrate*1000, config.Display, config.BroadcastVideoBitrate, config.BroadcastPreset,
|
||||
), nil
|
||||
}, config.BroadcastUrl, config.BroadcastAutostart),
|
||||
screencast: screencastNew(config.ScreencastEnabled, func() string {
|
||||
if config.ScreencastPipeline != "" {
|
||||
// replace {display} with valid display
|
||||
return strings.Replace(config.ScreencastPipeline, "{display}", config.Display, 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"ximagesrc display-name=%s show-pointer=true use-damage=false "+
|
||||
"! video/x-raw,framerate=%s "+
|
||||
"! videoconvert "+
|
||||
"! queue "+
|
||||
"! jpegenc quality=%s "+
|
||||
"! appsink name=appsink", config.Display, config.ScreencastRate, config.ScreencastQuality,
|
||||
)
|
||||
}()),
|
||||
|
||||
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
|
||||
if config.AudioPipeline != "" {
|
||||
// replace {device} with valid device
|
||||
return strings.Replace(config.AudioPipeline, "{device}", config.AudioDevice, 1), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"pulsesrc device=%s "+
|
||||
"! audio/x-raw,channels=2 "+
|
||||
"! audioconvert "+
|
||||
"! queue "+
|
||||
"! %s "+
|
||||
"! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline,
|
||||
), nil
|
||||
}, "audio"),
|
||||
video: streamSelectorNew(config.VideoCodec, videos, config.VideoIDs),
|
||||
|
||||
// sources
|
||||
webcam: streamSrcNew(config.WebcamEnabled, map[string]string{
|
||||
codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) +
|
||||
"! rtpvp8depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.VP9().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtpvp9depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.H264().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp " +
|
||||
"! rtph264depay " +
|
||||
"! decodebin " +
|
||||
"! videoconvert " +
|
||||
"! videorate " +
|
||||
"! videoscale " +
|
||||
fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) +
|
||||
"! identity drop-allocation=true " +
|
||||
fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice),
|
||||
}, "webcam"),
|
||||
microphone: streamSrcNew(config.MicrophoneEnabled, map[string]string{
|
||||
codec.Opus().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=OPUS ", codec.Opus().PayloadType) +
|
||||
"! rtpopusdepay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
// TODO: Test this pipeline.
|
||||
codec.G722().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
|
||||
"! application/x-rtp clock-rate=8000 " +
|
||||
"! rtpg722depay " +
|
||||
"! decodebin " +
|
||||
fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice),
|
||||
}, "microphone"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,55 +195,51 @@ func (manager *CaptureManagerCtx) Start() {
|
|||
}
|
||||
}
|
||||
|
||||
go gst.RunMainLoop()
|
||||
go func() {
|
||||
for {
|
||||
before, ok := <-manager.desktop.GetScreenSizeChangeChannel()
|
||||
if !ok {
|
||||
manager.logger.Info().Msg("screen size change channel was closed")
|
||||
return
|
||||
}
|
||||
manager.desktop.OnBeforeScreenSizeChange(func() {
|
||||
manager.video.destroyPipelines()
|
||||
|
||||
if before {
|
||||
// before screen size change, we need to destroy all pipelines
|
||||
if manager.broadcast.Started() {
|
||||
manager.broadcast.destroyPipeline()
|
||||
}
|
||||
|
||||
if manager.video.Started() {
|
||||
manager.video.destroyPipeline()
|
||||
}
|
||||
if manager.screencast.Started() {
|
||||
manager.screencast.destroyPipeline()
|
||||
}
|
||||
})
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
manager.broadcast.destroyPipeline()
|
||||
}
|
||||
} else {
|
||||
// after screen size change, we need to recreate all pipelines
|
||||
manager.desktop.OnAfterScreenSizeChange(func() {
|
||||
err := manager.video.recreatePipelines()
|
||||
if err != nil {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate video pipelines")
|
||||
}
|
||||
|
||||
if manager.video.Started() {
|
||||
err := manager.video.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate video pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
if manager.broadcast.Started() {
|
||||
err := manager.broadcast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
|
||||
}
|
||||
}
|
||||
if manager.broadcast.Started() {
|
||||
err := manager.broadcast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if manager.screencast.Started() {
|
||||
err := manager.screencast.createPipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
manager.logger.Panic().Err(err).Msg("unable to recreate screencast pipeline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.broadcast.shutdown()
|
||||
manager.screencast.shutdown()
|
||||
|
||||
manager.audio.shutdown()
|
||||
manager.video.shutdown()
|
||||
|
||||
gst.QuitMainLoop()
|
||||
manager.webcam.shutdown()
|
||||
manager.microphone.shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -111,10 +248,22 @@ func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager {
|
|||
return manager.broadcast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Screencast() types.ScreencastManager {
|
||||
return manager.screencast
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager {
|
||||
return manager.audio
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Video() types.StreamSinkManager {
|
||||
func (manager *CaptureManagerCtx) Video() types.StreamSelectorManager {
|
||||
return manager.video
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Webcam() types.StreamSrcManager {
|
||||
return manager.webcam
|
||||
}
|
||||
|
||||
func (manager *CaptureManagerCtx) Microphone() types.StreamSrcManager {
|
||||
return manager.microphone
|
||||
}
|
||||
|
|
|
|||
257
server/internal/capture/screencast.go
Normal file
257
server/internal/capture/screencast.go
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/pkg/gst"
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
// timeout between intervals, when screencast pipeline is checked
|
||||
const screencastTimeout = 5 * time.Second
|
||||
|
||||
type ScreencastManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
pipeline gst.Pipeline
|
||||
pipelineStr string
|
||||
pipelineMu sync.Mutex
|
||||
|
||||
image types.Sample
|
||||
imageMu sync.Mutex
|
||||
tickerStop chan struct{}
|
||||
|
||||
enabled bool
|
||||
started bool
|
||||
expired int32
|
||||
|
||||
// metrics
|
||||
imagesCounter prometheus.Counter
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func screencastNew(enabled bool, pipelineStr string) *ScreencastManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "screencast").
|
||||
Logger()
|
||||
|
||||
manager := &ScreencastManagerCtx{
|
||||
logger: logger,
|
||||
pipelineStr: pipelineStr,
|
||||
tickerStop: make(chan struct{}),
|
||||
enabled: enabled,
|
||||
started: false,
|
||||
|
||||
// metrics
|
||||
imagesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "screencast_images_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created images.",
|
||||
}),
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "screencast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "screencast",
|
||||
"video_id": "main",
|
||||
"codec_name": "-",
|
||||
"codec_type": "-",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
manager.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer manager.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(screencastTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.tickerStop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if manager.Started() && !atomic.CompareAndSwapInt32(&manager.expired, 0, 1) {
|
||||
manager.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.destroyPipeline()
|
||||
|
||||
close(manager.tickerStop)
|
||||
manager.wg.Wait()
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) Enabled() bool {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
return manager.enabled
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) Started() bool {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
return manager.started
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) Image() ([]byte, error) {
|
||||
atomic.StoreInt32(&manager.expired, 0)
|
||||
|
||||
err := manager.start()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.imageMu.Lock()
|
||||
defer manager.imageMu.Unlock()
|
||||
|
||||
if manager.image.Data == nil {
|
||||
return nil, errors.New("image data not found")
|
||||
}
|
||||
|
||||
return manager.image.Data, nil
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) start() error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if !manager.enabled {
|
||||
return errors.New("screencast not enabled")
|
||||
}
|
||||
|
||||
err := manager.createPipeline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) stop() {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
manager.started = false
|
||||
manager.destroyPipeline()
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) createPipeline() error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline != nil {
|
||||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
manager.logger.Info().
|
||||
Str("str", manager.pipelineStr).
|
||||
Msgf("creating pipeline")
|
||||
|
||||
manager.pipeline, err = gst.CreatePipeline(manager.pipelineStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.pipeline.AttachAppsink("appsink")
|
||||
manager.pipeline.Play()
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
// get first image
|
||||
select {
|
||||
case image, ok := <-manager.pipeline.Sample():
|
||||
if !ok {
|
||||
return errors.New("unable to get first image")
|
||||
} else {
|
||||
manager.setImage(image)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
return errors.New("timeouted while waiting for first image")
|
||||
}
|
||||
|
||||
manager.wg.Add(1)
|
||||
pipeline := manager.pipeline
|
||||
|
||||
go func() {
|
||||
manager.logger.Debug().Msg("started receiving images")
|
||||
defer manager.wg.Done()
|
||||
|
||||
for {
|
||||
image, ok := <-pipeline.Sample()
|
||||
if !ok {
|
||||
manager.logger.Debug().Msg("stopped receiving images")
|
||||
return
|
||||
}
|
||||
|
||||
manager.setImage(image)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) setImage(image types.Sample) {
|
||||
manager.imageMu.Lock()
|
||||
manager.image = image
|
||||
manager.imageMu.Unlock()
|
||||
|
||||
manager.imagesCounter.Inc()
|
||||
}
|
||||
|
||||
func (manager *ScreencastManagerCtx) destroyPipeline() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline == nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.pipelinesActive.Set(0)
|
||||
}
|
||||
206
server/internal/capture/streamselector.go
Normal file
206
server/internal/capture/streamselector.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
type StreamSelectorManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
codec codec.RTPCodec
|
||||
streams map[string]types.StreamSinkManager
|
||||
streamIDs []string
|
||||
}
|
||||
|
||||
func streamSelectorNew(codec codec.RTPCodec, streams map[string]types.StreamSinkManager, streamIDs []string) *StreamSelectorManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "stream-selector").
|
||||
Logger()
|
||||
|
||||
return &StreamSelectorManagerCtx{
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
streams: streams,
|
||||
streamIDs: streamIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.destroyPipelines()
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) destroyPipelines() {
|
||||
for _, stream := range manager.streams {
|
||||
if stream.Started() {
|
||||
stream.DestroyPipeline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) recreatePipelines() error {
|
||||
for _, stream := range manager.streams {
|
||||
if stream.Started() {
|
||||
err := stream.CreatePipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) IDs() []string {
|
||||
return manager.streamIDs
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) Codec() codec.RTPCodec {
|
||||
return manager.codec
|
||||
}
|
||||
|
||||
func (manager *StreamSelectorManagerCtx) GetStream(selector types.StreamSelector) (types.StreamSinkManager, bool) {
|
||||
// select stream by ID
|
||||
if selector.ID != "" {
|
||||
// select lower stream
|
||||
if selector.Type == types.StreamSelectorTypeLower {
|
||||
var lastStream types.StreamSinkManager
|
||||
for i := len(manager.streamIDs) - 1; i >= 0; i-- {
|
||||
streamID := manager.streamIDs[i]
|
||||
if streamID == selector.ID {
|
||||
return lastStream, lastStream != nil
|
||||
}
|
||||
stream, ok := manager.streams[streamID]
|
||||
if ok {
|
||||
lastStream = stream
|
||||
}
|
||||
}
|
||||
// we couldn't find a lower stream
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// select higher stream
|
||||
if selector.Type == types.StreamSelectorTypeHigher {
|
||||
var lastStream types.StreamSinkManager
|
||||
for _, streamID := range manager.streamIDs {
|
||||
if streamID == selector.ID {
|
||||
return lastStream, lastStream != nil
|
||||
}
|
||||
stream, ok := manager.streams[streamID]
|
||||
if ok {
|
||||
lastStream = stream
|
||||
}
|
||||
}
|
||||
// we couldn't find a higher stream
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// select exact stream
|
||||
stream, ok := manager.streams[selector.ID]
|
||||
return stream, ok
|
||||
}
|
||||
|
||||
// select stream by bitrate
|
||||
if selector.Bitrate != 0 {
|
||||
// select stream by nearest bitrate
|
||||
if selector.Type == types.StreamSelectorTypeNearest {
|
||||
return manager.nearestBitrate(selector.Bitrate), true
|
||||
}
|
||||
|
||||
// select lower stream
|
||||
if selector.Type == types.StreamSelectorTypeLower {
|
||||
// start from the highest stream, and go down, until we find a lower stream
|
||||
for i := len(manager.streamIDs) - 1; i >= 0; i-- {
|
||||
streamID := manager.streamIDs[i]
|
||||
stream := manager.streams[streamID]
|
||||
// if stream should be considered in calculation
|
||||
considered := stream.Bitrate() != 0 && stream.Started()
|
||||
if considered && stream.Bitrate() < selector.Bitrate {
|
||||
return stream, true
|
||||
}
|
||||
}
|
||||
// we couldn't find a lower stream
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// select higher stream
|
||||
if selector.Type == types.StreamSelectorTypeHigher {
|
||||
// start from the lowest stream, and go up, until we find a higher stream
|
||||
for _, streamID := range manager.streamIDs {
|
||||
stream := manager.streams[streamID]
|
||||
// if stream should be considered in calculation
|
||||
considered := stream.Bitrate() != 0 && stream.Started()
|
||||
if considered && stream.Bitrate() > selector.Bitrate {
|
||||
return stream, true
|
||||
}
|
||||
}
|
||||
// we couldn't find a higher stream
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// select stream by exact bitrate
|
||||
for _, stream := range manager.streams {
|
||||
if stream.Bitrate() == selector.Bitrate {
|
||||
return stream, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we couldn't find a stream
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// TODO: This is a very naive implementation, we should use a binary search instead.
|
||||
func (manager *StreamSelectorManagerCtx) nearestBitrate(bitrate uint64) types.StreamSinkManager {
|
||||
type streamDiff struct {
|
||||
id string
|
||||
bitrateDiff int
|
||||
}
|
||||
|
||||
sortDiff := func(a, b int) bool {
|
||||
switch {
|
||||
case a < 0 && b < 0:
|
||||
return a > b
|
||||
case a >= 0:
|
||||
if b >= 0 {
|
||||
return a <= b
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var diffs []streamDiff
|
||||
|
||||
for _, stream := range manager.streams {
|
||||
// if stream should be considered in calculation
|
||||
considered := stream.Bitrate() != 0 && stream.Started()
|
||||
if !considered {
|
||||
continue
|
||||
}
|
||||
diffs = append(diffs, streamDiff{
|
||||
id: stream.ID(),
|
||||
bitrateDiff: int(bitrate) - int(stream.Bitrate()),
|
||||
})
|
||||
}
|
||||
|
||||
// no streams available
|
||||
if len(diffs) == 0 {
|
||||
// return first (lowest) stream
|
||||
return manager.streams[manager.streamIDs[0]]
|
||||
}
|
||||
|
||||
sort.Slice(diffs, func(i, j int) bool {
|
||||
return sortDiff(diffs[i].bitrateDiff, diffs[j].bitrateDiff)
|
||||
})
|
||||
|
||||
bestDiff := diffs[0]
|
||||
return manager.streams[bestDiff.id]
|
||||
}
|
||||
|
|
@ -2,41 +2,122 @@ package capture
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"m1k1o/neko/pkg/gst"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
var moveSinkListenerMu = sync.Mutex{}
|
||||
|
||||
type StreamSinkManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
sampleChannel chan types.Sample
|
||||
id string
|
||||
|
||||
// wait for a keyframe before sending samples
|
||||
waitForKf bool
|
||||
|
||||
bitrate uint64 // atomic
|
||||
brBuckets map[int]float64
|
||||
|
||||
logger zerolog.Logger
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
codec codec.RTPCodec
|
||||
pipeline *gst.Pipeline
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineFn func() (string, error)
|
||||
|
||||
listeners int
|
||||
listeners map[uintptr]types.SampleListener
|
||||
listenersKf map[uintptr]types.SampleListener // keyframe lobby
|
||||
listenersMu sync.Mutex
|
||||
|
||||
// metrics
|
||||
currentListeners prometheus.Gauge
|
||||
totalBytes prometheus.Counter
|
||||
pipelinesCounter prometheus.Counter
|
||||
pipelinesActive prometheus.Gauge
|
||||
}
|
||||
|
||||
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), video_id string) *StreamSinkManagerCtx {
|
||||
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), id string) *StreamSinkManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "stream-sink").
|
||||
Str("video_id", video_id).Logger()
|
||||
Str("id", id).Logger()
|
||||
|
||||
manager := &StreamSinkManagerCtx{
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
pipelineFn: pipelineFn,
|
||||
sampleChannel: make(chan types.Sample),
|
||||
id: id,
|
||||
|
||||
// only wait for keyframes if the codec is video
|
||||
waitForKf: codec.IsVideo(),
|
||||
|
||||
bitrate: 0,
|
||||
brBuckets: map[int]float64{},
|
||||
|
||||
logger: logger,
|
||||
codec: codec,
|
||||
pipelineFn: pipelineFn,
|
||||
|
||||
listeners: map[uintptr]types.SampleListener{},
|
||||
listenersKf: map[uintptr]types.SampleListener{},
|
||||
|
||||
// metrics
|
||||
currentListeners: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "streamsink_listeners",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Current number of listeners for a pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
totalBytes: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "streamsink_bytes",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of bytes created by the pipeline.",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsink",
|
||||
"video_id": id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
return manager
|
||||
|
|
@ -45,7 +126,25 @@ func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), vide
|
|||
func (manager *StreamSinkManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.destroyPipeline()
|
||||
manager.listenersMu.Lock()
|
||||
for key := range manager.listeners {
|
||||
delete(manager.listeners, key)
|
||||
}
|
||||
for key := range manager.listenersKf {
|
||||
delete(manager.listenersKf, key)
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.DestroyPipeline()
|
||||
manager.wg.Wait()
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) ID() string {
|
||||
return manager.id
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Bitrate() uint64 {
|
||||
return atomic.LoadUint64(&manager.bitrate)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
|
||||
|
|
@ -53,8 +152,8 @@ func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
|
|||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) start() error {
|
||||
if manager.listeners == 0 {
|
||||
err := manager.createPipeline()
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
err := manager.CreatePipeline()
|
||||
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
|
||||
return err
|
||||
}
|
||||
|
|
@ -66,45 +165,123 @@ func (manager *StreamSinkManagerCtx) start() error {
|
|||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) stop() {
|
||||
if manager.listeners == 0 {
|
||||
manager.destroyPipeline()
|
||||
if len(manager.listeners)+len(manager.listenersKf) == 0 {
|
||||
manager.DestroyPipeline()
|
||||
manager.logger.Info().Msgf("last listener, stopping")
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) addListener() {
|
||||
func (manager *StreamSinkManagerCtx) addListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
emitKeyframe := false
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
manager.listeners++
|
||||
if manager.waitForKf {
|
||||
// if this is the first listener, we need to emit a keyframe
|
||||
emitKeyframe = len(manager.listenersKf) == 0
|
||||
// if we're waiting for a keyframe, add it to the keyframe lobby
|
||||
manager.listenersKf[ptr] = listener
|
||||
} else {
|
||||
// otherwise, add it as a regular listener
|
||||
manager.listeners[ptr] = listener
|
||||
}
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("adding listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
|
||||
// if we will be waiting for a keyframe, emit one now
|
||||
if manager.pipeline != nil && emitKeyframe {
|
||||
manager.pipeline.EmitVideoKeyframe()
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) removeListener() {
|
||||
func (manager *StreamSinkManagerCtx) removeListener(listener types.SampleListener) {
|
||||
ptr := reflect.ValueOf(listener).Pointer()
|
||||
|
||||
manager.listenersMu.Lock()
|
||||
manager.listeners--
|
||||
delete(manager.listeners, ptr)
|
||||
delete(manager.listenersKf, ptr) // if it's a keyframe listener, remove it too
|
||||
manager.listenersMu.Unlock()
|
||||
|
||||
manager.logger.Debug().Interface("ptr", ptr).Msgf("removing listener")
|
||||
manager.currentListeners.Set(float64(manager.ListenersCount()))
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) AddListener() error {
|
||||
func (manager *StreamSinkManagerCtx) AddListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// start if stopped
|
||||
if err := manager.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add listener
|
||||
manager.addListener()
|
||||
manager.addListener(listener)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) RemoveListener() error {
|
||||
func (manager *StreamSinkManagerCtx) RemoveListener(listener types.SampleListener) error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
// remove listener
|
||||
manager.removeListener()
|
||||
manager.removeListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// moving listeners between streams ensures, that target pipeline is running
|
||||
// before listener is added, and stops source pipeline if there are 0 listeners
|
||||
func (manager *StreamSinkManagerCtx) MoveListenerTo(listener types.SampleListener, stream types.StreamSinkManager) error {
|
||||
if listener == nil {
|
||||
return errors.New("listener cannot be nil")
|
||||
}
|
||||
|
||||
targetStream, ok := stream.(*StreamSinkManagerCtx)
|
||||
if !ok {
|
||||
return errors.New("target stream manager does not support moving listeners")
|
||||
}
|
||||
|
||||
// we need to acquire both mutextes, from source stream and from target stream
|
||||
// in order to do that safely (without possibility of deadlock) we need third
|
||||
// global mutex, that ensures atomic locking
|
||||
|
||||
// lock global mutex
|
||||
moveSinkListenerMu.Lock()
|
||||
|
||||
// lock source stream
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
// lock target stream
|
||||
targetStream.mu.Lock()
|
||||
defer targetStream.mu.Unlock()
|
||||
|
||||
// unlock global mutex
|
||||
moveSinkListenerMu.Unlock()
|
||||
|
||||
// start if stopped
|
||||
if err := targetStream.start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap listeners
|
||||
manager.removeListener(listener)
|
||||
targetStream.addListener(listener)
|
||||
|
||||
// stop if started
|
||||
manager.stop()
|
||||
|
|
@ -116,14 +293,14 @@ func (manager *StreamSinkManagerCtx) ListenersCount() int {
|
|||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
return manager.listeners
|
||||
return len(manager.listeners) + len(manager.listenersKf)
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) Started() bool {
|
||||
return manager.ListenersCount() > 0
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) createPipeline() error {
|
||||
func (manager *StreamSinkManagerCtx) CreatePipeline() error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
|
|
@ -146,18 +323,79 @@ func (manager *StreamSinkManagerCtx) createPipeline() error {
|
|||
return err
|
||||
}
|
||||
|
||||
appsinkSubfix := "audio"
|
||||
if manager.codec.IsVideo() {
|
||||
appsinkSubfix = "video"
|
||||
}
|
||||
|
||||
manager.pipeline.AttachAppsink("appsink"+appsinkSubfix, manager.sampleChannel)
|
||||
manager.pipeline.AttachAppsink("appsink")
|
||||
manager.pipeline.Play()
|
||||
|
||||
manager.wg.Add(1)
|
||||
pipeline := manager.pipeline
|
||||
|
||||
go func() {
|
||||
manager.logger.Debug().Msg("started emitting samples")
|
||||
defer manager.wg.Done()
|
||||
|
||||
for {
|
||||
sample, ok := <-pipeline.Sample()
|
||||
if !ok {
|
||||
manager.logger.Debug().Msg("stopped emitting samples")
|
||||
return
|
||||
}
|
||||
|
||||
manager.onSample(sample)
|
||||
}
|
||||
}()
|
||||
|
||||
manager.pipelinesCounter.Inc()
|
||||
manager.pipelinesActive.Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) destroyPipeline() {
|
||||
func (manager *StreamSinkManagerCtx) saveSampleBitrate(timestamp time.Time, delta float64) {
|
||||
// get unix timestamp in seconds
|
||||
sec := timestamp.Unix()
|
||||
// last bucket is timestamp rounded to 3 seconds - 1 second
|
||||
last := int((sec - 1) % 3)
|
||||
// current bucket is timestamp rounded to 3 seconds
|
||||
curr := int(sec % 3)
|
||||
// next bucket is timestamp rounded to 3 seconds + 1 second
|
||||
next := int((sec + 1) % 3)
|
||||
|
||||
if manager.brBuckets[next] != 0 {
|
||||
// atomic update bitrate
|
||||
atomic.StoreUint64(&manager.bitrate, uint64(manager.brBuckets[last]))
|
||||
// empty next bucket
|
||||
manager.brBuckets[next] = 0
|
||||
}
|
||||
|
||||
// add rate to current bucket
|
||||
manager.brBuckets[curr] += delta
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) onSample(sample types.Sample) {
|
||||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
|
||||
// save to metrics
|
||||
length := float64(sample.Length)
|
||||
manager.totalBytes.Add(length)
|
||||
manager.saveSampleBitrate(sample.Timestamp, length)
|
||||
|
||||
// if is not delta unit -> it can be decoded independently -> it is a keyframe
|
||||
if manager.waitForKf && !sample.DeltaUnit && len(manager.listenersKf) > 0 {
|
||||
// if current sample is a keyframe, move listeners from
|
||||
// keyframe lobby to actual listeners map and clear lobby
|
||||
for k, v := range manager.listenersKf {
|
||||
manager.listeners[k] = v
|
||||
}
|
||||
manager.listenersKf = make(map[uintptr]types.SampleListener)
|
||||
}
|
||||
|
||||
for _, l := range manager.listeners {
|
||||
l.WriteSample(sample)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) DestroyPipeline() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
|
|
@ -168,8 +406,9 @@ func (manager *StreamSinkManagerCtx) destroyPipeline() {
|
|||
manager.pipeline.Destroy()
|
||||
manager.logger.Info().Msgf("destroying pipeline")
|
||||
manager.pipeline = nil
|
||||
}
|
||||
|
||||
func (manager *StreamSinkManagerCtx) GetSampleChannel() chan types.Sample {
|
||||
return manager.sampleChannel
|
||||
manager.pipelinesActive.Set(0)
|
||||
|
||||
manager.brBuckets = make(map[int]float64)
|
||||
atomic.StoreUint64(&manager.bitrate, 0)
|
||||
}
|
||||
|
|
|
|||
197
server/internal/capture/streamsrc.go
Normal file
197
server/internal/capture/streamsrc.go
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/pkg/gst"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
type StreamSrcManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
enabled bool
|
||||
codecPipeline map[string]string // codec -> pipeline
|
||||
|
||||
codec codec.RTPCodec
|
||||
pipeline gst.Pipeline
|
||||
pipelineMu sync.Mutex
|
||||
pipelineStr string
|
||||
|
||||
// metrics
|
||||
pushedData map[string]prometheus.Summary
|
||||
pipelinesCounter map[string]prometheus.Counter
|
||||
pipelinesActive map[string]prometheus.Gauge
|
||||
}
|
||||
|
||||
func streamSrcNew(enabled bool, codecPipeline map[string]string, video_id string) *StreamSrcManagerCtx {
|
||||
logger := log.With().
|
||||
Str("module", "capture").
|
||||
Str("submodule", "stream-src").
|
||||
Str("video_id", video_id).Logger()
|
||||
|
||||
pushedData := map[string]prometheus.Summary{}
|
||||
pipelinesCounter := map[string]prometheus.Counter{}
|
||||
pipelinesActive := map[string]prometheus.Gauge{}
|
||||
|
||||
for codecName, pipeline := range codecPipeline {
|
||||
codec, ok := codec.ParseStr(codecName)
|
||||
if !ok {
|
||||
logger.Fatal().
|
||||
Str("codec", codecName).
|
||||
Str("pipeline", pipeline).
|
||||
Msg("unknown codec name")
|
||||
}
|
||||
|
||||
pushedData[codecName] = promauto.NewSummary(prometheus.SummaryOpts{
|
||||
Name: "streamsrc_data_bytes",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Data pushed to a pipeline (in bytes).",
|
||||
ConstLabels: map[string]string{
|
||||
"video_id": video_id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
})
|
||||
pipelinesCounter[codecName] = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pipelines_total",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of created pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsrc",
|
||||
"video_id": video_id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
})
|
||||
pipelinesActive[codecName] = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "pipelines_active",
|
||||
Namespace: "neko",
|
||||
Subsystem: "capture",
|
||||
Help: "Total number of active pipelines.",
|
||||
ConstLabels: map[string]string{
|
||||
"submodule": "streamsrc",
|
||||
"video_id": video_id,
|
||||
"codec_name": codec.Name,
|
||||
"codec_type": codec.Type.String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &StreamSrcManagerCtx{
|
||||
logger: logger,
|
||||
enabled: enabled,
|
||||
codecPipeline: codecPipeline,
|
||||
|
||||
// metrics
|
||||
pushedData: pushedData,
|
||||
pipelinesCounter: pipelinesCounter,
|
||||
pipelinesActive: pipelinesActive,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) shutdown() {
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
manager.Stop()
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) Codec() codec.RTPCodec {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
return manager.codec
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) Start(codec codec.RTPCodec) error {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline != nil {
|
||||
return types.ErrCapturePipelineAlreadyExists
|
||||
}
|
||||
|
||||
if !manager.enabled {
|
||||
return errors.New("stream-src not enabled")
|
||||
}
|
||||
|
||||
found := false
|
||||
for codecName, pipeline := range manager.codecPipeline {
|
||||
if codecName == codec.Name {
|
||||
manager.pipelineStr = pipeline
|
||||
manager.codec = codec
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.New("no pipeline found for a codec")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
manager.logger.Info().
|
||||
Str("codec", manager.codec.Name).
|
||||
Str("src", manager.pipelineStr).
|
||||
Msgf("creating pipeline")
|
||||
|
||||
manager.pipeline, err = gst.CreatePipeline(manager.pipelineStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.pipeline.AttachAppsrc("appsrc")
|
||||
manager.pipeline.Play()
|
||||
|
||||
manager.pipelinesCounter[manager.codec.Name].Inc()
|
||||
manager.pipelinesActive[manager.codec.Name].Set(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) Stop() {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline == nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Destroy()
|
||||
manager.pipeline = nil
|
||||
|
||||
manager.logger.Info().
|
||||
Str("codec", manager.codec.Name).
|
||||
Str("src", manager.pipelineStr).
|
||||
Msgf("destroying pipeline")
|
||||
|
||||
manager.pipelinesActive[manager.codec.Name].Set(0)
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) Push(bytes []byte) {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
if manager.pipeline == nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.pipeline.Push(bytes)
|
||||
manager.pushedData[manager.codec.Name].Observe(float64(len(bytes)))
|
||||
}
|
||||
|
||||
func (manager *StreamSrcManagerCtx) Started() bool {
|
||||
manager.pipelineMu.Lock()
|
||||
defer manager.pipelineMu.Unlock()
|
||||
|
||||
return manager.pipeline != nil
|
||||
}
|
||||
|
|
@ -1,55 +1,197 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
// Legacy capture configuration
|
||||
type HwEnc int
|
||||
|
||||
// Legacy capture configuration
|
||||
const (
|
||||
HwEncNone HwEnc = iota
|
||||
HwEncUnset HwEnc = iota
|
||||
HwEncNone
|
||||
HwEncVAAPI
|
||||
HwEncNVENC
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
// video
|
||||
Display string
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoHWEnc HwEnc // TODO: Pipeline builder.
|
||||
VideoBitrate uint // TODO: Pipeline builder.
|
||||
VideoMaxFPS int16 // TODO: Pipeline builder.
|
||||
VideoPipeline string
|
||||
Display string
|
||||
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoIDs []string
|
||||
VideoPipelines map[string]types.VideoConfig
|
||||
|
||||
// audio
|
||||
AudioDevice string
|
||||
AudioCodec codec.RTPCodec
|
||||
AudioBitrate uint // TODO: Pipeline builder.
|
||||
AudioPipeline string
|
||||
|
||||
// broadcast
|
||||
BroadcastPipeline string
|
||||
BroadcastUrl string
|
||||
BroadcastAutostart bool
|
||||
BroadcastAudioBitrate int
|
||||
BroadcastVideoBitrate int
|
||||
BroadcastPreset string
|
||||
BroadcastPipeline string
|
||||
BroadcastUrl string
|
||||
BroadcastAutostart bool
|
||||
|
||||
ScreencastEnabled bool
|
||||
ScreencastRate string
|
||||
ScreencastQuality string
|
||||
ScreencastPipeline string
|
||||
|
||||
WebcamEnabled bool
|
||||
WebcamDevice string
|
||||
WebcamWidth int
|
||||
WebcamHeight int
|
||||
|
||||
MicrophoneEnabled bool
|
||||
MicrophoneDevice string
|
||||
}
|
||||
|
||||
func (Capture) Init(cmd *cobra.Command) error {
|
||||
//
|
||||
// video
|
||||
//
|
||||
// audio
|
||||
cmd.PersistentFlags().String("capture.audio.device", "audio_output.monitor", "pulseaudio device to capture")
|
||||
if err := viper.BindPFlag("capture.audio.device", cmd.PersistentFlags().Lookup("capture.audio.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("display", ":99.0", "XDisplay to capture")
|
||||
cmd.PersistentFlags().String("capture.audio.codec", "opus", "audio codec to be used")
|
||||
if err := viper.BindPFlag("capture.audio.codec", cmd.PersistentFlags().Lookup("capture.audio.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.audio.pipeline", "", "gstreamer pipeline used for audio streaming")
|
||||
if err := viper.BindPFlag("capture.audio.pipeline", cmd.PersistentFlags().Lookup("capture.audio.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// videos
|
||||
cmd.PersistentFlags().String("capture.video.display", "", "X display to capture")
|
||||
if err := viper.BindPFlag("capture.video.display", cmd.PersistentFlags().Lookup("capture.video.display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.video.codec", "vp8", "video codec to be used")
|
||||
if err := viper.BindPFlag("capture.video.codec", cmd.PersistentFlags().Lookup("capture.video.codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("capture.video.ids", []string{}, "ordered list of video ids")
|
||||
if err := viper.BindPFlag("capture.video.ids", cmd.PersistentFlags().Lookup("capture.video.ids")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming")
|
||||
if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// broadcast
|
||||
cmd.PersistentFlags().Int("capture.broadcast.audio_bitrate", 128, "broadcast audio bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.audio_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.broadcast.video_bitrate", 4096, "broadcast video bitrate in KB/s")
|
||||
if err := viper.BindPFlag("capture.broadcast.video_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.video_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.preset", "veryfast", "broadcast speed preset for h264 encoding")
|
||||
if err := viper.BindPFlag("capture.broadcast.preset", cmd.PersistentFlags().Lookup("capture.broadcast.preset")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.pipeline", "", "gstreamer pipeline used for broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.pipeline", cmd.PersistentFlags().Lookup("capture.broadcast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.broadcast.url", "", "initial URL for broadcasting, setting this value will automatically start broadcasting")
|
||||
if err := viper.BindPFlag("capture.broadcast.url", cmd.PersistentFlags().Lookup("capture.broadcast.url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("capture.broadcast.autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
if err := viper.BindPFlag("capture.broadcast.autostart", cmd.PersistentFlags().Lookup("capture.broadcast.autostart")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// screencast
|
||||
cmd.PersistentFlags().Bool("capture.screencast.enabled", false, "enable screencast")
|
||||
if err := viper.BindPFlag("capture.screencast.enabled", cmd.PersistentFlags().Lookup("capture.screencast.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.rate", "10/1", "screencast frame rate")
|
||||
if err := viper.BindPFlag("capture.screencast.rate", cmd.PersistentFlags().Lookup("capture.screencast.rate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.quality", "60", "screencast JPEG quality")
|
||||
if err := viper.BindPFlag("capture.screencast.quality", cmd.PersistentFlags().Lookup("capture.screencast.quality")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.screencast.pipeline", "", "gstreamer pipeline used for screencasting")
|
||||
if err := viper.BindPFlag("capture.screencast.pipeline", cmd.PersistentFlags().Lookup("capture.screencast.pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// webcam
|
||||
cmd.PersistentFlags().Bool("capture.webcam.enabled", false, "enable webcam stream")
|
||||
if err := viper.BindPFlag("capture.webcam.enabled", cmd.PersistentFlags().Lookup("capture.webcam.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sudo apt install v4l2loopback-dkms v4l2loopback-utils
|
||||
// sudo apt-get install linux-headers-`uname -r` linux-modules-extra-`uname -r`
|
||||
// sudo modprobe v4l2loopback exclusive_caps=1
|
||||
cmd.PersistentFlags().String("capture.webcam.device", "/dev/video0", "v4l2sink device used for webcam")
|
||||
if err := viper.BindPFlag("capture.webcam.device", cmd.PersistentFlags().Lookup("capture.webcam.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.width", 1280, "webcam stream width")
|
||||
if err := viper.BindPFlag("capture.webcam.width", cmd.PersistentFlags().Lookup("capture.webcam.width")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("capture.webcam.height", 720, "webcam stream height")
|
||||
if err := viper.BindPFlag("capture.webcam.height", cmd.PersistentFlags().Lookup("capture.webcam.height")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// microphone
|
||||
cmd.PersistentFlags().Bool("capture.microphone.enabled", true, "enable microphone stream")
|
||||
if err := viper.BindPFlag("capture.microphone.enabled", cmd.PersistentFlags().Lookup("capture.microphone.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("capture.microphone.device", "audio_input", "pulseaudio device used for microphone")
|
||||
if err := viper.BindPFlag("capture.microphone.device", cmd.PersistentFlags().Lookup("capture.microphone.device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Capture) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("display", "", "V2: XDisplay to capture")
|
||||
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("video_codec", "vp8", "video codec to be used")
|
||||
cmd.PersistentFlags().String("video_codec", "", "V2: video codec to be used")
|
||||
if err := viper.BindPFlag("video_codec", cmd.PersistentFlags().Lookup("video_codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -78,22 +220,22 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("hwenc", "", "use hardware accelerated encoding")
|
||||
cmd.PersistentFlags().String("hwenc", "", "V2: use hardware accelerated encoding")
|
||||
if err := viper.BindPFlag("hwenc", cmd.PersistentFlags().Lookup("hwenc")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("video_bitrate", 3072, "video bitrate in kbit/s")
|
||||
cmd.PersistentFlags().Int("video_bitrate", 0, "V2: video bitrate in kbit/s")
|
||||
if err := viper.BindPFlag("video_bitrate", cmd.PersistentFlags().Lookup("video_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("max_fps", 25, "maximum fps delivered via WebRTC, 0 is for no maximum")
|
||||
cmd.PersistentFlags().Int("max_fps", 0, "V2: maximum fps delivered via WebRTC, 0 is for no maximum")
|
||||
if err := viper.BindPFlag("max_fps", cmd.PersistentFlags().Lookup("max_fps")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("video", "", "video codec parameters to use for streaming")
|
||||
cmd.PersistentFlags().String("video", "", "V2: video codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -102,12 +244,12 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
// audio
|
||||
//
|
||||
|
||||
cmd.PersistentFlags().String("device", "audio_output.monitor", "audio device to capture")
|
||||
cmd.PersistentFlags().String("device", "", "V2: audio device to capture")
|
||||
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("audio_codec", "opus", "audio codec to be used")
|
||||
cmd.PersistentFlags().String("audio_codec", "", "V2: audio codec to be used")
|
||||
if err := viper.BindPFlag("audio_codec", cmd.PersistentFlags().Lookup("audio_codec")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -137,12 +279,12 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
// audio codecs
|
||||
|
||||
cmd.PersistentFlags().Int("audio_bitrate", 128, "audio bitrate in kbit/s")
|
||||
cmd.PersistentFlags().Int("audio_bitrate", 0, "V2: audio bitrate in kbit/s")
|
||||
if err := viper.BindPFlag("audio_bitrate", cmd.PersistentFlags().Lookup("audio_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("audio", "", "audio codec parameters to use for streaming")
|
||||
cmd.PersistentFlags().String("audio", "", "V2: audio codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -151,17 +293,17 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
// broadcast
|
||||
//
|
||||
|
||||
cmd.PersistentFlags().String("broadcast_pipeline", "", "custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced")
|
||||
cmd.PersistentFlags().String("broadcast_pipeline", "", "V2: custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced")
|
||||
if err := viper.BindPFlag("broadcast_pipeline", cmd.PersistentFlags().Lookup("broadcast_pipeline")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("broadcast_url", "", "a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI")
|
||||
cmd.PersistentFlags().String("broadcast_url", "", "V2: a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI")
|
||||
if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("broadcast_autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
cmd.PersistentFlags().Bool("broadcast_autostart", false, "V2: automatically start broadcasting when neko starts and broadcast_url is set")
|
||||
if err := viper.BindPFlag("broadcast_autostart", cmd.PersistentFlags().Lookup("broadcast_autostart")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -172,86 +314,220 @@ func (Capture) Init(cmd *cobra.Command) error {
|
|||
func (s *Capture) Set() {
|
||||
var ok bool
|
||||
|
||||
//
|
||||
s.Display = viper.GetString("capture.video.display")
|
||||
|
||||
// Display is provided by env variable unless explicitly set
|
||||
if s.Display == "" {
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
}
|
||||
|
||||
// video
|
||||
//
|
||||
|
||||
s.Display = viper.GetString("display")
|
||||
|
||||
videoCodec := viper.GetString("video_codec")
|
||||
videoCodec := viper.GetString("capture.video.codec")
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
|
||||
if !ok || !s.VideoCodec.IsVideo() {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
|
||||
s.VideoIDs = viper.GetStringSlice("capture.video.ids")
|
||||
if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.VideoPipelines),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse video pipelines")
|
||||
}
|
||||
|
||||
// default video
|
||||
if len(s.VideoPipelines) == 0 {
|
||||
log.Warn().Msgf("no video pipelines specified, using defaults")
|
||||
|
||||
s.VideoCodec = codec.VP8()
|
||||
s.VideoPipelines = map[string]types.VideoConfig{
|
||||
"main": {
|
||||
Fps: "25",
|
||||
GstEncoder: "vp8enc",
|
||||
GstParams: map[string]string{
|
||||
"target-bitrate": "round(3072 * 650)",
|
||||
"cpu-used": "4",
|
||||
"end-usage": "cbr",
|
||||
"threads": "4",
|
||||
"deadline": "1",
|
||||
"undershoot": "95",
|
||||
"buffer-size": "(3072 * 4)",
|
||||
"buffer-initial-size": "(3072 * 2)",
|
||||
"buffer-optimal-size": "(3072 * 3)",
|
||||
"keyframe-max-dist": "25",
|
||||
"min-quantizer": "4",
|
||||
"max-quantizer": "20",
|
||||
},
|
||||
},
|
||||
}
|
||||
s.VideoIDs = []string{"main"}
|
||||
}
|
||||
|
||||
// audio
|
||||
s.AudioDevice = viper.GetString("capture.audio.device")
|
||||
s.AudioPipeline = viper.GetString("capture.audio.pipeline")
|
||||
|
||||
audioCodec := viper.GetString("capture.audio.codec")
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || !s.AudioCodec.IsAudio() {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
|
||||
// broadcast
|
||||
s.BroadcastAudioBitrate = viper.GetInt("capture.broadcast.audio_bitrate")
|
||||
s.BroadcastVideoBitrate = viper.GetInt("capture.broadcast.video_bitrate")
|
||||
s.BroadcastPreset = viper.GetString("capture.broadcast.preset")
|
||||
s.BroadcastPipeline = viper.GetString("capture.broadcast.pipeline")
|
||||
s.BroadcastUrl = viper.GetString("capture.broadcast.url")
|
||||
s.BroadcastAutostart = viper.GetBool("capture.broadcast.autostart")
|
||||
|
||||
// screencast
|
||||
s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled")
|
||||
s.ScreencastRate = viper.GetString("capture.screencast.rate")
|
||||
s.ScreencastQuality = viper.GetString("capture.screencast.quality")
|
||||
s.ScreencastPipeline = viper.GetString("capture.screencast.pipeline")
|
||||
|
||||
// webcam
|
||||
s.WebcamEnabled = viper.GetBool("capture.webcam.enabled")
|
||||
s.WebcamDevice = viper.GetString("capture.webcam.device")
|
||||
s.WebcamWidth = viper.GetInt("capture.webcam.width")
|
||||
s.WebcamHeight = viper.GetInt("capture.webcam.height")
|
||||
|
||||
// microphone
|
||||
s.MicrophoneEnabled = viper.GetBool("capture.microphone.enabled")
|
||||
s.MicrophoneDevice = viper.GetString("capture.microphone.device")
|
||||
}
|
||||
|
||||
func (s *Capture) SetV2() {
|
||||
var ok bool
|
||||
|
||||
//
|
||||
// video
|
||||
//
|
||||
|
||||
if display := viper.GetString("display"); 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")
|
||||
}
|
||||
|
||||
if videoCodec := viper.GetString("video_codec"); videoCodec != "" {
|
||||
s.VideoCodec, ok = codec.ParseStr(videoCodec)
|
||||
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
|
||||
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
|
||||
s.VideoCodec = codec.VP8()
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead")
|
||||
}
|
||||
|
||||
if viper.GetBool("vp8") {
|
||||
s.VideoCodec = codec.VP8()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_VIDEO_CODEC=vp8' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead")
|
||||
} else if viper.GetBool("vp9") {
|
||||
s.VideoCodec = codec.VP9()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_VIDEO_CODEC=vp9' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead")
|
||||
} else if viper.GetBool("h264") {
|
||||
s.VideoCodec = codec.H264()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_VIDEO_CODEC=h264' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead")
|
||||
} else if viper.GetBool("av1") {
|
||||
s.VideoCodec = codec.AV1()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead")
|
||||
}
|
||||
|
||||
videoHWEnc := strings.ToLower(viper.GetString("hwenc"))
|
||||
switch videoHWEnc {
|
||||
case "":
|
||||
fallthrough
|
||||
case "none":
|
||||
s.VideoHWEnc = HwEncNone
|
||||
case "vaapi":
|
||||
s.VideoHWEnc = HwEncVAAPI
|
||||
case "nvenc":
|
||||
s.VideoHWEnc = HwEncNVENC
|
||||
default:
|
||||
log.Warn().Str("hwenc", videoHWEnc).Msgf("unknown video hw encoder, using CPU")
|
||||
videoHWEnc := HwEncUnset
|
||||
if hwenc := strings.ToLower(viper.GetString("hwenc")); hwenc != "" {
|
||||
switch hwenc {
|
||||
case "none":
|
||||
videoHWEnc = HwEncNone
|
||||
case "vaapi":
|
||||
videoHWEnc = HwEncVAAPI
|
||||
case "nvenc":
|
||||
videoHWEnc = HwEncNVENC
|
||||
default:
|
||||
log.Warn().Str("hwenc", hwenc).Msgf("unknown video hw encoder, using CPU")
|
||||
}
|
||||
}
|
||||
|
||||
s.VideoBitrate = viper.GetUint("video_bitrate")
|
||||
s.VideoMaxFPS = int16(viper.GetInt("max_fps"))
|
||||
s.VideoPipeline = viper.GetString("video")
|
||||
videoBitrate := viper.GetUint("video_bitrate")
|
||||
videoMaxFPS := int16(viper.GetInt("max_fps"))
|
||||
videoPipeline := viper.GetString("video")
|
||||
|
||||
// video pipeline
|
||||
if videoHWEnc != HwEncUnset || videoBitrate != 0 || videoMaxFPS != 0 || videoPipeline != "" {
|
||||
pipeline, err := NewVideoPipeline(s.VideoCodec, s.Display, videoPipeline, videoMaxFPS, videoBitrate, videoHWEnc)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("unable to create video pipeline, using default")
|
||||
} else {
|
||||
s.VideoPipelines = map[string]types.VideoConfig{
|
||||
"main": {
|
||||
GstPipeline: pipeline,
|
||||
},
|
||||
}
|
||||
// TODO: add deprecated warning and proper alternative
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// audio
|
||||
//
|
||||
|
||||
s.AudioDevice = viper.GetString("device")
|
||||
if audioDevice := viper.GetString("device"); audioDevice != "" {
|
||||
s.AudioDevice = audioDevice
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_DEVICE' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_DEVICE' instead")
|
||||
}
|
||||
|
||||
audioCodec := viper.GetString("audio_codec")
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
if audioCodec := viper.GetString("audio_codec"); audioCodec != "" {
|
||||
s.AudioCodec, ok = codec.ParseStr(audioCodec)
|
||||
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
|
||||
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
|
||||
s.AudioCodec = codec.Opus()
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead")
|
||||
}
|
||||
|
||||
if viper.GetBool("opus") {
|
||||
s.AudioCodec = codec.Opus()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_VIDEO_CODEC=opus' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead")
|
||||
} else if viper.GetBool("g722") {
|
||||
s.AudioCodec = codec.G722()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_VIDEO_CODEC=g722' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead")
|
||||
} else if viper.GetBool("pcmu") {
|
||||
s.AudioCodec = codec.PCMU()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_VIDEO_CODEC=pcmu' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead")
|
||||
} else if viper.GetBool("pcma") {
|
||||
s.AudioCodec = codec.PCMA()
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_VIDEO_CODEC=pcma' instead")
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead")
|
||||
}
|
||||
|
||||
s.AudioBitrate = viper.GetUint("audio_bitrate")
|
||||
s.AudioPipeline = viper.GetString("audio")
|
||||
audioBitrate := viper.GetUint("audio_bitrate")
|
||||
audioPipeline := viper.GetString("audio")
|
||||
|
||||
// audio pipeline
|
||||
if 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")
|
||||
} else {
|
||||
s.AudioPipeline = pipeline
|
||||
}
|
||||
// TODO: add deprecated warning and proper alternative
|
||||
}
|
||||
|
||||
//
|
||||
// broadcast
|
||||
//
|
||||
|
||||
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
|
||||
s.BroadcastUrl = viper.GetString("broadcast_url")
|
||||
s.BroadcastAutostart = viper.GetBool("broadcast_autostart")
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package capture
|
||||
// Legacy pipeline configuration for gstreamer.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"m1k1o/neko/pkg/gst"
|
||||
"m1k1o/neko/pkg/types/codec"
|
||||
)
|
||||
|
||||
/*
|
||||
|
|
@ -34,7 +34,7 @@ const (
|
|||
audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! "
|
||||
)
|
||||
|
||||
func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) (string, error) {
|
||||
func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) string {
|
||||
video := fmt.Sprintf(videoSrc, display, 25)
|
||||
audio := fmt.Sprintf(audioSrc, device)
|
||||
|
||||
|
|
@ -50,10 +50,10 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url
|
|||
pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", url, audio, video)
|
||||
}
|
||||
|
||||
return pipelineStr, nil
|
||||
return pipelineStr
|
||||
}
|
||||
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc config.HwEnc) (string, error) {
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc HwEnc) (string, error) {
|
||||
pipelineStr := " ! appsink name=appsinkvideo"
|
||||
|
||||
// if using custom pipeline
|
||||
|
|
@ -69,7 +69,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
|||
|
||||
switch rtpCodec.Name {
|
||||
case codec.VP8().Name:
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if hwenc == HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -144,13 +144,13 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
|||
vbvbuf = bitrate
|
||||
}
|
||||
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if hwenc == HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate)
|
||||
} else if hwenc == config.HwEncNVENC {
|
||||
} else if hwenc == HwEncNVENC {
|
||||
if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -5,20 +5,67 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
type Desktop struct {
|
||||
Display string
|
||||
|
||||
ScreenWidth int
|
||||
ScreenHeight int
|
||||
ScreenRate int16
|
||||
ScreenSize types.ScreenSize
|
||||
|
||||
UseInputDriver bool
|
||||
InputSocket string
|
||||
|
||||
Unminimize bool
|
||||
UploadDrop bool
|
||||
FileChooserDialog bool
|
||||
}
|
||||
|
||||
func (Desktop) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("screen", "1280x720@30", "default screen resolution and framerate")
|
||||
cmd.PersistentFlags().String("desktop.display", "", "X display to use for desktop sharing")
|
||||
if err := viper.BindPFlag("desktop.display", cmd.PersistentFlags().Lookup("desktop.display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("desktop.screen", "1280x720@30", "default screen size and framerate")
|
||||
if err := viper.BindPFlag("desktop.screen", cmd.PersistentFlags().Lookup("desktop.screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen")
|
||||
if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection")
|
||||
if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized")
|
||||
if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.upload_drop", true, "whether drop upload is enabled")
|
||||
if err := viper.BindPFlag("desktop.upload_drop", cmd.PersistentFlags().Lookup("desktop.upload_drop")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("desktop.file_chooser_dialog", false, "whether to handle file chooser dialog externally")
|
||||
if err := viper.BindPFlag("desktop.file_chooser_dialog", cmd.PersistentFlags().Lookup("desktop.file_chooser_dialog")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Desktop) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("screen", "", "V2: default screen resolution and framerate")
|
||||
if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -27,15 +74,21 @@ func (Desktop) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Desktop) Set() {
|
||||
// Display is provided by env variable
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
s.Display = viper.GetString("desktop.display")
|
||||
|
||||
s.ScreenWidth = 1280
|
||||
s.ScreenHeight = 720
|
||||
s.ScreenRate = 30
|
||||
// Display is provided by env variable unless explicitly set
|
||||
if s.Display == "" {
|
||||
s.Display = os.Getenv("DISPLAY")
|
||||
}
|
||||
|
||||
s.ScreenSize = types.ScreenSize{
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
Rate: 30,
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
res := r.FindStringSubmatch(viper.GetString("screen"))
|
||||
res := r.FindStringSubmatch(viper.GetString("desktop.screen"))
|
||||
|
||||
if len(res) > 0 {
|
||||
width, err1 := strconv.ParseInt(res[1], 10, 64)
|
||||
|
|
@ -43,9 +96,35 @@ func (s *Desktop) Set() {
|
|||
rate, err3 := strconv.ParseInt(res[3], 10, 64)
|
||||
|
||||
if err1 == nil && err2 == nil && err3 == nil {
|
||||
s.ScreenWidth = int(width)
|
||||
s.ScreenHeight = int(height)
|
||||
s.ScreenRate = int16(rate)
|
||||
s.ScreenSize.Width = int(width)
|
||||
s.ScreenSize.Height = int(height)
|
||||
s.ScreenSize.Rate = int16(rate)
|
||||
}
|
||||
}
|
||||
|
||||
s.UseInputDriver = viper.GetBool("desktop.input.enabled")
|
||||
s.InputSocket = viper.GetString("desktop.input.socket")
|
||||
s.Unminimize = viper.GetBool("desktop.unminimize")
|
||||
s.UploadDrop = viper.GetBool("desktop.upload_drop")
|
||||
s.FileChooserDialog = viper.GetBool("desktop.file_chooser_dialog")
|
||||
}
|
||||
|
||||
func (s *Desktop) SetV2() {
|
||||
if viper.IsSet("screen") {
|
||||
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
|
||||
res := r.FindStringSubmatch(viper.GetString("screen"))
|
||||
|
||||
if len(res) > 0 {
|
||||
width, err1 := strconv.ParseInt(res[1], 10, 64)
|
||||
height, err2 := strconv.ParseInt(res[2], 10, 64)
|
||||
rate, err3 := strconv.ParseInt(res[3], 10, 64)
|
||||
|
||||
if err1 == nil && err2 == nil && err3 == nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
159
server/internal/config/member.go
Normal file
159
server/internal/config/member.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko/internal/member/file"
|
||||
"m1k1o/neko/internal/member/multiuser"
|
||||
"m1k1o/neko/internal/member/object"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
Provider string
|
||||
|
||||
// providers
|
||||
File file.Config
|
||||
Object object.Config
|
||||
Multiuser multiuser.Config
|
||||
}
|
||||
|
||||
func (Member) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("member.provider", "multiuser", "choose member provider")
|
||||
if err := viper.BindPFlag("member.provider", cmd.PersistentFlags().Lookup("member.provider")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// file provider
|
||||
cmd.PersistentFlags().String("member.file.path", "", "member file provider: storage path")
|
||||
if err := viper.BindPFlag("member.file.path", cmd.PersistentFlags().Lookup("member.file.path")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("member.file.hash", true, "member file provider: whether to hash passwords using sha256 (recommended)")
|
||||
if err := viper.BindPFlag("member.file.hash", cmd.PersistentFlags().Lookup("member.file.hash")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// object provider
|
||||
cmd.PersistentFlags().String("member.object.users", "[]", "member object provider: users in JSON format")
|
||||
if err := viper.BindPFlag("member.object.users", cmd.PersistentFlags().Lookup("member.object.users")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// multiuser provider
|
||||
cmd.PersistentFlags().String("member.multiuser.user_password", "neko", "member multiuser provider: user password")
|
||||
if err := viper.BindPFlag("member.multiuser.user_password", cmd.PersistentFlags().Lookup("member.multiuser.user_password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("member.multiuser.admin_password", "admin", "member multiuser provider: admin password")
|
||||
if err := viper.BindPFlag("member.multiuser.admin_password", cmd.PersistentFlags().Lookup("member.multiuser.admin_password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("member.multiuser.user_profile", "{}", "member multiuser provider: user profile in JSON format")
|
||||
if err := viper.BindPFlag("member.multiuser.user_profile", cmd.PersistentFlags().Lookup("member.multiuser.user_profile")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("member.multiuser.admin_profile", "{}", "member multiuser provider: admin profile in JSON format")
|
||||
if err := viper.BindPFlag("member.multiuser.admin_profile", cmd.PersistentFlags().Lookup("member.multiuser.admin_profile")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Member) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("password", "", "V2: password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("password_admin", "", "V2: admin password for connecting to stream")
|
||||
if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Member) Set() {
|
||||
s.Provider = viper.GetString("member.provider")
|
||||
|
||||
// file provider
|
||||
s.File.Path = viper.GetString("member.file.path")
|
||||
s.File.Hash = viper.GetBool("member.file.hash")
|
||||
|
||||
// object provider
|
||||
if err := viper.UnmarshalKey("member.object.users", &s.Object.Users, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.Object.Users),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse member object users")
|
||||
}
|
||||
|
||||
// multiuser provider
|
||||
s.Multiuser.UserPassword = viper.GetString("member.multiuser.user_password")
|
||||
s.Multiuser.AdminPassword = viper.GetString("member.multiuser.admin_password")
|
||||
|
||||
// default user profile
|
||||
s.Multiuser.UserProfile = types.MemberProfile{
|
||||
IsAdmin: false,
|
||||
CanLogin: true,
|
||||
CanConnect: true,
|
||||
CanWatch: true,
|
||||
CanHost: true,
|
||||
CanShareMedia: true,
|
||||
CanAccessClipboard: true,
|
||||
SendsInactiveCursor: true,
|
||||
CanSeeInactiveCursors: false,
|
||||
}
|
||||
|
||||
// override user profile
|
||||
if err := viper.UnmarshalKey("member.multiuser.user_profile", &s.Multiuser.UserProfile, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.Multiuser.UserProfile),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse member multiuser user profile")
|
||||
}
|
||||
|
||||
// default admin profile
|
||||
s.Multiuser.AdminProfile = types.MemberProfile{
|
||||
IsAdmin: true,
|
||||
CanLogin: true,
|
||||
CanConnect: true,
|
||||
CanWatch: true,
|
||||
CanHost: true,
|
||||
CanShareMedia: true,
|
||||
CanAccessClipboard: true,
|
||||
SendsInactiveCursor: true,
|
||||
CanSeeInactiveCursors: true,
|
||||
}
|
||||
|
||||
// override admin profile
|
||||
if err := viper.UnmarshalKey("member.multiuser.admin_profile", &s.Multiuser.AdminProfile, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode(s.Multiuser.AdminProfile),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse member multiuser admin profile")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Member) SetV2() {
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
37
server/internal/config/plugins.go
Normal file
37
server/internal/config/plugins.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Plugins struct {
|
||||
Enabled bool
|
||||
Dir string
|
||||
Required bool
|
||||
}
|
||||
|
||||
func (Plugins) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().Bool("plugins.enabled", false, "load plugins in runtime")
|
||||
if err := viper.BindPFlag("plugins.enabled", cmd.PersistentFlags().Lookup("plugins.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("plugins.dir", "./bin/plugins", "path to neko plugins to load")
|
||||
if err := viper.BindPFlag("plugins.dir", cmd.PersistentFlags().Lookup("plugins.dir")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("plugins.required", false, "if true, neko will exit if there is an error when loading a plugin")
|
||||
if err := viper.BindPFlag("plugins.required", cmd.PersistentFlags().Lookup("plugins.required")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Plugins) Set() {
|
||||
s.Enabled = viper.GetBool("plugins.enabled")
|
||||
s.Dir = viper.GetString("plugins.dir")
|
||||
s.Required = viper.GetBool("plugins.required")
|
||||
}
|
||||
|
|
@ -1,29 +1,69 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Root struct {
|
||||
Debug bool
|
||||
Logs bool
|
||||
CfgFile string
|
||||
Config string
|
||||
|
||||
LogLevel zerolog.Level
|
||||
LogTime string
|
||||
LogJson bool
|
||||
LogNocolor bool
|
||||
LogDir string
|
||||
}
|
||||
|
||||
func (Root) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringP("config", "c", "", "configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// just a shortcut
|
||||
cmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode")
|
||||
if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolP("logs", "l", false, "save logs to file")
|
||||
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("config", "", "configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
cmd.PersistentFlags().String("log.time", "unix", "time format used in logs (unix, unixms, unixmicro)")
|
||||
if err := viper.BindPFlag("log.time", cmd.PersistentFlags().Lookup("log.time")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.json", false, "logs in JSON format")
|
||||
if err := viper.BindPFlag("log.json", cmd.PersistentFlags().Lookup("log.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("log.nocolor", false, "no ANSI colors in non-JSON output")
|
||||
if err := viper.BindPFlag("log.nocolor", cmd.PersistentFlags().Lookup("log.nocolor")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("log.dir", "", "logging directory to store logs")
|
||||
if err := viper.BindPFlag("log.dir", cmd.PersistentFlags().Lookup("log.dir")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Root) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().BoolP("logs", "l", false, "V2: save logs to file")
|
||||
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +71,53 @@ func (Root) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Root) Set() {
|
||||
s.Logs = viper.GetBool("logs")
|
||||
s.Debug = viper.GetBool("debug")
|
||||
s.CfgFile = viper.GetString("config")
|
||||
s.Config = viper.GetString("config")
|
||||
|
||||
logLevel := viper.GetString("log.level")
|
||||
level, err := zerolog.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("unknown log level %s", logLevel)
|
||||
} else {
|
||||
s.LogLevel = level
|
||||
}
|
||||
|
||||
logTime := viper.GetString("log.time")
|
||||
switch logTime {
|
||||
case "unix":
|
||||
s.LogTime = zerolog.TimeFormatUnix
|
||||
case "unixms":
|
||||
s.LogTime = zerolog.TimeFormatUnixMs
|
||||
case "unixmicro":
|
||||
s.LogTime = zerolog.TimeFormatUnixMicro
|
||||
default:
|
||||
log.Warn().Msgf("unknown log time %s", logTime)
|
||||
}
|
||||
|
||||
s.LogJson = viper.GetBool("log.json")
|
||||
s.LogNocolor = viper.GetBool("log.nocolor")
|
||||
s.LogDir = viper.GetString("log.dir")
|
||||
|
||||
if viper.GetBool("debug") && s.LogLevel != zerolog.TraceLevel {
|
||||
s.LogLevel = zerolog.DebugLevel
|
||||
}
|
||||
|
||||
// support for NO_COLOR env variable: https://no-color.org/
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
s.LogNocolor = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Root) SetV2() {
|
||||
if viper.IsSet("logs") {
|
||||
if viper.GetBool("logs") {
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko/internal/utils"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
|
@ -17,41 +17,92 @@ type Server struct {
|
|||
Proxy bool
|
||||
Static string
|
||||
PathPrefix string
|
||||
PProf bool
|
||||
Metrics bool
|
||||
CORS []string
|
||||
}
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "address/port/socket to serve neko")
|
||||
cmd.PersistentFlags().String("server.bind", "127.0.0.1:8080", "address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("server.bind", cmd.PersistentFlags().Lookup("server.bind")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.cert", "", "path to the SSL cert used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.cert", cmd.PersistentFlags().Lookup("server.cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.key", "", "path to the SSL key used to secure the neko server")
|
||||
if err := viper.BindPFlag("server.key", cmd.PersistentFlags().Lookup("server.key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.proxy", false, "trust reverse proxy headers")
|
||||
if err := viper.BindPFlag("server.proxy", cmd.PersistentFlags().Lookup("server.proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.static", "", "path to neko client files to serve")
|
||||
if err := viper.BindPFlag("server.static", cmd.PersistentFlags().Lookup("server.static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("server.path_prefix", "/", "path prefix for HTTP requests")
|
||||
if err := viper.BindPFlag("server.path_prefix", cmd.PersistentFlags().Lookup("server.path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.pprof", false, "enable pprof endpoint available at /debug/pprof")
|
||||
if err := viper.BindPFlag("server.pprof", cmd.PersistentFlags().Lookup("server.pprof")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("server.metrics", true, "enable prometheus metrics available at /metrics")
|
||||
if err := viper.BindPFlag("server.metrics", cmd.PersistentFlags().Lookup("server.metrics")); err != nil {
|
||||
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")
|
||||
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Server) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("bind", "", "V2: address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("cert", "", "path to the SSL cert used to secure the neko server")
|
||||
cmd.PersistentFlags().String("cert", "", "V2: path to the SSL cert used to secure the neko server")
|
||||
if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("key", "", "path to the SSL key used to secure the neko server")
|
||||
cmd.PersistentFlags().String("key", "", "V2: path to the SSL key used to secure the neko server")
|
||||
if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode")
|
||||
cmd.PersistentFlags().Bool("proxy", false, "V2: enable reverse proxy mode")
|
||||
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("static", "./www", "path to neko client files to serve")
|
||||
cmd.PersistentFlags().String("static", "", "V2: path to neko client files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("path_prefix", "/", "path prefix for HTTP requests")
|
||||
cmd.PersistentFlags().String("path_prefix", "", "V2: path prefix for HTTP requests")
|
||||
if err := viper.BindPFlag("path_prefix", cmd.PersistentFlags().Lookup("path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("cors", []string{"*"}, "list of allowed origins for CORS")
|
||||
cmd.PersistentFlags().StringSlice("cors", []string{}, "V2: list of allowed origins for CORS")
|
||||
if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -60,21 +111,68 @@ func (Server) Init(cmd *cobra.Command) error {
|
|||
}
|
||||
|
||||
func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
s.Static = viper.GetString("static")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
s.Cert = viper.GetString("server.cert")
|
||||
s.Key = viper.GetString("server.key")
|
||||
s.Bind = viper.GetString("server.bind")
|
||||
s.Proxy = viper.GetBool("server.proxy")
|
||||
s.Static = viper.GetString("server.static")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("server.path_prefix")))
|
||||
s.PProf = viper.GetBool("server.pprof")
|
||||
s.Metrics = viper.GetBool("server.metrics")
|
||||
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
s.CORS = viper.GetStringSlice("server.cors")
|
||||
in, _ := utils.ArrayIn("*", s.CORS)
|
||||
if len(s.CORS) == 0 || in {
|
||||
s.CORS = []string{"*"}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(r *http.Request, origin string) bool {
|
||||
func (s *Server) SetV2() {
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("cors") {
|
||||
s.CORS = viper.GetStringSlice("cors")
|
||||
in, _ := utils.ArrayIn("*", s.CORS)
|
||||
if len(s.CORS) == 0 || in {
|
||||
s.CORS = []string{"*"}
|
||||
}
|
||||
log.Warn().Msg("you are using v2 configuration 'NEKO_CORS' which is deprecated, please use 'NEKO_SERVER_CORS' instead")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HasCors() bool {
|
||||
return len(s.CORS) > 0
|
||||
}
|
||||
|
||||
func (s *Server) AllowOrigin(origin string) bool {
|
||||
// if CORS is disabled, allow all origins
|
||||
if len(s.CORS) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// if CORS is enabled, allow only origins in the list
|
||||
in, _ := utils.ArrayIn(origin, s.CORS)
|
||||
return in || s.CORS[0] == "*"
|
||||
}
|
||||
|
|
|
|||
159
server/internal/config/session.go
Normal file
159
server/internal/config/session.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
File string
|
||||
|
||||
PrivateMode bool
|
||||
LockedLogins bool
|
||||
LockedControls bool
|
||||
ControlProtection bool
|
||||
ImplicitHosting bool
|
||||
InactiveCursors bool
|
||||
MercifulReconnect bool
|
||||
APIToken string
|
||||
|
||||
CookieEnabled bool
|
||||
CookieName string
|
||||
CookieExpiration time.Duration
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
func (Session) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("session.file", "", "if sessions should be stored in a file, otherwise they will be stored only in memory")
|
||||
if err := viper.BindPFlag("session.file", cmd.PersistentFlags().Lookup("session.file")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.private_mode", false, "whether private mode should be enabled initially")
|
||||
if err := viper.BindPFlag("session.private_mode", cmd.PersistentFlags().Lookup("session.private_mode")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.locked_logins", false, "whether logins should be locked for users initially")
|
||||
if err := viper.BindPFlag("session.locked_logins", cmd.PersistentFlags().Lookup("session.locked_logins")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.locked_controls", false, "whether controls should be locked for users initially")
|
||||
if err := viper.BindPFlag("session.locked_controls", cmd.PersistentFlags().Lookup("session.locked_controls")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.control_protection", false, "users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("session.control_protection", cmd.PersistentFlags().Lookup("session.control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.implicit_hosting", true, "allow implicit control switching")
|
||||
if err := viper.BindPFlag("session.implicit_hosting", cmd.PersistentFlags().Lookup("session.implicit_hosting")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.inactive_cursors", false, "show inactive cursors on the screen")
|
||||
if err := viper.BindPFlag("session.inactive_cursors", cmd.PersistentFlags().Lookup("session.inactive_cursors")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.merciful_reconnect", true, "allow reconnecting to websocket even if previous connection was not closed")
|
||||
if err := viper.BindPFlag("session.merciful_reconnect", cmd.PersistentFlags().Lookup("session.merciful_reconnect")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("session.api_token", "", "API token for interacting with external services")
|
||||
if err := viper.BindPFlag("session.api_token", cmd.PersistentFlags().Lookup("session.api_token")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cookie
|
||||
cmd.PersistentFlags().Bool("session.cookie.enabled", true, "whether cookies authentication should be enabled")
|
||||
if err := viper.BindPFlag("session.cookie.enabled", cmd.PersistentFlags().Lookup("session.cookie.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("session.cookie.name", "NEKO_SESSION", "name of the cookie that holds token")
|
||||
if err := viper.BindPFlag("session.cookie.name", cmd.PersistentFlags().Lookup("session.cookie.name")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("session.cookie.expiration", 365*24, "expiration of the cookie in hours")
|
||||
if err := viper.BindPFlag("session.cookie.expiration", cmd.PersistentFlags().Lookup("session.cookie.expiration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("session.cookie.secure", true, "use secure cookies")
|
||||
if err := viper.BindPFlag("session.cookie.secure", cmd.PersistentFlags().Lookup("session.cookie.secure")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Session) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringSlice("locks", []string{}, "V2: resources, that will be locked when starting (control, login)")
|
||||
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("control_protection", false, "V2: control protection means, users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("implicit_control", false, "V2: if enabled members can gain control implicitly")
|
||||
if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Set() {
|
||||
s.File = viper.GetString("session.file")
|
||||
|
||||
s.PrivateMode = viper.GetBool("session.private_mode")
|
||||
s.LockedLogins = viper.GetBool("session.locked_logins")
|
||||
s.LockedControls = viper.GetBool("session.locked_controls")
|
||||
s.ControlProtection = viper.GetBool("session.control_protection")
|
||||
s.ImplicitHosting = viper.GetBool("session.implicit_hosting")
|
||||
s.InactiveCursors = viper.GetBool("session.inactive_cursors")
|
||||
s.MercifulReconnect = viper.GetBool("session.merciful_reconnect")
|
||||
s.APIToken = viper.GetString("session.api_token")
|
||||
|
||||
s.CookieEnabled = viper.GetBool("session.cookie.enabled")
|
||||
s.CookieName = viper.GetString("session.cookie.name")
|
||||
s.CookieExpiration = time.Duration(viper.GetInt("session.cookie.expiration")) * time.Hour
|
||||
s.CookieSecure = viper.GetBool("session.cookie.secure")
|
||||
}
|
||||
|
||||
func (s *Session) SetV2() {
|
||||
if viper.IsSet("locks") {
|
||||
locks := viper.GetStringSlice("locks")
|
||||
for _, lock := range locks {
|
||||
switch lock {
|
||||
// TODO: file_transfer
|
||||
case "control":
|
||||
s.LockedControls = true
|
||||
case "login":
|
||||
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")
|
||||
}
|
||||
|
||||
if viper.IsSet("implicit_control") {
|
||||
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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,131 +4,396 @@ import (
|
|||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICEServers []webrtc.ICEServer
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
NAT1To1IPs []string
|
||||
TCPMUX int
|
||||
UDPMUX int
|
||||
// default stun server
|
||||
const defStunSrv = "stun:stun.l.google.com:19302"
|
||||
|
||||
ImplicitControl bool
|
||||
type WebRTCEstimator struct {
|
||||
Enabled bool
|
||||
Passive bool
|
||||
Debug bool
|
||||
InitialBitrate int
|
||||
|
||||
// how often to read and process bandwidth estimation reports
|
||||
ReadInterval time.Duration
|
||||
// how long to wait for stable connection (only neutral or upward trend) before upgrading
|
||||
StableDuration time.Duration
|
||||
// how long to wait for unstable connection (downward trend) before downgrading
|
||||
UnstableDuration time.Duration
|
||||
// how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading
|
||||
StalledDuration time.Duration
|
||||
// how long to wait before downgrading again after previous downgrade
|
||||
DowngradeBackoff time.Duration
|
||||
// how long to wait before upgrading again after previous upgrade
|
||||
UpgradeBackoff time.Duration
|
||||
// how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade
|
||||
DiffThreshold float64
|
||||
}
|
||||
|
||||
type WebRTC struct {
|
||||
ICELite bool
|
||||
ICETrickle bool
|
||||
ICEServersFrontend []types.ICEServer
|
||||
ICEServersBackend []types.ICEServer
|
||||
EphemeralMin uint16
|
||||
EphemeralMax uint16
|
||||
TCPMux int
|
||||
UDPMux int
|
||||
|
||||
NAT1To1IPs []string
|
||||
IpRetrievalUrl string
|
||||
|
||||
Estimator WebRTCEstimator
|
||||
}
|
||||
|
||||
func (WebRTC) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("epr", "59000-59100", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
cmd.PersistentFlags().Bool("webrtc.icelite", false, "configures whether or not the ICE agent should be a lite agent")
|
||||
if err := viper.BindPFlag("webrtc.icelite", cmd.PersistentFlags().Lookup("webrtc.icelite")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.icetrickle", true, "configures whether cadidates should be sent asynchronously using Trickle ICE")
|
||||
if err := viper.BindPFlag("webrtc.icetrickle", cmd.PersistentFlags().Lookup("webrtc.icetrickle")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Looks like this is conflicting with the frontend and backend ICE servers since latest versions
|
||||
//cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
//if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys")
|
||||
if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("webrtc.epr", cmd.PersistentFlags().Lookup("webrtc.epr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.tcpmux", 0, "single TCP mux port for all peers")
|
||||
if err := viper.BindPFlag("webrtc.tcpmux", cmd.PersistentFlags().Lookup("webrtc.tcpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.udpmux", 0, "single UDP mux port for all peers, replaces EPR")
|
||||
if err := viper.BindPFlag("webrtc.udpmux", cmd.PersistentFlags().Lookup("webrtc.udpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("webrtc.nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used")
|
||||
if err := viper.BindPFlag("webrtc.nat1to1", cmd.PersistentFlags().Lookup("webrtc.nat1to1")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("webrtc.ip_retrieval_url", "https://checkip.amazonaws.com", "URL address used for retrieval of the external IP address")
|
||||
if err := viper.BindPFlag("webrtc.ip_retrieval_url", cmd.PersistentFlags().Lookup("webrtc.ip_retrieval_url")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// bandwidth estimator
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.enabled", false, "enables the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.enabled", cmd.PersistentFlags().Lookup("webrtc.estimator.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.passive", false, "passive estimator mode, when it does not switch pipelines, only estimates")
|
||||
if err := viper.BindPFlag("webrtc.estimator.passive", cmd.PersistentFlags().Lookup("webrtc.estimator.passive")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("webrtc.estimator.debug", false, "enables debug logging for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.debug", cmd.PersistentFlags().Lookup("webrtc.estimator.debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("webrtc.estimator.initial_bitrate", 1_000_000, "initial bitrate for the bandwidth estimator")
|
||||
if err := viper.BindPFlag("webrtc.estimator.initial_bitrate", cmd.PersistentFlags().Lookup("webrtc.estimator.initial_bitrate")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.read_interval", 2*time.Second, "how often to read and process bandwidth estimation reports")
|
||||
if err := viper.BindPFlag("webrtc.estimator.read_interval", cmd.PersistentFlags().Lookup("webrtc.estimator.read_interval")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stable_duration", 12*time.Second, "how long to wait for stable connection (upward or neutral trend) before upgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.unstable_duration", 6*time.Second, "how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.unstable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.unstable_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.stalled_duration", 24*time.Second, "how long to wait for stalled bandwidth estimation before downgrading")
|
||||
if err := viper.BindPFlag("webrtc.estimator.stalled_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stalled_duration")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.downgrade_backoff", 10*time.Second, "how long to wait before downgrading again after previous downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.downgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.downgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("webrtc.estimator.upgrade_backoff", 5*time.Second, "how long to wait before upgrading again after previous upgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.upgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.upgrade_backoff")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Float64("webrtc.estimator.diff_threshold", 0.15, "how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade")
|
||||
if err := viper.BindPFlag("webrtc.estimator.diff_threshold", cmd.PersistentFlags().Lookup("webrtc.estimator.diff_threshold")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (WebRTC) InitV2(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("epr", "", "V2: limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("epr", cmd.PersistentFlags().Lookup("epr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used")
|
||||
cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "V2: sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used")
|
||||
if err := viper.BindPFlag("nat1to1", cmd.PersistentFlags().Lookup("nat1to1")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("tcpmux", 0, "single TCP mux port for all peers")
|
||||
cmd.PersistentFlags().Int("tcpmux", 0, "V2: single TCP mux port for all peers")
|
||||
if err := viper.BindPFlag("tcpmux", cmd.PersistentFlags().Lookup("tcpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Int("udpmux", 0, "single UDP mux port for all peers")
|
||||
cmd.PersistentFlags().Int("udpmux", 0, "V2: single UDP mux port for all peers")
|
||||
if err := viper.BindPFlag("udpmux", cmd.PersistentFlags().Lookup("udpmux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("ipfetch", "http://checkip.amazonaws.com", "automatically fetch IP address from given URL when nat1to1 is not present")
|
||||
cmd.PersistentFlags().String("ipfetch", "", "V2: automatically fetch IP address from given URL when nat1to1 is not present")
|
||||
if err := viper.BindPFlag("ipfetch", cmd.PersistentFlags().Lookup("ipfetch")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("icelite", false, "configures whether or not the ice agent should be a lite agent")
|
||||
cmd.PersistentFlags().Bool("icelite", false, "V2: configures whether or not the ice agent should be a lite agent")
|
||||
if err := viper.BindPFlag("icelite", cmd.PersistentFlags().Lookup("icelite")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("iceserver", []string{"stun:stun.l.google.com:19302"}, "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
cmd.PersistentFlags().StringSlice("iceserver", []string{}, "V2: describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
if err := viper.BindPFlag("iceserver", cmd.PersistentFlags().Lookup("iceserver")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("iceservers", "", "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
cmd.PersistentFlags().String("iceservers", "", "V2: describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer")
|
||||
if err := viper.BindPFlag("iceservers", cmd.PersistentFlags().Lookup("iceservers")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Should be moved to session config.
|
||||
cmd.PersistentFlags().Bool("implicit_control", false, "if enabled members can gain control implicitly")
|
||||
if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebRTC) Set() {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
s.TCPMUX = viper.GetInt("tcpmux")
|
||||
s.UDPMUX = viper.GetInt("udpmux")
|
||||
s.ICELite = viper.GetBool("icelite")
|
||||
s.ICEServers = []webrtc.ICEServer{}
|
||||
s.ICELite = viper.GetBool("webrtc.icelite")
|
||||
s.ICETrickle = viper.GetBool("webrtc.icetrickle")
|
||||
|
||||
iceServersJson := viper.GetString("iceservers")
|
||||
if iceServersJson != "" {
|
||||
err := json.Unmarshal([]byte(iceServersJson), &s.ICEServers)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to process iceservers")
|
||||
// parse frontend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse frontend ICE servers")
|
||||
}
|
||||
|
||||
// parse backend ice servers
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse backend ICE servers")
|
||||
}
|
||||
|
||||
if s.ICELite && len(s.ICEServersBackend) > 0 {
|
||||
log.Warn().Msgf("ICE Lite is enabled, but backend ICE servers are configured. Backend ICE servers will be ignored.")
|
||||
}
|
||||
|
||||
// if no frontend or backend ice servers are configured
|
||||
if len(s.ICEServersFrontend) == 0 && len(s.ICEServersBackend) == 0 {
|
||||
// parse global ice servers
|
||||
var iceServers []types.ICEServer
|
||||
if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook(
|
||||
utils.JsonStringAutoDecode([]types.ICEServer{}),
|
||||
)); err != nil {
|
||||
log.Warn().Err(err).Msgf("unable to parse global ICE servers")
|
||||
}
|
||||
|
||||
// add default stun server if none are configured
|
||||
if len(iceServers) == 0 {
|
||||
iceServers = append(iceServers, types.ICEServer{
|
||||
URLs: []string{defStunSrv},
|
||||
})
|
||||
}
|
||||
|
||||
s.ICEServersFrontend = append(s.ICEServersFrontend, iceServers...)
|
||||
s.ICEServersBackend = append(s.ICEServersBackend, iceServers...)
|
||||
}
|
||||
|
||||
s.TCPMux = viper.GetInt("webrtc.tcpmux")
|
||||
s.UDPMux = viper.GetInt("webrtc.udpmux")
|
||||
|
||||
epr := viper.GetString("webrtc.epr")
|
||||
if epr != "" {
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
min, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral min port")
|
||||
}
|
||||
|
||||
max, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msgf("unable to parse ephemeral max port")
|
||||
}
|
||||
|
||||
s.EphemeralMin = uint16(min)
|
||||
s.EphemeralMax = uint16(max)
|
||||
}
|
||||
|
||||
if s.EphemeralMin > s.EphemeralMax {
|
||||
log.Panic().Msgf("ephemeral min port cannot be bigger than max")
|
||||
}
|
||||
}
|
||||
|
||||
iceServerSlice := viper.GetStringSlice("iceserver")
|
||||
if len(iceServerSlice) > 0 {
|
||||
s.ICEServers = append(s.ICEServers, webrtc.ICEServer{URLs: iceServerSlice})
|
||||
if epr == "" && s.TCPMux == 0 && s.UDPMux == 0 {
|
||||
// using default epr range
|
||||
s.EphemeralMin = 59000
|
||||
s.EphemeralMax = 59100
|
||||
|
||||
log.Warn().
|
||||
Uint16("min", s.EphemeralMin).
|
||||
Uint16("max", s.EphemeralMax).
|
||||
Msgf("no TCP, UDP mux or epr specified, using default epr range")
|
||||
}
|
||||
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
ipfetch := viper.GetString("ipfetch")
|
||||
ip, err := utils.GetIP(ipfetch)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address")
|
||||
}
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
}
|
||||
|
||||
min := uint16(59000)
|
||||
max := uint16(59100)
|
||||
epr := viper.GetString("epr")
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
start, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
s.NAT1To1IPs = viper.GetStringSlice("webrtc.nat1to1")
|
||||
s.IpRetrievalUrl = viper.GetString("webrtc.ip_retrieval_url")
|
||||
if s.IpRetrievalUrl != "" && len(s.NAT1To1IPs) == 0 {
|
||||
ip, err := utils.HttpRequestGET(s.IpRetrievalUrl)
|
||||
if err == nil {
|
||||
min = uint16(start)
|
||||
}
|
||||
|
||||
end, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err == nil {
|
||||
max = uint16(end)
|
||||
s.NAT1To1IPs = append(s.NAT1To1IPs, ip)
|
||||
} else {
|
||||
log.Warn().Err(err).Msgf("IP retrieval failed")
|
||||
}
|
||||
}
|
||||
|
||||
if min > max {
|
||||
s.EphemeralMin = max
|
||||
s.EphemeralMax = min
|
||||
} else {
|
||||
s.EphemeralMin = min
|
||||
s.EphemeralMax = max
|
||||
}
|
||||
// bandwidth estimator
|
||||
|
||||
// TODO: Should be moved to session config.
|
||||
s.ImplicitControl = viper.GetBool("implicit_control")
|
||||
s.Estimator.Enabled = viper.GetBool("webrtc.estimator.enabled")
|
||||
s.Estimator.Passive = viper.GetBool("webrtc.estimator.passive")
|
||||
s.Estimator.Debug = viper.GetBool("webrtc.estimator.debug")
|
||||
s.Estimator.InitialBitrate = viper.GetInt("webrtc.estimator.initial_bitrate")
|
||||
s.Estimator.ReadInterval = viper.GetDuration("webrtc.estimator.read_interval")
|
||||
s.Estimator.StableDuration = viper.GetDuration("webrtc.estimator.stable_duration")
|
||||
s.Estimator.UnstableDuration = viper.GetDuration("webrtc.estimator.unstable_duration")
|
||||
s.Estimator.StalledDuration = viper.GetDuration("webrtc.estimator.stalled_duration")
|
||||
s.Estimator.DowngradeBackoff = viper.GetDuration("webrtc.estimator.downgrade_backoff")
|
||||
s.Estimator.UpgradeBackoff = viper.GetDuration("webrtc.estimator.upgrade_backoff")
|
||||
s.Estimator.DiffThreshold = viper.GetFloat64("webrtc.estimator.diff_threshold")
|
||||
}
|
||||
|
||||
func (s *WebRTC) SetV2() {
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
if viper.IsSet("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")
|
||||
}
|
||||
|
||||
if viper.IsSet("iceservers") {
|
||||
iceServers := []types.ICEServer{}
|
||||
iceServersJson := viper.GetString("iceservers")
|
||||
if iceServersJson != "" {
|
||||
err := json.Unmarshal([]byte(iceServersJson), &iceServers)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to process iceservers")
|
||||
}
|
||||
}
|
||||
s.ICEServersFrontend = 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")
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
if viper.IsSet("ipfetch") {
|
||||
if len(s.NAT1To1IPs) == 0 {
|
||||
ipfetch := viper.GetString("ipfetch")
|
||||
ip, err := utils.HttpRequestGET(ipfetch)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address")
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
if viper.IsSet("epr") {
|
||||
min := uint16(59000)
|
||||
max := uint16(59100)
|
||||
epr := viper.GetString("epr")
|
||||
ports := strings.SplitN(epr, "-", -1)
|
||||
if len(ports) > 1 {
|
||||
start, err := strconv.ParseUint(ports[0], 10, 16)
|
||||
if err == nil {
|
||||
min = uint16(start)
|
||||
}
|
||||
|
||||
end, err := strconv.ParseUint(ports[1], 10, 16)
|
||||
if err == nil {
|
||||
max = uint16(end)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
Password string
|
||||
AdminPassword string
|
||||
Locks []string
|
||||
|
||||
ControlProtection bool
|
||||
|
||||
FileTransferEnabled bool
|
||||
FileTransferPath string
|
||||
}
|
||||
|
||||
func (WebSocket) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("password", "neko", "password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("password_admin", "admin", "admin password for connecting to stream")
|
||||
if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)")
|
||||
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("control_protection", false, "control protection means, users can gain control only if at least one admin is in the room")
|
||||
if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// File transfer
|
||||
|
||||
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
|
||||
if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer")
|
||||
if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebSocket) Set() {
|
||||
s.Password = viper.GetString("password")
|
||||
s.AdminPassword = viper.GetString("password_admin")
|
||||
s.Locks = viper.GetStringSlice("locks")
|
||||
|
||||
s.ControlProtection = viper.GetBool("control_protection")
|
||||
|
||||
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
|
||||
s.FileTransferPath = viper.GetString("file_transfer_path")
|
||||
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
|
||||
}
|
||||
|
|
@ -1,11 +1,122 @@
|
|||
package desktop
|
||||
|
||||
import "m1k1o/neko/internal/desktop/clipboard"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
func (manager *DesktopManagerCtx) ReadClipboard() string {
|
||||
return clipboard.Read()
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/xevent"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
|
||||
text, err := manager.ClipboardGetBinary("STRING")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Rich text must not always be available, can fail silently.
|
||||
html, _ := manager.ClipboardGetBinary("text/html")
|
||||
|
||||
return &types.ClipboardText{
|
||||
Text: string(text),
|
||||
HTML: string(html),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) WriteClipboard(data string) {
|
||||
clipboard.Write(data)
|
||||
func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error {
|
||||
// TODO: Refactor.
|
||||
// Current implementation is unable to set multiple targets. HTML
|
||||
// is set, if available. Otherwise plain text.
|
||||
|
||||
if data.HTML != "" {
|
||||
return manager.ClipboardSetBinary("text/html", []byte(data.HTML))
|
||||
}
|
||||
|
||||
return manager.ClipboardSetBinary("STRING", []byte(data.Text))
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", mime)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Refactor.
|
||||
// We need to wait until the data came to the clipboard.
|
||||
wait := make(chan struct{})
|
||||
xevent.Emmiter.Once("clipboard-updated", func(payload ...any) {
|
||||
wait <- struct{}{}
|
||||
})
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
_, err = stdin.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdin.Close()
|
||||
|
||||
// TODO: Refactor.
|
||||
// cmd.Wait()
|
||||
<-wait
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) {
|
||||
cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", "TARGETS")
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
var response []string
|
||||
targets := strings.Split(stdout.String(), "\n")
|
||||
for _, target := range targets {
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(target, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
response = append(response, target)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
#include "clipboard.h"
|
||||
|
||||
static clipboard_c *CLIPBOARD = NULL;
|
||||
|
||||
clipboard_c *getClipboard(void) {
|
||||
if (CLIPBOARD == NULL) {
|
||||
CLIPBOARD = clipboard_new(NULL);
|
||||
}
|
||||
|
||||
return CLIPBOARD;
|
||||
}
|
||||
|
||||
void ClipboardSet(char *src) {
|
||||
clipboard_c *cb = getClipboard();
|
||||
clipboard_set_text_ex(cb, src, strlen(src), 0);
|
||||
}
|
||||
|
||||
char *ClipboardGet() {
|
||||
clipboard_c *cb = getClipboard();
|
||||
return clipboard_text_ex(cb, NULL, 0);
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package clipboard
|
||||
|
||||
/*
|
||||
#cgo linux LDFLAGS: /usr/local/lib/libclipboard.a -lxcb
|
||||
|
||||
#include "clipboard.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func Read() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.ClipboardGet()
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
return C.GoString(clipboardUnsafe)
|
||||
}
|
||||
|
||||
func Write(data string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
clipboardUnsafe := C.CString(data)
|
||||
defer C.free(unsafe.Pointer(clipboardUnsafe))
|
||||
|
||||
C.ClipboardSet(clipboardUnsafe)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <libclipboard.h>
|
||||
#include <string.h>
|
||||
|
||||
clipboard_c *getClipboard(void);
|
||||
|
||||
void ClipboardSet(char *src);
|
||||
char *ClipboardGet();
|
||||
68
server/internal/desktop/drop.go
Normal file
68
server/internal/desktop/drop.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"m1k1o/neko/pkg/drop"
|
||||
)
|
||||
|
||||
// repeat move event multiple times
|
||||
const dropMoveRepeat = 4
|
||||
|
||||
// wait after each repeated move event
|
||||
const dropMoveDelay = 100 * time.Millisecond
|
||||
|
||||
func (manager *DesktopManagerCtx) DropFiles(x int, y int, files []string) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
drop.Emmiter.Clear()
|
||||
|
||||
drop.Emmiter.Once("create", func(payload ...any) {
|
||||
manager.Move(0, 0)
|
||||
})
|
||||
|
||||
drop.Emmiter.Once("cursor-enter", func(payload ...any) {
|
||||
//nolint
|
||||
manager.ButtonDown(1)
|
||||
})
|
||||
|
||||
drop.Emmiter.Once("button-press", func(payload ...any) {
|
||||
manager.Move(x, y)
|
||||
})
|
||||
|
||||
drop.Emmiter.Once("begin", func(payload ...any) {
|
||||
for i := 0; i < dropMoveRepeat; i++ {
|
||||
manager.Move(x, y)
|
||||
time.Sleep(dropMoveDelay)
|
||||
}
|
||||
|
||||
//nolint
|
||||
manager.ButtonUp(1)
|
||||
})
|
||||
|
||||
finished := make(chan bool)
|
||||
drop.Emmiter.Once("finish", func(payload ...any) {
|
||||
b, ok := payload[0].(bool)
|
||||
// workaround until https://github.com/kataras/go-events/pull/8 is merged
|
||||
if !ok {
|
||||
b = (payload[0].([]any))[0].(bool)
|
||||
}
|
||||
finished <- b
|
||||
})
|
||||
|
||||
manager.ResetKeys()
|
||||
go drop.OpenWindow(files)
|
||||
|
||||
select {
|
||||
case succeeded := <-finished:
|
||||
return succeeded
|
||||
case <-time.After(1 * time.Second):
|
||||
drop.CloseWindow()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) IsUploadDropEnabled() bool {
|
||||
return manager.config.UploadDrop
|
||||
}
|
||||
102
server/internal/desktop/filechooserdialog.go
Normal file
102
server/internal/desktop/filechooserdialog.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"m1k1o/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
// name of the window that is being controlled
|
||||
const fileChooserDialogName = "Open File"
|
||||
|
||||
// short sleep value between fake user interactions
|
||||
const fileChooserDialogShortSleep = "0.2"
|
||||
|
||||
// long sleep value between fake user interactions
|
||||
const fileChooserDialogLongSleep = "0.4"
|
||||
|
||||
func (manager *DesktopManagerCtx) HandleFileChooserDialog(uri string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// TODO: Use native API.
|
||||
err1 := exec.Command(
|
||||
"xdotool",
|
||||
"search", "--name", fileChooserDialogName, "windowfocus",
|
||||
"sleep", fileChooserDialogShortSleep,
|
||||
"key", "--clearmodifiers", "ctrl+l",
|
||||
"type", "--args", "1", uri+"//",
|
||||
"sleep", fileChooserDialogShortSleep,
|
||||
"key", "Delete", // remove autocomplete results
|
||||
"sleep", fileChooserDialogShortSleep,
|
||||
"key", "Return",
|
||||
"sleep", fileChooserDialogLongSleep,
|
||||
"key", "Down",
|
||||
"key", "--clearmodifiers", "ctrl+a",
|
||||
"key", "Return",
|
||||
"sleep", fileChooserDialogLongSleep,
|
||||
).Run()
|
||||
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
|
||||
// TODO: Use native API.
|
||||
err2 := exec.Command(
|
||||
"xdotool",
|
||||
"search", "--name", fileChooserDialogName,
|
||||
).Run()
|
||||
|
||||
// if last command didn't return error, consider dialog as still open
|
||||
if err2 == nil {
|
||||
return errors.New("unable to select files in dialog")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) CloseFileChooserDialog() {
|
||||
for i := 0; i < 5; i++ {
|
||||
mu.Lock()
|
||||
|
||||
manager.logger.Debug().Msg("attempting to close file chooser dialog")
|
||||
|
||||
// TODO: Use native API.
|
||||
err := exec.Command(
|
||||
"xdotool",
|
||||
"search", "--name", fileChooserDialogName, "windowfocus",
|
||||
).Run()
|
||||
|
||||
if err != nil {
|
||||
mu.Unlock()
|
||||
manager.logger.Info().Msg("file chooser dialog is closed")
|
||||
return
|
||||
}
|
||||
|
||||
// custom press Alt + F4
|
||||
// because xdotool is failing to send proper Alt+F4
|
||||
|
||||
//nolint
|
||||
manager.KeyPress(xorg.XK_Alt_L, xorg.XK_F4)
|
||||
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) IsFileChooserDialogEnabled() bool {
|
||||
return manager.config.FileChooserDialog
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) IsFileChooserDialogOpened() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// TODO: Use native API.
|
||||
err := exec.Command(
|
||||
"xdotool",
|
||||
"search", "--name", fileChooserDialogName,
|
||||
).Run()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -1,36 +1,47 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/desktop/xevent"
|
||||
"m1k1o/neko/internal/desktop/xorg"
|
||||
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/xevent"
|
||||
"m1k1o/neko/pkg/xinput"
|
||||
"m1k1o/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
var mu = sync.Mutex{}
|
||||
|
||||
type DesktopManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
wg sync.WaitGroup
|
||||
shutdown chan struct{}
|
||||
config *config.Desktop
|
||||
|
||||
screenSizeChangeChannel chan bool
|
||||
logger zerolog.Logger
|
||||
wg sync.WaitGroup
|
||||
shutdown chan struct{}
|
||||
emmiter events.EventEmmiter
|
||||
config *config.Desktop
|
||||
screenSize types.ScreenSize // cached screen size
|
||||
input xinput.Driver
|
||||
}
|
||||
|
||||
func New(config *config.Desktop) *DesktopManagerCtx {
|
||||
return &DesktopManagerCtx{
|
||||
logger: log.With().Str("module", "desktop").Logger(),
|
||||
shutdown: make(chan struct{}),
|
||||
config: config,
|
||||
var input xinput.Driver
|
||||
if config.UseInputDriver {
|
||||
input = xinput.NewDriver(config.InputSocket)
|
||||
} else {
|
||||
input = xinput.NewDummy()
|
||||
}
|
||||
|
||||
screenSizeChangeChannel: make(chan bool),
|
||||
return &DesktopManagerCtx{
|
||||
logger: log.With().Str("module", "desktop").Logger(),
|
||||
shutdown: make(chan struct{}),
|
||||
emmiter: events.New(),
|
||||
config: config,
|
||||
screenSize: config.ScreenSize,
|
||||
input: input,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,31 +50,48 @@ func (manager *DesktopManagerCtx) Start() {
|
|||
manager.logger.Panic().Str("display", manager.config.Display).Msg("unable to open display")
|
||||
}
|
||||
|
||||
// X11 can throw errors below, and the default error handler exits
|
||||
xevent.SetupErrorHandler()
|
||||
|
||||
xorg.GetScreenConfigurations()
|
||||
|
||||
err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)
|
||||
manager.logger.Err(err).
|
||||
Str("screen_size", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)).
|
||||
Msgf("setting initial screen size")
|
||||
screenSize, err := xorg.ChangeScreenSize(manager.config.ScreenSize)
|
||||
if err != nil {
|
||||
manager.logger.Err(err).
|
||||
Str("screen_size", screenSize.String()).
|
||||
Msgf("unable to set initial screen size")
|
||||
} else {
|
||||
// cache screen size
|
||||
manager.screenSize = screenSize
|
||||
manager.logger.Info().
|
||||
Str("screen_size", screenSize.String()).
|
||||
Msgf("setting initial screen size")
|
||||
}
|
||||
|
||||
err = manager.input.Connect()
|
||||
if err != nil {
|
||||
// TODO: fail silently to dummy driver?
|
||||
manager.logger.Panic().Err(err).Msg("unable to connect to input driver")
|
||||
}
|
||||
|
||||
// set up event listeners
|
||||
xevent.Unminimize = manager.config.Unminimize
|
||||
xevent.FileChooserDialog = manager.config.FileChooserDialog
|
||||
go xevent.EventLoop(manager.config.Display)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
msg, ok := <-xevent.EventErrorChannel
|
||||
if !ok {
|
||||
manager.logger.Info().Msg("xevent error channel was closed")
|
||||
return
|
||||
}
|
||||
// in case it was opened
|
||||
if manager.config.FileChooserDialog {
|
||||
go manager.CloseFileChooserDialog()
|
||||
}
|
||||
|
||||
manager.logger.Warn().
|
||||
Uint8("error_code", msg.Error_code).
|
||||
Str("message", msg.Message).
|
||||
Uint8("request_code", msg.Request_code).
|
||||
Uint8("minor_code", msg.Minor_code).
|
||||
Msg("X event error occurred")
|
||||
}
|
||||
}()
|
||||
manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) {
|
||||
manager.logger.Warn().
|
||||
Uint8("error_code", error_code).
|
||||
Str("message", message).
|
||||
Uint8("request_code", request_code).
|
||||
Uint8("minor_code", minor_code).
|
||||
Msg("X event error occured")
|
||||
})
|
||||
|
||||
manager.wg.Add(1)
|
||||
|
||||
|
|
@ -73,26 +101,36 @@ func (manager *DesktopManagerCtx) Start() {
|
|||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
const debounceDuration = 10 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.shutdown:
|
||||
return
|
||||
case <-ticker.C:
|
||||
xorg.CheckKeys(time.Second * 10)
|
||||
xorg.CheckKeys(debounceDuration)
|
||||
manager.input.Debounce(debounceDuration)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetScreenSizeChangeChannel() chan bool {
|
||||
return manager.screenSizeChangeChannel
|
||||
func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) {
|
||||
manager.emmiter.On("before_screen_size_change", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) {
|
||||
manager.emmiter.On("after_screen_size_change", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msgf("desktop shutting down")
|
||||
manager.logger.Info().Msgf("shutdown")
|
||||
|
||||
close(manager.shutdown)
|
||||
close(manager.screenSizeChangeChannel)
|
||||
manager.wg.Wait()
|
||||
|
||||
xorg.DisplayClose()
|
||||
|
|
|
|||
|
|
@ -1,18 +1,35 @@
|
|||
package desktop
|
||||
|
||||
import (
|
||||
"m1k1o/neko/internal/desktop/xevent"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/pkg/xevent"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) GetCursorChangedChannel() chan uint64 {
|
||||
return xevent.CursorChangedChannel
|
||||
func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) {
|
||||
xevent.Emmiter.On("cursor-changed", func(payload ...any) {
|
||||
listener(payload[0].(uint64))
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetClipboardUpdatedChannel() chan struct{} {
|
||||
return xevent.ClipboardUpdatedChannel
|
||||
func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) {
|
||||
xevent.Emmiter.On("clipboard-updated", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetEventErrorChannel() chan types.DesktopErrorMessage {
|
||||
return xevent.EventErrorChannel
|
||||
func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) {
|
||||
xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) {
|
||||
xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) {
|
||||
listener()
|
||||
})
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) {
|
||||
xevent.Emmiter.On("event-error", func(payload ...any) {
|
||||
listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
#include "xevent.h"
|
||||
|
||||
static int XEventError(Display *display, XErrorEvent *event) {
|
||||
char message[100];
|
||||
|
||||
int error;
|
||||
error = XGetErrorText(display, event->error_code, message, sizeof(message));
|
||||
if (error) {
|
||||
goXEventError(event, "Could not get error message.");
|
||||
} else {
|
||||
goXEventError(event, message);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void XEventLoop(char *name) {
|
||||
Display *display = XOpenDisplay(name);
|
||||
Window root = RootWindow(display, 0);
|
||||
|
||||
int xfixes_event_base, xfixes_error_base;
|
||||
if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Atom WM_WINDOW_ROLE = XInternAtom(display, "WM_WINDOW_ROLE", 1);
|
||||
Atom XA_CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0);
|
||||
XFixesSelectSelectionInput(display, root, XA_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
|
||||
XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask);
|
||||
XSelectInput(display, root, SubstructureNotifyMask);
|
||||
|
||||
XSync(display, 0);
|
||||
XSetErrorHandler(XEventError);
|
||||
|
||||
while (goXEventActive()) {
|
||||
XEvent event;
|
||||
XNextEvent(display, &event);
|
||||
|
||||
// XFixesDisplayCursorNotify
|
||||
if (event.type == xfixes_event_base + 1) {
|
||||
XFixesCursorNotifyEvent notifyEvent = *((XFixesCursorNotifyEvent *) &event);
|
||||
if (notifyEvent.subtype == XFixesDisplayCursorNotify) {
|
||||
goXEventCursorChanged(notifyEvent);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// XFixesSelectionNotifyEvent
|
||||
if (event.type == xfixes_event_base + XFixesSelectionNotify) {
|
||||
XFixesSelectionNotifyEvent notifyEvent = *((XFixesSelectionNotifyEvent *) &event);
|
||||
if (notifyEvent.subtype == XFixesSetSelectionOwnerNotify && notifyEvent.selection == XA_CLIPBOARD) {
|
||||
goXEventClipboardUpdated();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureNotify
|
||||
if (event.type == ConfigureNotify) {
|
||||
Window window = event.xconfigure.window;
|
||||
|
||||
char *name;
|
||||
XFetchName(display, window, &name);
|
||||
|
||||
XTextProperty role;
|
||||
XGetTextProperty(display, window, &role, WM_WINDOW_ROLE);
|
||||
|
||||
goXEventConfigureNotify(display, window, name, role.value);
|
||||
XFree(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// UnmapNotify
|
||||
if (event.type == UnmapNotify) {
|
||||
Window window = event.xunmap.window;
|
||||
goXEventUnmapNotify(window);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
XCloseDisplay(display);
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package xevent
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lX11 -lXfixes
|
||||
|
||||
#include "xevent.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"m1k1o/neko/internal/types"
|
||||
)
|
||||
|
||||
var CursorChangedChannel chan uint64
|
||||
var ClipboardUpdatedChannel chan struct{}
|
||||
var EventErrorChannel chan types.DesktopErrorMessage
|
||||
|
||||
func init() {
|
||||
CursorChangedChannel = make(chan uint64)
|
||||
ClipboardUpdatedChannel = make(chan struct{})
|
||||
EventErrorChannel = make(chan types.DesktopErrorMessage)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// TODO: Reserved for future use.
|
||||
<-CursorChangedChannel
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func EventLoop(display string) {
|
||||
displayUnsafe := C.CString(display)
|
||||
defer C.free(unsafe.Pointer(displayUnsafe))
|
||||
|
||||
C.XEventLoop(displayUnsafe)
|
||||
}
|
||||
|
||||
// TODO: Shutdown function.
|
||||
//close(CursorChangedChannel)
|
||||
//close(ClipboardUpdatedChannel)
|
||||
//close(EventErrorChannel)
|
||||
|
||||
//export goXEventCursorChanged
|
||||
func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) {
|
||||
CursorChangedChannel <- uint64(event.cursor_serial)
|
||||
}
|
||||
|
||||
//export goXEventClipboardUpdated
|
||||
func goXEventClipboardUpdated() {
|
||||
ClipboardUpdatedChannel <- struct{}{}
|
||||
}
|
||||
|
||||
//export goXEventConfigureNotify
|
||||
func goXEventConfigureNotify(display *C.Display, window C.Window, name *C.char, role *C.char) {
|
||||
|
||||
}
|
||||
|
||||
//export goXEventUnmapNotify
|
||||
func goXEventUnmapNotify(window C.Window) {
|
||||
|
||||
}
|
||||
|
||||
//export goXEventError
|
||||
func goXEventError(event *C.XErrorEvent, message *C.char) {
|
||||
EventErrorChannel <- types.DesktopErrorMessage{
|
||||
Error_code: uint8(event.error_code),
|
||||
Message: C.GoString(message),
|
||||
Request_code: uint8(event.request_code),
|
||||
Minor_code: uint8(event.minor_code),
|
||||
}
|
||||
}
|
||||
|
||||
//export goXEventActive
|
||||
func goXEventActive() C.int {
|
||||
return C.int(1)
|
||||
}
|
||||
36
server/internal/desktop/xinput.go
Normal file
36
server/internal/desktop/xinput.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package desktop
|
||||
|
||||
import "m1k1o/neko/pkg/xinput"
|
||||
|
||||
func (manager *DesktopManagerCtx) inputRelToAbs(x, y int) (int, int) {
|
||||
return (x * xinput.AbsX) / manager.screenSize.Width, (y * xinput.AbsY) / manager.screenSize.Height
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) HasTouchSupport() bool {
|
||||
// we assume now, that if the input driver is enabled, we have touch support
|
||||
return manager.config.UseInputDriver
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) TouchBegin(touchId uint32, x, y int, pressure uint8) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
x, y = manager.inputRelToAbs(x, y)
|
||||
return manager.input.TouchBegin(touchId, x, y, pressure)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) TouchUpdate(touchId uint32, x, y int, pressure uint8) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
x, y = manager.inputRelToAbs(x, y)
|
||||
return manager.input.TouchUpdate(touchId, x, y, pressure)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) TouchEnd(touchId uint32, x, y int, pressure uint8) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
x, y = manager.inputRelToAbs(x, y)
|
||||
return manager.input.TouchEnd(touchId, x, y, pressure)
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"regexp"
|
||||
"time"
|
||||
|
||||
"m1k1o/neko/internal/desktop/xorg"
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/xorg"
|
||||
)
|
||||
|
||||
func (manager *DesktopManagerCtx) Move(x, y int) {
|
||||
|
|
@ -18,8 +18,8 @@ func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) {
|
|||
return xorg.GetCursorPosition()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) Scroll(x, y int) {
|
||||
xorg.Scroll(x, y)
|
||||
func (manager *DesktopManagerCtx) Scroll(deltaX, deltaY int, controlKey bool) {
|
||||
xorg.Scroll(deltaX, deltaY, controlKey)
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ButtonDown(code uint32) error {
|
||||
|
|
@ -66,23 +66,44 @@ func (manager *DesktopManagerCtx) ResetKeys() {
|
|||
xorg.ResetKeys()
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) ScreenConfigurations() map[int]types.ScreenConfiguration {
|
||||
return xorg.ScreenConfigurations
|
||||
func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize {
|
||||
var configs []types.ScreenSize
|
||||
for _, size := range xorg.ScreenConfigurations {
|
||||
for _, fps := range size.Rates {
|
||||
// filter out all irrelevant rates
|
||||
if fps > 60 || (fps > 30 && fps%10 != 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
configs = append(configs, types.ScreenSize{
|
||||
Width: size.Width,
|
||||
Height: size.Height,
|
||||
Rate: fps,
|
||||
})
|
||||
}
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) error {
|
||||
func (manager *DesktopManagerCtx) SetScreenSize(screenSize types.ScreenSize) (types.ScreenSize, error) {
|
||||
mu.Lock()
|
||||
manager.GetScreenSizeChangeChannel() <- true
|
||||
manager.emmiter.Emit("before_screen_size_change")
|
||||
|
||||
defer func() {
|
||||
manager.GetScreenSizeChangeChannel() <- false
|
||||
manager.emmiter.Emit("after_screen_size_change")
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
return xorg.ChangeScreenSize(size.Width, size.Height, size.Rate)
|
||||
screenSize, err := xorg.ChangeScreenSize(screenSize)
|
||||
if err == nil {
|
||||
// cache the new screen size
|
||||
manager.screenSize = screenSize
|
||||
}
|
||||
|
||||
return screenSize, err
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetScreenSize() *types.ScreenSize {
|
||||
func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize {
|
||||
return xorg.GetScreenSize()
|
||||
}
|
||||
|
||||
|
|
@ -119,24 +140,56 @@ func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) {
|
|||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) {
|
||||
if mod.NumLock != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock)
|
||||
if mod.Shift != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModShift, *mod.Shift)
|
||||
}
|
||||
|
||||
if mod.CapsLock != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModCapsLock, *mod.CapsLock)
|
||||
}
|
||||
|
||||
if mod.Control != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModControl, *mod.Control)
|
||||
}
|
||||
|
||||
if mod.Alt != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModAlt, *mod.Alt)
|
||||
}
|
||||
|
||||
if mod.NumLock != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock)
|
||||
}
|
||||
|
||||
if mod.Meta != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModMeta, *mod.Meta)
|
||||
}
|
||||
|
||||
if mod.Super != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModSuper, *mod.Super)
|
||||
}
|
||||
|
||||
if mod.AltGr != nil {
|
||||
xorg.SetKeyboardModifier(xorg.KbdModAltGr, *mod.AltGr)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers {
|
||||
modifiers := xorg.GetKeyboardModifiers()
|
||||
|
||||
NumLock := (modifiers & xorg.KbdModNumLock) != 0
|
||||
CapsLock := (modifiers & xorg.KbdModCapsLock) != 0
|
||||
isset := func(mod xorg.KbdMod) *bool {
|
||||
x := modifiers&mod != 0
|
||||
return &x
|
||||
}
|
||||
|
||||
return types.KeyboardModifiers{
|
||||
NumLock: &NumLock,
|
||||
CapsLock: &CapsLock,
|
||||
Shift: isset(xorg.KbdModShift),
|
||||
CapsLock: isset(xorg.KbdModCapsLock),
|
||||
Control: isset(xorg.KbdModControl),
|
||||
Alt: isset(xorg.KbdModAlt),
|
||||
NumLock: isset(xorg.KbdModNumLock),
|
||||
Meta: isset(xorg.KbdModMeta),
|
||||
Super: isset(xorg.KbdModSuper),
|
||||
AltGr: isset(xorg.KbdModAltGr),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
123
server/internal/http/batch.go
Normal file
123
server/internal/http/batch.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type BatchRequest struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type BatchResponse struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func (b *BatchResponse) Error(httpErr *utils.HTTPError) (err error) {
|
||||
b.Body, err = json.Marshal(httpErr)
|
||||
b.Status = httpErr.Code
|
||||
return
|
||||
}
|
||||
|
||||
type batchHandler struct {
|
||||
Router types.Router
|
||||
PathPrefix string
|
||||
Excluded []string
|
||||
}
|
||||
|
||||
func (b *batchHandler) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||
var requests []BatchRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&requests); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responses := make([]BatchResponse, len(requests))
|
||||
for i, request := range requests {
|
||||
res := BatchResponse{
|
||||
Path: request.Path,
|
||||
Method: request.Method,
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(request.Path, b.PathPrefix) {
|
||||
res.Error(utils.HttpBadRequest("this path is not allowed in batch requests"))
|
||||
responses[i] = res
|
||||
continue
|
||||
}
|
||||
|
||||
if exists, _ := utils.ArrayIn(request.Path, b.Excluded); exists {
|
||||
res.Error(utils.HttpBadRequest("this path is excluded from batch requests"))
|
||||
responses[i] = res
|
||||
continue
|
||||
}
|
||||
|
||||
// prepare request
|
||||
req, err := http.NewRequest(request.Method, request.Path, bytes.NewBuffer(request.Body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy headers
|
||||
for k, vv := range r.Header {
|
||||
for _, v := range vv {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// execute request
|
||||
rr := newResponseRecorder()
|
||||
b.Router.ServeHTTP(rr, req)
|
||||
|
||||
// read response
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write response
|
||||
responses[i] = BatchResponse{
|
||||
Path: request.Path,
|
||||
Method: request.Method,
|
||||
Body: body,
|
||||
Status: rr.Code,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.HttpSuccess(w, responses)
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
Code int
|
||||
HeaderMap http.Header
|
||||
Body *bytes.Buffer
|
||||
}
|
||||
|
||||
func newResponseRecorder() *responseRecorder {
|
||||
return &responseRecorder{
|
||||
Code: http.StatusOK,
|
||||
HeaderMap: make(http.Header),
|
||||
Body: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseRecorder) Header() http.Header {
|
||||
return w.HeaderMap
|
||||
}
|
||||
|
||||
func (w *responseRecorder) Write(b []byte) (int, error) {
|
||||
return w.Body.Write(b)
|
||||
}
|
||||
|
||||
func (w *responseRecorder) WriteHeader(code int) {
|
||||
w.Code = code
|
||||
}
|
||||
36
server/internal/http/debug.go
Normal file
36
server/internal/http/debug.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
func pprofHandler(r types.Router) {
|
||||
r.Get("/debug/pprof/", func(w http.ResponseWriter, r *http.Request) error {
|
||||
pprof.Index(w, r)
|
||||
return nil
|
||||
})
|
||||
|
||||
r.Get("/debug/pprof/{action}", func(w http.ResponseWriter, r *http.Request) error {
|
||||
action := chi.URLParam(r, "action")
|
||||
|
||||
switch action {
|
||||
case "cmdline":
|
||||
pprof.Cmdline(w, r)
|
||||
case "profile":
|
||||
pprof.Profile(w, r)
|
||||
case "symbol":
|
||||
pprof.Symbol(w, r)
|
||||
case "trace":
|
||||
pprof.Trace(w, r)
|
||||
default:
|
||||
pprof.Handler(action).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types"
|
||||
)
|
||||
|
||||
const FILE_UPLOAD_BUF_SIZE = 65000
|
||||
|
||||
type Server struct {
|
||||
logger zerolog.Logger
|
||||
router *chi.Mux
|
||||
http *http.Server
|
||||
conf *config.Server
|
||||
}
|
||||
|
||||
func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop types.DesktopManager) *Server {
|
||||
logger := log.With().Str("module", "http").Logger()
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
if conf.Proxy {
|
||||
router.Use(middleware.RealIP)
|
||||
}
|
||||
router.Use(middleware.RequestLogger(&logformatter{logger}))
|
||||
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
router.Use(middleware.Compress(5, "application/octet-stream"))
|
||||
|
||||
router.Use(cors.Handler(cors.Options{
|
||||
AllowOriginFunc: conf.AllowOrigin,
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
if conf.PathPrefix != "/" {
|
||||
router.Use(func(h http.Handler) http.Handler {
|
||||
return http.StripPrefix(conf.PathPrefix, h)
|
||||
})
|
||||
}
|
||||
|
||||
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := webSocketHandler.Upgrade(w, r)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to upgrade websocket conection")
|
||||
}
|
||||
})
|
||||
|
||||
router.Get("/stats", func(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.URL.Query().Get("pwd")
|
||||
isAdmin, err := webSocketHandler.IsAdmin(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
stats := webSocketHandler.Stats()
|
||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed writing json error response")
|
||||
}
|
||||
})
|
||||
|
||||
router.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.URL.Query().Get("pwd")
|
||||
isAdmin, err := webSocketHandler.IsAdmin(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if webSocketHandler.IsLocked("login") {
|
||||
http.Error(w, "room is locked", http.StatusLocked)
|
||||
return
|
||||
}
|
||||
|
||||
quality, err := strconv.Atoi(r.URL.Query().Get("quality"))
|
||||
if err != nil {
|
||||
quality = 90
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
|
||||
img := desktop.GetScreenshotImage()
|
||||
if err := jpeg.Encode(w, img, &jpeg.Options{Quality: quality}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// allow downloading and uploading files
|
||||
if webSocketHandler.FileTransferEnabled() {
|
||||
router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.URL.Query().Get("pwd")
|
||||
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAuthorized {
|
||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
|
||||
if filename == "" || badChars {
|
||||
http.Error(w, "bad filename", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
filePath := webSocketHandler.FileTransferPath(filename)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
http.Error(w, "not found or unable to open", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||
io.Copy(w, f)
|
||||
})
|
||||
|
||||
router.Post("/file", func(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.URL.Query().Get("pwd")
|
||||
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAuthorized {
|
||||
http.Error(w, "bad authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = r.ParseMultipartForm(32 << 20)
|
||||
if err != nil || r.MultipartForm == nil {
|
||||
logger.Warn().Err(err).Msg("failed to parse multipart form")
|
||||
http.Error(w, "error parsing form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for _, formheader := range r.MultipartForm.File["files"] {
|
||||
filePath := webSocketHandler.FileTransferPath(formheader.Filename)
|
||||
|
||||
formfile, err := formheader.Open()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to open formdata file")
|
||||
http.Error(w, "error writing file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer formfile.Close()
|
||||
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
http.Error(w, "unable to open file for writing", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
io.Copy(f, formfile)
|
||||
}
|
||||
|
||||
err = r.MultipartForm.RemoveAll()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to remove multipart form")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("true"))
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir(conf.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(conf.Static + r.URL.Path); !os.IsNotExist(err) {
|
||||
fs.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: conf.Bind,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
return &Server{
|
||||
logger: logger,
|
||||
router: router,
|
||||
http: server,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
if s.conf.Cert != "" && s.conf.Key != "" {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("https listening on %s", s.http.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
s.logger.Warn().Msgf("http listening on %s", s.http.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() error {
|
||||
return s.http.Shutdown(context.Background())
|
||||
}
|
||||
357
server/internal/http/legacy/handler.go
Normal file
357
server/internal/http/legacy/handler.go
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"m1k1o/neko/internal/api"
|
||||
"m1k1o/neko/internal/api/room"
|
||||
oldEvent "m1k1o/neko/internal/http/legacy/event"
|
||||
oldMessage "m1k1o/neko/internal/http/legacy/message"
|
||||
oldTypes "m1k1o/neko/internal/http/legacy/types"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/pkg/types/message"
|
||||
"m1k1o/neko/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
|
||||
},
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
DefaultDialer = websocket.DefaultDialer
|
||||
)
|
||||
|
||||
type LegacyHandler struct {
|
||||
logger zerolog.Logger
|
||||
serverAddr string
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func New() *LegacyHandler {
|
||||
// Init
|
||||
|
||||
return &LegacyHandler{
|
||||
logger: log.With().Str("module", "legacy").Logger(),
|
||||
serverAddr: "127.0.0.1:8080",
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LegacyHandler) Route(r types.Router) {
|
||||
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error {
|
||||
s := newSession(h.logger, h.serverAddr)
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+url.QueryEscape(s.token), nil)
|
||||
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
|
||||
if err = s.toBackend(event.SIGNAL_REQUEST, message.SignalRequest{}); 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 <- err
|
||||
dst.WriteMessage(websocket.CloseMessage, m)
|
||||
break
|
||||
}
|
||||
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: err.Error(),
|
||||
})
|
||||
continue
|
||||
} else if errors.Is(err, ErrWebsocketSend) {
|
||||
errc <- err
|
||||
break
|
||||
} else {
|
||||
h.logger.Error().Err(err).Msg("couldn't rewrite text message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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: %v"
|
||||
case err = <-errBackend:
|
||||
message = "websocketproxy: Error when copying from client to backend: %v"
|
||||
}
|
||||
|
||||
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 {
|
||||
s := newSession(h.logger, h.serverAddr)
|
||||
|
||||
// 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 current control status
|
||||
control := room.ControlStatusPayload{}
|
||||
err = s.apiReq(http.MethodGet, "/api/room/control", nil, &control)
|
||||
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)
|
||||
} else if session.State.NotConnectedSince != nil {
|
||||
//
|
||||
// TODO: This wont work if the user is removed after the session is closed
|
||||
//
|
||||
// populate last admin left time
|
||||
if session.Profile.IsAdmin && (stats.LastAdminLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastAdminLeftAt)) {
|
||||
stats.LastAdminLeftAt = session.State.NotConnectedSince
|
||||
}
|
||||
// populate last user left time
|
||||
if !session.Profile.IsAdmin && (stats.LastUserLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastUserLeftAt)) {
|
||||
stats.LastUserLeftAt = session.State.NotConnectedSince
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locks, err := s.settingsToLocks(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats.Host = control.HostId
|
||||
// TODO: stats.Banned, not implemented yet
|
||||
stats.Locked = locks
|
||||
stats.ServerStartedAt = h.startedAt
|
||||
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 {
|
||||
s := newSession(h.logger, h.serverAddr)
|
||||
|
||||
// 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 {
|
||||
s := newSession(h.logger, h.serverAddr)
|
||||
|
||||
// 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 {
|
||||
s := newSession(h.logger, h.serverAddr)
|
||||
|
||||
// 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.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := w.Write([]byte("true"))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"m1k1o/neko/internal/types"
|
||||
"m1k1o/neko/internal/http/legacy/types"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
195
server/internal/http/legacy/session.go
Normal file
195
server/internal/http/legacy/session.go
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package legacy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
oldTypes "m1k1o/neko/internal/http/legacy/types"
|
||||
|
||||
"m1k1o/neko/internal/api"
|
||||
"m1k1o/neko/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 {
|
||||
logger zerolog.Logger
|
||||
serverAddr string
|
||||
|
||||
id string
|
||||
token string
|
||||
name string
|
||||
isAdmin bool
|
||||
client *http.Client
|
||||
|
||||
lastHostID string
|
||||
lockedControls bool
|
||||
lockedLogins bool
|
||||
lockedFileTransfer bool
|
||||
sessions map[string]*memberStruct
|
||||
|
||||
connClient *websocket.Conn
|
||||
connBackend *websocket.Conn
|
||||
}
|
||||
|
||||
func newSession(logger zerolog.Logger, serverAddr string) *session {
|
||||
return &session{
|
||||
logger: logger,
|
||||
serverAddr: serverAddr,
|
||||
client: http.DefaultClient,
|
||||
sessions: make(map[string]*memberStruct),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) req(method, path string, headers http.Header, request io.Reader) (io.ReadCloser, http.Header, error) {
|
||||
req, err := http.NewRequest(method, "http://"+s.serverAddr+path, 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := http.Header{
|
||||
"Content-Type": []string{"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 {
|
||||
msg, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.connClient.WriteMessage(websocket.TextMessage, msg)
|
||||
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 {
|
||||
rawPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(&types.WebSocketMessage{
|
||||
Event: event,
|
||||
Payload: rawPayload,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.connBackend.WriteMessage(websocket.TextMessage, msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrWebsocketSend, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) create(username, password string) error {
|
||||
data := api.SessionDataPayload{}
|
||||
|
||||
err := s.apiReq(http.MethodPost, "/api/login", api.SessionLoginPayload{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.id = data.ID
|
||||
s.token = data.Token
|
||||
s.name = data.Profile.Name
|
||||
s.isAdmin = data.Profile.IsAdmin
|
||||
|
||||
// if Cookie auth, the token will be empty
|
||||
if s.token == "" {
|
||||
return fmt.Errorf("token not found - make sure you are not using Cookie auth on the server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
type Stats struct {
|
||||
Connections uint32 `json:"connections"`
|
||||
|
|
@ -21,24 +18,11 @@ type Stats struct {
|
|||
ImplicitControl bool `json:"implicit_control"`
|
||||
}
|
||||
|
||||
type WebSocket interface {
|
||||
Address() string
|
||||
Send(v interface{}) error
|
||||
Destroy() error
|
||||
}
|
||||
|
||||
type WebSocketHandler interface {
|
||||
Start()
|
||||
Shutdown() error
|
||||
Upgrade(w http.ResponseWriter, r *http.Request) error
|
||||
Stats() Stats
|
||||
IsLocked(resource string) bool
|
||||
IsAdmin(password string) (bool, error)
|
||||
|
||||
// File Transfer
|
||||
CanTransferFiles(password string) (bool, error)
|
||||
FileTransferPath(filename string) string
|
||||
FileTransferEnabled() bool
|
||||
type Member struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"displayname"`
|
||||
Admin bool `json:"admin"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type FileListItem struct {
|
||||
|
|
@ -46,3 +30,9 @@ type FileListItem struct {
|
|||
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"`
|
||||
}
|
||||
362
server/internal/http/legacy/wstobackend.go
Normal file
362
server/internal/http/legacy/wstobackend.go
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v3"
|
||||
|
||||
oldEvent "m1k1o/neko/internal/http/legacy/event"
|
||||
oldMessage "m1k1o/neko/internal/http/legacy/message"
|
||||
|
||||
"m1k1o/neko/internal/api/room"
|
||||
"m1k1o/neko/internal/plugins/chat"
|
||||
"m1k1o/neko/internal/plugins/filetransfer"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/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 {
|
||||
// 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 != "" {
|
||||
s.name = request.DisplayName
|
||||
|
||||
err = s.apiReq(http.MethodPost, "/api/profile", map[string]any{
|
||||
"name": request.DisplayName,
|
||||
}, nil)
|
||||
if 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
|
||||
msg, err := json.Marshal(&oldMessage.EmoteSend{
|
||||
Event: oldEvent.CHAT_EMOTE,
|
||||
ID: s.id,
|
||||
Emote: request.Emote,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// loopback emote
|
||||
err = s.connClient.WriteMessage(websocket.TextMessage, msg)
|
||||
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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: No WS equivalent, call HTTP API.
|
||||
return fmt.Errorf("event not implemented: %s", header.Event)
|
||||
|
||||
case oldEvent.ADMIN_KICK:
|
||||
request := &oldMessage.Admin{}
|
||||
err := json.Unmarshal(msg, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: we need to send a message to the user before kicking them
|
||||
// that they are being kicked so they will not automatically rejoin
|
||||
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)
|
||||
}
|
||||
}
|
||||
733
server/internal/http/legacy/wstoclient.go
Normal file
733
server/internal/http/legacy/wstoclient.go
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
package legacy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
|
||||
oldEvent "m1k1o/neko/internal/http/legacy/event"
|
||||
oldMessage "m1k1o/neko/internal/http/legacy/message"
|
||||
oldTypes "m1k1o/neko/internal/http/legacy/types"
|
||||
|
||||
"m1k1o/neko/internal/plugins/chat"
|
||||
"m1k1o/neko/internal/plugins/filetransfer"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/types/event"
|
||||
"m1k1o/neko/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,
|
||||
Admin: profile.IsAdmin,
|
||||
Muted: !settings.CanSend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func screenConfigurations(screenSizes []types.ScreenSize) map[int]oldTypes.ScreenConfiguration {
|
||||
rates := map[string][]int16{}
|
||||
for _, size := range screenSizes {
|
||||
key := fmt.Sprintf("%dx%d", size.Width, size.Height)
|
||||
rates[key] = append(rates[key], size.Rate)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ratesMap := map[int]int16{}
|
||||
for i, rate := range rates[key] {
|
||||
ratesMap[i] = rate
|
||||
}
|
||||
|
||||
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 {
|
||||
return s.toClient(&oldMessage.ControlTarget{
|
||||
Event: oldEvent.CONTROL_GIVE,
|
||||
ID: request.HostID,
|
||||
Target: request.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
FileTransfer: true, // TODO: We don't know if file transfer is enabled, we would need to check the global config somehow.
|
||||
})
|
||||
|
||||
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_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_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,
|
||||
Files: files,
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,24 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type logformatter struct {
|
||||
type logFormatter struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
req := map[string]interface{}{}
|
||||
func (l *logFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
// exclude health & metrics from logs
|
||||
if r.RequestURI == "/health" || r.RequestURI == "/metrics" {
|
||||
return &nulllog{}
|
||||
}
|
||||
|
||||
req := map[string]any{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
req["id"] = reqID
|
||||
|
|
@ -32,43 +40,96 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
|||
req["agent"] = r.UserAgent()
|
||||
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
fields["req"] = req
|
||||
|
||||
return &logentry{
|
||||
fields: fields,
|
||||
logger: l.logger,
|
||||
return &logEntry{
|
||||
logger: l.logger.With().Interface("req", req).Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
type logentry struct {
|
||||
logger zerolog.Logger
|
||||
fields map[string]interface{}
|
||||
errors []map[string]interface{}
|
||||
type logEntry struct {
|
||||
logger zerolog.Logger
|
||||
err error
|
||||
panic *logPanic
|
||||
session types.Session
|
||||
}
|
||||
|
||||
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
res := map[string]interface{}{}
|
||||
type logPanic struct {
|
||||
message string
|
||||
stack string
|
||||
}
|
||||
|
||||
func (e *logEntry) Panic(v any, stack []byte) {
|
||||
e.panic = &logPanic{
|
||||
message: fmt.Sprintf("%+v", v),
|
||||
stack: string(stack),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *logEntry) Error(err error) {
|
||||
e.err = err
|
||||
}
|
||||
|
||||
func (e *logEntry) SetSession(session types.Session) {
|
||||
e.session = session
|
||||
}
|
||||
|
||||
func (e *logEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
|
||||
res := map[string]any{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
e.fields["res"] = res
|
||||
e.fields["module"] = "http"
|
||||
logger := e.logger.With().Interface("res", res).Logger()
|
||||
|
||||
if len(e.errors) > 0 {
|
||||
e.fields["errors"] = e.errors
|
||||
e.logger.Error().Fields(e.fields).Msgf("request failed (%d)", status)
|
||||
} else {
|
||||
e.logger.Debug().Fields(e.fields).Msgf("request complete (%d)", status)
|
||||
// add session ID to logs (if exists)
|
||||
if e.session != nil {
|
||||
logger = logger.With().Str("session_id", e.session.ID()).Logger()
|
||||
}
|
||||
|
||||
// handle panic error message
|
||||
if e.panic != nil {
|
||||
logger.WithLevel(zerolog.PanicLevel).
|
||||
Err(e.err).
|
||||
Str("stack", e.panic.stack).
|
||||
Msgf("request failed (%d): %s", status, e.panic.message)
|
||||
return
|
||||
}
|
||||
|
||||
// handle panic error message
|
||||
if e.err != nil {
|
||||
httpErr, ok := e.err.(*utils.HTTPError)
|
||||
if !ok {
|
||||
logger.Err(e.err).Msgf("request failed (%d)", status)
|
||||
return
|
||||
}
|
||||
|
||||
if httpErr.Message == "" {
|
||||
httpErr.Message = http.StatusText(httpErr.Code)
|
||||
}
|
||||
|
||||
var logLevel zerolog.Level
|
||||
if httpErr.Code < 500 {
|
||||
logLevel = zerolog.WarnLevel
|
||||
} else {
|
||||
logLevel = zerolog.ErrorLevel
|
||||
}
|
||||
|
||||
message := httpErr.Message
|
||||
if httpErr.InternalMsg != "" {
|
||||
message = httpErr.InternalMsg
|
||||
}
|
||||
|
||||
logger.WithLevel(logLevel).Err(httpErr.InternalErr).Msgf("request failed (%d): %s", status, message)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("request complete (%d)", status)
|
||||
}
|
||||
|
||||
func (e *logentry) Panic(v interface{}, stack []byte) {
|
||||
err := map[string]interface{}{}
|
||||
err["message"] = fmt.Sprintf("%+v", v)
|
||||
err["stack"] = string(stack)
|
||||
type nulllog struct{}
|
||||
|
||||
e.errors = append(e.errors, err)
|
||||
func (e *nulllog) Panic(v any, stack []byte) {}
|
||||
func (e *nulllog) Error(err error) {}
|
||||
func (e *nulllog) SetSession(session types.Session) {}
|
||||
func (e *nulllog) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
|
||||
}
|
||||
|
|
|
|||
136
server/internal/http/manager.go
Normal file
136
server/internal/http/manager.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/http/legacy"
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
type HttpManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
config *config.Server
|
||||
router types.Router
|
||||
http *http.Server
|
||||
}
|
||||
|
||||
func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, config *config.Server) *HttpManagerCtx {
|
||||
logger := log.With().Str("module", "http").Logger()
|
||||
|
||||
opts := []RouterOption{
|
||||
WithRequestID(), // create a request id for each request
|
||||
}
|
||||
|
||||
// use real ip if behind proxy
|
||||
// before logger so it can log the real ip
|
||||
if config.Proxy {
|
||||
opts = append(opts, WithRealIP())
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
WithLogger(logger),
|
||||
WithRecoverer(), // recover from panics without crashing server
|
||||
)
|
||||
|
||||
if config.HasCors() {
|
||||
opts = append(opts, WithCORS(config.AllowOrigin))
|
||||
}
|
||||
|
||||
if config.PathPrefix != "/" {
|
||||
opts = append(opts, WithPathPrefix(config.PathPrefix))
|
||||
}
|
||||
|
||||
router := newRouter(opts...)
|
||||
|
||||
router.Route("/api", ApiManager.Route)
|
||||
|
||||
router.Get("/api/ws", WebSocketManager.Upgrade(func(r *http.Request) bool {
|
||||
return config.AllowOrigin(r.Header.Get("Origin"))
|
||||
}))
|
||||
|
||||
// Legacy handler
|
||||
legacy.New().Route(router)
|
||||
|
||||
batch := batchHandler{
|
||||
Router: router,
|
||||
PathPrefix: "/api",
|
||||
Excluded: []string{
|
||||
"/api/batch", // do not allow batchception
|
||||
"/api/ws",
|
||||
},
|
||||
}
|
||||
router.Post("/api/batch", batch.Handle)
|
||||
|
||||
router.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := w.Write([]byte("true"))
|
||||
return err
|
||||
})
|
||||
|
||||
if config.Metrics {
|
||||
router.Get("/metrics", func(w http.ResponseWriter, r *http.Request) error {
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if config.Static != "" {
|
||||
fs := http.FileServer(http.Dir(config.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := os.Stat(config.Static + r.URL.Path)
|
||||
if err == nil {
|
||||
fs.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if config.PProf {
|
||||
pprofHandler(router)
|
||||
}
|
||||
|
||||
return &HttpManagerCtx{
|
||||
logger: logger,
|
||||
config: config,
|
||||
router: router,
|
||||
http: &http.Server{
|
||||
Addr: config.Bind,
|
||||
Handler: router,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *HttpManagerCtx) Start() {
|
||||
if manager.config.Cert != "" && manager.config.Key != "" {
|
||||
go func() {
|
||||
if err := manager.http.ListenAndServeTLS(manager.config.Cert, manager.config.Key); err != http.ErrServerClosed {
|
||||
manager.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
manager.logger.Info().Msgf("https listening on %s", manager.http.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := manager.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
manager.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
manager.logger.Info().Msgf("http listening on %s", manager.http.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *HttpManagerCtx) Shutdown() error {
|
||||
manager.logger.Info().Msg("shutdown")
|
||||
|
||||
return manager.http.Shutdown(context.Background())
|
||||
}
|
||||
172
server/internal/http/router.go
Normal file
172
server/internal/http/router.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"m1k1o/neko/pkg/auth"
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
type RouterOption func(*router)
|
||||
|
||||
func WithRequestID() RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(middleware.RequestID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(logger zerolog.Logger) RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(middleware.RequestLogger(&logFormatter{logger}))
|
||||
}
|
||||
}
|
||||
|
||||
func WithRecoverer() RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(middleware.Recoverer)
|
||||
}
|
||||
}
|
||||
|
||||
func WithCORS(allowOrigin func(origin string) bool) RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(cors.Handler(cors.Options{
|
||||
AllowOriginFunc: func(r *http.Request, origin string) bool {
|
||||
return allowOrigin(origin)
|
||||
},
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func WithPathPrefix(prefix string) RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(func(h http.Handler) http.Handler {
|
||||
return http.StripPrefix(prefix, h)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func WithRealIP() RouterOption {
|
||||
return func(r *router) {
|
||||
r.chi.Use(middleware.RealIP)
|
||||
}
|
||||
}
|
||||
|
||||
type router struct {
|
||||
chi chi.Router
|
||||
}
|
||||
|
||||
func newRouter(opts ...RouterOption) types.Router {
|
||||
r := &router{chi.NewRouter()}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *router) Group(fn func(types.Router)) {
|
||||
r.chi.Group(func(c chi.Router) {
|
||||
fn(&router{c})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *router) Route(pattern string, fn func(types.Router)) {
|
||||
r.chi.Route(pattern, func(c chi.Router) {
|
||||
fn(&router{c})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *router) Get(pattern string, fn types.RouterHandler) {
|
||||
r.chi.Get(pattern, routeHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) Post(pattern string, fn types.RouterHandler) {
|
||||
r.chi.Post(pattern, routeHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) Put(pattern string, fn types.RouterHandler) {
|
||||
r.chi.Put(pattern, routeHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) Patch(pattern string, fn types.RouterHandler) {
|
||||
r.chi.Patch(pattern, routeHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) Delete(pattern string, fn types.RouterHandler) {
|
||||
r.chi.Delete(pattern, routeHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) With(fn types.MiddlewareHandler) types.Router {
|
||||
c := r.chi.With(middlewareHandler(fn))
|
||||
return &router{c}
|
||||
}
|
||||
|
||||
func (r *router) Use(fn types.MiddlewareHandler) {
|
||||
r.chi.Use(middlewareHandler(fn))
|
||||
}
|
||||
|
||||
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.chi.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func errorHandler(err error, w http.ResponseWriter, r *http.Request) {
|
||||
httpErr, ok := err.(*utils.HTTPError)
|
||||
if !ok {
|
||||
httpErr = utils.HttpInternalServerError().WithInternalErr(err)
|
||||
}
|
||||
|
||||
utils.HttpJsonResponse(w, httpErr.Code, httpErr)
|
||||
}
|
||||
|
||||
func routeHandler(fn types.RouterHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// get custom log entry pointer from context
|
||||
logEntry, _ := r.Context().Value(middleware.LogEntryCtxKey).(*logEntry)
|
||||
|
||||
if err := fn(w, r); err != nil {
|
||||
logEntry.Error(err)
|
||||
errorHandler(err, w, r)
|
||||
}
|
||||
|
||||
// set session if exits
|
||||
if session, ok := auth.GetSession(r); ok {
|
||||
logEntry.SetSession(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareHandler(fn types.MiddlewareHandler) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// get custom log entry pointer from context
|
||||
logEntry, _ := r.Context().Value(middleware.LogEntryCtxKey).(*logEntry)
|
||||
|
||||
ctx, err := fn(w, r)
|
||||
if err != nil {
|
||||
logEntry.Error(err)
|
||||
errorHandler(err, w, r)
|
||||
|
||||
// set session if exits
|
||||
if session, ok := auth.GetSession(r); ok {
|
||||
logEntry.SetSession(session)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
if ctx != nil {
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
204
server/internal/member/file/provider.go
Normal file
204
server/internal/member/file/provider.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
func New(config Config) types.MemberProvider {
|
||||
return &MemberProviderCtx{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type MemberProviderCtx struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) hash(password string) string {
|
||||
// if hash is disabled, return password as plain text
|
||||
if !provider.config.Hash {
|
||||
return password
|
||||
}
|
||||
|
||||
sha256 := sha256.New()
|
||||
sha256.Write([]byte(password))
|
||||
hashedPassword := sha256.Sum(nil)
|
||||
return base64.StdEncoding.EncodeToString(hashedPassword)
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
|
||||
// id will be also username
|
||||
id := username
|
||||
|
||||
entry, err := provider.getEntry(id)
|
||||
if err != nil {
|
||||
return "", types.MemberProfile{}, err
|
||||
}
|
||||
|
||||
if entry.Password != provider.hash(password) {
|
||||
return "", types.MemberProfile{}, types.ErrMemberInvalidPassword
|
||||
}
|
||||
|
||||
return id, entry.Profile, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
|
||||
// id will be also username
|
||||
id := username
|
||||
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, ok := entries[id]
|
||||
if ok {
|
||||
return "", types.ErrMemberAlreadyExists
|
||||
}
|
||||
|
||||
entries[id] = MemberEntry{
|
||||
Password: provider.hash(password),
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
return id, provider.serialize(entries)
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry, ok := entries[id]
|
||||
if !ok {
|
||||
return types.ErrMemberDoesNotExist
|
||||
}
|
||||
|
||||
entry.Profile = profile
|
||||
entries[id] = entry
|
||||
|
||||
return provider.serialize(entries)
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry, ok := entries[id]
|
||||
if !ok {
|
||||
return types.ErrMemberDoesNotExist
|
||||
}
|
||||
|
||||
entry.Password = provider.hash(password)
|
||||
entries[id] = entry
|
||||
|
||||
return provider.serialize(entries)
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
|
||||
entry, err := provider.getEntry(id)
|
||||
if err != nil {
|
||||
return types.MemberProfile{}, err
|
||||
}
|
||||
|
||||
return entry.Profile, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
|
||||
profiles := map[string]types.MemberProfile{}
|
||||
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
i := 0
|
||||
for id, entry := range entries {
|
||||
if i >= offset && (limit == 0 || i < offset+limit) {
|
||||
profiles[id] = entry.Profile
|
||||
}
|
||||
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Delete(id string) error {
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, ok := entries[id]
|
||||
if !ok {
|
||||
return types.ErrMemberDoesNotExist
|
||||
}
|
||||
|
||||
delete(entries, id)
|
||||
|
||||
return provider.serialize(entries)
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) deserialize() (map[string]MemberEntry, error) {
|
||||
file, err := os.OpenFile(provider.config.Path, os.O_RDONLY|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(raw) == 0 {
|
||||
return map[string]MemberEntry{}, nil
|
||||
}
|
||||
|
||||
var entries map[string]MemberEntry
|
||||
if err := json.Unmarshal([]byte(raw), &entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) getEntry(id string) (MemberEntry, error) {
|
||||
entries, err := provider.deserialize()
|
||||
if err != nil {
|
||||
return MemberEntry{}, err
|
||||
}
|
||||
|
||||
entry, ok := entries[id]
|
||||
if !ok {
|
||||
return MemberEntry{}, types.ErrMemberDoesNotExist
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) serialize(data map[string]MemberEntry) error {
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(provider.config.Path, raw, os.ModePerm)
|
||||
}
|
||||
48
server/internal/member/file/provider_test.go
Normal file
48
server/internal/member/file/provider_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
// Ensure that hashes are the same after encoding and decoding using json
|
||||
func TestMemberProviderCtx_hash(t *testing.T) {
|
||||
provider := &MemberProviderCtx{
|
||||
config: Config{
|
||||
Hash: true,
|
||||
},
|
||||
}
|
||||
|
||||
// generate random strings
|
||||
passwords := []string{}
|
||||
for i := 0; i < 10; i++ {
|
||||
password, err := utils.NewUID(32)
|
||||
if err != nil {
|
||||
t.Errorf("utils.NewUID() returned error: %s", err)
|
||||
}
|
||||
passwords = append(passwords, password)
|
||||
}
|
||||
|
||||
for _, password := range passwords {
|
||||
hashedPassword := provider.hash(password)
|
||||
|
||||
// json encode password hash
|
||||
hashedPasswordJSON, err := json.Marshal(hashedPassword)
|
||||
if err != nil {
|
||||
t.Errorf("json.Marshal() returned error: %s", err)
|
||||
}
|
||||
|
||||
// json decode password hash json
|
||||
var hashedPasswordStr string
|
||||
err = json.Unmarshal(hashedPasswordJSON, &hashedPasswordStr)
|
||||
if err != nil {
|
||||
t.Errorf("json.Unmarshal() returned error: %s", err)
|
||||
}
|
||||
|
||||
if hashedPasswordStr != hashedPassword {
|
||||
t.Errorf("hashedPasswordStr: %s != hashedPassword: %s", hashedPasswordStr, hashedPassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
server/internal/member/file/types.go
Normal file
15
server/internal/member/file/types.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
type MemberEntry struct {
|
||||
Password string `json:"password"`
|
||||
Profile types.MemberProfile `json:"profile"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Path string
|
||||
Hash bool
|
||||
}
|
||||
168
server/internal/member/manager.go
Normal file
168
server/internal/member/manager.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package member
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/member/file"
|
||||
"m1k1o/neko/internal/member/multiuser"
|
||||
"m1k1o/neko/internal/member/noauth"
|
||||
"m1k1o/neko/internal/member/object"
|
||||
"m1k1o/neko/pkg/types"
|
||||
)
|
||||
|
||||
func New(sessions types.SessionManager, config *config.Member) *MemberManagerCtx {
|
||||
manager := &MemberManagerCtx{
|
||||
logger: log.With().Str("module", "member").Logger(),
|
||||
sessions: sessions,
|
||||
config: config,
|
||||
}
|
||||
|
||||
switch config.Provider {
|
||||
case "file":
|
||||
manager.provider = file.New(config.File)
|
||||
case "object":
|
||||
manager.provider = object.New(config.Object)
|
||||
case "multiuser":
|
||||
manager.provider = multiuser.New(config.Multiuser)
|
||||
case "noauth":
|
||||
fallthrough
|
||||
default:
|
||||
manager.provider = noauth.New()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
type MemberManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
sessions types.SessionManager
|
||||
config *config.Member
|
||||
providerMu sync.Mutex
|
||||
provider types.MemberProvider
|
||||
loginMu sync.Mutex
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Connect() error {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.Connect()
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Disconnect() error {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.Disconnect()
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.Authenticate(username, password)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.Insert(username, password, profile)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Select(id string) (types.MemberProfile, error) {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
// get primarily from corresponding session, if exists
|
||||
session, ok := manager.sessions.Get(id)
|
||||
if ok {
|
||||
return session.Profile(), nil
|
||||
}
|
||||
|
||||
return manager.provider.Select(id)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.SelectAll(limit, offset)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) UpdateProfile(id string, profile types.MemberProfile) error {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
// update corresponding session, if exists
|
||||
err := manager.sessions.Update(id, profile)
|
||||
if err != nil && !errors.Is(err, types.ErrSessionNotFound) {
|
||||
manager.logger.Err(err).Msg("error while updating session")
|
||||
}
|
||||
|
||||
return manager.provider.UpdateProfile(id, profile)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) UpdatePassword(id string, password string) error {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
return manager.provider.UpdatePassword(id, password)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Delete(id string) error {
|
||||
manager.providerMu.Lock()
|
||||
defer manager.providerMu.Unlock()
|
||||
|
||||
// destroy corresponding session, if exists
|
||||
err := manager.sessions.Delete(id)
|
||||
if err != nil && !errors.Is(err, types.ErrSessionNotFound) {
|
||||
manager.logger.Err(err).Msg("error while deleting session")
|
||||
}
|
||||
|
||||
return manager.provider.Delete(id)
|
||||
}
|
||||
|
||||
//
|
||||
// member -> session
|
||||
//
|
||||
|
||||
func (manager *MemberManagerCtx) Login(username string, password string) (types.Session, string, error) {
|
||||
manager.loginMu.Lock()
|
||||
defer manager.loginMu.Unlock()
|
||||
|
||||
id, profile, err := manager.provider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !profile.IsAdmin && manager.sessions.Settings().LockedLogins {
|
||||
return nil, "", types.ErrSessionLoginsLocked
|
||||
}
|
||||
|
||||
session, ok := manager.sessions.Get(id)
|
||||
if ok {
|
||||
if session.State().IsConnected {
|
||||
return nil, "", types.ErrSessionAlreadyConnected
|
||||
}
|
||||
|
||||
// TODO: Replace session.
|
||||
if err := manager.sessions.Delete(id); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return manager.sessions.Create(id, profile)
|
||||
}
|
||||
|
||||
func (manager *MemberManagerCtx) Logout(id string) error {
|
||||
manager.loginMu.Lock()
|
||||
defer manager.loginMu.Unlock()
|
||||
|
||||
return manager.sessions.Delete(id)
|
||||
}
|
||||
82
server/internal/member/multiuser/provider.go
Normal file
82
server/internal/member/multiuser/provider.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package multiuser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func New(config Config) types.MemberProvider {
|
||||
return &MemberProviderCtx{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type MemberProviderCtx struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
|
||||
// generate random token
|
||||
token, err := utils.NewUID(5)
|
||||
if err != nil {
|
||||
return "", types.MemberProfile{}, err
|
||||
}
|
||||
|
||||
// id is username with token
|
||||
id := fmt.Sprintf("%s-%s", username, token)
|
||||
|
||||
// if logged in as administrator
|
||||
if provider.config.AdminPassword == password {
|
||||
profile := provider.config.AdminProfile
|
||||
if profile.Name == "" {
|
||||
profile.Name = username
|
||||
}
|
||||
return id, profile, nil
|
||||
}
|
||||
|
||||
// if logged in as user
|
||||
if provider.config.UserPassword == password {
|
||||
profile := provider.config.UserProfile
|
||||
if profile.Name == "" {
|
||||
profile.Name = username
|
||||
}
|
||||
return id, profile, nil
|
||||
}
|
||||
|
||||
return "", types.MemberProfile{}, types.ErrMemberInvalidPassword
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
|
||||
return "", errors.New("new user is created on first login in multiuser mode")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
|
||||
return errors.New("password can only be modified in config while in multiuser mode")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
|
||||
return types.MemberProfile{}, errors.New("cannot select user in multiuser mode")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
|
||||
return map[string]types.MemberProfile{}, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Delete(id string) error {
|
||||
return errors.New("cannot delete user in multiuser mode")
|
||||
}
|
||||
10
server/internal/member/multiuser/types.go
Normal file
10
server/internal/member/multiuser/types.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package multiuser
|
||||
|
||||
import "m1k1o/neko/pkg/types"
|
||||
|
||||
type Config struct {
|
||||
AdminPassword string
|
||||
UserPassword string
|
||||
AdminProfile types.MemberProfile
|
||||
UserProfile types.MemberProfile
|
||||
}
|
||||
75
server/internal/member/noauth/provider.go
Normal file
75
server/internal/member/noauth/provider.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package noauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"m1k1o/neko/pkg/types"
|
||||
"m1k1o/neko/pkg/utils"
|
||||
)
|
||||
|
||||
func New() types.MemberProvider {
|
||||
return &MemberProviderCtx{
|
||||
profile: types.MemberProfile{
|
||||
IsAdmin: true,
|
||||
CanLogin: true,
|
||||
CanConnect: true,
|
||||
CanWatch: true,
|
||||
CanHost: true,
|
||||
CanShareMedia: true,
|
||||
CanAccessClipboard: true,
|
||||
SendsInactiveCursor: true,
|
||||
CanSeeInactiveCursors: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type MemberProviderCtx struct {
|
||||
profile types.MemberProfile
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
|
||||
// generate random token
|
||||
token, err := utils.NewUID(5)
|
||||
if err != nil {
|
||||
return "", types.MemberProfile{}, err
|
||||
}
|
||||
|
||||
// id is username with token
|
||||
id := fmt.Sprintf("%s-%s", username, token)
|
||||
|
||||
provider.profile.Name = username
|
||||
return id, provider.profile, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
|
||||
return "", errors.New("new user is created on first login in noauth mode")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
|
||||
return errors.New("noauth mode does not have password")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
|
||||
return types.MemberProfile{}, errors.New("cannot select user in noauth mode")
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
|
||||
return map[string]types.MemberProfile{}, nil
|
||||
}
|
||||
|
||||
func (provider *MemberProviderCtx) Delete(id string) error {
|
||||
return errors.New("cannot delete user in noauth mode")
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue