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

@ -1,2 +1,3 @@
DISPLAY=:0
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",
"output": "${workspaceFolder}/bin/debug/neko",
"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}"
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 (
"fmt"
"github.com/rs/zerolog/log"
"n.eko.moe/neko"
"n.eko.moe/neko/cmd"
"n.eko.moe/neko/internal/utils"
"github.com/rs/zerolog/log"
)
func main() {
fmt.Print(utils.Colorf(utils.Header, "server", neko.Service.Version))
fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
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 (
"fmt"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"n.eko.moe/neko"
"n.eko.moe/neko/internal/preflight"
"github.com/spf13/cobra"
)
func Execute() error {
@ -15,8 +16,8 @@ func Execute() error {
var root = &cobra.Command{
Use: "neko",
Short: "",
Long: ``,
Short: "neko streaming server",
Long: `neko streaming server`,
Version: neko.Service.Version.String(),
}
@ -28,8 +29,8 @@ func init() {
})
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() {
command := &cobra.Command{
Use: "serve",
Short: "",
Long: ``,
Short: "serve neko streaming server",
Long: `serve neko streaming server`,
Run: neko.Service.ServeCommand,
}
configs := []config.Config{
neko.Service.Serve,
neko.Service.Server,
neko.Service.WebRTC,
neko.Service.WebSocket,
}
cobra.OnInitialize(func() {
@ -29,7 +31,7 @@ func init() {
for _, cfg := range configs {
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 (
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/kataras/go-events v0.0.2
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/pkg/errors v0.8.1
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=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -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-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57 h1:0BtenaNSwWghFTsHDQGTdurEdCMm7lsNmWfMoBS2JiY=
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57/go.mod h1:P6/F9lmSF2Z/74P/m80qEm6ApjE5HmB+rSzfBCNqIPo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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/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=
@ -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/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1 h1:h0wbuSK8xUNmMwDdCxZx2OLdkVck6Bb31zj4CxCN5I4=
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -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/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4=
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss=
@ -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/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9 h1:7vKLsPiS3XTrejZHaoKDS3/26K3t10rAtuf1+fRfIVA=
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9/go.mod h1:n1o8s7fg6QGcgIsN9AmWQnBi6KrcbEUX0kFVUwTP53g=
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY=
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk=
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shirou/gopsutil v2.19.6+incompatible h1:49/Gru26Lne9Cl3IoAVDZVM09hvkSrUodgIIsCVRwbs=
github.com/shirou/gopsutil v2.19.6+incompatible/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@ -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/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53 h1:tYb/9KQi8dTCSDia2NwbuhUbKlaurqC/S7MFQo96nLk=
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53/go.mod h1:5txYrXKrQG6ZJYdGIiMVVxiOhbdACnwBcHzIwGQ7Nkw=
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8 h1:9Y+hoKBYa+UtzGqkODfs8c0Q6gp2UfniVNsHQWghPi0=
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8/go.mod h1:52+3yYrTNjWKh+CkQozNRCLWCE/X666yAWPGbYC3DZI=
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b h1:psGhQitWSo4KBpLghvJPlhHxTJ8LQl1y0ekjSreqvu4=
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -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-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

View File

@ -5,4 +5,4 @@ import "github.com/spf13/cobra"
type Config interface {
Init(cmd *cobra.Command) error
Set()
}
}

View File

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

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;
} SampleHandlerUserData;
void gstreamer_init(void) {
gst_init(NULL, NULL);
}
GMainLoop *gstreamer_send_main_loop = NULL;
void gstreamer_send_start_mainloop(void) {
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) {
gst_init(NULL, NULL);
GError *error = NULL;
return gst_parse_launch(pipeline, &error);
}

View File

@ -17,9 +17,20 @@ import (
"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
type Pipeline struct {
@ -32,6 +43,7 @@ type Pipeline struct {
var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex
var registry *C.GstRegistry
const (
videoClockRate = 90000
@ -39,6 +51,11 @@ const (
pcmClockRate = 8000
)
func init() {
C.gstreamer_init()
registry = C.gst_registry_get()
}
// CreatePipeline creates a GStreamer Pipeline
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
pipelineStr := "appsink name=appsink"
@ -46,33 +63,97 @@ func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string
switch codecName {
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
clockRate = videoClockRate
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
panic(err)
}
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
clockRate = videoClockRate
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
panic(err)
}
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
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:
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
// opusenc
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
clockRate = audioClockRate
if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
panic(err)
}
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
clockRate = audioClockRate
if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
panic(err)
}
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
clockRate = pcmClockRate
if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
panic(err)
}
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
clockRate = pcmClockRate
if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
panic(err)
}
default:
panic("Unhandled codec " + codecName)
}
@ -105,6 +186,21 @@ func (p *Pipeline) Stop() {
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
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
pipelinesLock.Lock()

View File

@ -9,8 +9,10 @@
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
GstElement *gstreamer_send_create_pipeline(char *pipeline);
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
void gstreamer_send_stop_pipeline(GstElement *pipeline);
void gstreamer_send_start_mainloop(void);
void gstreamer_init(void);
#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
import (
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/log"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/log"
)
type (
Endpoint func(http.ResponseWriter, *http.Request) error
Endpoint func(http.ResponseWriter, *http.Request) error
ErrResponse struct {
Status int `json:"status,omitempty"`
Err string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
Code string `json:"code,omitempty"`
RequestID string `json:"request,omitempty"`
}
ErrResponse struct {
Status int `json:"status,omitempty"`
Err string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
Code string `json:"code,omitempty"`
RequestID string `json:"request,omitempty"`
}
)
func Handle(handler Endpoint) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
WriteError(w, r, err)
}
}
fn := func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
WriteError(w, r, err)
}
}
return http.HandlerFunc(fn)
return http.HandlerFunc(fn)
}
var nonErrorsCodes = map[int]bool{
404: true,
404: true,
}
func errResponse(input interface{}) *ErrResponse {
var res *ErrResponse
var err interface{}
var res *ErrResponse
var err interface{}
switch input.(type) {
case *HandlerError:
e := input.(*HandlerError)
res = &ErrResponse{
Status: e.Status,
Err: http.StatusText(e.Status),
Message: e.Message,
}
err = e.Err
default:
res = &ErrResponse{
Status: http.StatusInternalServerError,
Err: http.StatusText(http.StatusInternalServerError),
}
err = input
}
switch input.(type) {
case *HandlerError:
e := input.(*HandlerError)
res = &ErrResponse{
Status: e.Status,
Err: http.StatusText(e.Status),
Message: e.Message,
}
err = e.Err
default:
res = &ErrResponse{
Status: http.StatusInternalServerError,
Err: http.StatusText(http.StatusInternalServerError),
}
err = input
}
if err != nil {
switch err.(type) {
case *error:
e := err.(error)
res.Details = e.Error()
break
default:
res.Details = fmt.Sprintf("%+v", err)
break
}
}
if err != nil {
switch err.(type) {
case *error:
e := err.(error)
res.Details = e.Error()
break
default:
res.Details = fmt.Sprintf("%+v", err)
break
}
}
return res
return res
}
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
hlog := log.With().
Str("module", "http").
Logger()
hlog := log.With().
Str("module", "http").
Logger()
res := errResponse(err)
res := errResponse(err)
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
res.RequestID = reqID
}
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
res.RequestID = reqID
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(res.Status)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(res.Status)
if err := json.NewEncoder(w).Encode(res); err != nil {
hlog.Warn().Err(err).Msg("Failed writing json error response")
}
if err := json.NewEncoder(w).Encode(res); err != nil {
hlog.Warn().Err(err).Msg("Failed writing json error response")
}
if !nonErrorsCodes[res.Status] {
logEntry := middleware.GetLogEntry(r)
if logEntry != nil {
logEntry.Panic(err, debug.Stack())
} else {
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
}
}
if !nonErrorsCodes[res.Status] {
logEntry := middleware.GetLogEntry(r)
if logEntry != nil {
logEntry.Panic(err, debug.Stack())
} else {
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
}
}
}

View File

@ -3,15 +3,15 @@ package endpoint
import "fmt"
type HandlerError struct {
Status int
Message string
Err error
Status int
Message string
Err error
}
func (e *HandlerError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
}
if e.Err != nil {
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
e.fields["res"] = res
e.fields["module"] = "api"
e.fields["module"] = "http"
if len(e.errors) > 0 {
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 {
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
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type ctxKey struct {
name string
name 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
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 {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
endpoint.WriteError(w, r, rvr)
}
}()
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
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
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
)
// 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.
func init() {
hostname, err := os.Hostname()
if hostname == "" || err != nil {
hostname = "localhost"
}
var buf [12]byte
var b64 string
for len(b64) < 10 {
rand.Read(buf[:])
b64 = base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
}
hostname, err := os.Hostname()
if hostname == "" || err != nil {
hostname = "localhost"
}
var buf [12]byte
var b64 string
for len(b64) < 10 {
rand.Read(buf[:])
b64 = base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
}
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
}
// 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
// counter.
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
}
ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
}
ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
// GetReqID returns a request ID from the given context if one is present.
// Returns the empty string if a request ID cannot be found.
func GetReqID(ctx context.Context) string {
if ctx == nil {
return ""
}
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID
}
return ""
if ctx == nil {
return ""
}
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID
}
return ""
}
// NextRequestID generates the next request ID in the sequence.
func NextRequestID() uint64 {
return atomic.AddUint64(&reqid, 1)
return atomic.AddUint64(&reqid, 1)
}

View File

@ -17,7 +17,7 @@ func JSON(w http.ResponseWriter, data interface{}, status int) error {
if err != nil {
return &endpoint.HandlerError{
Status: http.StatusInternalServerError,
Message: "Unable to write JSON response",
Message: "unable to write JSON response",
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()
if file == "" {
logger.Warn().Msg("Preflight complete without config file")
logger.Warn().Msg("preflight complete without config file")
} else {
logger.Info().Msg("Preflight complete")
logger.Info().Msg("preflight complete")
}
}

View File

@ -42,17 +42,17 @@ func Logs(name string) {
if err == nil {
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
if err != nil {
log.Panic().Err(err).Msg("Failed to rotate log file")
log.Panic().Err(err).Msg("failed to rotate log file")
}
}
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Panic().Err(err).Msg("Failed to create log file")
log.Panic().Err(err).Msg("failed to create log file")
}
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Logger Dropped %d messages", missed)
fmt.Printf("logger dropped %d messages", missed)
})
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 (
"sync"
@ -7,14 +7,23 @@ import (
"github.com/pion/webrtc/v2"
)
type session struct {
id string
type Session struct {
ID string
Name string
Admin bool
socket *websocket.Conn
peer *webrtc.PeerConnection
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()
defer session.mu.Unlock()
@ -25,7 +34,7 @@ func (session *session) send(v interface{}) error {
return nil
}
func (session *session) destroy() error {
func (session *Session) destroy() error {
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
if err := session.peer.Close(); err != nil {
return err
@ -37,5 +46,6 @@ func (session *session) destroy() error {
return err
}
}
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
import (
"math/rand"
"net/http"
"fmt"
"time"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v2"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"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/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.RegisterDefaultCodecs()
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec)
if err != nil {
return nil, err
setings := webrtc.SettingEngine{
LoggerFactory: loggerFactory{
logger: logger,
},
}
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{
logger: log.With().Str("service", "webrtc").Logger(),
engine: engine,
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)),
video: video,
videoPipeline: videoPipeline,
audio: audio,
audioPipeline: audioPipeline,
controller: "",
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
},
},
logger: logger,
engine: engine,
setings: setings,
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine), webrtc.WithSettingEngine(setings)),
cleanup: time.NewTicker(1 * time.Second),
shutdown: make(chan bool),
sessions: sessions,
conf: conf,
config: webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
@ -64,49 +45,182 @@ func NewManager(password string) (*WebRTCManager, error) {
},
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
},
}, nil
}
}
type WebRTCManager struct {
logger zerolog.Logger
upgrader websocket.Upgrader
engine webrtc.MediaEngine
api *webrtc.API
setings webrtc.SettingEngine
config webrtc.Configuration
password string
controller string
sessions map[string]*session
debounce map[int]time.Time
shutdown chan bool
cleanup *time.Ticker
sessions *session.SessionManager
api *webrtc.API
video *webrtc.Track
audio *webrtc.Track
videoPipeline *gst.Pipeline
audioPipeline *gst.Pipeline
cleanup *time.Ticker
conf *config.WebRTC
shutdown chan bool
}
func (manager *WebRTCManager) Start() error {
manager.videoPipeline.Start()
manager.audioPipeline.Start()
func (m *WebRTCManager) 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() {
defer func() {
m.logger.Info().Msg("shutdown")
}()
for {
select {
case <-manager.shutdown:
case <-m.shutdown:
return
case <-manager.cleanup.C:
manager.checkKeys()
case <-m.cleanup.C:
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
}
func (manager *WebRTCManager) Shutdown() error {
manager.cleanup.Stop()
manager.shutdown <- true
manager.videoPipeline.Stop()
manager.audioPipeline.Stop()
func (m *WebRTCManager) CreatePeer(id string, sdp string) error {
session, ok := m.sessions.Get(id)
if !ok {
return fmt.Errorf("invalid session id %s", id)
}
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
}

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
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"runtime"
"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/structs"
"n.eko.moe/neko/internal/http"
"n.eko.moe/neko/internal/session"
"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/log"
"github.com/spf13/cobra"
)
const Header = `&34
_ __ __
/ | / /__ / /______ \ /\
/ |/ / _ \/ //_/ __ \ ) ( ')
/ /| / __/ ,< / /_/ / ( / )
/_/ |_/\___/_/|_|\____/ \(__)|
&1&37 nurdism/neko &33%s v%s&0
`
var (
//
buildDate = ""
@ -41,7 +47,7 @@ var Service *Neko
func init() {
Service = &Neko{
Version: &structs.Version{
Version: &Version{
Major: major,
Minor: minor,
Patch: patch,
@ -53,122 +59,97 @@ func init() {
Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
},
Root: &config.Root{},
Serve: &config.Serve{},
Root: &config.Root{},
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 {
Version *structs.Version
Root *config.Root
Serve *config.Serve
Logger zerolog.Logger
http *http.Server
manager *webrtc.WebRTCManager
Version *Version
Root *config.Root
Server *config.Server
WebRTC *config.WebRTC
WebSocket *config.WebSocket
logger zerolog.Logger
server *http.Server
sessions *session.SessionManager
webRTCManager *webrtc.WebRTCManager
webSocketHandler *websocket.WebSocketHandler
}
func (neko *Neko) Preflight() {
neko.Logger = log.With().Str("service", "neko").Logger()
neko.logger = log.With().Str("service", "neko").Logger()
}
func (neko *Neko) Start() {
router := chi.NewRouter()
sessions := session.New()
manager, err := webrtc.NewManager(neko.Serve.Password)
if err != nil {
neko.Logger.Panic().Err(err).Msg("Can not create webrtc manager")
}
webRTCManager := webrtc.New(sessions, neko.WebRTC)
webRTCManager.Start()
if err := manager.Start(); err != nil {
neko.Logger.Panic().Err(err).Msg("Can not start webrtc manager")
}
webSocketHandler := websocket.New(sessions, webRTCManager, neko.WebSocket)
webSocketHandler.Start()
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
server := http.New(neko.Server, webSocketHandler)
server.Start()
router.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("."))
})
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
neko.sessions = sessions
neko.webRTCManager = webRTCManager
neko.webSocketHandler = webSocketHandler
neko.server = server
}
func (neko *Neko) Shutdown() {
if neko.manager != nil {
if err := neko.manager.Shutdown(); err != nil {
neko.Logger.Err(err).Msg("WebRTC manager shutdown with an error")
} else {
neko.Logger.Debug().Msg("WebRTC manager shutdown")
}
if err := neko.webRTCManager.Shutdown(); err != nil {
neko.logger.Err(err).Msg("webrtc manager shutdown with an error")
} else {
neko.logger.Debug().Msg("webrtc manager shutdown")
}
if neko.http != nil {
if err := neko.http.Shutdown(context.Background()); err != nil {
neko.Logger.Err(err).Msg("HTTP server shutdown with an error")
} else {
neko.Logger.Debug().Msg("HTTP server shutdown")
}
if err := neko.webSocketHandler.Shutdown(); err != nil {
neko.logger.Err(err).Msg("websocket handler shutdown with an error")
} else {
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) {
neko.Logger.Info().Msg("Starting HTTP/S server")
neko.logger.Info().Msg("starting neko server")
neko.Start()
neko.Logger.Info().Msg("Service ready")
neko.logger.Info().Msg("neko ready")
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
sig := <-quit
neko.Logger.Warn().Msgf("Received %s, attempting graceful shutdown: \n", sig)
neko.logger.Warn().Msgf("received %s, attempting graceful shutdown: \n", sig)
neko.Shutdown()
neko.Logger.Info().Msg("Shutting down complete")
neko.logger.Info().Msg("shutdown complete")
}