Archived
2
0

large refactor, fixes #2

This commit is contained in:
Craig 2020-01-18 23:30:09 +00:00
parent 2729c66ccc
commit 7aa034f3ba
59 changed files with 2766 additions and 1345 deletions

View File

@ -9,6 +9,6 @@ npm install && npm run build
cd ../ cd ../
sudo docker build -f Dockerfile -t nurdism/neko . sudo docker build -f Dockerfile -t nurdism/neko .
# sudo docker run -p 8080:8080 --shm-size=2gb nurdism/neko:latest # sudo docker run -p 8080:8080 --shm-size=1gb nurdism/neko:latest
# sudo docker run --network host --shm-size=2gb nurdism/neko:latest # sudo docker run --network host --shm-size=1gb nurdism/neko:latest
# sudo docker run --network host --shm-size=2gb -it nurdism/neko:latest /bin/bash # sudo docker run --network host --shm-size=1gb -it nurdism/neko:latest /bin/bash

View File

@ -2,6 +2,16 @@
# usefull debugging tools pavucontrol htop x11vnc # usefull debugging tools pavucontrol htop x11vnc
# 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 mkdir -p /var/run/dbus /etc/neko sudo mkdir -p /var/run/dbus /etc/neko
sudo /etc/init.d/dbus start sudo /etc/init.d/dbus start
@ -19,16 +29,6 @@ if [ ! -f /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.x
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 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 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 ../server/bin/neko /usr/bin/neko
sudo cp -R ../client/dist /var/www/ sudo cp -R ../client/dist /var/www/

View File

@ -6,7 +6,7 @@
<img src="https://github.com/nurdism/neko/raw/master/.github/demo.gif" width="650" height="auto"/> <img src="https://github.com/nurdism/neko/raw/master/.github/demo.gif" width="650" height="auto"/>
</div> </div>
# n.eko # **n**.eko
This is a proof of concept project I threw together over the last few days, it's ugly, it's 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 a proof of concept project I threw together over the last few days, it's ugly, it's 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. The client 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. This is by no means a fully featured clone of rabbit. The client 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.
@ -15,15 +15,23 @@ This is by no means a fully featured clone of rabbit. The client has no concept
I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the domain [n.eko.moe](https://n.eko.moe/) and I love the logo /shrug I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the domain [n.eko.moe](https://n.eko.moe/) and I love the logo /shrug
### Super easy mode setup ### Super easy mode setup
1. Head on to [Digital Ocean](https://digitalocean.com/) and create an account 1. Deploy a server
2. Go [here](https://marketplace.digitalocean.com/apps/docker) and click on "Create Docker Droplet"
3. Configure the droplet: *Recomended specs:*
* **576p** [$15/mo] Not Recommended | Resolution | Cores | Ram | Recommendation |
* **720p** [$40/mo] Good Performance |------------|-------|-----|------------------|
* **720p** [$80/mo] Recommended | **576p** | 2 | 2gb | Not Recommended |
* **720p+** [$160/mo] Best Performance | **720p** | 4 | 4gb | Good Performance |
4. [Login to the droplet over ssh](https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/) | **720p** | 6 | 6gb | Recommended |
5. Run these commands: | **720p+** | 8 | 8gb | Best Performance |
2. [SSH into your VPS](https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/)
3. Install Docker
```
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
```
4. Run these commands:
``` ```
ufw allow 80/tcp ufw allow 80/tcp
wget https://raw.githubusercontent.com/nurdism/neko/master/docker-compose.yaml wget https://raw.githubusercontent.com/nurdism/neko/master/docker-compose.yaml
@ -33,14 +41,12 @@ I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the doma
> *Protip*: Run `nano docker-compose.yaml` to edit the settings, then press *ctrl+x* to exit and save the file. > *Protip*: Run `nano docker-compose.yaml` to edit the settings, then press *ctrl+x* to exit and save the file.
Heres the cool part, this will only cost you a little bit (maybe a few cents), *as long as you remember to delete the droplet after you are done!* Droplets are charged per hour, so when you want to share, just create a new droplet and start sharing.
### Running the container: ### Running the container:
``` ```
sudo docker run -p 8080:8080 -e NEKO_PASSWORD='secret' --shm-size=2gb nurdism/neko:latest sudo docker run -p 8080:8080 -e NEKO_PASSWORD='secret' --shm-size=1gb nurdism/neko:latest
``` ```
*Note:* `--shm-size=2gb` is required, firefox-esr tabs will crash (not sure if 2gb is *really* needed) *Note:* `--shm-size=1gb` is required, firefox-esr tabs will crash
### Config ### Config
``` ```
@ -57,14 +63,3 @@ NEKO_CERT= // (SSL)Cert
### Development ### Development
*Highly* recommend you use a [dev container](https://code.visualstudio.com/docs/remote/containers) for [vscode](https://code.visualstudio.com/), I've included the `.devcontainer` I've used to develop this app. To build neko run: *Highly* recommend you use a [dev container](https://code.visualstudio.com/docs/remote/containers) for [vscode](https://code.visualstudio.com/), I've included the `.devcontainer` I've used to develop this app. To build neko run:
`cd .docker && ./build` `cd .docker && ./build`
### Goals
* Remove need for gstreamer, handle encoding/capturing with go
* Make firefox work with client (wrtc issues)
* Remove the need for supervisor and handle starting/stopping the applications
* Find ouw witch audio/video codec is the cheapest as far as data and performance goes (with minimal quality loss)
* Slim down the [robotgo](github.com/go-vgo/robotgo) package and *just* handle the key io
* Add user names for the client
* Add (text) chat for the client
* Add that cool emoji thing rabb.it had
* Slim down the docker container (use alpine)

View File

@ -11,6 +11,7 @@
"no-case-declarations": "off", "no-case-declarations": "off",
"no-dupe-class-members": "off", "no-dupe-class-members": "off",
"no-console": "off", "no-console": "off",
"no-empty": "off"
} }
} }

View File

@ -379,13 +379,11 @@
<script lang="ts"> <script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator' import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
const MOUSE_MOVE = 0x01 const OP_MOVE = 0x01
const MOUSE_UP = 0x02 const OP_SCROLL = 0x02
const MOUSE_DOWN = 0x03 const OP_KEY_DOWN = 0x03
const MOUSE_CLK = 0x04 const OP_KEY_UP = 0x04
const KEY_DOWN = 0x05 // const OP_KEY_CLK = 0x05
const KEY_UP = 0x06
const KEY_CLK = 0x07
@Component({ name: 'stream-video' }) @Component({ name: 'stream-video' })
export default class extends Vue { export default class extends Vue {
@ -429,6 +427,7 @@
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.onResise) window.removeEventListener('resize', this.onResise)
this.onClose()
} }
toggleControl() { toggleControl() {
@ -443,6 +442,7 @@
} }
toggleMedia() { toggleMedia() {
console.log(`[NEKO] toggleMedia`, this.playing)
if (!this.playing) { if (!this.playing) {
this._player this._player
.play() .play()
@ -479,8 +479,8 @@
this.ws.onmessage = this.onMessage.bind(this) this.ws.onmessage = this.onMessage.bind(this)
this.ws.onerror = event => console.error((event as ErrorEvent).error) this.ws.onerror = event => console.error((event as ErrorEvent).error)
this.ws.onclose = event => this.onClose.bind(this) this.ws.onclose = event => this.onClose.bind(this)
this.onConnecting()
this.timeout = setTimeout(this.onTimeout.bind(this), 5000) this.timeout = setTimeout(this.onTimeout.bind(this), 5000)
this.onConnecting()
} }
createPeer() { createPeer() {
@ -488,7 +488,10 @@
return return
} }
this.peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }) this.peer = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.services.mozilla.com' }],
})
this.peer.onicecandidate = event => { this.peer.onicecandidate = event => {
if (event.candidate === null && this.peer!.localDescription) { if (event.candidate === null && this.peer!.localDescription) {
this.ws!.send( this.ws!.send(
@ -512,6 +515,7 @@
break break
} }
} }
this.peer.ontrack = this.onTrack.bind(this) this.peer.ontrack = this.onTrack.bind(this)
this.peer.addTransceiver('audio', { direction: 'recvonly' }) this.peer.addTransceiver('audio', { direction: 'recvonly' })
this.peer.addTransceiver('video', { direction: 'recvonly' }) this.peer.addTransceiver('video', { direction: 'recvonly' })
@ -538,52 +542,33 @@
case 'mousemove': case 'mousemove':
buffer = new ArrayBuffer(7) buffer = new ArrayBuffer(7)
payload = new DataView(buffer) payload = new DataView(buffer)
payload.setUint8(0, MOUSE_MOVE) payload.setUint8(0, OP_MOVE)
payload.setUint16(1, 4, true) payload.setUint16(1, 4, true)
payload.setUint16(3, Math.round((this.width / data.rect.width) * (data.x - data.rect.left)), true) payload.setUint16(3, Math.round((this.width / data.rect.width) * (data.x - data.rect.left)), true)
payload.setUint16(5, Math.round((this.height / data.rect.height) * (data.y - data.rect.top)), true) payload.setUint16(5, Math.round((this.height / data.rect.height) * (data.y - data.rect.top)), true)
break break
case 'wheel': case 'wheel':
buffer = new ArrayBuffer(4) buffer = new ArrayBuffer(7)
payload = new DataView(buffer) payload = new DataView(buffer)
payload.setUint8(0, MOUSE_CLK) payload.setUint8(0, OP_SCROLL)
payload.setUint16(1, 1, true) payload.setUint16(1, 4, true)
payload.setInt16(3, (data.x * -1) / 10, true)
const ydir = Math.sign(data.y) payload.setInt16(5, (data.y * -1) / 10, true)
const xdir = Math.sign(data.x)
if ((!xdir && !ydir) || (xdir && ydir)) return
if (ydir && ydir < 0) payload.setUint8(3, 4)
if (ydir && ydir > 0) payload.setUint8(3, 5)
if (xdir && xdir < 0) payload.setUint8(3, 6)
if (xdir && xdir > 0) payload.setUint8(3, 7)
break
case 'mousedown':
buffer = new ArrayBuffer(4)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_DOWN)
payload.setUint16(1, 1, true)
payload.setUint8(3, data.key)
break
case 'mouseup':
buffer = new ArrayBuffer(4)
payload = new DataView(buffer)
payload.setUint8(0, MOUSE_UP)
payload.setUint16(1, 1, true)
payload.setUint8(3, data.key)
break break
case 'keydown': case 'keydown':
case 'mousedown':
buffer = new ArrayBuffer(5) buffer = new ArrayBuffer(5)
payload = new DataView(buffer) payload = new DataView(buffer)
payload.setUint8(0, KEY_DOWN) payload.setUint8(0, OP_KEY_DOWN)
payload.setUint16(1, 2, true) payload.setUint16(1, 1, true)
payload.setUint16(3, data.key, true) payload.setUint16(3, data.key, true)
break break
case 'keyup': case 'keyup':
case 'mouseup':
buffer = new ArrayBuffer(5) buffer = new ArrayBuffer(5)
payload = new DataView(buffer) payload = new DataView(buffer)
payload.setUint8(0, KEY_UP) payload.setUint8(0, OP_KEY_UP)
payload.setUint16(1, 2, true) payload.setUint16(1, 1, true)
payload.setUint16(3, data.key, true) payload.setUint16(3, data.key, true)
break break
} }
@ -645,16 +630,19 @@
onWheel(e: WheelEvent) { onWheel(e: WheelEvent) {
this.onMousePos(e) this.onMousePos(e)
this.updateControles('wheel', { x: e.deltaX, y: e.deltaY }) this.updateControles('wheel', { x: e.deltaX, y: e.deltaY })
console.log('wheel', { x: e.deltaX, y: e.deltaY })
} }
onMouseDown(e: MouseEvent) { onMouseDown(e: MouseEvent) {
this.onMousePos(e) this.onMousePos(e)
this.updateControles('mousedown', { key: e.button }) this.updateControles('mousedown', { key: e.button })
console.log('mousedown', { key: e.button })
} }
onMouseUp(e: MouseEvent) { onMouseUp(e: MouseEvent) {
this.onMousePos(e) this.onMousePos(e)
this.updateControles('mouseup', { key: e.button }) this.updateControles('mouseup', { key: e.button })
console.log('mouseup', { key: e.button })
} }
onMouseMove(e: MouseEvent) { onMouseMove(e: MouseEvent) {
@ -675,6 +663,7 @@
return return
} }
this.updateControles('keydown', { key: e.keyCode }) this.updateControles('keydown', { key: e.keyCode })
console.log('keydown', { key: e.keyCode })
} }
onKeyUp(e: KeyboardEvent) { onKeyUp(e: KeyboardEvent) {
@ -682,6 +671,7 @@
return return
} }
this.updateControles('keyup', { key: e.keyCode }) this.updateControles('keyup', { key: e.keyCode })
console.log('keyup', { key: e.keyCode })
} }
onResise() { onResise() {
@ -769,7 +759,7 @@
}) })
break break
default: default:
console.warn(`[NEKO] Unknown message event ${event}`) console.warn(`[NEKO] unknown message event ${event}`)
} }
} }
@ -778,6 +768,8 @@
return return
} }
console.log(`[NEKO] track recieved`, event)
this.stream = event.streams[0] this.stream = event.streams[0]
if (!this.stream) { if (!this.stream) {
return return
@ -830,11 +822,25 @@
this.controlling = false this.controlling = false
this.connected = false this.connected = false
this.connecting = false this.connecting = false
this.ws = undefined
this.peer = undefined if (this.ws) {
try {
this.ws.close()
} catch (err) {}
this.ws = undefined
}
if (this.peer) {
try {
this.peer.close()
} catch (err) {}
this.peer = undefined
}
if (this.playing) { if (this.playing) {
this.toggleMedia() this.toggleMedia()
} }
this.$notify({ this.$notify({
group: 'neko', group: 'neko',
type: 'error', type: 'error',

View File

@ -42,6 +42,11 @@
"gruntfuggly.todo-tree", "gruntfuggly.todo-tree",
"swyphcosmo.spellchecker", "swyphcosmo.spellchecker",
"eamodio.gitlens" "eamodio.gitlens"
] ],
"files.associations": {
"iostream": "cpp",
"xtest.h": "c",
"xlib.h": "c"
}
} }
} }

View File

@ -1,2 +1,3 @@
DISPLAY=:0 DISPLAY=:0
PULSE_SERVER=unix:/tmp/pulseaudio.socket PULSE_SERVER=unix:/tmp/pulseaudio.socket
GST_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/gstreamer-1.0/

View File

@ -10,7 +10,7 @@
"envFile": "${workspaceFolder}/.env.development", "envFile": "${workspaceFolder}/.env.development",
"output": "${workspaceFolder}/bin/debug/neko", "output": "${workspaceFolder}/bin/debug/neko",
"cwd": "${workspaceFolder}/", "cwd": "${workspaceFolder}/",
"args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "123"] "args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "neko", "--admin", "admin"]
} }
] ]
} }

View File

@ -6,4 +6,4 @@ 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}" LDFLAGS=-ldflags "-s -X version.buildTime=${BUILD_TIME} -X version.gitRevision=${GIT_DIRTY}${GIT_REVISION} -X version.gitBranch=${GIT_BRANCH}"
build: build:
go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go

View File

@ -3,16 +3,16 @@ package main
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log"
"n.eko.moe/neko" "n.eko.moe/neko"
"n.eko.moe/neko/cmd" "n.eko.moe/neko/cmd"
"n.eko.moe/neko/internal/utils" "n.eko.moe/neko/internal/utils"
"github.com/rs/zerolog/log"
) )
func main() { func main() {
fmt.Print(utils.Colorf(utils.Header, "server", neko.Service.Version)) fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
log.Panic().Err(err).Msg("Failed to execute command") log.Panic().Err(err).Msg("failed to execute command")
} }
} }

View File

@ -3,10 +3,11 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"n.eko.moe/neko" "n.eko.moe/neko"
"n.eko.moe/neko/internal/preflight" "n.eko.moe/neko/internal/preflight"
"github.com/spf13/cobra"
) )
func Execute() error { func Execute() error {
@ -15,8 +16,8 @@ func Execute() error {
var root = &cobra.Command{ var root = &cobra.Command{
Use: "neko", Use: "neko",
Short: "", Short: "neko streaming server",
Long: ``, Long: `neko streaming server`,
Version: neko.Service.Version.String(), Version: neko.Service.Version.String(),
} }
@ -28,8 +29,8 @@ func init() {
}) })
if err := neko.Service.Root.Init(root); err != nil { if err := neko.Service.Root.Init(root); err != nil {
neko.Service.Logger.Panic().Err(err).Msg("Unable to run command") log.Panic().Err(err).Msg("unable to run root command")
} }
root.SetVersionTemplate(fmt.Sprintf("Version: %s\n", neko.Service.Version)) root.SetVersionTemplate(fmt.Sprintf("version: %s\n", neko.Service.Version))
} }

View File

@ -11,13 +11,15 @@ import (
func init() { func init() {
command := &cobra.Command{ command := &cobra.Command{
Use: "serve", Use: "serve",
Short: "", Short: "serve neko streaming server",
Long: ``, Long: `serve neko streaming server`,
Run: neko.Service.ServeCommand, Run: neko.Service.ServeCommand,
} }
configs := []config.Config{ configs := []config.Config{
neko.Service.Serve, neko.Service.Server,
neko.Service.WebRTC,
neko.Service.WebSocket,
} }
cobra.OnInitialize(func() { cobra.OnInitialize(func() {
@ -29,7 +31,7 @@ func init() {
for _, cfg := range configs { for _, cfg := range configs {
if err := cfg.Init(command); err != nil { if err := cfg.Init(command); err != nil {
log.Panic().Err(err).Msg("Unable to run command") log.Panic().Err(err).Msg("unable to run serve command")
} }
} }

View File

@ -4,9 +4,10 @@ go 1.13
require ( require (
github.com/go-chi/chi v4.0.3+incompatible 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/gorilla/websocket v1.4.1
github.com/kataras/go-events v0.0.2
github.com/matoous/go-nanoid v1.1.0 github.com/matoous/go-nanoid v1.1.0
github.com/pion/logging v0.2.2
github.com/pion/webrtc/v2 v2.1.18 github.com/pion/webrtc/v2 v2.1.18
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/rs/zerolog v1.17.2 github.com/rs/zerolog v1.17.2

View File

@ -1,13 +1,7 @@
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= 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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/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/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/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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -38,12 +32,7 @@ github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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-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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -74,6 +63,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 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/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kataras/go-events v0.0.2 h1:fhyUPXvUbrjIPmH4vRdrAAGoNzdcwJPQmjhg47m1nMU=
github.com/kataras/go-events v0.0.2/go.mod h1:6IxMW59VJdEIqj3bjFGJvGLRdb0WHtrlxPZy9qXctcg=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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/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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -85,8 +76,6 @@ 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/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 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= 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.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -106,11 +95,6 @@ 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/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 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 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 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 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 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss=
@ -165,21 +149,11 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= 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/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/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/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/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 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= 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/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/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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@ -211,12 +185,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 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 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/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/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/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= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -231,9 +199,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-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/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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -244,7 +209,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-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-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-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 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -262,9 +226,6 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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-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 h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

View File

@ -5,16 +5,14 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type Serve struct { type Server struct {
Cert string Cert string
Key string Key string
Bind string Bind string
Password string
Static string Static string
} }
func (Serve) Init(cmd *cobra.Command) error { func (Server) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko") cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko")
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil { if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
return err return err
@ -30,12 +28,7 @@ func (Serve) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream") cmd.PersistentFlags().String("static", "./www", "Neko client files to serve")
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 { if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
return err return err
} }
@ -43,10 +36,9 @@ func (Serve) Init(cmd *cobra.Command) error {
return nil return nil
} }
func (s *Serve) Set() { func (s *Server) Set() {
s.Cert = viper.GetString("cert") s.Cert = viper.GetString("cert")
s.Key = viper.GetString("key") s.Key = viper.GetString("key")
s.Bind = viper.GetString("bind") s.Bind = viper.GetString("bind")
s.Password = viper.GetString("password")
s.Static = viper.GetString("static") s.Static = viper.GetString("static")
} }

View File

@ -0,0 +1,60 @@
package config
import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type WebRTC struct {
Device string
AudioCodec string
AudioParams string
Display string
VideoCodec string
VideoParams string
}
func (WebRTC) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("device", "auto_null.monitor", "Audio device to capture")
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
return err
}
cmd.PersistentFlags().String("ac", "opus", "Audio codec to use for streaming")
if err := viper.BindPFlag("acodec", cmd.PersistentFlags().Lookup("ac")); err != nil {
return err
}
cmd.PersistentFlags().String("ap", "", "Audio codec parameters to use for streaming")
if err := viper.BindPFlag("aparams", cmd.PersistentFlags().Lookup("ap")); err != nil {
return err
}
cmd.PersistentFlags().String("display", ":0.0", "XDisplay to capture")
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
return err
}
cmd.PersistentFlags().String("vc", "vp8", "Video codec to use for streaming")
if err := viper.BindPFlag("vcodec", cmd.PersistentFlags().Lookup("vc")); err != nil {
return err
}
cmd.PersistentFlags().String("vp", "", "Video codec parameters to use for streaming")
if err := viper.BindPFlag("vparams", cmd.PersistentFlags().Lookup("vp")); err != nil {
return err
}
return nil
}
func (s *WebRTC) Set() {
s.Device = strings.ToLower(viper.GetString("device"))
s.AudioCodec = strings.ToLower(viper.GetString("acodec"))
s.AudioParams = strings.ToLower(viper.GetString("aparams"))
s.Display = strings.ToLower(viper.GetString("display"))
s.VideoCodec = strings.ToLower(viper.GetString("vcodec"))
s.VideoParams = strings.ToLower(viper.GetString("vparams"))
}

View File

@ -0,0 +1,30 @@
package config
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type WebSocket struct {
Password string
AdminPassword string
}
func (WebSocket) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
return err
}
cmd.PersistentFlags().String("admin", "admin", "Admin password for connecting to stream")
if err := viper.BindPFlag("admin", cmd.PersistentFlags().Lookup("admin")); err != nil {
return err
}
return nil
}
func (s *WebSocket) Set() {
s.Password = viper.GetString("password")
s.AdminPassword = viper.GetString("admin")
}

View File

@ -0,0 +1,15 @@
package event
const SDP_REPLY = "sdp/reply"
const SDP_PROVIDE = "sdp/provide"
const CONTROL_RELEASE = "control/release"
const CONTROL_RELEASED = "control/released"
const CONTROL_REQUEST = "control/request"
const CONTROL_GIVE = "control/give"
const CONTROL_GIVEN = "control/given"
const CONTROL_LOCKED = "control/locked"
const CONTROL_REQUESTING = "control/requesting"
const IDENTITY_PROVIDE = "identity/provide"
const IDENTITY_NAME = "identity/name"

View File

@ -6,6 +6,10 @@ typedef struct SampleHandlerUserData {
int pipelineId; int pipelineId;
} SampleHandlerUserData; } SampleHandlerUserData;
void gstreamer_init(void) {
gst_init(NULL, NULL);
}
GMainLoop *gstreamer_send_main_loop = NULL; GMainLoop *gstreamer_send_main_loop = NULL;
void gstreamer_send_start_mainloop(void) { void gstreamer_send_start_mainloop(void) {
gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE); gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE);
@ -60,7 +64,6 @@ GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer use
} }
GstElement *gstreamer_send_create_pipeline(char *pipeline) { GstElement *gstreamer_send_create_pipeline(char *pipeline) {
gst_init(NULL, NULL);
GError *error = NULL; GError *error = NULL;
return gst_parse_launch(pipeline, &error); return gst_parse_launch(pipeline, &error);
} }

View File

@ -17,9 +17,20 @@ import (
"github.com/pion/webrtc/v2/pkg/media" "github.com/pion/webrtc/v2/pkg/media"
) )
func init() { /*
go C.gstreamer_send_start_mainloop() apt-get install \
} libgstreamer1.0-0 \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly\
gstreamer1.0-libav \
gstreamer1.0-doc \
gstreamer1.0-tools \
gstreamer1.0-x \
gstreamer1.0-alsa \
gstreamer1.0-pulseaudio
*/
// Pipeline is a wrapper for a GStreamer Pipeline // Pipeline is a wrapper for a GStreamer Pipeline
type Pipeline struct { type Pipeline struct {
@ -32,6 +43,7 @@ type Pipeline struct {
var pipelines = make(map[int]*Pipeline) var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex var pipelinesLock sync.Mutex
var registry *C.GstRegistry
const ( const (
videoClockRate = 90000 videoClockRate = 90000
@ -39,6 +51,11 @@ const (
pcmClockRate = 8000 pcmClockRate = 8000
) )
func init() {
C.gstreamer_init()
registry = C.gst_registry_get()
}
// CreatePipeline creates a GStreamer Pipeline // CreatePipeline creates a GStreamer Pipeline
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline { func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
pipelineStr := "appsink name=appsink" pipelineStr := "appsink name=appsink"
@ -46,33 +63,97 @@ func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string
switch codecName { switch codecName {
case webrtc.VP8: case webrtc.VP8:
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1
pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr
clockRate = videoClockRate clockRate = videoClockRate
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
panic(err)
}
case webrtc.VP9: case webrtc.VP9:
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp9enc
// Causes panic!
pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr
clockRate = videoClockRate clockRate = videoClockRate
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
panic(err)
}
case webrtc.H264: 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 // https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
// gstreamer1.0-plugins-ugly
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
// gstreamer1.0-plugins-bad
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
pipelineStr = pipelineSrc + " ! openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
clockRate = videoClockRate clockRate = videoClockRate
if err := CheckPlugins([]string{"ximagesrc"}); err != nil {
panic(err)
}
if err := CheckPlugins([]string{"openh264"}); err != nil {
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
if err := CheckPlugins([]string{"x264"}); err != nil {
panic(err)
}
}
case webrtc.Opus: case webrtc.Opus:
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
// opusenc
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
clockRate = audioClockRate clockRate = audioClockRate
if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
panic(err)
}
case webrtc.G722: case webrtc.G722:
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c
// gstreamer1.0-libav
// avenc_g722
pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr
clockRate = audioClockRate clockRate = audioClockRate
if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
panic(err)
}
case webrtc.PCMU: case webrtc.PCMU:
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! mulawenc
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr
clockRate = pcmClockRate clockRate = pcmClockRate
if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
panic(err)
}
case webrtc.PCMA: case webrtc.PCMA:
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! alawenc
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr
clockRate = pcmClockRate clockRate = pcmClockRate
if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
panic(err)
}
default: default:
panic("Unhandled codec " + codecName) panic("Unhandled codec " + codecName)
} }
@ -105,6 +186,21 @@ func (p *Pipeline) Stop() {
C.gstreamer_send_stop_pipeline(p.Pipeline) C.gstreamer_send_stop_pipeline(p.Pipeline)
} }
func CheckPlugins(plugins []string) error {
var plugin *C.GstPlugin
for _, pluginstr := range plugins {
plugincstr := C.CString(pluginstr)
plugin = C.gst_registry_find_plugin(registry, plugincstr)
C.free(unsafe.Pointer(plugincstr))
if plugin == nil {
return fmt.Errorf("Required gstreamer plugin %s not found", pluginstr)
}
}
return nil
}
//export goHandlePipelineBuffer //export goHandlePipelineBuffer
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) { func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
pipelinesLock.Lock() pipelinesLock.Lock()

View File

@ -9,8 +9,10 @@
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId); extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
GstElement *gstreamer_send_create_pipeline(char *pipeline); GstElement *gstreamer_send_create_pipeline(char *pipeline);
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId); void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
void gstreamer_send_stop_pipeline(GstElement *pipeline); void gstreamer_send_stop_pipeline(GstElement *pipeline);
void gstreamer_send_start_mainloop(void); void gstreamer_send_start_mainloop(void);
void gstreamer_init(void);
#endif #endif

94
server/internal/hid/hid.c Normal file
View File

@ -0,0 +1,94 @@
#include "hid.h"
static Display *display = NULL;
static char *name = ":0.0";
static int registered = 0;
static int dirty = 0;
Display *getXDisplay(void) {
/* Close the display if displayName has changed */
if (dirty) {
closeXDisplay();
dirty = 0;
}
if (display == NULL) {
/* First try the user set displayName */
display = XOpenDisplay(name);
/* Then try using environment variable DISPLAY */
if (display == NULL) {
display = XOpenDisplay(NULL);
}
if (display == NULL) {
fputs("Could not open main display\n", stderr);
} else if (!registered) {
atexit(&closeXDisplay);
registered = 1;
}
}
return display;
}
void closeXDisplay(void) {
if (display != NULL) {
XCloseDisplay(display);
display = NULL;
}
}
void setXDisplay(char *input) {
name = strdup(input);
dirty = 1;
}
void mouseMove(int x, int y) {
Display *display = getXDisplay();
XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y);
XSync(display, 0);
}
void mouseScroll(int x, int y) {
int ydir = 4; /* Button 4 is up, 5 is down. */
int xdir = 6;
Display *display = getXDisplay();
if (y < 0) {
ydir = 5;
}
if (x < 0) {
xdir = 7;
}
int xi;
int yi;
for (xi = 0; xi < abs(x); xi++) {
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
XTestFakeButtonEvent(display, xdir, 0, CurrentTime);
}
for (yi = 0; yi < abs(y); yi++) {
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
}
XSync(display, 0);
}
void mouseEvent(unsigned int button, int down) {
Display *display = getXDisplay();
XTestFakeButtonEvent(display, button, down, CurrentTime);
XSync(display, 0);
}
void keyEvent(unsigned long key, int down) {
Display *display = getXDisplay();
KeyCode code = XKeysymToKeycode(display, key);
XTestFakeKeyEvent(display, code, down, CurrentTime);
XSync(display, 0);
}

236
server/internal/hid/hid.go Normal file
View File

@ -0,0 +1,236 @@
package hid
/*
#cgo linux CFLAGS: -I/usr/src
#cgo linux LDFLAGS: -L/usr/src -lX11 -lXtst
#include "hid.h"
*/
import "C"
import (
"fmt"
"time"
"n.eko.moe/neko/internal/hid/keycode"
)
var debounce = make(map[int]time.Time)
var buttons = make(map[int]keycode.Button)
var keys = make(map[int]keycode.Key)
func init() {
keys[keycode.BACKSPACE.Code] = keycode.BACKSPACE
keys[keycode.TAB.Code] = keycode.TAB
keys[keycode.CLEAR.Code] = keycode.CLEAR
keys[keycode.ENTER.Code] = keycode.ENTER
keys[keycode.SHIFT.Code] = keycode.SHIFT
keys[keycode.CTRL.Code] = keycode.CTRL
keys[keycode.ALT.Code] = keycode.ALT
keys[keycode.PAUSE.Code] = keycode.PAUSE
keys[keycode.CAPS_LOCK.Code] = keycode.CAPS_LOCK
keys[keycode.ESCAPE.Code] = keycode.ESCAPE
keys[keycode.SPACE.Code] = keycode.SPACE
keys[keycode.PAGE_UP.Code] = keycode.PAGE_UP
keys[keycode.PAGE_DOWN.Code] = keycode.PAGE_DOWN
keys[keycode.END.Code] = keycode.END
keys[keycode.HOME.Code] = keycode.HOME
keys[keycode.LEFT_ARROW.Code] = keycode.LEFT_ARROW
keys[keycode.UP_ARROW.Code] = keycode.UP_ARROW
keys[keycode.RIGHT_ARROW.Code] = keycode.RIGHT_ARROW
keys[keycode.DOWN_ARROW.Code] = keycode.DOWN_ARROW
keys[keycode.INSERT.Code] = keycode.INSERT
keys[keycode.DELETE.Code] = keycode.DELETE
keys[keycode.KEY_0.Code] = keycode.KEY_0
keys[keycode.KEY_1.Code] = keycode.KEY_1
keys[keycode.KEY_2.Code] = keycode.KEY_2
keys[keycode.KEY_3.Code] = keycode.KEY_3
keys[keycode.KEY_4.Code] = keycode.KEY_4
keys[keycode.KEY_5.Code] = keycode.KEY_5
keys[keycode.KEY_6.Code] = keycode.KEY_6
keys[keycode.KEY_7.Code] = keycode.KEY_7
keys[keycode.KEY_8.Code] = keycode.KEY_8
keys[keycode.KEY_9.Code] = keycode.KEY_9
keys[keycode.KEY_A.Code] = keycode.KEY_A
keys[keycode.KEY_B.Code] = keycode.KEY_B
keys[keycode.KEY_C.Code] = keycode.KEY_C
keys[keycode.KEY_D.Code] = keycode.KEY_D
keys[keycode.KEY_E.Code] = keycode.KEY_E
keys[keycode.KEY_F.Code] = keycode.KEY_F
keys[keycode.KEY_G.Code] = keycode.KEY_G
keys[keycode.KEY_H.Code] = keycode.KEY_H
keys[keycode.KEY_I.Code] = keycode.KEY_I
keys[keycode.KEY_J.Code] = keycode.KEY_J
keys[keycode.KEY_K.Code] = keycode.KEY_K
keys[keycode.KEY_L.Code] = keycode.KEY_L
keys[keycode.KEY_M.Code] = keycode.KEY_M
keys[keycode.KEY_N.Code] = keycode.KEY_N
keys[keycode.KEY_O.Code] = keycode.KEY_O
keys[keycode.KEY_P.Code] = keycode.KEY_P
keys[keycode.KEY_Q.Code] = keycode.KEY_Q
keys[keycode.KEY_R.Code] = keycode.KEY_R
keys[keycode.KEY_S.Code] = keycode.KEY_S
keys[keycode.KEY_T.Code] = keycode.KEY_T
keys[keycode.KEY_U.Code] = keycode.KEY_U
keys[keycode.KEY_V.Code] = keycode.KEY_V
keys[keycode.KEY_W.Code] = keycode.KEY_W
keys[keycode.KEY_X.Code] = keycode.KEY_X
keys[keycode.KEY_Y.Code] = keycode.KEY_Y
keys[keycode.KEY_Z.Code] = keycode.KEY_Z
keys[keycode.WIN_LEFT.Code] = keycode.WIN_LEFT
keys[keycode.WIN_RIGHT.Code] = keycode.WIN_RIGHT
keys[keycode.PAD_0.Code] = keycode.PAD_0
keys[keycode.PAD_1.Code] = keycode.PAD_1
keys[keycode.PAD_2.Code] = keycode.PAD_2
keys[keycode.PAD_3.Code] = keycode.PAD_3
keys[keycode.PAD_4.Code] = keycode.PAD_4
keys[keycode.PAD_5.Code] = keycode.PAD_5
keys[keycode.PAD_6.Code] = keycode.PAD_6
keys[keycode.PAD_7.Code] = keycode.PAD_7
keys[keycode.PAD_8.Code] = keycode.PAD_8
keys[keycode.PAD_9.Code] = keycode.PAD_9
keys[keycode.MULTIPLY.Code] = keycode.MULTIPLY
keys[keycode.ADD.Code] = keycode.ADD
keys[keycode.SUBTRACT.Code] = keycode.SUBTRACT
keys[keycode.DECIMAL.Code] = keycode.DECIMAL
keys[keycode.DIVIDE.Code] = keycode.DIVIDE
keys[keycode.KEY_F1.Code] = keycode.KEY_F1
keys[keycode.KEY_F2.Code] = keycode.KEY_F2
keys[keycode.KEY_F3.Code] = keycode.KEY_F3
keys[keycode.KEY_F4.Code] = keycode.KEY_F4
keys[keycode.KEY_F5.Code] = keycode.KEY_F5
keys[keycode.KEY_F6.Code] = keycode.KEY_F6
keys[keycode.KEY_F7.Code] = keycode.KEY_F7
keys[keycode.KEY_F8.Code] = keycode.KEY_F8
keys[keycode.KEY_F9.Code] = keycode.KEY_F9
keys[keycode.KEY_F10.Code] = keycode.KEY_F10
keys[keycode.KEY_F11.Code] = keycode.KEY_F11
keys[keycode.KEY_F12.Code] = keycode.KEY_F12
keys[keycode.NUM_LOCK.Code] = keycode.NUM_LOCK
keys[keycode.SCROLL_LOCK.Code] = keycode.SCROLL_LOCK
keys[keycode.SEMI_COLON.Code] = keycode.SEMI_COLON
keys[keycode.EQUAL.Code] = keycode.EQUAL
keys[keycode.COMMA.Code] = keycode.COMMA
keys[keycode.DASH.Code] = keycode.DASH
keys[keycode.PERIOD.Code] = keycode.PERIOD
keys[keycode.FORWARD_SLASH.Code] = keycode.FORWARD_SLASH
keys[keycode.GRAVE.Code] = keycode.GRAVE
keys[keycode.OPEN_BRACKET.Code] = keycode.OPEN_BRACKET
keys[keycode.BACK_SLASH.Code] = keycode.BACK_SLASH
keys[keycode.CLOSE_BRAKET.Code] = keycode.CLOSE_BRAKET
keys[keycode.SINGLE_QUOTE.Code] = keycode.SINGLE_QUOTE
buttons[keycode.LEFT_BUTTON.Code] = keycode.LEFT_BUTTON
buttons[keycode.CENTER_BUTTON.Code] = keycode.CENTER_BUTTON
buttons[keycode.RIGHT_BUTTON.Code] = keycode.RIGHT_BUTTON
buttons[keycode.SCROLL_UP_BUTTON.Code] = keycode.SCROLL_UP_BUTTON
buttons[keycode.SCROLL_DOWN_BUTTON.Code] = keycode.SCROLL_DOWN_BUTTON
buttons[keycode.SCROLL_LEFT_BUTTON.Code] = keycode.SCROLL_LEFT_BUTTON
buttons[keycode.SCROLL_RIGHT_BUTTON.Code] = keycode.SCROLL_RIGHT_BUTTON
}
func Display(display string) {
C.setXDisplay(C.CString(display))
}
func Move(x, y int) {
C.mouseMove(C.int(x), C.int(y))
}
func Scroll(x, y int) {
C.mouseScroll(C.int(x), C.int(y))
}
func ButtonDown(code int) (*keycode.Button, error) {
button, ok := buttons[code]
if !ok {
return nil, fmt.Errorf("invalid button %v", code)
}
if _, ok := debounce[code]; ok {
return nil, fmt.Errorf("debounced button %v(%v)", button.Name, code)
}
debounce[code] = time.Now()
C.mouseEvent(C.uint(button.Keysym), C.int(1))
return &button, nil
}
func KeyDown(code int) (*keycode.Key, error) {
key, ok := keys[code]
if !ok {
return nil, fmt.Errorf("invalid key %v", code)
}
if _, ok := debounce[code]; ok {
return nil, fmt.Errorf("debounced key %v(%v)", key.Name, code)
}
debounce[code] = time.Now()
C.keyEvent(C.ulong(key.Keysym), C.int(1))
return &key, nil
}
func ButtonUp(code int) (*keycode.Button, error) {
button, ok := buttons[code]
if !ok {
return nil, fmt.Errorf("invalid button %v", code)
}
if _, ok := debounce[code]; !ok {
return nil, fmt.Errorf("debounced button %v(%v)", button.Name, code)
}
delete(debounce, code)
C.mouseEvent(C.uint(button.Keysym), C.int(0))
return &button, nil
}
func KeyUp(code int) (*keycode.Key, error) {
key, ok := keys[code]
if !ok {
return nil, fmt.Errorf("invalid key %v", code)
}
if _, ok := debounce[code]; !ok {
return nil, fmt.Errorf("debounced key %v(%v)", key.Name, code)
}
delete(debounce, code)
C.keyEvent(C.ulong(key.Keysym), C.int(0))
return &key, nil
}
func Reset() {
for key := range debounce {
if (key < 8) {
ButtonUp(key)
} else {
KeyUp(key)
}
delete(debounce, key)
}
}
func Check(duration time.Duration) {
t := time.Now()
for key, start := range debounce {
if t.Sub(start) < duration {
continue
}
if (key < 8) {
ButtonUp(key)
} else {
KeyUp(key)
}
delete(debounce, key)
}
}

34
server/internal/hid/hid.h Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#ifndef XDISPLAY_H
#define XDISPLAY_H
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h> /* For fputs() */
#include <string.h> /* For strdup() */
/* Returns the main display, closed either on exit or when closeMainDisplay()
* is invoked. This removes a bit of the overhead of calling XOpenDisplay() &
* XCloseDisplay() everytime the main display needs to be used.
*
* Note that this is almost certainly not thread safe. */
Display *getXDisplay(void);
void closeXDisplay(void);
void mouseMove(int x, int y);
void mouseScroll(int x, int y);
void mouseEvent(unsigned int button, int down);
void keyEvent(unsigned long key, int down);
#ifdef __cplusplus
extern "C"
{
#endif
void setXDisplay(char *input);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,49 @@
package keycode
type Button struct {
Name string
Code int
Keysym int
}
var LEFT_BUTTON = Button{
Name: "LEFT",
Code: 0,
Keysym: 1,
}
var CENTER_BUTTON = Button{
Name: "CENTER",
Code: 1,
Keysym: 2,
}
var RIGHT_BUTTON = Button{
Name: "RIGHT",
Code: 2,
Keysym: 3,
}
var SCROLL_UP_BUTTON = Button{
Name: "SCROLL_UP",
Code: 3,
Keysym: 4,
}
var SCROLL_DOWN_BUTTON = Button{
Name: "SCROLL_DOWN",
Code: 4,
Keysym: 5,
}
var SCROLL_LEFT_BUTTON = Button{
Name: "SCROLL_LEFT",
Code: 5,
Keysym: 6,
}
var SCROLL_RIGHT_BUTTON = Button{
Name: "SCROLL_RIGHT",
Code: 6,
Keysym: 7,
}

View File

@ -0,0 +1,701 @@
package keycode
type Key struct {
Name string
Value string
Code int
Keysym int
}
var BACKSPACE = Key{
Name: "BACKSPACE",
Value: "BackSpace",
Code: 8,
Keysym: int(0xff08),
}
var TAB = Key{
Name: "TAB",
Value: "Tab",
Code: 9,
Keysym: int(0xFF09),
}
var CLEAR = Key{
Name: "CLEAR",
Value: "Clear",
Code: 12,
Keysym: int(0xFF0B),
}
var ENTER = Key{
Name: "ENTER",
Value: "Enter",
Code: 13,
Keysym: int(0xFF0D),
}
var SHIFT = Key{
Name: "SHIFT",
Value: "Shift",
Code: 16,
Keysym: int(0xFFE1),
}
var CTRL = Key{
Name: "CTRL",
Value: "Ctrl",
Code: 17,
Keysym: int(0xFFE3),
}
var ALT = Key{
Name: "ALT",
Value: "Alt",
Code: 18,
Keysym: int(0xFFE9),
}
var PAUSE = Key{
Name: "PAUSE",
Value: "Pause",
Code: 19,
Keysym: int(0xFF13),
}
var CAPS_LOCK = Key{
Name: "CAPS_LOCK",
Value: "Caps Lock",
Code: 20,
Keysym: int(0xFFE5),
}
var ESCAPE = Key{
Name: "ESCAPE",
Value: "Escape",
Code: 27,
Keysym: int(0xFF1B),
}
var SPACE = Key{
Name: "SPACE",
Value: " ",
Code: 32,
Keysym: int(0x0020),
}
var PAGE_UP = Key{
Name: "PAGE_UP",
Value: "Page Up",
Code: 33,
Keysym: int(0xFF55),
}
var PAGE_DOWN = Key{
Name: "PAGE_DOWN",
Value: "Page Down",
Code: 34,
Keysym: int(0xFF56),
}
var END = Key{
Name: "END",
Value: "End",
Code: 35,
Keysym: int(0xFF57),
}
var HOME = Key{
Name: "HOME",
Value: "Home",
Code: 36,
Keysym: int(0xFF50),
}
var LEFT_ARROW = Key{
Name: "LEFT_ARROW",
Value: "Left Arrow",
Code: 37,
Keysym: int(0xFF51),
}
var UP_ARROW = Key{
Name: "UP_ARROW",
Value: "Up Arrow",
Code: 38,
Keysym: int(0xFF52),
}
var RIGHT_ARROW = Key{
Name: "RIGHT_ARROW",
Value: "Right Arrow",
Code: 39,
Keysym: int(0xFF53),
}
var DOWN_ARROW = Key{
Name: "DOWN_ARROW",
Value: "Down Arrow",
Code: 40,
Keysym: int(0xFF54),
}
var INSERT = Key{
Name: "INSERT",
Value: "Insert",
Code: 45,
Keysym: int(0xFF63),
}
var DELETE = Key{
Name: "DELETE",
Value: "Delete",
Code: 46,
Keysym: int(0xFFFF),
}
var KEY_0 = Key{
Name: "KEY_0",
Value: "0",
Code: 48,
Keysym: int(0x0030),
}
var KEY_1 = Key{
Name: "KEY_1",
Value: "1",
Code: 49,
Keysym: int(0x0031),
}
var KEY_2 = Key{
Name: "KEY_2",
Value: "2",
Code: 50,
Keysym: int(0x0032),
}
var KEY_3 = Key{
Name: "KEY_3",
Value: "3",
Code: 51,
Keysym: int(0x0033),
}
var KEY_4 = Key{
Name: "KEY_4",
Value: "4",
Code: 52,
Keysym: int(0x0034),
}
var KEY_5 = Key{
Name: "KEY_5",
Value: "5",
Code: 53,
Keysym: int(0x0035),
}
var KEY_6 = Key{
Name: "KEY_6",
Value: "6",
Code: 54,
Keysym: int(0x0036),
}
var KEY_7 = Key{
Name: "KEY_7",
Value: "7",
Code: 55,
Keysym: int(0x0037),
}
var KEY_8 = Key{
Name: "KEY_8",
Value: "8",
Code: 56,
Keysym: int(0x0038),
}
var KEY_9 = Key{
Name: "KEY_9",
Value: "9",
Code: 57,
Keysym: int(0x0039),
}
var KEY_A = Key{
Name: "KEY_A",
Value: "a",
Code: 65,
Keysym: int(0x0061),
}
var KEY_B = Key{
Name: "KEY_B",
Value: "b",
Code: 66,
Keysym: int(0x0062),
}
var KEY_C = Key{
Name: "KEY_C",
Value: "c",
Code: 67,
Keysym: int(0x0063),
}
var KEY_D = Key{
Name: "KEY_D",
Value: "d",
Code: 68,
Keysym: int(0x0064),
}
var KEY_E = Key{
Name: "KEY_E",
Value: "e",
Code: 69,
Keysym: int(0x0065),
}
var KEY_F = Key{
Name: "KEY_F",
Value: "f",
Code: 70,
Keysym: int(0x0066),
}
var KEY_G = Key{
Name: "KEY_G",
Value: "g",
Code: 71,
Keysym: int(0x0067),
}
var KEY_H = Key{
Name: "KEY_H",
Value: "h",
Code: 72,
Keysym: int(0x0068),
}
var KEY_I = Key{
Name: "KEY_I",
Value: "i",
Code: 73,
Keysym: int(0x0069),
}
var KEY_J = Key{
Name: "KEY_J",
Value: "j",
Code: 74,
Keysym: int(0x006a),
}
var KEY_K = Key{
Name: "KEY_K",
Value: "k",
Code: 75,
Keysym: int(0x006b),
}
var KEY_L = Key{
Name: "KEY_L",
Value: "l",
Code: 76,
Keysym: int(0x006c),
}
var KEY_M = Key{
Name: "KEY_M",
Value: "m",
Code: 77,
Keysym: int(0x006d),
}
var KEY_N = Key{
Name: "KEY_N",
Value: "n",
Code: 78,
Keysym: int(0x006e),
}
var KEY_O = Key{
Name: "KEY_O",
Value: "o",
Code: 79,
Keysym: int(0x006f),
}
var KEY_P = Key{
Name: "KEY_P",
Value: "p",
Code: 80,
Keysym: int(0x0070),
}
var KEY_Q = Key{
Name: "KEY_Q",
Value: "q",
Code: 81,
Keysym: int(0x0071),
}
var KEY_R = Key{
Name: "KEY_R",
Value: "r",
Code: 82,
Keysym: int(0x0072),
}
var KEY_S = Key{
Name: "KEY_S",
Value: "s",
Code: 83,
Keysym: int(0x0073),
}
var KEY_T = Key{
Name: "KEY_T",
Value: "t",
Code: 84,
Keysym: int(0x0074),
}
var KEY_U = Key{
Name: "KEY_U",
Value: "u",
Code: 85,
Keysym: int(0x0075),
}
var KEY_V = Key{
Name: "KEY_V",
Value: "v",
Code: 86,
Keysym: int(0x0076),
}
var KEY_W = Key{
Name: "KEY_W",
Value: "w",
Code: 87,
Keysym: int(0x0077),
}
var KEY_X = Key{
Name: "KEY_X",
Value: "x",
Code: 88,
Keysym: int(0x0078),
}
var KEY_Y = Key{
Name: "KEY_Y",
Value: "y",
Code: 89,
Keysym: int(0x0079),
}
var KEY_Z = Key{
Name: "KEY_Z",
Value: "z",
Code: 90,
Keysym: int(0x007a),
}
var WIN_LEFT = Key{
Name: "WIN_LEFT",
Value: "Win Left",
Code: 91,
Keysym: int(0xFFEB),
}
var WIN_RIGHT = Key{
Name: "WIN_RIGHT",
Value: "Win Right",
Code: 92,
Keysym: int(0xFF67),
}
var PAD_0 = Key{
Name: "PAD_0",
Value: "Num Pad 0",
Code: 96,
Keysym: int(0xFFB0),
}
var PAD_1 = Key{
Name: "PAD_1",
Value: "Num Pad 1",
Code: 97,
Keysym: int(0xFFB1),
}
var PAD_2 = Key{
Name: "PAD_2",
Value: "Num Pad 2",
Code: 98,
Keysym: int(0xFFB2),
}
var PAD_3 = Key{
Name: "PAD_3",
Value: "Num Pad 3",
Code: 99,
Keysym: int(0xFFB3),
}
var PAD_4 = Key{
Name: "PAD_4",
Value: "Num Pad 4",
Code: 100,
Keysym: int(0xFFB4),
}
var PAD_5 = Key{
Name: "PAD_5",
Value: "Num Pad 5",
Code: 101,
Keysym: int(0xFFB5),
}
var PAD_6 = Key{
Name: "PAD_6",
Value: "Num Pad 6",
Code: 102,
Keysym: int(0xFFB6),
}
var PAD_7 = Key{
Name: "PAD_7",
Value: "Num Pad 7",
Code: 103,
Keysym: int(0xFFB7),
}
var PAD_8 = Key{
Name: "PAD_8",
Value: "Num Pad 8",
Code: 104,
Keysym: int(0xFFB8),
}
var PAD_9 = Key{
Name: "PAD_9",
Value: "Num Pad 9",
Code: 105,
Keysym: int(0xFFB9),
}
var MULTIPLY = Key{
Name: "MULTIPLY",
Value: "*",
Code: 106,
Keysym: int(0xFFAA),
}
var ADD = Key{
Name: "ADD",
Value: "+",
Code: 107,
Keysym: int(0xFFAB),
}
var SUBTRACT = Key{
Name: "SUBTRACT",
Value: "-",
Code: 109,
Keysym: int(0xFFAD),
}
var DECIMAL = Key{
Name: "DECIMAL",
Value: ".",
Code: 110,
Keysym: int(0xFFAE),
}
var DIVIDE = Key{
Name: "DIVIDE",
Value: "/",
Code: 111,
Keysym: int(0xFFAF),
}
var KEY_F1 = Key{
Name: "KEY_F1",
Value: "f1",
Code: 112,
Keysym: int(0xFFBE),
}
var KEY_F2 = Key{
Name: "KEY_F2",
Value: "f2",
Code: 113,
Keysym: int(0xFFBF),
}
var KEY_F3 = Key{
Name: "KEY_F3",
Value: "f3",
Code: 114,
Keysym: int(0xFFC0),
}
var KEY_F4 = Key{
Name: "KEY_F4",
Value: "f4",
Code: 115,
Keysym: int(0xFFC1),
}
var KEY_F5 = Key{
Name: "KEY_F5",
Value: "f5",
Code: 116,
Keysym: int(0xFFC2),
}
var KEY_F6 = Key{
Name: "KEY_F6",
Value: "f6",
Code: 117,
Keysym: int(0xFFC3),
}
var KEY_F7 = Key{
Name: "KEY_F7",
Value: "f7",
Code: 118,
Keysym: int(0xFFC4),
}
var KEY_F8 = Key{
Name: "KEY_F8",
Value: "f8",
Code: 119,
Keysym: int(0xFFC5),
}
var KEY_F9 = Key{
Name: "KEY_F9",
Value: "f9",
Code: 120,
Keysym: int(0xFFC6),
}
var KEY_F10 = Key{
Name: "KEY_F10",
Value: "f10",
Code: 121,
Keysym: int(0xFFC7),
}
var KEY_F11 = Key{
Name: "KEY_F11",
Value: "f11",
Code: 122,
Keysym: int(0xFFC8),
}
var KEY_F12 = Key{
Name: "KEY_F12",
Value: "f12",
Code: 123,
Keysym: int(0xFFC9),
}
var NUM_LOCK = Key{
Name: "NUM_LOCK",
Value: "Num Lock",
Code: 144,
Keysym: int(0xFF7F),
}
var SCROLL_LOCK = Key{
Name: "SCROLL_LOCK",
Value: "Scroll Lock",
Code: 145,
Keysym: int(0xFF14),
}
var SEMI_COLON = Key{
Name: "SEMI_COLON",
Value: ";",
Code: 186,
Keysym: int(0x003b),
}
var EQUAL = Key{
Name: "EQUAL",
Value: "=",
Code: 187,
Keysym: int(0x003d),
}
var COMMA = Key{
Name: "COMMA",
Value: ",",
Code: 188,
Keysym: int(0x002c),
}
var DASH = Key{
Name: "DASH",
Value: "-",
Code: 189,
Keysym: int(0x002d),
}
var PERIOD = Key{
Name: "PERIOD",
Value: ".",
Code: 190,
Keysym: int(0x002e),
}
var FORWARD_SLASH = Key{
Name: "FORWARD_SLASH",
Value: "/",
Code: 191,
Keysym: int(0x002f),
}
var GRAVE = Key{
Name: "GRAVE",
Value: "`",
Code: 192,
Keysym: int(0x0060),
}
var OPEN_BRACKET = Key{
Name: "OPEN_BRACKET",
Value: "[",
Code: 219,
Keysym: int(0x005b),
}
var BACK_SLASH = Key{
Name: "BACK_SLASH",
Value: "\\",
Code: 220,
Keysym: int(0x005c),
}
var CLOSE_BRAKET = Key{
Name: "CLOSE_BRAKET",
Value: "]",
Code: 221,
Keysym: int(0x005d),
}
var SINGLE_QUOTE = Key{
Name: "SINGLE_QUOTE",
Value: "'",
Code: 222,
Keysym: int(0x0022),
}

View File

@ -1,102 +1,102 @@
package endpoint package endpoint
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type ( type (
Endpoint func(http.ResponseWriter, *http.Request) error Endpoint func(http.ResponseWriter, *http.Request) error
ErrResponse struct { ErrResponse struct {
Status int `json:"status,omitempty"` Status int `json:"status,omitempty"`
Err string `json:"error,omitempty"` Err string `json:"error,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
Code string `json:"code,omitempty"` Code string `json:"code,omitempty"`
RequestID string `json:"request,omitempty"` RequestID string `json:"request,omitempty"`
} }
) )
func Handle(handler Endpoint) http.HandlerFunc { func Handle(handler Endpoint) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil { if err := handler(w, r); err != nil {
WriteError(w, r, err) WriteError(w, r, err)
} }
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
var nonErrorsCodes = map[int]bool{ var nonErrorsCodes = map[int]bool{
404: true, 404: true,
} }
func errResponse(input interface{}) *ErrResponse { func errResponse(input interface{}) *ErrResponse {
var res *ErrResponse var res *ErrResponse
var err interface{} var err interface{}
switch input.(type) { switch input.(type) {
case *HandlerError: case *HandlerError:
e := input.(*HandlerError) e := input.(*HandlerError)
res = &ErrResponse{ res = &ErrResponse{
Status: e.Status, Status: e.Status,
Err: http.StatusText(e.Status), Err: http.StatusText(e.Status),
Message: e.Message, Message: e.Message,
} }
err = e.Err err = e.Err
default: default:
res = &ErrResponse{ res = &ErrResponse{
Status: http.StatusInternalServerError, Status: http.StatusInternalServerError,
Err: http.StatusText(http.StatusInternalServerError), Err: http.StatusText(http.StatusInternalServerError),
} }
err = input err = input
} }
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *error: case *error:
e := err.(error) e := err.(error)
res.Details = e.Error() res.Details = e.Error()
break break
default: default:
res.Details = fmt.Sprintf("%+v", err) res.Details = fmt.Sprintf("%+v", err)
break break
} }
} }
return res return res
} }
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) { func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
hlog := log.With(). hlog := log.With().
Str("module", "http"). Str("module", "http").
Logger() Logger()
res := errResponse(err) res := errResponse(err)
if reqID := middleware.GetReqID(r.Context()); reqID != "" { if reqID := middleware.GetReqID(r.Context()); reqID != "" {
res.RequestID = reqID res.RequestID = reqID
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(res.Status) w.WriteHeader(res.Status)
if err := json.NewEncoder(w).Encode(res); err != nil { if err := json.NewEncoder(w).Encode(res); err != nil {
hlog.Warn().Err(err).Msg("Failed writing json error response") hlog.Warn().Err(err).Msg("Failed writing json error response")
} }
if !nonErrorsCodes[res.Status] { if !nonErrorsCodes[res.Status] {
logEntry := middleware.GetLogEntry(r) logEntry := middleware.GetLogEntry(r)
if logEntry != nil { if logEntry != nil {
logEntry.Panic(err, debug.Stack()) logEntry.Panic(err, debug.Stack())
} else { } else {
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err) hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
} }
} }
} }

View File

@ -3,15 +3,15 @@ package endpoint
import "fmt" import "fmt"
type HandlerError struct { type HandlerError struct {
Status int Status int
Message string Message string
Err error Err error
} }
func (e *HandlerError) Error() string { func (e *HandlerError) Error() string {
if e.Err != nil { if e.Err != nil {
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error()) return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
} }
return e.Message return e.Message
} }

View File

@ -0,0 +1,87 @@
package http
import (
"context"
"fmt"
"net/http"
"os"
"github.com/go-chi/chi"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"n.eko.moe/neko/internal/config"
"n.eko.moe/neko/internal/http/endpoint"
"n.eko.moe/neko/internal/http/middleware"
"n.eko.moe/neko/internal/websocket"
)
type Server struct {
logger zerolog.Logger
router *chi.Mux
http *http.Server
conf *config.Server
}
func New(conf *config.Server, webSocketHandler *websocket.WebSocketHandler) *Server {
logger := log.With().Str("module", "webrtc").Logger()
router := chi.NewRouter()
// router.Use(middleware.Recoverer) // Recover from panics without crashing server
router.Use(middleware.RequestID) // Create a request ID for each request
router.Use(middleware.Logger) // Log API request calls
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
webSocketHandler.Upgrade(w, r)
})
fs := http.FileServer(http.Dir(conf.Static))
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
if _, err := os.Stat(conf.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("file '%s' is not found", r.RequestURI),
}
}))
server := &http.Server{
Addr: conf.Bind,
Handler: router,
}
return &Server{
logger: logger,
router: router,
http: server,
conf: conf,
}
}
func (s *Server) Start() {
if s.conf.Cert != "" && s.conf.Key != "" {
go func() {
if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed {
s.logger.Panic().Err(err).Msg("unable to start https server")
}
}()
s.logger.Info().Msgf("https listening on %s", s.http.Addr)
} else {
go func() {
if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
s.logger.Panic().Err(err).Msg("unable to start http server")
}
}()
s.logger.Warn().Msgf("http listening on %s", s.http.Addr)
}
}
func (s *Server) Shutdown() error {
return s.http.Shutdown(context.Background())
}

View File

@ -61,13 +61,13 @@ func (e *entry) Write(status, bytes int, elapsed time.Duration) {
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0 res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
e.fields["res"] = res e.fields["res"] = res
e.fields["module"] = "api" e.fields["module"] = "http"
if len(e.errors) > 0 { if len(e.errors) > 0 {
e.fields["errors"] = e.errors e.fields["errors"] = e.errors
log.Error().Fields(e.fields).Msgf("Request failed (%d)", status) log.Error().Fields(e.fields).Msgf("request failed (%d)", status)
} else { } else {
log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status) log.Debug().Fields(e.fields).Msgf("request complete (%d)", status)
} }
} }

View File

@ -4,9 +4,9 @@ package middleware
// a pointer so it fits in an interface{} without allocation. This technique // 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. // for defining context keys was copied from Go 1.7's new use of context in net/http.
type ctxKey struct { type ctxKey struct {
name string name string
} }
func (k *ctxKey) String() string { func (k *ctxKey) String() string {
return "neko/ctx/" + k.name return "neko/ctx/" + k.name
} }

View File

@ -4,21 +4,21 @@ package middleware
// https://github.com/zenazn/goji/tree/master/web/middleware // https://github.com/zenazn/goji/tree/master/web/middleware
import ( import (
"net/http" "net/http"
"n.eko.moe/neko/internal/http/endpoint" "n.eko.moe/neko/internal/http/endpoint"
) )
func Recoverer(next http.Handler) http.Handler { func Recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
defer func() { defer func() {
if rvr := recover(); rvr != nil { if rvr := recover(); rvr != nil {
endpoint.WriteError(w, r, rvr) endpoint.WriteError(w, r, rvr)
} }
}() }()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }

View File

@ -1,14 +1,14 @@
package middleware package middleware
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"sync/atomic" "sync/atomic"
) )
// Key to use when setting the request ID. // Key to use when setting the request ID.
@ -37,19 +37,19 @@ var reqid uint64
// than a millionth of a percent chance of generating two colliding IDs. // than a millionth of a percent chance of generating two colliding IDs.
func init() { func init() {
hostname, err := os.Hostname() hostname, err := os.Hostname()
if hostname == "" || err != nil { if hostname == "" || err != nil {
hostname = "localhost" hostname = "localhost"
} }
var buf [12]byte var buf [12]byte
var b64 string var b64 string
for len(b64) < 10 { for len(b64) < 10 {
rand.Read(buf[:]) rand.Read(buf[:])
b64 = base64.StdEncoding.EncodeToString(buf[:]) b64 = base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64) b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
} }
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10]) prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
} }
// RequestID is a middleware that injects a request ID into the context of each // RequestID is a middleware that injects a request ID into the context of each
@ -58,32 +58,32 @@ func init() {
// process, and where the last number is an atomically incremented request // process, and where the last number is an atomically incremented request
// counter. // counter.
func RequestID(next http.Handler) http.Handler { func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
requestID := r.Header.Get("X-Request-Id") requestID := r.Header.Get("X-Request-Id")
if requestID == "" { if requestID == "" {
myid := atomic.AddUint64(&reqid, 1) myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid) requestID = fmt.Sprintf("%s-%06d", prefix, myid)
} }
ctx = context.WithValue(ctx, RequestIDKey, requestID) ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
// GetReqID returns a request ID from the given context if one is present. // GetReqID returns a request ID from the given context if one is present.
// Returns the empty string if a request ID cannot be found. // Returns the empty string if a request ID cannot be found.
func GetReqID(ctx context.Context) string { func GetReqID(ctx context.Context) string {
if ctx == nil { if ctx == nil {
return "" return ""
} }
if reqID, ok := ctx.Value(RequestIDKey).(string); ok { if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID return reqID
} }
return "" return ""
} }
// NextRequestID generates the next request ID in the sequence. // NextRequestID generates the next request ID in the sequence.
func NextRequestID() uint64 { func NextRequestID() uint64 {
return atomic.AddUint64(&reqid, 1) return atomic.AddUint64(&reqid, 1)
} }

View File

@ -17,7 +17,7 @@ func JSON(w http.ResponseWriter, data interface{}, status int) error {
if err != nil { if err != nil {
return &endpoint.HandlerError{ return &endpoint.HandlerError{
Status: http.StatusInternalServerError, Status: http.StatusInternalServerError,
Message: "Unable to write JSON response", Message: "unable to write JSON response",
Err: err, Err: err,
} }
} }

View File

@ -1,203 +0,0 @@
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] = "t"
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"
}

View File

@ -1,21 +0,0 @@
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"
}

View File

@ -0,0 +1,15 @@
package message
type Message struct {
Event string `json:"event"`
}
type IdentityProvide struct {
Message
ID string `json:"id"`
}
type SDP struct {
Message
SDP string `json:"sdp"`
}

View File

@ -1,71 +0,0 @@
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)
}

View File

@ -41,8 +41,8 @@ func Config(name string) {
Logger() Logger()
if file == "" { if file == "" {
logger.Warn().Msg("Preflight complete without config file") logger.Warn().Msg("preflight complete without config file")
} else { } else {
logger.Info().Msg("Preflight complete") logger.Info().Msg("preflight complete")
} }
} }

View File

@ -42,17 +42,17 @@ func Logs(name string) {
if err == nil { if err == nil {
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log")) err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
if err != nil { if err != nil {
log.Panic().Err(err).Msg("Failed to rotate log file") log.Panic().Err(err).Msg("failed to rotate log file")
} }
} }
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666) logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
if err != nil { if err != nil {
log.Panic().Err(err).Msg("Failed to create log file") log.Panic().Err(err).Msg("failed to create log file")
} }
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) { logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Logger Dropped %d messages", missed) fmt.Printf("logger dropped %d messages", missed)
}) })
log.Logger = log.Output(io.MultiWriter(console, logger)) log.Logger = log.Output(io.MultiWriter(console, logger))

View File

@ -0,0 +1,172 @@
package session
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/kataras/go-events"
"github.com/pion/webrtc/v2"
"n.eko.moe/neko/internal/utils"
)
func New() *SessionManager {
return &SessionManager{
host: "",
members: make(map[string]*Session),
emmiter: events.New(),
}
}
type SessionManager struct {
host string
members map[string]*Session
emmiter events.EventEmmiter
}
func (m *SessionManager) New(id string, admin bool, socket *websocket.Conn) *Session {
session := &Session{
ID: id,
Admin: admin,
socket: socket,
}
m.members[id] = session
m.emmiter.Emit("created", id, session)
return session
}
func (m *SessionManager) IsHost(id string) bool {
return m.host == id
}
func (m *SessionManager) HasHost() bool {
return m.host != ""
}
func (m *SessionManager) SetHost(id string) error {
_, ok := m.members[id]
if ok {
m.host = id
m.emmiter.Emit("host", id)
return nil
}
return fmt.Errorf("invalid session id %s", id)
}
func (m *SessionManager) GetHost() (*Session, bool) {
host, ok := m.members[m.host]
return host, ok
}
func (m *SessionManager) ClearHost() {
id := m.host
m.host = ""
m.emmiter.Emit("host_cleared", id)
}
func (m *SessionManager) Has(id string) bool {
_, ok := m.members[id]
return ok
}
func (m *SessionManager) Get(id string) (*Session, bool) {
session, ok := m.members[id]
return session, ok
}
func (m *SessionManager) Set(id string, session *Session) {
m.members[id] = session
}
func (m *SessionManager) Destroy(id string) error {
session, ok := m.members[id]
if ok {
err := session.destroy()
delete(m.members, id)
m.emmiter.Emit("destroyed", id)
return err
}
return nil
}
func (m *SessionManager) SetSocket(id string, socket *websocket.Conn) (bool, error) {
session, ok := m.members[id]
if ok {
session.socket = socket
return true, nil
}
return false, fmt.Errorf("invalid session id %s", id)
}
func (m *SessionManager) SetPeer(id string, peer *webrtc.PeerConnection) (bool, error) {
session, ok := m.members[id]
if ok {
session.peer = peer
return true, nil
}
return false, fmt.Errorf("invalid session id %s", id)
}
func (m *SessionManager) SetName(id string, name string) (bool, error) {
session, ok := m.members[id]
if ok {
session.Name = name
return true, nil
}
return false, fmt.Errorf("invalid session id %s", id)
}
func (m *SessionManager) Clear() error {
return nil
}
func (m *SessionManager) Brodcast(v interface{}, exclude interface{}) error {
if exclude != nil {
for id, sess := range m.members {
if in, _ := utils.ArrayIn(id, exclude); in {
continue
}
if err := sess.Send(v); err != nil {
return err
}
}
} else {
for _, sess := range m.members {
if err := sess.Send(v); err != nil {
return err
}
}
}
return nil
}
func (m *SessionManager) OnHost(listener func(id string)) {
m.emmiter.On("host", func(payload ...interface{}) {
listener(payload[0].(string))
})
}
func (m *SessionManager) OnHostCleared(listener func(id string)) {
m.emmiter.On("host_cleared", func(payload ...interface{}) {
listener(payload[0].(string))
})
}
func (m *SessionManager) OnCreated(listener func(id string, session *Session)) {
m.emmiter.On("created", func(payload ...interface{}) {
listener(payload[0].(string), payload[1].(*Session))
})
}
func (m *SessionManager) OnDestroy(listener func(id string)) {
m.emmiter.On("destroyed", func(payload ...interface{}) {
listener(payload[0].(string))
})
}

View File

@ -1,4 +1,4 @@
package webrtc package session
import ( import (
"sync" "sync"
@ -7,14 +7,23 @@ import (
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
) )
type session struct { type Session struct {
id string ID string
Name string
Admin bool
socket *websocket.Conn socket *websocket.Conn
peer *webrtc.PeerConnection peer *webrtc.PeerConnection
mu sync.Mutex mu sync.Mutex
} }
func (session *session) send(v interface{}) error { // TODO: write to peer data channel
func (session *Session) Write(v interface{}) error {
session.mu.Lock()
defer session.mu.Unlock()
return nil
}
func (session *Session) Send(v interface{}) error {
session.mu.Lock() session.mu.Lock()
defer session.mu.Unlock() defer session.mu.Unlock()
@ -25,7 +34,7 @@ func (session *session) send(v interface{}) error {
return nil return nil
} }
func (session *session) destroy() error { func (session *Session) destroy() error {
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected { if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
if err := session.peer.Close(); err != nil { if err := session.peer.Close(); err != nil {
return err return err
@ -37,5 +46,6 @@ func (session *session) destroy() error {
return err return err
} }
} }
return nil return nil
} }

View File

@ -1,21 +0,0 @@
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)
}

View File

@ -0,0 +1,24 @@
package utils
import (
"reflect"
)
func ArrayIn(val interface{}, array interface{}) (exists bool, index int) {
exists = false
index = -1
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
index = i
exists = true
return
}
}
}
return
}

View File

@ -1,10 +0,0 @@
package utils
const Header = `&34
_ __ __
/ | / /__ / /______ \ /\
/ |/ / _ \/ //_/ __ \ ) ( ')
/ /| / __/ ,< / /_/ / ( / )
/_/ |_/\___/_/|_|\____/ \(__)|
&1&37 nurdism/neko &33%s v%s&0
`

View File

@ -0,0 +1,10 @@
package utils
import "encoding/json"
func Unmarshal(in interface{}, raw []byte, callback func() error) error {
if err := json.Unmarshal(raw, &in); err != nil {
return err
}
return callback()
}

View File

@ -1,25 +0,0 @@
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)
}

View File

@ -1,22 +0,0 @@
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
}

View File

@ -0,0 +1,138 @@
package webrtc
import (
"bytes"
"encoding/binary"
"strconv"
"github.com/pion/webrtc/v2"
"n.eko.moe/neko/internal/hid"
)
const OP_MOVE = 0x01
const OP_SCROLL = 0x02
const OP_KEY_DOWN = 0x03
const OP_KEY_UP = 0x04
const OP_KEY_CLK = 0x05
type PayloadHeader struct {
Event uint8
Length uint16
}
type PayloadMove struct {
PayloadHeader
X uint16
Y uint16
}
type PayloadScroll struct {
PayloadHeader
X int16
Y int16
}
type PayloadKey struct {
PayloadHeader
Key uint16
}
func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error {
if !m.sessions.IsHost(id) {
return nil
}
buffer := bytes.NewBuffer(msg.Data)
header := &PayloadHeader{}
hbytes := make([]byte, 3)
if _, err := buffer.Read(hbytes); err != nil {
return err
}
if err := binary.Read(bytes.NewBuffer(hbytes), binary.LittleEndian, header); err != nil {
return err
}
buffer = bytes.NewBuffer(msg.Data)
switch header.Event {
case OP_MOVE:
payload := &PayloadMove{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
hid.Move(int(payload.X), int(payload.Y))
break
case OP_SCROLL:
payload := &PayloadScroll{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
m.logger.
Debug().
Str("x", strconv.Itoa(int(payload.X))).
Str("y", strconv.Itoa(int(payload.Y))).
Msg("scroll")
hid.Scroll(int(payload.X), int(payload.Y))
break
case OP_KEY_DOWN:
payload := &PayloadKey{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
if payload.Key < 8 {
button, err := hid.ButtonDown(int(payload.Key))
if err != nil {
m.logger.Warn().Err(err).Msg("key down failed")
return nil
}
m.logger.Debug().Msgf("button down %s(%d)", button.Name, payload.Key)
} else {
key, err := hid.KeyDown(int(payload.Key))
if err != nil {
m.logger.Warn().Err(err).Msg("key down failed")
return nil
}
m.logger.Debug().Msgf("key down %s(%d)", key.Name, payload.Key)
}
break
case OP_KEY_UP:
payload := &PayloadKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
if payload.Key < 8 {
button, err := hid.ButtonUp(int(payload.Key))
if err != nil {
m.logger.Warn().Err(err).Msg("button up failed")
return nil
}
m.logger.Debug().Msgf("button up %s(%d)", button.Name, payload.Key)
} else {
key, err := hid.KeyUp(int(payload.Key))
if err != nil {
m.logger.Warn().Err(err).Msg("keyup failed")
return nil
}
m.logger.Debug().Msgf("key up %s(%d)", key.Name, payload.Key)
}
break
case OP_KEY_CLK:
// unused
break
}
return nil
}

View File

@ -0,0 +1,32 @@
package webrtc
import (
"github.com/pion/logging"
"github.com/rs/zerolog"
)
type logger struct {
logger zerolog.Logger
}
func (l logger) Trace(msg string) { l.logger.Trace().Msg(msg) }
func (l logger) Tracef(format string, args ...interface{}) { l.logger.Trace().Msgf(format, args...) }
func (l logger) Debug(msg string) { l.logger.Debug().Msg(msg) }
func (l logger) Debugf(format string, args ...interface{}) { l.logger.Debug().Msgf(format, args...) }
func (l logger) Info(msg string) { l.logger.Info().Msg(msg) }
func (l logger) Infof(format string, args ...interface{}) { l.logger.Info().Msgf(format, args...) }
func (l logger) Warn(msg string) { l.logger.Warn().Msg(msg) }
func (l logger) Warnf(format string, args ...interface{}) { l.logger.Warn().Msgf(format, args...) }
func (l logger) Error(msg string) { l.logger.Error().Msg(msg) }
func (l logger) Errorf(format string, args ...interface{}) { l.logger.Error().Msgf(format, args...) }
type loggerFactory struct {
logger zerolog.Logger
}
func (l loggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
l.logger.Debug().Msgf("creating logger for %s", subsystem)
return logger{
logger: l.logger.With().Str("subsystem", subsystem).Logger(),
}
}

View File

@ -1,61 +1,42 @@
package webrtc package webrtc
import ( import (
"math/rand" "fmt"
"net/http"
"time" "time"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"n.eko.moe/neko/internal/config"
"n.eko.moe/neko/internal/event"
"n.eko.moe/neko/internal/gst" "n.eko.moe/neko/internal/gst"
"n.eko.moe/neko/internal/hid"
"n.eko.moe/neko/internal/message"
"n.eko.moe/neko/internal/session"
) )
func NewManager(password string) (*WebRTCManager, error) { func New(sessions *session.SessionManager, conf *config.WebRTC) *WebRTCManager {
logger := log.With().Str("module", "webrtc").Logger()
engine := webrtc.MediaEngine{} engine := webrtc.MediaEngine{}
engine.RegisterDefaultCodecs()
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000) setings := webrtc.SettingEngine{
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec) LoggerFactory: loggerFactory{
if err != nil { logger: logger,
return nil, err },
} }
engine.RegisterCodec(videoCodec)
audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec)
if err != nil {
return nil, err
}
engine.RegisterCodec(audioCodec)
videoPipeline := gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert")
// ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc
audioPipeline := gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert")
// 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{ return &WebRTCManager{
logger: log.With().Str("service", "webrtc").Logger(), logger: logger,
engine: engine, engine: engine,
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)), setings: setings,
video: video, api: webrtc.NewAPI(webrtc.WithMediaEngine(engine), webrtc.WithSettingEngine(setings)),
videoPipeline: videoPipeline, cleanup: time.NewTicker(1 * time.Second),
audio: audio, shutdown: make(chan bool),
audioPipeline: audioPipeline, sessions: sessions,
controller: "", conf: conf,
password: password,
sessions: make(map[string]*session),
debounce: make(map[int]time.Time),
cleanup: time.NewTicker(500 * time.Second),
shutdown: make(chan bool),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
},
config: webrtc.Configuration{ config: webrtc.Configuration{
ICEServers: []webrtc.ICEServer{ ICEServers: []webrtc.ICEServer{
{ {
@ -64,49 +45,182 @@ func NewManager(password string) (*WebRTCManager, error) {
}, },
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback, SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
}, },
}, nil }
} }
type WebRTCManager struct { type WebRTCManager struct {
logger zerolog.Logger logger zerolog.Logger
upgrader websocket.Upgrader
engine webrtc.MediaEngine engine webrtc.MediaEngine
api *webrtc.API setings webrtc.SettingEngine
config webrtc.Configuration config webrtc.Configuration
password string sessions *session.SessionManager
controller string api *webrtc.API
sessions map[string]*session
debounce map[int]time.Time
shutdown chan bool
cleanup *time.Ticker
video *webrtc.Track video *webrtc.Track
audio *webrtc.Track audio *webrtc.Track
videoPipeline *gst.Pipeline videoPipeline *gst.Pipeline
audioPipeline *gst.Pipeline audioPipeline *gst.Pipeline
cleanup *time.Ticker
conf *config.WebRTC
shutdown chan bool
} }
func (manager *WebRTCManager) Start() error { func (m *WebRTCManager) Start() {
manager.videoPipeline.Start()
manager.audioPipeline.Start() hid.Display(m.conf.Display)
switch m.conf.VideoCodec {
case "vp8":
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeVP8); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
case "vp9":
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeVP9); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
case "h264":
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeH264); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
default:
m.logger.Panic().Err(errors.Errorf("unknown video codec %s", m.conf.AudioCodec)).Msg("unable to start webrtc manager")
}
switch m.conf.AudioCodec {
case "opus":
if err := m.createAudioTrack(webrtc.DefaultPayloadTypeOpus); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
case "g722":
if err := m.createAudioTrack(webrtc.DefaultPayloadTypeG722); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
case "pcmu":
if err := m.createAudioTrack(webrtc.DefaultPayloadTypePCMU); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
case "pcma":
if err := m.createAudioTrack(webrtc.DefaultPayloadTypePCMA); err != nil {
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
}
default:
m.logger.Panic().Err(errors.Errorf("unknown audio codec %s", m.conf.AudioCodec)).Msg("unable to start webrtc manager")
}
m.videoPipeline.Start()
m.audioPipeline.Start()
go func() { go func() {
defer func() {
m.logger.Info().Msg("shutdown")
}()
for { for {
select { select {
case <-manager.shutdown: case <-m.shutdown:
return return
case <-manager.cleanup.C: case <-m.cleanup.C:
manager.checkKeys() hid.Check(time.Second * 10)
} }
} }
}() }()
m.sessions.OnHostCleared(func(id string) {
hid.Reset()
})
m.sessions.OnCreated(func(id string, session *session.Session) {
m.logger.Debug().Str("id", id).Msg("session created")
})
m.sessions.OnDestroy(func(id string) {
m.logger.Debug().Str("id", id).Msg("session destroyed")
})
// TODO: log resolution, bit rate and codec parameters
m.logger.Info().
Str("video_display", m.conf.Display).
Str("video_codec", m.conf.VideoCodec).
Str("audio_device", m.conf.Device).
Str("audio_codec", m.conf.AudioCodec).
Msgf("webrtc streaming")
}
func (m *WebRTCManager) Shutdown() error {
m.logger.Info().Msgf("webrtc shutting down")
m.cleanup.Stop()
m.shutdown <- true
m.videoPipeline.Stop()
m.audioPipeline.Stop()
return nil return nil
} }
func (manager *WebRTCManager) Shutdown() error { func (m *WebRTCManager) CreatePeer(id string, sdp string) error {
manager.cleanup.Stop() session, ok := m.sessions.Get(id)
manager.shutdown <- true if !ok {
manager.videoPipeline.Stop() return fmt.Errorf("invalid session id %s", id)
manager.audioPipeline.Stop() }
peer, err := m.api.NewPeerConnection(m.config)
if err != nil {
return err
}
if _, err := peer.AddTransceiverFromTrack(m.video, webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
}); err != nil {
return err
}
if _, err := peer.AddTransceiverFromTrack(m.audio, webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
}); err != nil {
return err
}
peer.SetRemoteDescription(webrtc.SessionDescription{
SDP: sdp,
Type: webrtc.SDPTypeOffer,
})
answer, err := peer.CreateAnswer(nil)
if err != nil {
return err
}
if err = peer.SetLocalDescription(answer); err != nil {
return err
}
if err := session.Send(message.SDP{
Message: message.Message{Event: event.SDP_REPLY},
SDP: answer.SDP,
}); err != nil {
return err
}
peer.OnDataChannel(func(d *webrtc.DataChannel) {
d.OnMessage(func(msg webrtc.DataChannelMessage) {
if err = m.handle(id, msg); err != nil {
m.logger.Warn().Err(err).Msg("data handle failed")
}
})
})
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
switch connectionState {
case webrtc.PeerConnectionStateDisconnected:
case webrtc.PeerConnectionStateFailed:
m.logger.Info().Str("id", id).Msg("peer disconnected")
m.sessions.Destroy(id)
break
case webrtc.PeerConnectionStateConnected:
m.logger.Info().Str("id", id).Msg("peer connected")
break
}
})
m.sessions.SetPeer(id, peer)
return nil return nil
} }

View File

@ -1,15 +0,0 @@
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"`
}

View File

@ -1,266 +0,0 @@
package webrtc
import (
"bytes"
"encoding/binary"
"encoding/json"
"time"
"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
}
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{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
robotgo.Move(int(payload.X), int(payload.Y))
break
case 0x02: // MOUSE_UP
payload := &dataMouseKey{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Mouse[code]; ok {
if _, ok := manager.debounce[code]; !ok {
return nil
}
delete(manager.debounce, code)
robotgo.MouseToggle("up", key)
manager.logger.Debug().Msgf("MOUSE_UP key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_UP key: %v", code)
}
break
case 0x03: // MOUSE_DOWN
payload := &dataMouseKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Mouse[code]; ok {
if _, ok := manager.debounce[code]; ok {
return nil
}
manager.debounce[code] = time.Now()
robotgo.MouseToggle("down", key)
manager.logger.Debug().Msgf("MOUSE_DOWN key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", code)
}
break
case 0x04: // MOUSE_CLK
payload := &dataMouseKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Mouse[code]; ok {
switch code {
case keys.MOUSE_WHEEL_DOWN:
robotgo.Scroll(0, -1)
break
case keys.MOUSE_WHEEL_UP:
robotgo.Scroll(0, 1)
break
case keys.MOUSE_WHEEL_LEFT:
robotgo.Scroll(-1, 0)
break
case keys.MOUSE_WHEEL_RIGH:
robotgo.Scroll(1, 0)
break
default:
robotgo.Click(key, false)
}
manager.logger.Debug().Msgf("MOUSE_CLK key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", code)
}
break
case 0x05: // KEY_DOWN
payload := &dataKeyboardKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Keyboard[code]; ok {
if _, ok := manager.debounce[code]; ok {
return nil
}
manager.debounce[code] = time.Now()
robotgo.KeyToggle(key, "down")
manager.logger.Debug().Msgf("KEY_DOWN key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", code)
}
break
case 0x06: // KEY_UP
payload := &dataKeyboardKey{}
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Keyboard[code]; ok {
if _, ok := manager.debounce[code]; !ok {
return nil
}
delete(manager.debounce, code)
robotgo.KeyToggle(key, "up")
manager.logger.Debug().Msgf("KEY_UP key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", code)
}
break
case 0x07: // KEY_CLK
payload := &dataKeyboardKey{}
err := binary.Read(buffer, binary.LittleEndian, payload)
if err != nil {
return err
}
code := int(payload.Key)
if key, ok := keys.Keyboard[code]; ok {
robotgo.KeyTap(key)
manager.logger.Debug().Msgf("KEY_CLK key: %v (%v)", code, key)
} else {
manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", code)
}
break
}
return nil
}
func (manager *WebRTCManager) clearKeys() {
for code := range manager.debounce {
if key, ok := keys.Keyboard[code]; ok {
robotgo.MouseToggle(key, "up")
}
if key, ok := keys.Mouse[code]; ok {
robotgo.KeyToggle(key, "up")
}
delete(manager.debounce, code)
}
}
func (manager *WebRTCManager) checkKeys() {
t := time.Now()
for code, start := range manager.debounce {
if t.Sub(start) < (time.Second * 10) {
continue
}
if key, ok := keys.Keyboard[code]; ok {
robotgo.MouseToggle(key, "up")
}
if key, ok := keys.Mouse[code]; ok {
robotgo.KeyToggle(key, "up")
}
delete(manager.debounce, code)
}
}

View File

@ -0,0 +1,99 @@
package webrtc
import (
"fmt"
"math/rand"
"github.com/pion/webrtc/v2"
"github.com/pkg/errors"
"n.eko.moe/neko/internal/gst"
)
func (m *WebRTCManager) createVideoTrack(payloadType uint8) error {
clockrate := uint32(90000)
var codec *webrtc.RTPCodec
switch payloadType {
case webrtc.DefaultPayloadTypeVP8:
codec = webrtc.NewRTPVP8Codec(payloadType, clockrate)
break
case webrtc.DefaultPayloadTypeVP9:
codec = webrtc.NewRTPVP9Codec(payloadType, clockrate)
break
case webrtc.DefaultPayloadTypeH264:
codec = webrtc.NewRTPH264Codec(payloadType, clockrate)
break
default:
return errors.Errorf("unknown video codec %s", payloadType)
}
track, err := webrtc.NewTrack(payloadType, rand.Uint32(), "stream", "stream", codec)
if err != nil {
return err
}
var pipeline *gst.Pipeline
src := fmt.Sprintf("ximagesrc xid=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue", m.conf.Display)
switch payloadType {
case webrtc.DefaultPayloadTypeVP8:
pipeline = gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{track}, src)
break
case webrtc.DefaultPayloadTypeVP9:
pipeline = gst.CreatePipeline(webrtc.VP9, []*webrtc.Track{track}, src)
break
case webrtc.DefaultPayloadTypeH264:
pipeline = gst.CreatePipeline(webrtc.H264, []*webrtc.Track{track}, src)
break
}
m.video = track
m.videoPipeline = pipeline
return nil
}
func (m *WebRTCManager) createAudioTrack(payloadType uint8) error {
var codec *webrtc.RTPCodec
switch payloadType {
case webrtc.DefaultPayloadTypeOpus:
codec = webrtc.NewRTPOpusCodec(payloadType, 48000)
break
case webrtc.DefaultPayloadTypeG722:
codec = webrtc.NewRTPG722Codec(payloadType, 48000)
break
case webrtc.DefaultPayloadTypePCMU:
codec = webrtc.NewRTPPCMUCodec(payloadType, 8000)
break
case webrtc.DefaultPayloadTypePCMA:
codec = webrtc.NewRTPPCMACodec(payloadType, 8000)
break
default:
return errors.Errorf("unknown audio codec %s", payloadType)
}
track, err := webrtc.NewTrack(payloadType, rand.Uint32(), "stream", "stream", codec)
if err != nil {
return err
}
var pipeline *gst.Pipeline
src := fmt.Sprintf("pulsesrc device=%s ! audioconvert", m.conf.Device)
switch payloadType {
case webrtc.DefaultPayloadTypeOpus:
pipeline = gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{track}, src)
break
case webrtc.DefaultPayloadTypeG722:
pipeline = gst.CreatePipeline(webrtc.G722, []*webrtc.Track{track}, src)
break
case webrtc.DefaultPayloadTypePCMU:
pipeline = gst.CreatePipeline(webrtc.PCMU, []*webrtc.Track{track}, src)
break
case webrtc.DefaultPayloadTypePCMA:
pipeline = gst.CreatePipeline(webrtc.PCMA, []*webrtc.Track{track}, src)
break
}
m.audio = track
m.audioPipeline = pipeline
return nil
}

View File

@ -1,228 +0,0 @@
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 {
manager.logger.
Info().
Msg("Attempting to upgrade ws")
socket, err := manager.upgrader.Upgrade(w, r, nil)
if err != nil {
manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!")
return err
}
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 = ""
manager.clearKeys()
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 = ""
manager.clearKeys()
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
}

View File

@ -0,0 +1,205 @@
package websocket
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
gonanoid "github.com/matoous/go-nanoid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"n.eko.moe/neko/internal/config"
"n.eko.moe/neko/internal/session"
"n.eko.moe/neko/internal/webrtc"
)
func New(sessions *session.SessionManager, webrtc *webrtc.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
logger := log.With().Str("module", "websocket").Logger()
return &WebSocketHandler{
logger: logger,
conf: conf,
sessions: sessions,
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
},
handler: &MessageHandler{
logger: logger.With().Str("subsystem", "handler").Logger(),
sessions: sessions,
webrtc: webrtc,
},
}
}
// Send pings to peer with this period. Must be less than pongWait.
const pingPeriod = 60 * time.Second
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
type WebSocketHandler struct {
logger zerolog.Logger
upgrader websocket.Upgrader
handler *MessageHandler
conf *config.WebSocket
sessions *session.SessionManager
shutdown chan bool
}
func (ws *WebSocketHandler) Start() error {
go func() {
defer func() {
ws.logger.Info().Msg("shutdown")
}()
for {
select {
case <-ws.shutdown:
return
}
}
}()
ws.sessions.OnCreated(func(id string, session *session.Session) {
if err := ws.handler.Created(id, session); err != nil {
ws.logger.Warn().Str("id", id).Err(err).Msg("session created with and error")
} else {
ws.logger.Debug().Str("id", id).Msg("session created")
}
})
ws.sessions.OnDestroy(func(id string) {
if err := ws.handler.Destroyed(id); err != nil {
ws.logger.Warn().Str("id", id).Err(err).Msg("session destroyed with and error")
} else {
ws.logger.Debug().Str("id", id).Msg("session destroyed")
}
})
return nil
}
func (ws *WebSocketHandler) Shutdown() error {
ws.shutdown <- true
return nil
}
func (ws *WebSocketHandler) Upgrade(w http.ResponseWriter, r *http.Request) error {
ws.logger.Debug().Msg("attempting to upgrade connection")
socket, err := ws.upgrader.Upgrade(w, r, nil)
if err != nil {
ws.logger.Error().Err(err).Msg("failed to upgrade connection")
return err
}
id, admin, err := ws.authenticate(r)
if err != nil {
ws.logger.Warn().Err(err).Msg("authenticatetion failed")
if err = socket.Close(); err != nil {
return err
}
return nil
}
ws.sessions.New(id, admin, socket)
ws.logger.
Debug().
Str("session", id).
Str("address", socket.RemoteAddr().String()).
Msg("new connection created")
defer func() {
ws.logger.
Debug().
Str("session", id).
Str("address", socket.RemoteAddr().String()).
Msg("session ended")
}()
if err = ws.handler.Connected(id, socket); err != nil {
ws.logger.Error().Err(err).Msg("connection failed")
if err = socket.Close(); err != nil {
return err
}
return nil
}
ws.handle(socket, id)
return nil
}
func (ws *WebSocketHandler) authenticate(r *http.Request) (string, bool, error) {
id, err := gonanoid.Generate(alphabet, 32)
if err != nil {
return "", false, err
}
passwords, ok := r.URL.Query()["password"]
if !ok || len(passwords[0]) < 1 {
return "", false, fmt.Errorf("no password provided")
}
if passwords[0] == ws.conf.AdminPassword {
return id, true, nil
}
if passwords[0] == ws.conf.Password {
return id, false, nil
}
return "", false, fmt.Errorf("invalid password: %s", passwords[0])
}
func (ws *WebSocketHandler) handle(socket *websocket.Conn, id string) {
bytes := make(chan []byte)
cancel := make(chan struct{})
ticker := time.NewTicker(pingPeriod)
go func() {
defer func() {
ticker.Stop()
ws.logger.Debug().Str("address", socket.RemoteAddr().String()).Msg("handle socket ending")
ws.handler.Disconnected(id)
}()
for {
_, raw, err := socket.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
ws.logger.Warn().Err(err).Msg("read message error")
} else {
ws.logger.Debug().Err(err).Msg("read message error")
}
close(cancel)
break
}
bytes <- raw
}
}()
for {
select {
case raw := <-bytes:
ws.logger.Debug().
Str("session", id).
Str("raw", string(raw)).
Msg("recieved message from client")
if err := ws.handler.Message(id, raw); err != nil {
ws.logger.Error().Err(err).Msg("message handler has failed")
return
}
case <-cancel:
return
case _ = <-ticker.C:
if err := socket.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}

View File

@ -0,0 +1,131 @@
package websocket
import (
"encoding/json"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"n.eko.moe/neko/internal/event"
"n.eko.moe/neko/internal/message"
"n.eko.moe/neko/internal/session"
"n.eko.moe/neko/internal/utils"
"n.eko.moe/neko/internal/webrtc"
)
type MessageHandler struct {
logger zerolog.Logger
sessions *session.SessionManager
webrtc *webrtc.WebRTCManager
}
func (h *MessageHandler) Connected(id string, socket *websocket.Conn) error {
return nil
}
func (h *MessageHandler) Disconnected(id string) error {
return h.sessions.Destroy(id)
}
func (h *MessageHandler) Created(id string, session *session.Session) error {
if err := session.Send(message.IdentityProvide{
Message: message.Message{Event: event.IDENTITY_PROVIDE},
ID: id,
}); err != nil {
return err
}
return nil
}
func (h *MessageHandler) Destroyed(id string) error {
if h.sessions.IsHost(id) {
h.sessions.ClearHost()
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_RELEASED}, []string{id}); err != nil {
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_RELEASED)
}
}
return nil
}
func (h *MessageHandler) Message(id string, raw []byte) error {
header := message.Message{}
if err := json.Unmarshal(raw, &header); err != nil {
return err
}
session, ok := h.sessions.Get(id)
if !ok {
errors.Errorf("unknown session id %s", id)
}
switch header.Event {
case event.SDP_PROVIDE:
payload := message.SDP{}
return errors.Wrapf(utils.Unmarshal(&payload, raw, func() error { return h.webrtc.CreatePeer(id, payload.SDP) }), "%s failed", header.Event)
case event.CONTROL_RELEASE:
return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event)
case event.CONTROL_REQUEST:
return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event)
default:
return errors.Errorf("unknown message event %s", header.Event)
}
}
func (h *MessageHandler) controlRelease(id string, session *session.Session) error {
if !h.sessions.IsHost(id) {
return nil
}
h.logger.Debug().Str("id", id).Msgf("host called %s", event.CONTROL_RELEASED)
h.sessions.ClearHost()
if err := session.Send(message.Message{Event: event.CONTROL_RELEASE}); err != nil {
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_RELEASE)
return err
}
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_RELEASED}, []string{session.ID}); err != nil {
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_RELEASED)
return err
}
return nil
}
func (h *MessageHandler) controlRequest(id string, session *session.Session) error {
h.logger.Debug().Str("id", id).Msgf("user called %s", event.CONTROL_REQUEST)
if !h.sessions.HasHost() {
h.sessions.SetHost(id)
if err := session.Send(message.Message{Event: event.CONTROL_GIVE}); err != nil {
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_GIVE)
return err
}
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_GIVEN}, []string{session.ID}); err != nil {
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_GIVEN)
return err
}
return nil
}
if err := session.Send(message.Message{Event: event.CONTROL_LOCKED}); err != nil {
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_LOCKED)
return err
}
host, ok := h.sessions.GetHost()
if ok {
if err := host.Send(message.Message{Event: event.CONTROL_REQUESTING}); err != nil {
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_REQUESTING)
return err
}
}
return nil
}

View File

@ -1,25 +1,31 @@
package neko package neko
import ( import (
"context"
"fmt" "fmt"
"net/http"
"os" "os"
"os/signal" "os/signal"
"runtime" "runtime"
"n.eko.moe/neko/internal/config" "n.eko.moe/neko/internal/config"
"n.eko.moe/neko/internal/http/endpoint" "n.eko.moe/neko/internal/http"
"n.eko.moe/neko/internal/http/middleware" "n.eko.moe/neko/internal/session"
"n.eko.moe/neko/internal/structs"
"n.eko.moe/neko/internal/webrtc" "n.eko.moe/neko/internal/webrtc"
"n.eko.moe/neko/internal/websocket"
"github.com/go-chi/chi"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const Header = `&34
_ __ __
/ | / /__ / /______ \ /\
/ |/ / _ \/ //_/ __ \ ) ( ')
/ /| / __/ ,< / /_/ / ( / )
/_/ |_/\___/_/|_|\____/ \(__)|
&1&37 nurdism/neko &33%s v%s&0
`
var ( var (
// //
buildDate = "" buildDate = ""
@ -41,7 +47,7 @@ var Service *Neko
func init() { func init() {
Service = &Neko{ Service = &Neko{
Version: &structs.Version{ Version: &Version{
Major: major, Major: major,
Minor: minor, Minor: minor,
Patch: patch, Patch: patch,
@ -53,122 +59,97 @@ func init() {
Compiler: runtime.Compiler, Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}, },
Root: &config.Root{}, Root: &config.Root{},
Serve: &config.Serve{}, Server: &config.Server{},
WebRTC: &config.WebRTC{},
WebSocket: &config.WebSocket{},
} }
} }
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)
}
type Neko struct { type Neko struct {
Version *structs.Version Version *Version
Root *config.Root Root *config.Root
Serve *config.Serve Server *config.Server
Logger zerolog.Logger WebRTC *config.WebRTC
http *http.Server WebSocket *config.WebSocket
manager *webrtc.WebRTCManager
logger zerolog.Logger
server *http.Server
sessions *session.SessionManager
webRTCManager *webrtc.WebRTCManager
webSocketHandler *websocket.WebSocketHandler
} }
func (neko *Neko) Preflight() { func (neko *Neko) Preflight() {
neko.Logger = log.With().Str("service", "neko").Logger() neko.logger = log.With().Str("service", "neko").Logger()
} }
func (neko *Neko) Start() { func (neko *Neko) Start() {
router := chi.NewRouter() sessions := session.New()
manager, err := webrtc.NewManager(neko.Serve.Password) webRTCManager := webrtc.New(sessions, neko.WebRTC)
if err != nil { webRTCManager.Start()
neko.Logger.Panic().Err(err).Msg("Can not create webrtc manager")
}
if err := manager.Start(); err != nil { webSocketHandler := websocket.New(sessions, webRTCManager, neko.WebSocket)
neko.Logger.Panic().Err(err).Msg("Can not start webrtc manager") webSocketHandler.Start()
}
router.Use(middleware.Recoverer) // Recover from panics without crashing server server := http.New(neko.Server, webSocketHandler)
router.Use(middleware.RequestID) // Create a request ID for each request server.Start()
router.Use(middleware.Logger) // Log API request calls
router.Get("/ping", func(w http.ResponseWriter, r *http.Request) { neko.sessions = sessions
w.Header().Set("Content-Type", "text/plain") neko.webRTCManager = webRTCManager
w.WriteHeader(http.StatusOK) neko.webSocketHandler = webSocketHandler
w.Write([]byte(".")) neko.server = server
})
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
if err := manager.Upgrade(w, r); err != nil {
neko.Logger.Error().Err(err).Msg("session.destroy has failed")
}
})
fs := http.FileServer(http.Dir(neko.Serve.Static))
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
if _, err := os.Stat(neko.Serve.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),
}
}))
server := &http.Server{
Addr: neko.Serve.Bind,
Handler: router,
}
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
neko.manager = manager
} }
func (neko *Neko) Shutdown() { func (neko *Neko) Shutdown() {
if neko.manager != nil { if err := neko.webRTCManager.Shutdown(); err != nil {
if err := neko.manager.Shutdown(); err != nil { neko.logger.Err(err).Msg("webrtc manager shutdown with an error")
neko.Logger.Err(err).Msg("WebRTC manager shutdown with an error") } else {
} else { neko.logger.Debug().Msg("webrtc manager shutdown")
neko.Logger.Debug().Msg("WebRTC manager shutdown")
}
} }
if neko.http != nil {
if err := neko.http.Shutdown(context.Background()); err != nil { if err := neko.webSocketHandler.Shutdown(); err != nil {
neko.Logger.Err(err).Msg("HTTP server shutdown with an error") neko.logger.Err(err).Msg("websocket handler shutdown with an error")
} else { } else {
neko.Logger.Debug().Msg("HTTP server shutdown") neko.logger.Debug().Msg("websocket handler shutdown")
} }
if err := neko.server.Shutdown(); err != nil {
neko.logger.Err(err).Msg("server shutdown with an error")
} else {
neko.logger.Debug().Msg("server shutdown")
} }
} }
func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) { func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) {
neko.Logger.Info().Msg("Starting HTTP/S server") neko.logger.Info().Msg("starting neko server")
neko.Start() neko.Start()
neko.logger.Info().Msg("neko ready")
neko.Logger.Info().Msg("Service ready")
quit := make(chan os.Signal) quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
sig := <-quit sig := <-quit
neko.Logger.Warn().Msgf("Received %s, attempting graceful shutdown: \n", sig) neko.logger.Warn().Msgf("received %s, attempting graceful shutdown: \n", sig)
neko.Shutdown() neko.Shutdown()
neko.Logger.Info().Msg("Shutting down complete") neko.logger.Info().Msg("shutdown complete")
} }