initial commit - basic websocket server and db init
This commit is contained in:
commit
25e51fb2d5
12 changed files with 432 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
felt
|
||||
mongodb/data/*
|
||||
mongodb/.env
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Anmol Sethi <hi@nhooyr.io>
|
||||
Copyright (c) 2022 Derek Stevens <nilix@nilfm.cc>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
145
gametable/server.go
Normal file
145
gametable/server.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package gametable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"nhooyr.io/websocket"
|
||||
"golang.org/x/time/rate"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
"sync"
|
||||
"net/http"
|
||||
"log"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Subscriber struct {
|
||||
msgs chan []byte
|
||||
closeSlow func()
|
||||
}
|
||||
|
||||
type GameTableServer struct {
|
||||
subscribeMessageBuffer int
|
||||
publishLimiter *rate.Limiter
|
||||
logf func(f string, v ...interface{})
|
||||
serveMux http.ServeMux
|
||||
subscribersLock sync.Mutex
|
||||
subscribers map[*Subscriber]struct{}
|
||||
}
|
||||
|
||||
func New() *GameTableServer {
|
||||
srvr := &GameTableServer {
|
||||
subscribeMessageBuffer: 16,
|
||||
logf: log.Printf,
|
||||
subscribers: make(map[*Subscriber]struct{}),
|
||||
publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8),
|
||||
}
|
||||
srvr.serveMux.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
|
||||
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
||||
|
||||
return srvr
|
||||
}
|
||||
|
||||
func (self *GameTableServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
self.serveMux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (self *GameTableServer) subscribeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
self.logf("%v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "")
|
||||
|
||||
err = self.subscribe(r.Context(), c)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if websocket.CloseStatus(err) == websocket.StatusNormalClosure ||
|
||||
websocket.CloseStatus(err) == websocket.StatusGoingAway {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
self.logf("%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GameTableServer) subscribe(ctx context.Context, c *websocket.Conn) error {
|
||||
ctx = c.CloseRead(ctx)
|
||||
|
||||
s := &Subscriber{
|
||||
msgs: make(chan []byte, self.subscribeMessageBuffer),
|
||||
closeSlow: func() {
|
||||
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
|
||||
},
|
||||
}
|
||||
self.addSubscriber(s)
|
||||
defer self.deleteSubscriber(s)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.msgs:
|
||||
err := writeTimeout(ctx, time.Second*5, c, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GameTableServer) publishHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
body := http.MaxBytesReader(w, r.Body, 8192)
|
||||
msg, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
self.publish(msg)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (self *GameTableServer) publish(msg []byte) {
|
||||
self.subscribersLock.Lock()
|
||||
defer self.subscribersLock.Unlock()
|
||||
|
||||
// decode message and store in DB
|
||||
|
||||
self.publishLimiter.Wait(context.Background())
|
||||
|
||||
for s := range self.subscribers {
|
||||
select {
|
||||
case s.msgs <- msg:
|
||||
default:
|
||||
go s.closeSlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GameTableServer) addSubscriber(s *Subscriber) {
|
||||
self.subscribersLock.Lock()
|
||||
self.subscribers[s] = struct{}{}
|
||||
self.subscribersLock.Unlock()
|
||||
}
|
||||
|
||||
func (self *GameTableServer) deleteSubscriber(s *Subscriber) {
|
||||
self.subscribersLock.Lock()
|
||||
delete(self.subscribers, s)
|
||||
self.subscribersLock.Unlock()
|
||||
}
|
||||
|
||||
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.Write(ctx, websocket.MessageText, msg)
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module nilfm.cc/git/felt
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.10.3 // indirect
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
41
go.sum
Normal file
41
go.sum
Normal file
|
@ -0,0 +1,41 @@
|
|||
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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
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/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
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/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.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/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
52
main.go
Normal file
52
main.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"nilfm.cc/git/felt/gametable"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
l, err := net.Listen("tcp", os.Args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gt := gametable.New()
|
||||
s := &http.Server{
|
||||
Handler: gt,
|
||||
ReadTimeout: time.Second * 10,
|
||||
WriteTimeout: time.Second * 10,
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- s.Serve(l)
|
||||
}()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt)
|
||||
select {
|
||||
case err := <-errc:
|
||||
log.Printf("failed to serve: %v", err)
|
||||
case sig := <-sigs:
|
||||
log.Printf("terminating: %v", sig)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
return s.Shutdown(ctx)
|
||||
}
|
25
models/models.go
Normal file
25
models/models.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type TableKey struct {
|
||||
name: string
|
||||
passcode: string
|
||||
}
|
||||
|
||||
type DiceRoll struct {
|
||||
faces: uint8
|
||||
roll: uint8[]
|
||||
player: string
|
||||
note: string
|
||||
timestamp: time.Time
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
id: string
|
||||
spriteUri: string
|
||||
x: int
|
||||
y: int
|
||||
}
|
5
mongodb/.env.example
Normal file
5
mongodb/.env.example
Normal file
|
@ -0,0 +1,5 @@
|
|||
MONGO_INITDB_ROOT_USERNAME=root
|
||||
MONGO_INITDB_ROOT_PASSWORD=not_the_real_password
|
||||
MONGO_INITDB_DATABASE=admin
|
||||
dbUser=felt_api
|
||||
dbPwd=not_the_real_password_either
|
3
mongodb/Dockerfile
Normal file
3
mongodb/Dockerfile
Normal file
|
@ -0,0 +1,3 @@
|
|||
FROM mongo:6.0.2
|
||||
|
||||
COPY ./db_init.sh /docker-entrypoint-initdb.d/
|
76
mongodb/adapter.go
Normal file
76
mongodb/adapter.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package dbengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"nilfm.cc/git/felt/models"
|
||||
)
|
||||
|
||||
interface DbAdapter {
|
||||
Init(mongoUri: string): error
|
||||
|
||||
CreateTable(table: models.TableKey): error
|
||||
DestroyTable(table: models.TableKey): error
|
||||
|
||||
InsertDiceRoll(table: models.TableKey, diceRoll: models.DiceRoll): error
|
||||
GetDiceRolls(table: models.TableKey): models.DiceRoll[], error
|
||||
|
||||
SetMapImageUrl(table: models.TableKey, url: string): error
|
||||
GetMapImageUrl(table: models.TableKey): string, error
|
||||
|
||||
AddToken(table: models.TableKey, token: models.Token): error
|
||||
RemoveToken(table: models.TableKey, tokenId: string): error
|
||||
ModifyToken(table: models.TableKey, token: models.Token): error
|
||||
GetTokens(table: models.TableKey): models.Token[], error
|
||||
}
|
||||
|
||||
type DbEngine struct {
|
||||
client: mongo.Client
|
||||
}
|
||||
|
||||
func (self *DbEngine) Init(mongoUri: string) error {
|
||||
client, err := mongo.NewClient(options.Client().ApplyURI(mongoUri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.client = client
|
||||
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
err = client.Connect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
db := client.Database("felt")
|
||||
|
||||
err = self.ensureCollections(db)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *DbEngine) ensureCollections(db: mongo.Database) error {
|
||||
tables := db.Collection("tables")
|
||||
if tables == nil {
|
||||
createCmd := bson.D{
|
||||
{"create", "tables"},
|
||||
{"clusteredIndex", {
|
||||
{"key", {"name"}},
|
||||
{"unique", true},
|
||||
{"name", "idx_tables_unique_names"}
|
||||
}}
|
||||
}
|
||||
|
||||
var createResult bson.M
|
||||
err := db.RunCommand(
|
||||
context.WithTimeout(context.Background(), 10*time.Second),
|
||||
createCmd).Decode(&createResult)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
40
mongodb/db_init.sh
Normal file
40
mongodb/db_init.sh
Normal file
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# dbUser is the userName used from applicatoin code to interact with databases and dbPwd is the password for this user.
|
||||
# MONGO_INITDB_ROOT_USERNAME & MONGO_INITDB_ROOT_PASSWORD is the config for db admin.
|
||||
# admin user is expected to be already created when this script executes. We use it here to authenticate as admin to create
|
||||
# dbUser and databases.
|
||||
|
||||
echo ">>>>>>> trying to create database and users"
|
||||
if [ -n "${MONGO_INITDB_ROOT_USERNAME:-}" ] && [ -n "${MONGO_INITDB_ROOT_PASSWORD:-}" ] && [ -n "${dbUser:-}" ] && [ -n "${dbPwd:-}" ]; then
|
||||
mongosh -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD<<EOF
|
||||
|
||||
// create DB
|
||||
db=db.getSiblingDB('felt');
|
||||
use felt;
|
||||
|
||||
if (db.system.users.find({user:'$dbUser'}).count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create user account the API uses to connect to db
|
||||
db.createUser({
|
||||
user: '$dbUser',
|
||||
pwd: '$dbPwd',
|
||||
roles: [{
|
||||
role: 'readWrite',
|
||||
db: 'felt'
|
||||
}]
|
||||
});
|
||||
|
||||
// Insert default config options
|
||||
db.config.insertOne({
|
||||
_immutable: true
|
||||
});
|
||||
|
||||
EOF
|
||||
else
|
||||
echo "MONGO_INITDB_ROOT_USERNAME,MONGO_INITDB_ROOT_PASSWORD,dbUser and dbPwd must be provided. Some of these are missing, hence exiting database and user creation"
|
||||
exit 403
|
||||
fi
|
11
mongodb/run.sh
Executable file
11
mongodb/run.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ "$1" = "build" ]; then
|
||||
docker build -t felt_db ./
|
||||
fi
|
||||
|
||||
if [ ! -e .env ]; then
|
||||
cp .env.example .env
|
||||
fi
|
||||
|
||||
docker run -it --rm --env-file .env -p 27017:27017 -v felt_data:/data/db felt_db
|
Loading…
Reference in a new issue