grim/amongchat

Initial revision
draft default tip
2020-09-23, Gary Kramlich
dead8a9014bd
Parents
Children
Initial revision
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,2 @@
+syntax: glob
+amongchat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/globals/globals.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,4 @@
+package globals
+
+type Globals struct {
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/go.mod Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,9 @@
+module keep.imfreedom.org/grim/amongchat
+
+go 1.14
+
+require (
+ github.com/alecthomas/kong v0.2.11
+ github.com/gin-gonic/gin v1.6.3
+ github.com/gorilla/websocket v1.4.2
+)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/go.sum Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,46 @@
+github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4=
+github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/main.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "github.com/alecthomas/kong"
+
+ "keep.imfreedom.org/grim/amongchat/globals"
+ "keep.imfreedom.org/grim/amongchat/server"
+)
+
+type cli struct {
+ globals.Globals
+
+ Serve server.Cmd `kong:"cmd,help='Run the webserver',default='1'"`
+}
+
+func main() {
+ cli := cli{}
+ ctx := kong.Parse(
+ &cli,
+ kong.Name("among-chat"),
+ kong.Description("Among Us web socket exporter"),
+ kong.UsageOnError(),
+ )
+
+ err := ctx.Run(&cli.Globals)
+ ctx.FatalIfErrorf(err)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/muxer/client.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,44 @@
+package muxer
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type Client struct {
+ conn *websocket.Conn
+
+ lastContact time.Time
+
+ queue chan interface{}
+}
+
+func newClient(conn *websocket.Conn) *Client {
+ return &Client{
+ lastContact: time.Now(),
+ conn: conn,
+ }
+}
+
+func (m *Muxer) Register(conn *websocket.Conn) error {
+ auth := msgAuth{}
+ if err := conn.ReadJSON(&auth); err != nil {
+ conn.Close()
+ return err
+ }
+
+ if auth.Secret != m.secret {
+ conn.WriteJSON(msgError{Error: "invalid secret"})
+ conn.Close()
+
+ return fmt.Errorf("invalid secret")
+ }
+
+ client := newClient(conn)
+
+ m.register <- client
+
+ return nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/muxer/messages.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,13 @@
+package muxer
+
+type msgAuth struct {
+ Secret string `json:"secret"`
+}
+
+type msgError struct {
+ Error string `json:"error"`
+}
+
+type msgDebug struct {
+ Message string `json:"message"`
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/muxer/muxer.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,58 @@
+package muxer
+
+type Muxer struct {
+ secret string
+
+ clients map[*Client]bool
+
+ queue chan interface{}
+ register chan *Client
+ shutdown chan bool
+ unregister chan *Client
+}
+
+func New(secret string) *Muxer {
+ return &Muxer{
+ secret: secret,
+ queue: make(chan interface{}, 10),
+ register: make(chan *Client),
+ shutdown: make(chan bool),
+ unregister: make(chan *Client),
+ }
+}
+
+// Run manages all of the client connections and sends messages to them when
+// necessary.
+func (m *Muxer) Run() error {
+ for {
+ select {
+ case client := <-m.unregister:
+ if _, found := m.clients[client]; found {
+ delete(m.clients, client)
+ close(client.queue)
+ }
+ case client := <-m.register:
+ m.clients[client] = true
+ case message := <-m.queue:
+ for client := range m.clients {
+ select {
+ case client.queue <- message:
+ default:
+ m.unregister <- client
+ }
+ }
+ case <-m.shutdown:
+ break
+ }
+ }
+
+ return nil
+}
+
+// Close closes all connections and exits the run loop.
+func (m *Muxer) Close() {
+ for client := range m.clients {
+ m.unregister <- client
+ }
+ m.shutdown <- true
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/cmd.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,51 @@
+package server
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "keep.imfreedom.org/grim/amongchat/globals"
+ "keep.imfreedom.org/grim/amongchat/muxer"
+)
+
+type Cmd struct {
+ Secret string `kong:"flag,name='secret',required='True',env='AC_SECRET',help='A secret token to control access to the server.'"`
+ ListenAddr string `kong:"flag,name='listen-addr',default=':22022',env='AC_LISTEN_ADDR',help='The address that the server should listen on.'"`
+}
+
+func (c *Cmd) Run(g *globals.Globals) error {
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ errChan := make(chan error, 10)
+
+ muxer := muxer.New(c.Secret)
+ defer muxer.Close()
+
+ go func() {
+ if err := muxer.Run(); err != nil {
+ errChan <- err
+ }
+ }()
+
+ server := NewServer(c.ListenAddr, muxer)
+ defer server.Close()
+
+ go func() {
+ if err := server.Listen(); err != nil {
+ errChan <- err
+ }
+ }()
+
+ for {
+ select {
+ case err := <-errChan:
+ return err
+ case s := <-sigChan:
+ fmt.Printf("Captured signal %v. Exiting...\n", s)
+ errChan <- nil
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/server.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,49 @@
+package server
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "keep.imfreedom.org/grim/amongchat/muxer"
+)
+
+type Server struct {
+ listenAddr string
+
+ muxer *muxer.Muxer
+
+ server *http.Server
+}
+
+func NewServer(listenAddr string, muxer *muxer.Muxer) *Server {
+ return &Server{
+ listenAddr: listenAddr,
+ muxer: muxer,
+ server: &http.Server{
+ Addr: listenAddr,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ },
+ }
+}
+
+func (s *Server) Listen() error {
+ gin.SetMode(gin.ReleaseMode)
+
+ router := gin.Default()
+
+ router.GET("/ws", s.wsHandler)
+
+ s.server.Handler = router
+
+ fmt.Printf("listening on %s\n", s.listenAddr)
+
+ return s.server.ListenAndServe()
+}
+
+func (s *Server) Close() error {
+ return s.server.Close()
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/ws.go Wed Sep 23 05:14:17 2020 -0500
@@ -0,0 +1,25 @@
+package server
+
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+func (s *Server) wsHandler(c *gin.Context) {
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ fmt.Println("Failed to set websocket upgrade: %+v", err)
+ return
+ }
+
+ if err = s.muxer.Register(conn); err != nil {
+ fmt.Printf("failed to register ws: %v", err)
+ }
+}