From c2320e8701792eff321f995a6a8d11b4ae92b3d7 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Mon, 6 Jan 2020 21:57:17 +0100 Subject: [PATCH] Implement server --- go.mod | 13 ++++++ go.sum | 30 +++++++++++++ handler.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 55 ++++++++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handler.go create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..19b2de0 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/Luzifer/mapshare + +go 1.13 + +require ( + github.com/Luzifer/rconfig/v2 v2.2.1 + github.com/gofrs/uuid v3.2.0+incompatible + github.com/gorilla/mux v1.7.3 + github.com/gorilla/websocket v1.4.1 + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/sirupsen/logrus v1.4.2 + golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..12f76bd --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/Luzifer/rconfig v2.2.0+incompatible h1:Kle3+rshPM7LxciOheaR4EfHUzibkDDGws04sefQ5m8= +github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= +github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e h1:LwyF2AFISC9nVbS6MgzsaQNSUsRXI49GS+YQ5KX/QH0= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..0e352cd --- /dev/null +++ b/handler.go @@ -0,0 +1,121 @@ +package main + +import ( + "encoding/json" + "net/http" + "sync" + "time" + + "github.com/gofrs/uuid" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" +) + +type position struct { + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + Retained bool `json:"retained"` + Time time.Time `json:"time"` +} + +var ( + reqDistributors = map[string]map[string]chan position{} + reqDistributorsLock = new(sync.RWMutex) + reqRetainer = map[string]position{} + reqRetainerLock = new(sync.RWMutex) + + upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } +) + +func handleRedirectRandom(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, uuid.Must(uuid.NewV4()).String(), http.StatusFound) +} + +func handleMapFrontend(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "frontend/index.html") +} + +func handleMapSocket(w http.ResponseWriter, r *http.Request) { + var ( + vars = mux.Vars(r) + mapID = vars["mapID"] + sockID = uuid.Must(uuid.NewV4()).String() + updates = make(chan position, 10) + ) + + // Register update channel + reqDistributorsLock.Lock() + if _, ok := reqDistributors[mapID]; !ok { + reqDistributors[mapID] = make(map[string]chan position) + } + reqDistributors[mapID][sockID] = updates + reqDistributorsLock.Unlock() + + // In case a retained position is available queue it + reqRetainerLock.RLock() + if p, ok := reqRetainer[mapID]; ok { + updates <- p + } + reqRetainerLock.RUnlock() + + // Queue deregistration + defer func() { + reqDistributorsLock.Lock() + defer reqDistributorsLock.Unlock() + + delete(reqDistributors[mapID], sockID) + close(updates) + }() + + // Open socket + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.WithError(err).Debug("Unable to open websocket") + return + } + defer conn.Close() + + // Send updates + for pos := range updates { + if err = conn.WriteJSON(pos); err != nil { + log.WithError(err).Debug("Unable to send position") + return + } + } +} + +func handleMapSubmit(w http.ResponseWriter, r *http.Request) { + var ( + pos position + vars = mux.Vars(r) + mapID = vars["mapID"] + ) + + if err := json.NewDecoder(r.Body).Decode(&pos); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if pos.Retained { + reqRetainerLock.Lock() + reqRetainer[mapID] = pos + reqRetainerLock.Unlock() + } + + reqDistributorsLock.RLock() + defer reqDistributorsLock.RUnlock() + + distributors, ok := reqDistributors[mapID] + if !ok || len(distributors) == 0 { + // No subscribers at all + return + } + + for _, c := range distributors { + c <- pos + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..dbc1d85 --- /dev/null +++ b/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "net/http" + "os" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + + "github.com/Luzifer/rconfig/v2" +) + +var ( + cfg = struct { + Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + }{} + + version = "dev" +) + +func init() { + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("mapshare %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + r := mux.NewRouter() + + r.HandleFunc("/", handleRedirectRandom).Methods(http.MethodGet) + + r.PathPrefix("/asset/").Handler( + http.StripPrefix("/asset/", http.FileServer(http.Dir("frontend"))), + ).Methods(http.MethodGet) + + r.HandleFunc("/{mapID}", handleMapFrontend).Methods(http.MethodGet) + r.HandleFunc("/{mapID}", handleMapSubmit).Methods(http.MethodPut) + r.HandleFunc("/{mapID}/ws", handleMapSocket).Methods(http.MethodGet) + + log.WithError(http.ListenAndServe(cfg.Listen, r)).Error("HTTP server caused an error") +}