commit 0c8af21fab2b908f79efb38fe866db706f4f4bc8 Author: Craig Date: Mon Jan 13 08:05:38 2020 +0000 first commit diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..463bdd1 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,129 @@ +FROM golang:stretch + +# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" +# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs +# will be updated to match your local UID/GID (when using the dockerFile property). +# See https://aka.ms/vscode-remote/containers/non-root-user for details. +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Avoid warnings by switching to noninteractive +ENV DEBIAN_FRONTEND=noninteractive + +# Runtime for testing and compileing +RUN apt-get update \ + && apt-get -y install supervisor openbox dbus-x11 ttf-freefont xvfb pulseaudio consolekit firefox-esr x11vnc \ + && apt-get -y install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good gstreamer1.0-pulseaudio \ + && apt-get -y install gcc libc6-dev \ + && apt-get -y install libx11-dev xorg-dev libxtst-dev libpng++-dev \ + && apt-get -y install xcb libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev libxkbcommon-x11-dev \ + && apt-get -y install libxkbcommon-dev \ + && apt-get -y install xsel xclip + +# Configure apt, install packages and tools +RUN apt-get update \ + && apt-get -y install --no-install-recommends apt-utils apt-transport-https dialog 2>&1 \ + # + # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed + && apt-get -y install git iproute2 procps lsb-release xz-utils \ + # + # Install Docker CE CLI + && apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common lsb-release \ + && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | (OUT=$(apt-key add - 2>&1) || echo $OUT) \ + && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \ + && apt-get update \ + && apt-get install -y docker-ce-cli \ + # + # Install Docker Compose + && curl -sSL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose \ + && chmod +x /usr/local/bin/docker-compose \ + # + # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/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 for the non-root user + && apt-get install -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +ENV NODE_VERSION 12.14.1 + +RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \ + && case "${dpkgArch##*-}" in \ + amd64) ARCH='x64';; \ + ppc64el) ARCH='ppc64le';; \ + s390x) ARCH='s390x';; \ + arm64) ARCH='arm64';; \ + armhf) ARCH='armv7l';; \ + i386) ARCH='x86';; \ + *) echo "unsupported architecture"; exit 1 ;; \ + esac \ + # gpg keys listed at https://github.com/nodejs/node#release-keys + && set -ex \ + && for key in \ + 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ + FD3A5288F042B6850C66B31F09FE44734EB7990E \ + 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ + DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ + C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ + B9AE9905FFD7803F25714661B63B535A4C206CA9 \ + 77984A986EBC2AA786BC0F66B01FBB92821C587A \ + 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \ + 4ED778F539E3634C779C87C6D7062848A1AB005C \ + A48C2BEE680E841632CD4E44F07496B3EB3C1762 \ + B9E2F5981AA6E0CD28160D9FF13993A75599653C \ + ; do \ + gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys "$key" || \ + gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys "$key" || \ + gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "$key" ; \ + done \ + && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \ + && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ + && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ + && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ + && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \ + && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ + && ln -s /usr/local/bin/node /usr/local/bin/nodejs + +USER $USERNAME + +# Install packages and tools +RUN go get -x -d github.com/stamblerre/gocode 2>&1 \ + && go build -o gocode-gomod github.com/stamblerre/gocode \ + && mv gocode-gomod $GOPATH/bin/ \ + # + # Install Go tools + && go get -u -v \ + github.com/mdempsky/gocode \ + github.com/uudashr/gopkgs/cmd/gopkgs \ + github.com/ramya-rao-a/go-outline \ + github.com/acroca/go-symbols \ + github.com/godoctor/godoctor \ + golang.org/x/tools/cmd/guru \ + golang.org/x/tools/cmd/gorename \ + github.com/rogpeppe/godef \ + github.com/zmb3/gogetdoc \ + github.com/haya14busa/goplay/cmd/goplay \ + github.com/sqs/goreturns \ + github.com/josharian/impl \ + github.com/davidrjenni/reftools/cmd/fillstruct \ + github.com/fatih/gomodifytags \ + github.com/cweill/gotests/... \ + golang.org/x/tools/cmd/goimports \ + golang.org/x/lint/golint \ + golang.org/x/tools/gopls \ + github.com/alecthomas/gometalinter \ + honnef.co/go/tools/... \ + github.com/golangci/golangci-lint/cmd/golangci-lint \ + github.com/mgechev/revive \ + github.com/derekparker/delve/cmd/dlv 2>&1 + +ENV GO111MODULE=on + +# Switch back to dialog for any ad-hoc use of apt-get +ENV DEBIAN_FRONTEND=dialog + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..0076d05 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "neko", + "service": "neko", + "dockerComposeFile": "docker-compose.yaml", + "workspaceFolder": "/workspace", + "remoteUser": "vscode", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "go.gopath": "/go" + }, + "extensions": [ + "ms-vscode.go", + "octref.vetur", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-vscode-remote.vscode-remote-extensionpack", + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "editorconfig.editorconfig", + "psioniq.psi-header", + "gruntfuggly.todo-tree", + "swyphcosmo.spellchecker", + "eamodio.gitlens" + ] +} diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml new file mode 100644 index 0000000..bd08062 --- /dev/null +++ b/.devcontainer/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3.6' +services: + neko: + network_mode: host + build: + context: . + dockerfile: Dockerfile + shm_size: '2gb' + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + volumes: + - /home/nurd/neko:/workspace + - /var/run/docker.sock:/var/run/docker.sock + command: "/bin/sh -c \"while sleep 1000; do :; done\"" diff --git a/.docker/build.sh b/.docker/build.sh new file mode 100644 index 0000000..79a95f1 --- /dev/null +++ b/.docker/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd ../server +go get && make build + +cd ../client +npm install && npm run build + +cd ../ +docker build -f Dockerfile -t neko . \ No newline at end of file diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh new file mode 100644 index 0000000..fbd9fdc --- /dev/null +++ b/.docker/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "Starting dbus" +/etc/init.d/dbus start + +echo "Starting supervisord" +su -p -l $NEKO_USER -c '/usr/bin/supervisord -c /etc/neko/supervisord.conf' -s /bin/bash \ No newline at end of file diff --git a/.docker/openbox.xml b/.docker/openbox.xml new file mode 100644 index 0000000..255fe3d --- /dev/null +++ b/.docker/openbox.xml @@ -0,0 +1,760 @@ + + + + + + + + 10 + 20 + + + + + no + + yes + + + + yes + + no + + yes + + no + + 200 + + no + + + + + Smart + +
yes
+ + Primary + + 1 + +
+ + + Clearlooks + NLIMC + + yes + yes + + sans + 8 + + bold + + normal + + + + sans + 8 + + bold + + normal + + + + sans + 9 + + normal + + normal + + + + sans + 9 + + normal + + normal + + + + sans + 9 + + bold + + normal + + + + sans + 9 + + bold + + normal + + + + + + + 4 + 1 + + + + 875 + + + + + yes + Nonpixel + + Center + + + + + 10 + + 10 + + + + + + + 0 + 0 + 0 + 0 + + + + TopLeft + + 0 + 0 + no + Above + + Vertical + + no + 300 + + 300 + + Middle + + + + + C-g + + + + leftno + + + rightno + + + upno + + + downno + + + leftno + + + rightno + + + upno + + + downno + + + 1 + + + 2 + + + 3 + + + 4 + + + + + + + + + + + + + + + + + + + + scrot -s + + + + + + + + + + + + + + + + + + + + + + + + yesyes + + + + + + + + + + + + right + + + + + left + + + + + up + + + + + down + + + + + + + + true + Konqueror + + kfmclient openProfile filemanagement + + + + + scrot + + + + + 1 + + 500 + + 400 + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + previous + + + next + + + previous + + + next + + + previous + + + next + + + + + + + + + + + + + + no + + + + + + + + + + + yes + + + + + + + + + + + + + + + + + + + + + + + + + + + top + + + + + + left + + + + + + right + + + + + + bottom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vertical + + + horizontal + + + + + + + + + + + + + + + + + previous + + + next + + + + previous + + + next + + + previous + + + next + + + + + + + + + + + + + + + + + + + + previous + + + next + + + previous + + + next + + + + + + + + + + /var/lib/openbox/debian-menu.xml + menu.xml + 200 + + no + + 100 + + 400 + + yes + + yes + + + + + + + +
diff --git a/.docker/policies.json b/.docker/policies.json new file mode 100644 index 0000000..5cf126a --- /dev/null +++ b/.docker/policies.json @@ -0,0 +1,25 @@ +{ + "policies": { + "DisableAppUpdate": true, + "DisableTelemetry": true, + "DontCheckDefaultBrowser": true, + "BlockAboutConfig": true, + "OverrideFirstRunPage": "", + "OfferToSaveLogins": false, + "PromptForDownloadLocation":false, + "ExtensionSettings": { + "uBlock0@raymondhill.net": { + "installation_mode": "force_installed", + "install_url": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi" + } + }, + "WebsiteFilter": { + "Block": [], + "Exceptions": [] + }, + "Homepage": { + "Additional": [], + "StartPage": "none" + } + } +} \ No newline at end of file diff --git a/.docker/pulseaudio.pa b/.docker/pulseaudio.pa new file mode 100644 index 0000000..62ba416 --- /dev/null +++ b/.docker/pulseaudio.pa @@ -0,0 +1,4 @@ +unload-module module-suspend-on-idle + +# Allow pulse audio to be accessed via TCP (from localhost only), to allow other users to access the virtual devices +load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1 \ No newline at end of file diff --git a/.docker/supervisord.conf b/.docker/supervisord.conf new file mode 100644 index 0000000..2b38f39 --- /dev/null +++ b/.docker/supervisord.conf @@ -0,0 +1,32 @@ +[supervisord] +environment=PULSE_SERVER="unix:/tmp/pulseaudio.socket",DISPLAY=":%(ENV_NEKO_DISPLAY)s" +nodaemon=true +pidfile=/var/run/supervisord.pid +logfile=/dev/null +logfile_maxbytes=0 + +[program:xvfb] +command=/usr/bin/Xvfb :%(ENV_NEKO_DISPLAY)s -screen 0 %(ENV_NEKO_WIDTH)sx%(ENV_NEKO_HEIGHT)sx24+32 +redirect_stderr=true +autorestart=true +priority=300 + +[program:pulseaudio] +command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1 --file=/etc/neko/pulseaudio.pa +autorestart=true +priority=300 + +[program:openbox] +command=/usr/bin/openbox --config-file /etc/neko/openbox.xml +autorestart=true +priority=300 + +[program:firefox-esr] +command=/usr/lib/firefox-esr/firefox-esr --display=:%(ENV_NEKO_DISPLAY)s --setDefaultBrowser -width %(ENV_NEKO_WIDTH)s -height %(ENV_NEKO_HEIGHT)s %(ENV_NEKO_URL)s +autorestart=true +priority=400 + +[program:neko] +command=/usr/bin/neko serve -d --static "/var/www" +autorestart=true +priority=500 \ No newline at end of file diff --git a/.docker/supervisord.dev.conf b/.docker/supervisord.dev.conf new file mode 100644 index 0000000..9bf4927 --- /dev/null +++ b/.docker/supervisord.dev.conf @@ -0,0 +1,28 @@ +[supervisord] +environment=PULSE_SERVER="unix:/tmp/pulseaudio.socket",DISPLAY=":%(ENV_NEKO_DISPLAY)s" +nodaemon=true +pidfile=/var/run/supervisord.pid +#logfile=/dev/null +#logfile_maxbytes=0 +loglevel=debug + +[program:xvfb] +command=/usr/bin/Xvfb :%(ENV_NEKO_DISPLAY)s -screen 0 %(ENV_NEKO_WIDTH)sx%(ENV_NEKO_HEIGHT)sx24+32 +redirect_stderr=true +autorestart=true +priority=300 + +[program:pulseaudio] +command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1 --file=/etc/neko/pulseaudio.pa +autorestart=true +priority=300 + +[program:openbox] +command=/usr/bin/openbox --config-file /etc/neko/openbox.xml +autorestart=true +priority=300 + +[program:firefox-esr] +command=/usr/lib/firefox-esr/firefox-esr --display=:%(ENV_NEKO_DISPLAY)s --setDefaultBrowser -width %(ENV_NEKO_WIDTH)s -height %(ENV_NEKO_HEIGHT)s %(ENV_NEKO_URL)s +autorestart=true +priority=400 \ No newline at end of file diff --git a/.docker/test.sh b/.docker/test.sh new file mode 100755 index 0000000..41857d6 --- /dev/null +++ b/.docker/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# usefull debugging tools pavucontrol htop x11vnc + +sudo mkdir -p /var/run/dbus /etc/neko +sudo /etc/init.d/dbus start + +sudo cp supervisord.conf /etc/neko/supervisord.conf +sudo cp pulseaudio.pa /etc/neko/pulseaudio.pa +sudo cp openbox.xml /etc/neko/openbox.xml +sudo cp policies.json /usr/lib/firefox-esr/distribution/policies.json + +if [ ! -f /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi ]; then + sudo mkdir -p /usr/lib/firefox-esr/distribution/extensions + sudo curl -o /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/addon-607454-latest.xpi +fi + +if [ ! -f ../server/bin/neko ]; then + echo "build server before testing" + exit 1 +fi + +if [ ! -d ../client/dist/ ]; then + echo "build client before testing" + exit 1 +fi + +sudo cp ../server/bin/neko /usr/bin/neko +sudo cp -R ../client/dist /var/www/ + +sudo rm -rf $HOME/.mozilla +sudo rm -rf /var/run/supervisord.pid + +mkdir -p $HOME/.config/pulse +echo "default-server=unix:/tmp/pulseaudio.socket" > $HOME/.config/pulse/client.conf + +export NEKO_DISPLAY=0 +export NEKO_WIDTH=1280 +export NEKO_HEIGHT=720 +export NEKO_URL=https://www.youtube.com/embed/QH2-TGUlwu4 +export NEKO_PASSWORD=neko +export NEKO_BIND=0.0.0.0:80 +export NEKO_KEY= +export NEKO_CERT= + +supervisord --configuration ./supervisord.dev.conf \ No newline at end of file diff --git a/.docker/x11vnc.sh b/.docker/x11vnc.sh new file mode 100755 index 0000000..066f347 --- /dev/null +++ b/.docker/x11vnc.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ ! -f "${HOME}/.vnc/passwd" ]; then + x11vnc -storepasswd +fi + +/usr/bin/x11vnc -display :0 -6 -xkb -rfbport 5901 -rfbauth $HOME/.vnc/passwd -wait 20 -nap -noxrecord -nopw -noxfixes -noxdamage -repeat diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bab2215 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +* text=auto +*.css text +*.js text +*.ts text +*.json text +*.htm text +*.html text +*.env text +*.xml text +*.svg text +*.txt text +*.ini text +*.sql text +*.sh text +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary \ No newline at end of file diff --git a/.github/.gitkeep b/.github/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..914f0ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Log/Temp files +.tmp/ +tmp/ +*.tmp +.logs/ +logs/ +*.log +core + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Lock files +yarn.lock +package-lock.json + +# TypeScript incremental compilation cache +*.tsbuildinfo + +# Node modules +node_modules +dist +bin + +# Environment files +*.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..007524f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +FROM buildpack-deps:stretch + +ARG USERNAME=neko +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Avoid warnings by switching to noninteractive +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update \ + && apt-get -y install curl supervisor openbox dbus-x11 ttf-freefont xvfb pulseaudio consolekit firefox-esr \ + && apt-get -y install gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-pulseaudio libxcb-xkb-dev libxkbcommon-x11-dev \ + # + # 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 \ + # + # Install uBlock + && mkdir -p /usr/lib/firefox-esr/distribution/extensions \ + && curl -o /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/addon-607454-latest.xpi \ + # + # Make directories for neko + && mkdir -p /etc/neko /var/www \ + # + # Setup Pulse Audio + mkdir -p /home/$USERNAME/.config/pulse/ \ + && echo "default-server=unix:/tmp/pulseaudio.socket" > /home/$USERNAME/.config/pulse/client.conf \ + && chown -R $USERNAME:$USERNAME /home/$USERNAME \ + # + # Clean up + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/* + +# +# Copy configuation files +COPY .docker/pulseaudio.pa /etc/neko/pulseaudio.pa +COPY .docker/openbox.xml /etc/neko/openbox.xml +COPY .docker/supervisord.conf /etc/neko/supervisord.conf +COPY .docker/policies.json /usr/lib/firefox-esr/distribution/policies.json + +# +# Neko files +COPY client/dist/ /var/www +COPY server/bin/neko /usr/bin/neko + +# +# Neko Env +ENV NEKO_USER=$USERNAME +ENV NEKO_DISPLAY=0 +ENV NEKO_WIDTH=1280 +ENV NEKO_HEIGHT=720 +ENV NEKO_URL=https://www.youtube.com/embed/QH2-TGUlwu4 +ENV NEKO_PASSWORD=neko +ENV NEKO_BIND=0.0.0.0:80 +ENV NEKO_KEY= +ENV NEKO_CERT= + +# +# Copy entrypoint +COPY .docker/entrypoint.sh /entrypoint.sh + +# +# Run entrypoint +CMD ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9eabea0 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# n.eko +This is a proof of concept project I threw together over the last few days, its ugly its not perfect but it looks nice. This uses web rtc to stream a desktop inside of a docker container, I made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and my internet can't handle streaming and discord keeps crashing. I just want to watch anime with my friends ლ(ಠ益ಠლ) so I started digging throughout the net and found a few *kinda* clones, but non of them had the virtual browser, then I found [Turtus](https://github.com/Khauri/Turtus) and I was able to figure out the rest. + +This is by no means a fully featured clone of rabbit. It has no concept of other peers. It has bugs, but for the most part it works. I'm not sure what the future holds for this. If I continue to use it and like it, I'll probably keep pushing updates to it. I'd be happy to accept PRs for any improvements. + +### Why n.eko? +I like cats, I'm a weeb and a nerd, I own the domain [n.eko.moe](https://n.eko.moe/) and I love that logo I came across, had to use it for something /shrug + +### I need help setting this up! +Its a docker container, you need to have docker installed and then run + +``` +TODO: +``` + +### Development +*Highly* recommend you use a dev container for vscode, I've included the .devcontainer I've used to develop this app \ No newline at end of file diff --git a/client/.browserslistrc b/client/.browserslistrc new file mode 100644 index 0000000..d6471a3 --- /dev/null +++ b/client/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +last 2 versions diff --git a/client/.editorconfig b/client/.editorconfig new file mode 100644 index 0000000..3dce414 --- /dev/null +++ b/client/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 0000000..05bd58d --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,16 @@ +{ + "root": true, + "env": { + "node": true + }, + "extends": ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"], + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "rules": { + "no-case-declarations": "off", + "no-dupe-class-members": "off", + "no-console": "off", + } +} + diff --git a/client/.prettierrc b/client/.prettierrc new file mode 100644 index 0000000..83fc071 --- /dev/null +++ b/client/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "vueIndentScriptAndStyle": true +} diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 0000000..190a5e5 --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "files.encoding": "utf8", + "files.eol": "\n", + "typescript.tsdk": "./node_modules/typescript/lib", + "todo-tree.filtering.excludeGlobs": ["**/node_modules/**"], + "eslint.validate": [ + "vue", + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + ], + "vetur.validation.template": true, + "vetur.useWorkspaceDependencies": true, + "remote.extensionKind": { + "ms-azuretools.vscode-docker": "ui" + }, + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..e69de29 diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..a44753e --- /dev/null +++ b/client/package.json @@ -0,0 +1,34 @@ +{ + "name": "neko-client", + "version": "1.0.0", + "description": "Client for neko streaming server", + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^5.12.0", + "eventemitter3": "^4.0.0", + "vue": "^2.6.10", + "vue-class-component": "^7.0.2", + "vue-notification": "^1.3.20", + "vue-property-decorator": "^8.3.0" + }, + "devDependencies": { + "@vue/cli-plugin-eslint": "^4.1.0", + "@vue/cli-plugin-typescript": "^4.1.0", + "@vue/cli-plugin-vuex": "^4.1.0", + "@vue/cli-service": "^4.1.0", + "@vue/eslint-config-prettier": "^5.0.0", + "@vue/eslint-config-typescript": "^4.0.0", + "eslint": "^5.16.0", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-vue": "^5.0.0", + "node-sass": "^4.12.0", + "prettier": "^1.19.1", + "sass-loader": "^8.0.0", + "typescript": "~3.5.3", + "vue-template-compiler": "^2.6.10" + } +} diff --git a/client/public/android-chrome-192x192.png b/client/public/android-chrome-192x192.png new file mode 100644 index 0000000..70fb69b Binary files /dev/null and b/client/public/android-chrome-192x192.png differ diff --git a/client/public/android-chrome-512x512.png b/client/public/android-chrome-512x512.png new file mode 100644 index 0000000..ea23497 Binary files /dev/null and b/client/public/android-chrome-512x512.png differ diff --git a/client/public/apple-touch-icon.png b/client/public/apple-touch-icon.png new file mode 100644 index 0000000..d898e03 Binary files /dev/null and b/client/public/apple-touch-icon.png differ diff --git a/client/public/browserconfig.xml b/client/public/browserconfig.xml new file mode 100644 index 0000000..ededce1 --- /dev/null +++ b/client/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #19bd9c + + + diff --git a/client/public/favicon-16x16.png b/client/public/favicon-16x16.png new file mode 100644 index 0000000..eca620d Binary files /dev/null and b/client/public/favicon-16x16.png differ diff --git a/client/public/favicon-32x32.png b/client/public/favicon-32x32.png new file mode 100644 index 0000000..6f0007d Binary files /dev/null and b/client/public/favicon-32x32.png differ diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..9d4135b --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,23 @@ + + + + + + + n.eko + + + + + + + + + + +
+ + + diff --git a/client/public/mstile-144x144.png b/client/public/mstile-144x144.png new file mode 100644 index 0000000..979c7dd Binary files /dev/null and b/client/public/mstile-144x144.png differ diff --git a/client/public/mstile-150x150.png b/client/public/mstile-150x150.png new file mode 100644 index 0000000..0a38aa4 Binary files /dev/null and b/client/public/mstile-150x150.png differ diff --git a/client/public/mstile-310x150.png b/client/public/mstile-310x150.png new file mode 100644 index 0000000..6d1eb31 Binary files /dev/null and b/client/public/mstile-310x150.png differ diff --git a/client/public/mstile-310x310.png b/client/public/mstile-310x310.png new file mode 100644 index 0000000..0d9d25f Binary files /dev/null and b/client/public/mstile-310x310.png differ diff --git a/client/public/mstile-70x70.png b/client/public/mstile-70x70.png new file mode 100644 index 0000000..7b52d0b Binary files /dev/null and b/client/public/mstile-70x70.png differ diff --git a/client/public/safari-pinned-tab.svg b/client/public/safari-pinned-tab.svg new file mode 100644 index 0000000..8cf43f9 --- /dev/null +++ b/client/public/safari-pinned-tab.svg @@ -0,0 +1,73 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/client/public/site.webmanifest b/client/public/site.webmanifest new file mode 100644 index 0000000..54ae740 --- /dev/null +++ b/client/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "n.eko", + "short_name": "n.eko", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#19bd9c", + "background_color": "#19bd9c", + "display": "standalone" +} diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 0000000..dfcf29c --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,847 @@ + + + + + diff --git a/client/src/assets/logo.svg b/client/src/assets/logo.svg new file mode 100644 index 0000000..fb82e0b --- /dev/null +++ b/client/src/assets/logo.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/client/src/assets/styles/_reset.scss b/client/src/assets/styles/_reset.scss new file mode 100644 index 0000000..767c249 --- /dev/null +++ b/client/src/assets/styles/_reset.scss @@ -0,0 +1,358 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: 100%; + vertical-align: baseline; +} + +/* make sure to set some focus styles for accessibility */ +:focus { + outline: 0; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +input[type=search]::-webkit-search-cancel-button, +input[type=search]::-webkit-search-decoration, +input[type=search]::-webkit-search-results-button, +input[type=search]::-webkit-search-results-decoration { + -webkit-appearance: none; + -moz-appearance: none; +} + +input[type=search] { + -webkit-appearance: none; + -moz-appearance: none; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +textarea { + overflow: auto; + vertical-align: top; + resize: vertical; +} + +/** +* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. +*/ + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; + max-width: 100%; +} + +/** +* Prevent modern browsers from displaying `audio` without controls. +* Remove excess height in iOS 5 devices. +*/ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** +* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. +* Known issue: no IE 6 support. +*/ + +[hidden] { + display: none; +} + +/** +* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using +* `em` units. +* 2. Prevent iOS text size adjust after orientation change, without disabling +* user zoom. +*/ + +html { + font-size: 100%; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/** +* Address `outline` inconsistency between Chrome and other browsers. +*/ + +a:focus { + outline: none; +} + +/** +* Improve readability when focused and also mouse hovered in all browsers. +*/ + +a:active, +a:hover { + outline: 0; +} + +/** +* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. +* 2. Improve image quality when scaled in IE 7. +*/ + +img { + border: 0; /* 1 */ + -ms-interpolation-mode: bicubic; /* 2 */ +} + +/** +* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. +*/ + +figure { + margin: 0; +} + +/** +* Correct margin displayed oddly in IE 6/7. +*/ + +form { + margin: 0; +} + +/** +* Define consistent border, margin, and padding. +*/ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** +* 1. Correct color not being inherited in IE 6/7/8/9. +* 2. Correct text not wrapping in Firefox 3. +* 3. Correct alignment displayed oddly in IE 6/7. +*/ + +legend { + border: 0; /* 1 */ + padding: 0; + white-space: normal; /* 2 */ + *margin-left: -7px; /* 3 */ +} + +/** +* 1. Correct font size not being inherited in all browsers. +* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, +* and Chrome. +* 3. Improve appearance and consistency in all browsers. +*/ + +button, +input, +select, +textarea { + font-size: 100%; /* 1 */ + margin: 0; /* 2 */ + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ +} + +/** +* Address Firefox 3+ setting `line-height` on `input` using `!important` in +* the UA stylesheet. +*/ + +button, +input { + line-height: normal; +} + +/** +* Address inconsistent `text-transform` inheritance for `button` and `select`. +* All other form control elements do not inherit `text-transform` values. +* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. +* Correct `select` style inheritance in Firefox 4+ and Opera. +*/ + +button, +select { + text-transform: none; +} + +/** +* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +* and `video` controls. +* 2. Correct inability to style clickable `input` types in iOS. +* 3. Improve usability and consistency of cursor style between image-type +* `input` and others. +* 4. Remove inner spacing in IE 7 without affecting normal text inputs. +* Known issue: inner spacing remains in IE 6. +*/ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + *overflow: visible; /* 4 */ +} + +/** +* Re-set default cursor for disabled elements. +*/ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** +* 1. Address box sizing set to content-box in IE 8/9. +* 2. Remove excess padding in IE 8/9. +* 3. Remove excess padding in IE 7. +* Known issue: excess padding remains in IE 6. +*/ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ +} + +/** +* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. +* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome +* (include `-moz` to future-proof). +*/ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** +* Remove inner padding and search cancel button in Safari 5 and Chrome +* on OS X. +*/ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** +* Remove inner padding and border in Firefox 3+. +*/ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** +* 1. Remove default vertical scrollbar in IE 6/7/8/9. +* 2. Improve readability and alignment in all browsers. +*/ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + +/** +* Remove most spacing between table cells. +*/ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +html, +button, +input, +select, +textarea { + color: #222; +} + +::-moz-selection { + text-shadow: none; +} + +::selection { + text-shadow: none; +} + +img { + vertical-align: middle; +} + +fieldset { + border: 0; + margin: 0; + padding: 0; +} + +textarea { + resize: vertical; +} + +.chromeframe { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} diff --git a/client/src/assets/styles/_variables.scss b/client/src/assets/styles/_variables.scss new file mode 100644 index 0000000..82490f3 --- /dev/null +++ b/client/src/assets/styles/_variables.scss @@ -0,0 +1,10 @@ + + +$style-dark: #2c2c2c; +$style-darker: #1a1a1a; +$style-light: #fafafa; +$style-primary: #19bd9c; + +$style-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +$style-font-color: $style-dark; +$style-font-size: 14px; diff --git a/client/src/assets/styles/main.scss b/client/src/assets/styles/main.scss new file mode 100644 index 0000000..6a321ad --- /dev/null +++ b/client/src/assets/styles/main.scss @@ -0,0 +1,23 @@ +@charset "utf-8"; + +// Import variables +@import "variables"; + +// Reset CSS +@import "reset"; + +// Import Vendor +@import "vendor/font-awesome"; + +html, body { + -webkit-font-smoothing: subpixel-antialiased; + background-color: $style-dark; + font-family: $style-font-family; + font-size: $style-font-size; + color: $style-font-color; + overflow: hidden; + width: 100vw; + height: 100vh; + min-width: 320px; +} + diff --git a/client/src/assets/styles/vendor/_font-awesome.scss b/client/src/assets/styles/vendor/_font-awesome.scss new file mode 100644 index 0000000..1ea360b --- /dev/null +++ b/client/src/assets/styles/vendor/_font-awesome.scss @@ -0,0 +1,20 @@ +// Variables + +$fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; +$fa-font-size-base: 16px; +$fa-font-display: auto; +$fa-css-prefix: fa; +$fa-border-color: #eee; +$fa-inverse: #fff; +$fa-li-width: 2em; +$fa-fw-width: (20em / 16); +$fa-primary-opacity: 1; +$fa-secondary-opacity: .4; + +$fa-family-default: 'Font Awesome 5 Free'; + +// Import FA source files +@import "~@fortawesome/fontawesome-free/scss/brands"; +@import "~@fortawesome/fontawesome-free/scss/solid"; +@import "~@fortawesome/fontawesome-free/scss/regular"; +@import "~@fortawesome/fontawesome-free/scss/fontawesome"; diff --git a/client/src/main.ts b/client/src/main.ts new file mode 100644 index 0000000..b44d343 --- /dev/null +++ b/client/src/main.ts @@ -0,0 +1,12 @@ +import './assets/styles/main.scss' + +import Vue from 'vue' +import Notifications from 'vue-notification' +import App from './App.vue' + +Vue.config.productionTip = false +Vue.use(Notifications) + +new Vue({ + render: h => h(App), +}).$mount('#neko') diff --git a/client/src/types/shims-scss.d.ts b/client/src/types/shims-scss.d.ts new file mode 100644 index 0000000..5e761af --- /dev/null +++ b/client/src/types/shims-scss.d.ts @@ -0,0 +1 @@ +declare module '*.scss' {} diff --git a/client/src/types/shims-tsx.d.ts b/client/src/types/shims-tsx.d.ts new file mode 100644 index 0000000..c656c68 --- /dev/null +++ b/client/src/types/shims-tsx.d.ts @@ -0,0 +1,13 @@ +import Vue, { VNode } from 'vue' + +declare global { + namespace JSX { + // tslint:disable no-empty-interface + interface Element extends VNode {} + // tslint:disable no-empty-interface + interface ElementClass extends Vue {} + interface IntrinsicElements { + [elem: string]: any + } + } +} diff --git a/client/src/types/shims-vue.d.ts b/client/src/types/shims-vue.d.ts new file mode 100644 index 0000000..d9f24fa --- /dev/null +++ b/client/src/types/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..41a3a62 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "types": [ + "webpack-env" + ], + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/client/vue.config.js b/client/vue.config.js new file mode 100644 index 0000000..a3ae613 --- /dev/null +++ b/client/vue.config.js @@ -0,0 +1,11 @@ +module.exports = { + css: { + loaderOptions: { + sass: { + prependData: ` + @import "@/assets/styles/_variables.scss"; + `, + }, + }, + }, +} diff --git a/neko.code-workspace b/neko.code-workspace new file mode 100644 index 0000000..ad01bca --- /dev/null +++ b/neko.code-workspace @@ -0,0 +1,47 @@ +{ + "folders": [ + { + "path": "server" + }, + { + "path": "client" + }, + { + "path": "." + } + ], + "settings": { + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "files.encoding": "utf8", + "files.eol": "\n", + "typescript.tsdk": "./client/node_modules/typescript/lib", + "todo-tree.filtering.excludeGlobs": ["**/node_modules/**"], + "eslint.validate": [ + "vue", + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + ], + "vetur.validation.template": true, + "vetur.useWorkspaceDependencies": true, + "remote.extensionKind": { + "ms-azuretools.vscode-docker": "ui" + }, + "remote.containers.defaultExtensions": [ + "ms-vscode.go", + "octref.vetur", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-vscode-remote.vscode-remote-extensionpack", + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "editorconfig.editorconfig", + "gruntfuggly.todo-tree", + "swyphcosmo.spellchecker", + "eamodio.gitlens" + ] + } +} \ No newline at end of file diff --git a/server/.editorconfig b/server/.editorconfig new file mode 100644 index 0000000..3dce414 --- /dev/null +++ b/server/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/server/.vscode/launch.json b/server/.vscode/launch.json new file mode 100644 index 0000000..acee218 --- /dev/null +++ b/server/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "launch", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/neko", + "envFile": "${workspaceFolder}/.env", + "output": "${workspaceFolder}/bin/debug/neko", + "cwd": "${workspaceFolder}/", + "args": ["serve", "-d", "--bind", "0.0.0.0:3000", "--static", "../client/dist", "--password", "test"] + } + ] +} diff --git a/server/.vscode/settings.json b/server/.vscode/settings.json new file mode 100644 index 0000000..452ccec --- /dev/null +++ b/server/.vscode/settings.json @@ -0,0 +1,22 @@ + +{ + "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 + } + } + } diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..36ac540 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,9 @@ +BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'` +GIT_COMMIT=`git rev-parse --short HEAD` +GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` +GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"` + +LDFLAGS=-ldflags "-s -X version.buildTime=${BUILD_TIME} -X version.gitRevision=${GIT_DIRTY}${GIT_REVISION} -X version.gitBranch=${GIT_BRANCH}" + +build: + go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/cmd/neko/main.go b/server/cmd/neko/main.go new file mode 100644 index 0000000..bcdde3a --- /dev/null +++ b/server/cmd/neko/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "n.eko.moe/neko" + "n.eko.moe/neko/cmd" + "n.eko.moe/neko/internal/utils" + + "github.com/rs/zerolog/log" +) + +func main() { + fmt.Print(utils.Colorf(utils.Header, "server", neko.Service.Version)) + if err := cmd.Execute(); err != nil { + log.Panic().Err(err).Msg("Failed to execute command") + } +} diff --git a/server/cmd/root.go b/server/cmd/root.go new file mode 100644 index 0000000..ed6f57e --- /dev/null +++ b/server/cmd/root.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + + "n.eko.moe/neko" + "n.eko.moe/neko/internal/preflight" + + "github.com/spf13/cobra" +) + +func Execute() error { + return root.Execute() +} + +var root = &cobra.Command{ + Use: "neko", + Short: "", + Long: ``, + Version: neko.Service.Version.String(), +} + +func init() { + cobra.OnInitialize(func() { + preflight.Logs("neko") + preflight.Config("neko") + neko.Service.Root.Set() + }) + + if err := neko.Service.Root.Init(root); err != nil { + neko.Service.Logger.Panic().Err(err).Msg("Unable to run command") + } + + root.SetVersionTemplate(fmt.Sprintf("Version: %s\n", neko.Service.Version)) +} diff --git a/server/cmd/serve.go b/server/cmd/serve.go new file mode 100644 index 0000000..255ec8a --- /dev/null +++ b/server/cmd/serve.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "n.eko.moe/neko" + "n.eko.moe/neko/internal/config" +) + +func init() { + command := &cobra.Command{ + Use: "serve", + Short: "", + Long: ``, + Run: neko.Service.ServeCommand, + } + + configs := []config.Config{ + neko.Service.Serve, + } + + 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 command") + } + } + + root.AddCommand(command) +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..b3bbc47 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,15 @@ +module n.eko.moe/neko + +go 1.13 + +require ( + github.com/go-chi/chi v4.0.3+incompatible + github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57 + github.com/gorilla/websocket v1.4.1 + github.com/matoous/go-nanoid v1.1.0 + github.com/pion/webrtc/v2 v2.1.18 + github.com/pkg/errors v0.8.1 + github.com/rs/zerolog v1.17.2 + github.com/spf13/cobra v0.0.5 + github.com/spf13/viper v1.6.1 +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..acc09f3 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,300 @@ +bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= +github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57 h1:0BtenaNSwWghFTsHDQGTdurEdCMm7lsNmWfMoBS2JiY= +github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57/go.mod h1:P6/F9lmSF2Z/74P/m80qEm6ApjE5HmB+rSzfBCNqIPo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= +github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1 h1:h0wbuSK8xUNmMwDdCxZx2OLdkVck6Bb31zj4CxCN5I4= +github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/matoous/go-nanoid v1.1.0 h1:B4BSMxTVgYrCHqtovL/adb8GFkE4mPCNntOOrdZLeCk= +github.com/matoous/go-nanoid v1.1.0/go.mod h1:L+uFUqrYwDgNWu5R02GrSxxcqX7ghiFuKPlKEOZ90GE= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= +github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I= +github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE= +github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss= +github.com/pion/datachannel v1.4.13/go.mod h1:+rBUwEDonA63KXx994DP/ofyyGVAm6AIMvOqQZxjWRU= +github.com/pion/dtls/v2 v2.0.0-rc.3 h1:u9utI+EDJOjOWfrkGQsD8WNssPcTwfYIanFB6oI8K+4= +github.com/pion/dtls/v2 v2.0.0-rc.3/go.mod h1:x0XH+cN5z+l/+/4nYL8r4sB8g6+0d1Zp2Pfkcoz8BKY= +github.com/pion/ice v0.7.6 h1:EARj1MBq5NYaMtXVhYkK03i0RS/meejNHvZS++K5tSY= +github.com/pion/ice v0.7.6/go.mod h1:4xCajahEEvc5w0AM+Ujx/Rr2EczON/fKndi3jLyDdh4= +github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +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.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= +github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= +github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= +github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= +github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak= +github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= +github.com/pion/rtp v1.1.3/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE= +github.com/pion/rtp v1.1.4 h1:P6xh8Y8JfzR7+JAbI79X2M8kfYETaqbuM5Otm+Z+k6U= +github.com/pion/rtp v1.1.4/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE= +github.com/pion/sctp v1.7.3 h1:Pok18oncuAq/WjNxbyltfBSLvbv/6QSCyVJKYyDWP5M= +github.com/pion/sctp v1.7.3/go.mod h1:c6C9jaDGX7f5xeSRVju/140XatpO9sOVe81EwpfzAc8= +github.com/pion/sdp/v2 v2.3.1 h1:45dub4NRdwyDmQCD3GIY7DZuqC49GBUwBdjuetvdOr0= +github.com/pion/sdp/v2 v2.3.1/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA= +github.com/pion/srtp v1.2.6 h1:mHQuAMh0P67R7/j1F260u3O+fbRWLyjKLRPZYYvODFM= +github.com/pion/srtp v1.2.6/go.mod h1:rd8imc5htjfs99XiEoOjLMEOcVjME63UHx9Ek9IGst0= +github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= +github.com/pion/transport v0.8.9/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= +github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pion/turn v1.4.0 h1:7NUMRehQz4fIo53Qv9ui1kJ0Kr1CA82I81RHKHCeM80= +github.com/pion/turn v1.4.0/go.mod h1:aDSi6hWX/hd1+gKia9cExZOR0MU95O7zX9p3Gw/P2aU= +github.com/pion/webrtc/v2 v2.1.18 h1:g0VN0xfEUSlVNfQmlCD6yOeXy/tMaktESBmHMnBS3bk= +github.com/pion/webrtc/v2 v2.1.18/go.mod h1:m0rKlYgLRZWyhmcMWegpF6xtK1ASxmOg8DAR74ttzQY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9 h1:7vKLsPiS3XTrejZHaoKDS3/26K3t10rAtuf1+fRfIVA= +github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9/go.mod h1:n1o8s7fg6QGcgIsN9AmWQnBi6KrcbEUX0kFVUwTP53g= +github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY= +github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ= +github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk= +github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= +github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/shirou/gopsutil v2.19.6+incompatible h1:49/Gru26Lne9Cl3IoAVDZVM09hvkSrUodgIIsCVRwbs= +github.com/shirou/gopsutil v2.19.6+incompatible/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53 h1:tYb/9KQi8dTCSDia2NwbuhUbKlaurqC/S7MFQo96nLk= +github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53/go.mod h1:5txYrXKrQG6ZJYdGIiMVVxiOhbdACnwBcHzIwGQ7Nkw= +github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8 h1:9Y+hoKBYa+UtzGqkODfs8c0Q6gp2UfniVNsHQWghPi0= +github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8/go.mod h1:52+3yYrTNjWKh+CkQozNRCLWCE/X666yAWPGbYC3DZI= +github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b h1:psGhQitWSo4KBpLghvJPlhHxTJ8LQl1y0ekjSreqvu4= +github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190522155817-f3200d17e092/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-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= +golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/server/internal/config/config.go b/server/internal/config/config.go new file mode 100644 index 0000000..43b2695 --- /dev/null +++ b/server/internal/config/config.go @@ -0,0 +1,8 @@ +package config + +import "github.com/spf13/cobra" + +type Config interface { + Init(cmd *cobra.Command) error + Set() +} \ No newline at end of file diff --git a/server/internal/config/root.go b/server/internal/config/root.go new file mode 100644 index 0000000..4637026 --- /dev/null +++ b/server/internal/config/root.go @@ -0,0 +1,37 @@ +package config + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type Root struct { + Debug bool + Logs bool + CfgFile string +} + +func (Root) Init(cmd *cobra.Command) error { + 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 { + return err + } + + cmd.PersistentFlags().String("config", "", "Configuration file path") + if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil { + return err + } + + return nil +} + +func (s *Root) Set() { + s.Logs = viper.GetBool("logs") + s.Debug = viper.GetBool("debug") + s.CfgFile = viper.GetString("config") +} diff --git a/server/internal/config/serve.go b/server/internal/config/serve.go new file mode 100644 index 0000000..012f385 --- /dev/null +++ b/server/internal/config/serve.go @@ -0,0 +1,52 @@ +package config + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type Serve struct { + Cert string + Key string + Bind string + Password string + Static string +} + +func (Serve) Init(cmd *cobra.Command) error { + + cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "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") + 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") + if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil { + return err + } + + 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("static", "./www", "Static files to serve") + if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil { + return err + } + + return nil +} + +func (s *Serve) Set() { + s.Cert = viper.GetString("cert") + s.Key = viper.GetString("key") + s.Bind = viper.GetString("bind") + s.Password = viper.GetString("password") + s.Static = viper.GetString("static") +} diff --git a/server/internal/gst/gst.c b/server/internal/gst/gst.c new file mode 100644 index 0000000..64d4cfa --- /dev/null +++ b/server/internal/gst/gst.c @@ -0,0 +1,88 @@ +#include "gst.h" + +#include + +typedef struct SampleHandlerUserData { + int pipelineId; +} SampleHandlerUserData; + +GMainLoop *gstreamer_send_main_loop = NULL; +void gstreamer_send_start_mainloop(void) { + gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE); + + g_main_loop_run(gstreamer_send_main_loop); +} + +static gboolean gstreamer_send_bus_call(GstBus *bus, GstMessage *msg, gpointer data) { + switch (GST_MESSAGE_TYPE(msg)) { + + case GST_MESSAGE_EOS: + g_print("End of stream\n"); + exit(1); + break; + + case GST_MESSAGE_ERROR: { + gchar *debug; + GError *error; + + gst_message_parse_error(msg, &error, &debug); + g_free(debug); + + g_printerr("Error: %s\n", error->message); + g_error_free(error); + exit(1); + } + default: + break; + } + + return TRUE; +} + +GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) { + GstSample *sample = NULL; + GstBuffer *buffer = NULL; + gpointer copy = NULL; + gsize copy_size = 0; + SampleHandlerUserData *s = (SampleHandlerUserData *)user_data; + + g_signal_emit_by_name (object, "pull-sample", &sample); + if (sample) { + buffer = gst_sample_get_buffer(sample); + if (buffer) { + gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), ©, ©_size); + goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), s->pipelineId); + } + gst_sample_unref (sample); + } + + return GST_FLOW_OK; +} + +GstElement *gstreamer_send_create_pipeline(char *pipeline) { + gst_init(NULL, NULL); + GError *error = NULL; + return gst_parse_launch(pipeline, &error); +} + +void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) { + SampleHandlerUserData *s = calloc(1, sizeof(SampleHandlerUserData)); + s->pipelineId = pipelineId; + + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); + gst_bus_add_watch(bus, gstreamer_send_bus_call, NULL); + gst_object_unref(bus); + + GstElement *appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink"); + g_object_set(appsink, "emit-signals", TRUE, NULL); + g_signal_connect(appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), s); + gst_object_unref(appsink); + + gst_element_set_state(pipeline, GST_STATE_PLAYING); +} + +void gstreamer_send_stop_pipeline(GstElement *pipeline) { + gst_element_set_state(pipeline, GST_STATE_NULL); +} + + diff --git a/server/internal/gst/gst.go b/server/internal/gst/gst.go new file mode 100644 index 0000000..e748135 --- /dev/null +++ b/server/internal/gst/gst.go @@ -0,0 +1,125 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 + +#include "gst.h" + +*/ +import "C" +import ( + "fmt" + "io" + "sync" + "unsafe" + + "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v2/pkg/media" +) + +func init() { + go C.gstreamer_send_start_mainloop() +} + +// Pipeline is a wrapper for a GStreamer Pipeline +type Pipeline struct { + Pipeline *C.GstElement + tracks []*webrtc.Track + id int + codecName string + clockRate float32 +} + +var pipelines = make(map[int]*Pipeline) +var pipelinesLock sync.Mutex + +const ( + videoClockRate = 90000 + audioClockRate = 48000 + pcmClockRate = 8000 +) + +// CreatePipeline creates a GStreamer Pipeline +func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline { + pipelineStr := "appsink name=appsink" + var clockRate float32 + + switch codecName { + case webrtc.VP8: + pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr + clockRate = videoClockRate + + case webrtc.VP9: + pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr + clockRate = videoClockRate + + case webrtc.H264: + pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr + clockRate = videoClockRate + + case webrtc.Opus: + pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr + clockRate = audioClockRate + + case webrtc.G722: + pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr + clockRate = audioClockRate + + case webrtc.PCMU: + pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr + clockRate = pcmClockRate + + case webrtc.PCMA: + pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr + clockRate = pcmClockRate + + default: + panic("Unhandled codec " + codecName) + } + + pipelineStrUnsafe := C.CString(pipelineStr) + defer C.free(unsafe.Pointer(pipelineStrUnsafe)) + + pipelinesLock.Lock() + defer pipelinesLock.Unlock() + + pipeline := &Pipeline{ + Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe), + tracks: tracks, + id: len(pipelines), + codecName: codecName, + clockRate: clockRate, + } + + pipelines[pipeline.id] = pipeline + return pipeline +} + +// Start starts the GStreamer Pipeline +func (p *Pipeline) Start() { + C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id)) +} + +// Stop stops the GStreamer Pipeline +func (p *Pipeline) Stop() { + C.gstreamer_send_stop_pipeline(p.Pipeline) +} + +//export goHandlePipelineBuffer +func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) { + pipelinesLock.Lock() + pipeline, ok := pipelines[int(pipelineID)] + pipelinesLock.Unlock() + + if ok { + samples := uint32(pipeline.clockRate * (float32(duration) / 1000000000)) + for _, t := range pipeline.tracks { + if err := t.WriteSample(media.Sample{Data: C.GoBytes(buffer, bufferLen), Samples: samples}); err != nil && err != io.ErrClosedPipe { + panic(err) + } + } + } else { + fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID)) + } + C.free(buffer) +} diff --git a/server/internal/gst/gst.h b/server/internal/gst/gst.h new file mode 100644 index 0000000..dcdc6ba --- /dev/null +++ b/server/internal/gst/gst.h @@ -0,0 +1,16 @@ +#ifndef GST_H +#define GST_H + +#include +#include +#include +#include + +extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId); + +GstElement *gstreamer_send_create_pipeline(char *pipeline); +void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId); +void gstreamer_send_stop_pipeline(GstElement *pipeline); +void gstreamer_send_start_mainloop(void); + +#endif diff --git a/server/internal/http/api.go b/server/internal/http/api.go new file mode 100644 index 0000000..d0f9846 --- /dev/null +++ b/server/internal/http/api.go @@ -0,0 +1,14 @@ +package http + +import ( + "net/http" + + "n.eko.moe/neko/internal/http/handler" +) + +func New(bind, password, static string) *http.Server { + return &http.Server{ + Addr: bind, + Handler: handler.New(password, static), + } +} diff --git a/server/internal/http/endpoint/endpoint.go b/server/internal/http/endpoint/endpoint.go new file mode 100644 index 0000000..0018523 --- /dev/null +++ b/server/internal/http/endpoint/endpoint.go @@ -0,0 +1,102 @@ +package endpoint + +import ( + "encoding/json" + "fmt" + "net/http" + "runtime/debug" + + "github.com/go-chi/chi/middleware" + "github.com/rs/zerolog/log" +) + +type ( + Endpoint func(http.ResponseWriter, *http.Request) error + + ErrResponse struct { + Status int `json:"status,omitempty"` + Err string `json:"error,omitempty"` + Message string `json:"message,omitempty"` + Details string `json:"details,omitempty"` + Code string `json:"code,omitempty"` + RequestID string `json:"request,omitempty"` + } +) + +func Handle(handler Endpoint) http.HandlerFunc { + fn := func(w http.ResponseWriter, r *http.Request) { + if err := handler(w, r); err != nil { + WriteError(w, r, err) + } + } + + return http.HandlerFunc(fn) +} + +var nonErrorsCodes = map[int]bool{ + 404: true, +} + +func errResponse(input interface{}) *ErrResponse { + var res *ErrResponse + var err interface{} + + switch input.(type) { + case *HandlerError: + e := input.(*HandlerError) + res = &ErrResponse{ + Status: e.Status, + Err: http.StatusText(e.Status), + Message: e.Message, + } + err = e.Err + default: + res = &ErrResponse{ + Status: http.StatusInternalServerError, + Err: http.StatusText(http.StatusInternalServerError), + } + err = input + } + + if err != nil { + switch err.(type) { + case *error: + e := err.(error) + res.Details = e.Error() + break + default: + res.Details = fmt.Sprintf("%+v", err) + break + } + } + + return res +} + +func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) { + hlog := log.With(). + Str("module", "http"). + Logger() + + res := errResponse(err) + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + res.RequestID = reqID + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(res.Status) + + if err := json.NewEncoder(w).Encode(res); err != nil { + hlog.Warn().Err(err).Msg("Failed writing json error response") + } + + if !nonErrorsCodes[res.Status] { + logEntry := middleware.GetLogEntry(r) + if logEntry != nil { + logEntry.Panic(err, debug.Stack()) + } else { + hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err) + } + } +} diff --git a/server/internal/http/endpoint/error.go b/server/internal/http/endpoint/error.go new file mode 100644 index 0000000..3fe3951 --- /dev/null +++ b/server/internal/http/endpoint/error.go @@ -0,0 +1,17 @@ +package endpoint + +import "fmt" + +type HandlerError struct { + Status int + Message string + Err error +} + +func (e *HandlerError) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %s", e.Message, e.Err.Error()) + } + + return e.Message +} diff --git a/server/internal/http/handler/handler.go b/server/internal/http/handler/handler.go new file mode 100644 index 0000000..458f6a9 --- /dev/null +++ b/server/internal/http/handler/handler.go @@ -0,0 +1,55 @@ +package handler + +import ( + "fmt" + "net/http" + "os" + + "n.eko.moe/neko/internal/http/middleware" + "n.eko.moe/neko/internal/http/endpoint" + "n.eko.moe/neko/internal/webrtc" + + "github.com/go-chi/chi" +) + +type Handler struct { + router *chi.Mux + manager *webrtc.WebRTCManager +} + +func New(password, static string) *chi.Mux { + router := chi.NewRouter() + manager, err := webrtc.NewManager(password) + if err != nil { + panic(err) + } + + handler := &Handler{ + router: router, + manager: manager, + } + + router.Use(middleware.Recoverer) // Recover from panics without crashing server + // router.Use(middleware.Logger) // Log API request calls + + router.Get("/ping", endpoint.Handle(handler.Ping)) + router.Get("/ws", endpoint.Handle(handler.WebSocket)) + + fs := http.FileServer(http.Dir(static)) + router.Get("/*", func(w http.ResponseWriter, r *http.Request) { + if _, err := os.Stat(static + r.RequestURI); os.IsNotExist(err) { + http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r) + } else { + fs.ServeHTTP(w, r) + } + }) + + router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error { + return &endpoint.HandlerError{ + Status: http.StatusNotFound, + Message: fmt.Sprintf("Endpoint '%s' is not avalible", r.RequestURI), + } + })) + + return router +} diff --git a/server/internal/http/handler/ping.go b/server/internal/http/handler/ping.go new file mode 100644 index 0000000..b2e2018 --- /dev/null +++ b/server/internal/http/handler/ping.go @@ -0,0 +1,10 @@ +package handler + +import "net/http" + +func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) error { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte(".")) + return nil +} diff --git a/server/internal/http/handler/websocket.go b/server/internal/http/handler/websocket.go new file mode 100644 index 0000000..9087a41 --- /dev/null +++ b/server/internal/http/handler/websocket.go @@ -0,0 +1,9 @@ +package handler + +import ( + "net/http" +) + +func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) error { + return h.manager.Upgrade(w, r) +} diff --git a/server/internal/http/middleware/logger.go b/server/internal/http/middleware/logger.go new file mode 100644 index 0000000..3d7211d --- /dev/null +++ b/server/internal/http/middleware/logger.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/rs/zerolog/log" +) + +func Logger(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + req := map[string]interface{}{} + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + req["id"] = reqID + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + req["scheme"] = scheme + req["proto"] = r.Proto + req["method"] = r.Method + req["remote"] = r.RemoteAddr + req["agent"] = r.UserAgent() + req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) + + fields := map[string]interface{}{} + fields["req"] = req + + entry := &entry{ + fields: fields, + } + + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + t1 := time.Now() + + defer func() { + entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1)) + }() + + next.ServeHTTP(ww, r) + } + return http.HandlerFunc(fn) +} + +type entry struct { + fields map[string]interface{} + errors []map[string]interface{} +} + +func (e *entry) Write(status, bytes int, elapsed time.Duration) { + res := map[string]interface{}{} + 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"] = "api" + + if len(e.errors) > 0 { + e.fields["errors"] = e.errors + log.Error().Fields(e.fields).Msgf("Request failed (%d)", status) + } else { + log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status) + } +} + +func (e *entry) Panic(v interface{}, stack []byte) { + err := map[string]interface{}{} + err["message"] = fmt.Sprintf("%+v", v) + err["stack"] = string(stack) + + e.errors = append(e.errors, err) +} diff --git a/server/internal/http/middleware/middleware.go b/server/internal/http/middleware/middleware.go new file mode 100644 index 0000000..b151b9e --- /dev/null +++ b/server/internal/http/middleware/middleware.go @@ -0,0 +1,12 @@ +package middleware + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type ctxKey struct { + name string +} + +func (k *ctxKey) String() string { + return "neko/ctx/" + k.name +} diff --git a/server/internal/http/middleware/recover.go b/server/internal/http/middleware/recover.go new file mode 100644 index 0000000..53f28e3 --- /dev/null +++ b/server/internal/http/middleware/recover.go @@ -0,0 +1,24 @@ +package middleware + +// The original work was derived from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware + +import ( + "net/http" + + "n.eko.moe/neko/internal/http/endpoint" +) + +func Recoverer(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rvr := recover(); rvr != nil { + endpoint.WriteError(w, r, rvr) + } + }() + + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} diff --git a/server/internal/http/middleware/request.go b/server/internal/http/middleware/request.go new file mode 100644 index 0000000..00be35e --- /dev/null +++ b/server/internal/http/middleware/request.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + "os" + "strings" + "sync/atomic" +) + +// Key to use when setting the request ID. +type ctxKeyRequestID int + +// RequestIDKey is the key that holds the unique request ID in a request context. +const RequestIDKey ctxKeyRequestID = 0 + +var prefix string +var reqid uint64 + +// A quick note on the statistics here: we're trying to calculate the chance that +// two randomly generated base62 prefixes will collide. We use the formula from +// http://en.wikipedia.org/wiki/Birthday_problem +// +// P[m, n] \approx 1 - e^{-m^2/2n} +// +// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server +// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$ +// +// For a $k$ character base-62 identifier, we have $n(k) = 62^k$ +// +// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for +// our purposes, and is surely more than anyone would ever need in practice -- a +// process that is rebooted a handful of times a day for a hundred years has less +// than a millionth of a percent chance of generating two colliding IDs. + +func init() { + hostname, err := os.Hostname() + if hostname == "" || err != nil { + hostname = "localhost" + } + var buf [12]byte + var b64 string + for len(b64) < 10 { + rand.Read(buf[:]) + b64 = base64.StdEncoding.EncodeToString(buf[:]) + b64 = strings.NewReplacer("+", "", "/", "").Replace(b64) + } + + prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10]) +} + +// RequestID is a middleware that injects a request ID into the context of each +// request. A request ID is a string of the form "host.example.com/random-0001", +// where "random" is a base62 random string that uniquely identifies this go +// process, and where the last number is an atomically incremented request +// counter. +func RequestID(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID := r.Header.Get("X-Request-Id") + if requestID == "" { + myid := atomic.AddUint64(&reqid, 1) + requestID = fmt.Sprintf("%s-%06d", prefix, myid) + } + ctx = context.WithValue(ctx, RequestIDKey, requestID) + next.ServeHTTP(w, r.WithContext(ctx)) + } + return http.HandlerFunc(fn) +} + +// GetReqID returns a request ID from the given context if one is present. +// Returns the empty string if a request ID cannot be found. +func GetReqID(ctx context.Context) string { + if ctx == nil { + return "" + } + if reqID, ok := ctx.Value(RequestIDKey).(string); ok { + return reqID + } + return "" +} + +// NextRequestID generates the next request ID in the sequence. +func NextRequestID() uint64 { + return atomic.AddUint64(&reqid, 1) +} diff --git a/server/internal/http/response/response.go b/server/internal/http/response/response.go new file mode 100644 index 0000000..b051525 --- /dev/null +++ b/server/internal/http/response/response.go @@ -0,0 +1,32 @@ +package response + +import ( + "encoding/json" + "net/http" + + "n.eko.moe/neko/internal/http/endpoint" +) + +// JSON encodes data to rw in JSON format. Returns a pointer to a +// HandlerError if encoding fails. +func JSON(w http.ResponseWriter, data interface{}, status int) error { + w.WriteHeader(status) + w.Header().Set("Content-Type", "application/json") + + err := json.NewEncoder(w).Encode(data) + if err != nil { + return &endpoint.HandlerError{ + Status: http.StatusInternalServerError, + Message: "Unable to write JSON response", + Err: err, + } + } + + return nil +} + +// Empty merely sets the response code to NoContent (204). +func Empty(w http.ResponseWriter) error { + w.WriteHeader(http.StatusNoContent) + return nil +} diff --git a/server/internal/keys/keyboard.go b/server/internal/keys/keyboard.go new file mode 100644 index 0000000..65556a2 --- /dev/null +++ b/server/internal/keys/keyboard.go @@ -0,0 +1,203 @@ +package keys + +const KEY_0 = 48 +const KEY_1 = 49 +const KEY_2 = 50 +const KEY_3 = 51 +const KEY_4 = 52 +const KEY_5 = 53 +const KEY_6 = 54 +const KEY_7 = 55 +const KEY_8 = 56 +const KEY_9 = 57 + +const KEY_A = 65 +const KEY_B = 66 +const KEY_C = 67 +const KEY_D = 68 +const KEY_E = 69 +const KEY_F = 70 +const KEY_G = 71 +const KEY_H = 72 +const KEY_I = 73 +const KEY_J = 74 +const KEY_K = 75 +const KEY_L = 76 +const KEY_M = 77 +const KEY_N = 78 +const KEY_O = 79 +const KEY_P = 80 +const KEY_Q = 81 +const KEY_R = 82 +const KEY_S = 83 +const KEY_T = 84 +const KEY_U = 85 +const KEY_V = 86 +const KEY_W = 87 +const KEY_X = 88 +const KEY_Y = 89 +const KEY_Z = 90 + +const KEY_NUMPAD0 = 96 +const KEY_NUMPAD1 = 97 +const KEY_NUMPAD2 = 98 +const KEY_NUMPAD3 = 99 +const KEY_NUMPAD4 = 100 +const KEY_NUMPAD5 = 101 +const KEY_NUMPAD6 = 102 +const KEY_NUMPAD7 = 103 +const KEY_NUMPAD8 = 104 +const KEY_NUMPAD9 = 105 + +const KEY_F1 = 112 +const KEY_F2 = 113 +const KEY_F3 = 114 +const KEY_F4 = 115 +const KEY_F5 = 116 +const KEY_F6 = 117 +const KEY_F7 = 118 +const KEY_F8 = 119 +const KEY_F9 = 120 +const KEY_F10 = 121 +const KEY_F11 = 122 +const KEY_F12 = 123 + +const KEY_BACK_SPACE = 8 +const KEY_TAB = 9 +const KEY_ENTER = 13 +const KEY_ENTER_ALT = 14 +const KEY_SHIFT = 16 +const KEY_CONTROL = 17 +const KEY_ALT = 18 +const KEY_ESCAPE = 27 +const KEY_SPACE = 32 +const KEY_PAGE_UP = 33 +const KEY_PAGE_DOWN = 34 +const KEY_END = 35 +const KEY_LEFT = 37 +const KEY_UP = 38 +const KEY_RIGHT = 39 +const KEY_DOWN = 40 +const KEY_DELETE = 46 +const KEY_SEMICOLON = 59 +const KEY_SEMICOLON_ALT = 186 +const KEY_EQUALS = 61 +const KEY_EQUALS_ALT = 187 +const KEY_MULTIPLY = 106 +const KEY_ADD = 107 +const KEY_SEPARATOR = 108 +const KEY_SUBTRACT = 109 +const KEY_SUBTRACT_ALT = 189 +const KEY_DECIMAL = 110 +const KEY_DIVIDE = 111 +const KEY_COMMA = 188 +const KEY_PERIOD = 190 +const KEY_SLASH = 191 +const KEY_BACK_QUOTE = 192 +const KEY_BACK_SLASH = 220 +const KEY_OPEN_BRACKET = 219 +const KEY_CLOSE_BRACKET = 221 +const KEY_QUOTE = 222 + +var Keyboard = map[int]string{} + +func init() { + Keyboard[KEY_A] = "a" + Keyboard[KEY_B] = "b" + Keyboard[KEY_C] = "c" + Keyboard[KEY_D] = "d" + Keyboard[KEY_E] = "e" + Keyboard[KEY_F] = "f" + Keyboard[KEY_G] = "g" + Keyboard[KEY_H] = "h" + Keyboard[KEY_I] = "i" + Keyboard[KEY_J] = "j" + Keyboard[KEY_K] = "k" + Keyboard[KEY_L] = "l" + Keyboard[KEY_M] = "m" + Keyboard[KEY_N] = "n" + Keyboard[KEY_O] = "o" + Keyboard[KEY_P] = "p" + Keyboard[KEY_Q] = "q" + Keyboard[KEY_R] = "r" + Keyboard[KEY_S] = "s" + Keyboard[KEY_T] = "r" + Keyboard[KEY_U] = "u" + Keyboard[KEY_V] = "v" + Keyboard[KEY_W] = "w" + Keyboard[KEY_X] = "x" + Keyboard[KEY_Y] = "y" + Keyboard[KEY_Z] = "z" + + Keyboard[KEY_0] = "0" + Keyboard[KEY_1] = "1" + Keyboard[KEY_2] = "2" + Keyboard[KEY_3] = "3" + Keyboard[KEY_4] = "4" + Keyboard[KEY_5] = "5" + Keyboard[KEY_6] = "6" + Keyboard[KEY_7] = "7" + Keyboard[KEY_8] = "8" + Keyboard[KEY_9] = "9" + + Keyboard[KEY_NUMPAD0] = "0" + Keyboard[KEY_NUMPAD1] = "1" + Keyboard[KEY_NUMPAD2] = "2" + Keyboard[KEY_NUMPAD3] = "3" + Keyboard[KEY_NUMPAD4] = "4" + Keyboard[KEY_NUMPAD5] = "5" + Keyboard[KEY_NUMPAD6] = "6" + Keyboard[KEY_NUMPAD7] = "7" + Keyboard[KEY_NUMPAD8] = "8" + Keyboard[KEY_NUMPAD9] = "9" + + Keyboard[KEY_F1] = "f1" + Keyboard[KEY_F2] = "f2" + Keyboard[KEY_F3] = "f3" + Keyboard[KEY_F4] = "f4" + Keyboard[KEY_F5] = "f5" + Keyboard[KEY_F6] = "f6" + Keyboard[KEY_F7] = "f7" + Keyboard[KEY_F8] = "f8" + Keyboard[KEY_F9] = "f9" + Keyboard[KEY_F10] = "f10" + Keyboard[KEY_F11] = "f11" + Keyboard[KEY_F12] = "f12" + + Keyboard[KEY_QUOTE] = "'" + Keyboard[KEY_COMMA] = "," + Keyboard[KEY_PERIOD] = "." + Keyboard[KEY_SEMICOLON] = ";" + Keyboard[KEY_SEMICOLON_ALT] = ";" + Keyboard[KEY_SLASH] = "/" + Keyboard[KEY_BACK_SLASH] = "\\" + Keyboard[KEY_BACK_QUOTE] = "`" + Keyboard[KEY_OPEN_BRACKET] = "[" + Keyboard[KEY_CLOSE_BRACKET] = "]" + Keyboard[KEY_EQUALS] = "=" + Keyboard[KEY_EQUALS_ALT] = "=" + Keyboard[KEY_MULTIPLY] = "*" + Keyboard[KEY_ADD] = "+" + Keyboard[KEY_SEPARATOR] = "." + Keyboard[KEY_SUBTRACT] = "-" + Keyboard[KEY_SUBTRACT_ALT] = "-" + Keyboard[KEY_DECIMAL] = "." + Keyboard[KEY_DIVIDE] = "/" + Keyboard[KEY_BACK_SPACE] = "backspace" + Keyboard[KEY_DELETE] = "delete" + Keyboard[KEY_ENTER] = "enter" + Keyboard[KEY_ENTER_ALT] = "enter" + Keyboard[KEY_TAB] = "tab" + Keyboard[KEY_ESCAPE] = "escape" + Keyboard[KEY_UP] = "up" + Keyboard[KEY_DOWN] = "down" + Keyboard[KEY_RIGHT] = "right" + Keyboard[KEY_LEFT] = "left" + Keyboard[KEY_END] = "end" + Keyboard[KEY_PAGE_UP] = "pageup" + Keyboard[KEY_PAGE_DOWN] = "pagedown" + Keyboard[KEY_ALT] = "alt" + Keyboard[KEY_CONTROL] = "control" + Keyboard[KEY_SHIFT] = "shift" + Keyboard[KEY_SPACE] = "space" +} diff --git a/server/internal/keys/mouse.go b/server/internal/keys/mouse.go new file mode 100644 index 0000000..9fec3bf --- /dev/null +++ b/server/internal/keys/mouse.go @@ -0,0 +1,21 @@ +package keys + +const MOUSE_LEFT = 0 +const MOUSE_MIDDLE = 1 +const MOUSE_RIGHT = 2 +const MOUSE_WHEEL_UP = 4 +const MOUSE_WHEEL_DOWN = 5 +const MOUSE_WHEEL_RIGH = 6 +const MOUSE_WHEEL_LEFT = 7 + +var Mouse = map[int]string{} + +func init() { + Mouse[MOUSE_LEFT] = "left" + Mouse[MOUSE_MIDDLE] = "center" + Mouse[MOUSE_RIGHT] = "right" + Mouse[MOUSE_WHEEL_UP] = "wheelUp" + Mouse[MOUSE_WHEEL_DOWN] = "wheelDown" + Mouse[MOUSE_WHEEL_RIGH] = "wheelRight" + Mouse[MOUSE_WHEEL_LEFT] = "wheelLeft" +} diff --git a/server/internal/nanoid/nanoid.go b/server/internal/nanoid/nanoid.go new file mode 100644 index 0000000..c7a742c --- /dev/null +++ b/server/internal/nanoid/nanoid.go @@ -0,0 +1,71 @@ +package nanoid + +import ( + "math/rand" + "time" + + gonanoid "github.com/matoous/go-nanoid" +) + +var nano *NanoID + +func init() { + nano = &NanoID{ + alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + size: 16, + } +} + +func New(alphabet string, size int) *NanoID { + return &NanoID{ + alphabet: alphabet, + size: size, + } +} + +type NanoID struct { + alphabet string + size int +} + +func (n *NanoID) NewID() (string, error) { + return gonanoid.Generate(n.alphabet, n.size) +} + +func (n *NanoID) NewIDSize(size int) (string, error) { + return gonanoid.Generate(n.alphabet, size) +} + +func (n *NanoID) NewIDRang(max int, min int) (string, error) { + rand.Seed(time.Now().Unix()) + return gonanoid.Generate(n.alphabet, rand.Intn(max-min)+min) +} + +func (n *NanoID) GenerateID(alphabet string, size int) (string, error) { + return gonanoid.Generate(alphabet, size) +} + +func (n *NanoID) GenerateIDRange(alphabet string, max int, min int) (string, error) { + rand.Seed(time.Now().Unix()) + return gonanoid.Generate(alphabet, rand.Intn(max-min)+min) +} + +func NewID() (string, error) { + return nano.NewID() +} + +func NewIDSize(size int) (string, error) { + return nano.NewIDSize(size) +} + +func NewIDRang(max int, min int) (string, error) { + return nano.NewIDRang(max, min) +} + +func GenerateID(alphabet string, size int) (string, error) { + return nano.GenerateID(alphabet, size) +} + +func GenerateIDRange(alphabet string, max int, min int) (string, error) { + return nano.GenerateIDRange(alphabet, max, min) +} diff --git a/server/internal/preflight/config.go b/server/internal/preflight/config.go new file mode 100644 index 0000000..e5426d6 --- /dev/null +++ b/server/internal/preflight/config.go @@ -0,0 +1,48 @@ +package preflight + +import ( + "runtime" + + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +func Config(name string) { + config := viper.GetString("neko.config") + + if config != "" { + viper.SetConfigFile(config) // Use config file from the flag. + } else { + if runtime.GOOS == "linux" { + viper.AddConfigPath("/etc/neko/") + } + + viper.AddConfigPath(".") + viper.SetConfigName(name) + } + + viper.SetEnvPrefix("NEKO") + 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) + } + } + + file := viper.ConfigFileUsed() + logger := log.With(). + Bool("debug", viper.GetBool("neko.debug")). + Str("logging", viper.GetString("neko.logs")). + Str("config", file). + Logger() + + if file == "" { + logger.Warn().Msg("Preflight complete without config file") + } else { + logger.Info().Msg("Preflight complete") + } +} diff --git a/server/internal/preflight/logs.go b/server/internal/preflight/logs.go new file mode 100644 index 0000000..e921ad3 --- /dev/null +++ b/server/internal/preflight/logs.go @@ -0,0 +1,60 @@ +package preflight + +import ( + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/diode" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +func Logs(name string) { + zerolog.TimeFieldFormat = "" + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + if viper.GetBool("neko.debug") { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + console := zerolog.ConsoleWriter{Out: os.Stdout} + + if !viper.GetBool("neko.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, name+"-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)) + } +} diff --git a/server/internal/structs/version.go b/server/internal/structs/version.go new file mode 100644 index 0000000..e9ef414 --- /dev/null +++ b/server/internal/structs/version.go @@ -0,0 +1,21 @@ +package structs + +import "fmt" + +type Version struct { + Major string + Minor string + Patch string + Version string + GitVersion string + GitCommit string + GitTreeState string + BuildDate string + GoVersion string + Compiler string + Platform string +} + +func (i *Version) String() string { + return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch) +} diff --git a/server/internal/utils/color.go b/server/internal/utils/color.go new file mode 100644 index 0000000..919887c --- /dev/null +++ b/server/internal/utils/color.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "regexp" +) + +const ( + char = "&" +) + +// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html +var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`) + +func Color(str string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + groups := []string{} + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m" + lastIndex = v[1] + } + + return result + str[lastIndex:] +} + +func Colorf(format string, a ...interface{}) string { + return fmt.Sprintf(Color(format), a...) +} diff --git a/server/internal/utils/header.go b/server/internal/utils/header.go new file mode 100644 index 0000000..7c4646a --- /dev/null +++ b/server/internal/utils/header.go @@ -0,0 +1,10 @@ +package utils + +const Header = `&34 + _ __ __ + / | / /__ / /______ \ /\ + / |/ / _ \/ //_/ __ \ ) ( ') + / /| / __/ ,< / /_/ / ( / ) +/_/ |_/\___/_/|_|\____/ \(__)| +&1&37 nurdism/neko &33%s v%s&0 +` diff --git a/server/internal/utils/map.go b/server/internal/utils/map.go new file mode 100644 index 0000000..a1db944 --- /dev/null +++ b/server/internal/utils/map.go @@ -0,0 +1,25 @@ +package utils + +import ( + "sync" + "sync/atomic" +) + +type CountedSyncMap struct { + sync.Map + len uint64 +} + +func (m *CountedSyncMap) CountedDelete(key interface{}) { + m.Delete(key) + atomic.AddUint64(&m.len, ^uint64(0)) +} + +func (m *CountedSyncMap) CountedStore(key, value interface{}) { + m.Store(key, value) + atomic.AddUint64(&m.len, uint64(1)) +} + +func (m *CountedSyncMap) CountedLen() uint64 { + return atomic.LoadUint64(&m.len) +} diff --git a/server/internal/webrtc/data.go b/server/internal/webrtc/data.go new file mode 100644 index 0000000..177ef6a --- /dev/null +++ b/server/internal/webrtc/data.go @@ -0,0 +1,22 @@ +package webrtc + +type dataHeader struct { + Event uint8 + Length uint16 +} + +type dataMouseMove struct { + dataHeader + X int16 + Y int16 +} + +type dataMouseKey struct { + dataHeader + Key uint8 +} + +type dataKeyboardKey struct { + dataHeader + Key uint16 +} diff --git a/server/internal/webrtc/manager.go b/server/internal/webrtc/manager.go new file mode 100644 index 0000000..777241d --- /dev/null +++ b/server/internal/webrtc/manager.go @@ -0,0 +1,73 @@ +package webrtc + +import ( + "math/rand" + "net/http" + + "github.com/gorilla/websocket" + "github.com/pion/webrtc/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "n.eko.moe/neko/internal/gst" +) + +func NewManager(password string) (*WebRTCManager, error) { + engine := webrtc.MediaEngine{} + + videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000) + video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec) + if err != nil { + return nil, err + } + gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert").Start() + engine.RegisterCodec(videoCodec) + // ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc + + audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000) + audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec) + if err != nil { + return nil, err + } + gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert").Start() + engine.RegisterCodec(audioCodec) + // pulsesrc device=auto_null.monitor ! audioconvert | audiotestsrc + // gst-launch-1.0 -v pulsesrc device=auto_null.monitor ! audioconvert ! vorbisenc ! oggmux ! filesink location=alsasrc.ogg + + return &WebRTCManager{ + logger: log.With().Str("service", "webrtc").Logger(), + engine: engine, + api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)), + video: video, + audio: audio, + controller: "", + password: password, + sessions: make(map[string]*session), + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + }, + config: webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback, + }, + }, nil +} + +type WebRTCManager struct { + logger zerolog.Logger + upgrader websocket.Upgrader + engine webrtc.MediaEngine + api *webrtc.API + config webrtc.Configuration + password string + controller string + sessions map[string]*session + video *webrtc.Track + audio *webrtc.Track +} diff --git a/server/internal/webrtc/messages.go b/server/internal/webrtc/messages.go new file mode 100644 index 0000000..7cd948d --- /dev/null +++ b/server/internal/webrtc/messages.go @@ -0,0 +1,15 @@ +package webrtc + +type message struct { + Event string `json:"event"` +} + +type messageIdentityProvide struct { + message + ID string `json:"id"` +} + +type messageSDP struct { + message + SDP string `json:"sdp"` +} diff --git a/server/internal/webrtc/peer.go b/server/internal/webrtc/peer.go new file mode 100644 index 0000000..b1172c6 --- /dev/null +++ b/server/internal/webrtc/peer.go @@ -0,0 +1,220 @@ +package webrtc + +import ( + "bytes" + "encoding/binary" + "encoding/json" + + "github.com/go-vgo/robotgo" + "github.com/pion/webrtc/v2" + + "n.eko.moe/neko/internal/keys" +) + +func (manager *WebRTCManager) createPeer(session *session, raw []byte) error { + payload := messageSDP{} + if err := json.Unmarshal(raw, &payload); err != nil { + return err + } + + peer, err := manager.api.NewPeerConnection(manager.config) + if err != nil { + return err + } + + _, err = peer.AddTrack(manager.video) + if err != nil { + return err + } + + _, err = peer.AddTrack(manager.audio) + if err != nil { + return err + } + + peer.SetRemoteDescription(webrtc.SessionDescription{ + SDP: payload.SDP, + Type: webrtc.SDPTypeOffer, + }) + + answer, err := peer.CreateAnswer(nil) + if err != nil { + return err + } + + if err = peer.SetLocalDescription(answer); err != nil { + return err + } + + session.send(messageSDP{ + message{Event: "sdp/reply"}, + answer.SDP, + }) + + session.peer = peer + + peer.OnDataChannel(func(d *webrtc.DataChannel) { + d.OnMessage(func(msg webrtc.DataChannelMessage) { + if err = manager.onData(session, msg); err != nil { + manager.logger.Warn().Err(err).Msg("onData failed") + } + }) + }) + + peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) { + switch connectionState { + case webrtc.PeerConnectionStateDisconnected: + case webrtc.PeerConnectionStateFailed: + manager.destroy(session) + break + case webrtc.PeerConnectionStateConnected: + manager.logger.Info().Str("ID", session.id).Msg("Peer connected") + break + } + }) + + return nil +} + +var debounce = map[int]bool{} + +func (manager *WebRTCManager) onData(session *session, msg webrtc.DataChannelMessage) error { + if manager.controller != session.id { + return nil + } + + header := &dataHeader{} + buffer := bytes.NewBuffer(msg.Data) + byt := make([]byte, 3) + _, err := buffer.Read(byt) + if err != nil { + return err + } + + err = binary.Read(bytes.NewBuffer(byt), binary.LittleEndian, header) + if err != nil { + return err + } + + buffer = bytes.NewBuffer(msg.Data) + + switch header.Event { + case 0x01: // MOUSE_MOVE + payload := &dataMouseMove{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + robotgo.Move(int(payload.X), int(payload.Y)) + break + case 0x02: // MOUSE_UP + payload := &dataMouseKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + + if key, ok := keys.Mouse[int(payload.Key)]; ok { + if !debounce[int(payload.Key)] { + return nil + } + debounce[int(payload.Key)] = false + robotgo.MouseToggle("up", key) + } else { + manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key) + } + break + case 0x03: // MOUSE_DOWN + payload := &dataMouseKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + + if key, ok := keys.Mouse[int(payload.Key)]; ok { + if debounce[int(payload.Key)] { + return nil + } + debounce[int(payload.Key)] = true + + robotgo.MouseToggle("down", key) + } else { + manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key) + } + break + case 0x04: // MOUSE_CLK + payload := &dataMouseKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + + if key, ok := keys.Mouse[int(payload.Key)]; ok { + switch int(payload.Key) { + case keys.MOUSE_WHEEL_DOWN: + robotgo.Scroll(0, -10) + break + case keys.MOUSE_WHEEL_UP: + robotgo.Scroll(0, 10) + break + case keys.MOUSE_WHEEL_LEFT: + robotgo.Scroll(-10, 0) + break + case keys.MOUSE_WHEEL_RIGH: + robotgo.Scroll(10, 0) + break + default: + robotgo.Click(key, false) + } + } else { + manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", payload.Key) + } + break + case 0x05: // KEY_DOWN + payload := &dataKeyboardKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + if key, ok := keys.Keyboard[int(payload.Key)]; ok { + if debounce[int(payload.Key)] { + return nil + } + debounce[int(payload.Key)] = true + robotgo.KeyToggle(key, "down") + } else { + manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", payload.Key) + } + break + case 0x06: // KEY_UP + payload := &dataKeyboardKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + if key, ok := keys.Keyboard[int(payload.Key)]; ok { + if !debounce[int(payload.Key)] { + return nil + } + debounce[int(payload.Key)] = false + robotgo.KeyToggle(key, "up") + } else { + manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", payload.Key) + } + break + case 0x07: // KEY_CLK + payload := &dataKeyboardKey{} + err := binary.Read(buffer, binary.LittleEndian, payload) + if err != nil { + return err + } + if key, ok := keys.Keyboard[int(payload.Key)]; ok { + robotgo.KeyTap(key) + } else { + manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", payload.Key) + } + break + } + + return nil +} diff --git a/server/internal/webrtc/session.go b/server/internal/webrtc/session.go new file mode 100644 index 0000000..11504a9 --- /dev/null +++ b/server/internal/webrtc/session.go @@ -0,0 +1,41 @@ +package webrtc + +import ( + "sync" + + "github.com/gorilla/websocket" + "github.com/pion/webrtc/v2" +) + +type session struct { + id string + socket *websocket.Conn + peer *webrtc.PeerConnection + mu sync.Mutex +} + +func (session *session) send(v interface{}) error { + session.mu.Lock() + defer session.mu.Unlock() + + if session.socket != nil { + return session.socket.WriteJSON(v) + } + + return nil +} + +func (session *session) destroy() error { + if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected { + if err := session.peer.Close(); err != nil { + return err + } + } + + if session.socket != nil { + if err := session.socket.Close(); err != nil { + return err + } + } + return nil +} diff --git a/server/internal/webrtc/websocket.go b/server/internal/webrtc/websocket.go new file mode 100644 index 0000000..7677715 --- /dev/null +++ b/server/internal/webrtc/websocket.go @@ -0,0 +1,222 @@ +package webrtc + +import ( + "encoding/json" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + + "n.eko.moe/neko/internal/nanoid" +) + +const ( + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = 60 * time.Second +) + +func (manager *WebRTCManager) Upgrade(w http.ResponseWriter, r *http.Request) error { + socket, err := manager.upgrader.Upgrade(w, r, nil) + if err != nil { + manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!") + return nil + } + + sessionID, ok := manager.authenticate(r) + if ok != true { + manager.logger.Warn().Msg("Authenticatetion failed") + if err = socket.Close(); err != nil { + return err + } + return nil + } + + session := &session{ + id: sessionID, + socket: socket, + mu: sync.Mutex{}, + } + + manager.logger. + Info(). + Str("ID", sessionID). + Str("RemoteAddr", socket.RemoteAddr().String()). + Msg("Created Session") + + manager.sessions[sessionID] = session + + defer func() { + manager.destroy(session) + }() + + if err = manager.onConnected(session); err != nil { + manager.logger.Error().Err(err).Msg("onConnected failed!") + return nil + } + + manager.handleWS(session) + + return nil +} + +func (manager *WebRTCManager) authenticate(r *http.Request) (sessionID string, ok bool) { + + passwords, ok := r.URL.Query()["password"] + if !ok || len(passwords[0]) < 1 { + return "", false + } + + if passwords[0] != manager.password { + manager.logger.Warn().Str("Password", passwords[0]).Msg("Wrong password: ") + return "", false + } + + id, err := nanoid.NewIDSize(32) + if err != nil { + return "", false + } + return id, true +} + +func (manager *WebRTCManager) onConnected(session *session) error { + if err := session.send(messageIdentityProvide{ + message: message{Event: "identity/provide"}, + ID: session.id, + }); err != nil { + return err + } + return nil +} + +func (manager *WebRTCManager) onMessage(session *session, raw []byte) error { + message := message{} + if err := json.Unmarshal(raw, &message); err != nil { + return err + } + + switch message.Event { + case "sdp/provide": + return errors.Wrap(manager.createPeer(session, raw), "sdp/provide failed") + case "control/release": + return errors.Wrap(manager.controlRelease(session), "control/release failed") + case "control/request": + return errors.Wrap(manager.controlRequest(session), "control/request failed") + default: + manager.logger.Warn().Msgf("Unknown client method %s", message.Event) + } + + return nil +} + +func (manager *WebRTCManager) handleWS(session *session) { + bytes := make(chan []byte) + cancel := make(chan struct{}) + ticker := time.NewTicker(pingPeriod) + + go func() { + defer func() { + ticker.Stop() + manager.logger.Info().Str("RemoteAddr", session.socket.RemoteAddr().String()).Msg("Handle WS ending") + manager.destroy(session) + }() + + for { + _, raw, err := session.socket.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + manager.logger.Warn().Err(err).Msg("ReadMessage error") + } + break + } + bytes <- raw + } + }() + + for { + select { + case raw := <-bytes: + manager.logger.Info(). + Str("ID", session.id). + Str("Message", string(raw)). + Msg("Reading from Websocket") + if err := manager.onMessage(session, raw); err != nil { + manager.logger.Error().Err(err).Msg("onClientMessage has failed") + return + } + case <-cancel: + return + case _ = <-ticker.C: + if err := session.socket.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +func (manager *WebRTCManager) destroy(session *session) { + if manager.controller == session.id { + manager.controller = "" + for id, sess := range manager.sessions { + if id != session.id { + if err := sess.send(message{Event: "control/released"}); err != nil { + manager.logger.Error().Err(err).Msg("session.send has failed") + } + } + } + } + + if err := session.destroy(); err != nil { + manager.logger.Error().Err(err).Msg("session.destroy has failed") + } + + delete(manager.sessions, session.id) +} + +func (manager *WebRTCManager) controlRelease(session *session) error { + if manager.controller == session.id { + manager.controller = "" + + if err := session.send(message{Event: "control/release"}); err != nil { + return err + } + + for id, sess := range manager.sessions { + if id != session.id { + if err := sess.send(message{Event: "control/released"}); err != nil { + return err + } + } + } + } + return nil +} + +func (manager *WebRTCManager) controlRequest(session *session) error { + if manager.controller == "" { + manager.controller = session.id + + if err := session.send(message{Event: "control/give"}); err != nil { + return err + } + + for id, sess := range manager.sessions { + if id != session.id { + if err := sess.send(message{Event: "control/given"}); err != nil { + return err + } + } + } + } else { + if err := session.send(message{Event: "control/locked"}); err != nil { + return err + } + + controller, ok := manager.sessions[manager.controller] + if ok { + controller.send(message{Event: "control/requesting"}) + } + } + return nil +} diff --git a/server/neko.go b/server/neko.go new file mode 100644 index 0000000..a870f01 --- /dev/null +++ b/server/neko.go @@ -0,0 +1,116 @@ +package neko + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "runtime" + + "n.eko.moe/neko/internal/config" + "n.eko.moe/neko/internal/structs" + api "n.eko.moe/neko/internal/http" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var ( + // + buildDate = "" + // + gitCommit = "" + // + gitVersion = "" + // + gitState = "" + // Major version when you make incompatible API changes, + major = "0" + // Minor version when you add functionality in a backwards-compatible manner, and + minor = "0" + // Patch version when you make backwards-compatible bug fixeneko. + patch = "0" +) + +var Service *Neko + +func init() { + Service = &Neko{ + Version: &structs.Version{ + Major: major, + Minor: minor, + Patch: patch, + GitVersion: gitVersion, + GitCommit: gitCommit, + GitTreeState: gitState, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + }, + Root: &config.Root{}, + Serve: &config.Serve{}, + } +} + +type Neko struct { + Version *structs.Version + Root *config.Root + Serve *config.Serve + Logger zerolog.Logger + http *http.Server +} + +func (neko *Neko) Preflight() { + neko.Logger = log.With().Str("service", "neko").Logger() +} + +func (neko *Neko) Start() { + server := api.New(neko.Serve.Bind, neko.Serve.Password, neko.Serve.Static) + + if neko.Serve.Cert != "" && neko.Serve.Key != "" { + go func() { + if err := server.ListenAndServeTLS(neko.Serve.Cert, neko.Serve.Key); err != http.ErrServerClosed { + neko.Logger.Panic().Err(err).Msg("Unable to start https server") + } + }() + neko.Logger.Info().Msgf("HTTPS listening on %s", server.Addr) + } else { + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + neko.Logger.Panic().Err(err).Msg("Unable to start http server") + } + }() + neko.Logger.Warn().Msgf("HTTP listening on %s", server.Addr) + } + + neko.http = server +} + +func (neko *Neko) Shutdown() { + if neko.http != nil { + if err := neko.http.Shutdown(context.Background()); err != nil { + neko.Logger.Err(err).Msg("HTTP server shutdown with an error") + } else { + neko.Logger.Debug().Msg("HTTP server shutdown") + } + } +} + +func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) { + neko.Logger.Info().Msg("Starting HTTP/S server") + neko.Start() + + neko.Logger.Info().Msg("Service ready") + + quit := make(chan os.Signal) + signal.Notify(quit, os.Interrupt) + sig := <-quit + + neko.Logger.Warn().Msgf("Received %s, attempting graceful shutdown: \n", sig) + + neko.Shutdown() + neko.Logger.Info().Msg("Shutting down complete") +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..068ddcd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./client/tsconfig.json" +} \ No newline at end of file