From 72adff5c569c7fbee3003a1840ee5258b3caafbf Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 12:23:38 -0500 Subject: [PATCH 01/10] Implementing Mux --- go.mod | 1 + go.sum | 2 ++ pkg/instrumentation/vlc-server.go | 13 +++++++++++++ pkg/vlc-server/server.go | 24 +++++++++++++++++++++--- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 pkg/instrumentation/vlc-server.go diff --git a/go.mod b/go.mod index a6412240..9cdb9b2c 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/getsentry/sentry-go v0.8.0 github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 // indirect github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 github.com/jmoiron/sqlx v1.2.0 diff --git a/go.sum b/go.sum index 758d7bfc..da22efa6 100644 --- a/go.sum +++ b/go.sum @@ -239,6 +239,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= diff --git a/pkg/instrumentation/vlc-server.go b/pkg/instrumentation/vlc-server.go new file mode 100644 index 00000000..00967947 --- /dev/null +++ b/pkg/instrumentation/vlc-server.go @@ -0,0 +1,13 @@ +package instrumentation + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + VlcServerHttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "vlc_server_http_duration_seconds", + Help: "Duration of HTTP requests.", + }, []string{"path"}) +) diff --git a/pkg/vlc-server/server.go b/pkg/vlc-server/server.go index 5ce62590..f7d56c57 100644 --- a/pkg/vlc-server/server.go +++ b/pkg/vlc-server/server.go @@ -11,13 +11,16 @@ import ( "github.com/adanalife/tripbot/pkg/config" terrors "github.com/adanalife/tripbot/pkg/errors" "github.com/adanalife/tripbot/pkg/helpers" + "github.com/adanalife/tripbot/pkg/instrumentation" onscreensServer "github.com/adanalife/tripbot/pkg/onscreens-server" "github.com/davecgh/go-spew/spew" + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) //TODO: use more StatusExpectationFailed instead of http.StatusUnprocessableEntity -func handle(w http.ResponseWriter, r *http.Request) { +func HomeHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": // healthcheck URL, for tools to verify the stream is alive @@ -175,10 +178,14 @@ func handle(w http.ResponseWriter, r *http.Request) { func Start() { log.Println("Starting VLC web server on host", config.VlcServerHost) + r := mux.NewRouter() + r.Use(prometheusMiddleware) + r.HandleFunc("/", HomeHandler) + // make prometheus metrics available - http.Handle("/metrics", promhttp.Handler()) + r.Path("/metrics").Handler(promhttp.Handler()) - http.HandleFunc("/", handle) + http.Handle("/", r) // ListenAndServe() wants a port in the format ":NUM" //TODO: error if there's no colon to split on @@ -190,3 +197,14 @@ func Start() { terrors.Fatal(err, "couldn't start server") } } + +// prometheusMiddleware implements mux.MiddlewareFunc. +func prometheusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + path, _ := route.GetPathTemplate() + timer := prometheus.NewTimer(instrumentation.VlcServerHttpDuration.WithLabelValues(path)) + next.ServeHTTP(w, r) + timer.ObserveDuration() + }) +} From 425a628c9b102d94acfbebbce7455475cd47261d Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 12:38:53 -0500 Subject: [PATCH 02/10] Progress --- .../{vlc-server.go => common.go} | 4 ++-- pkg/instrumentation/tripbot.go | 4 ++++ pkg/server/server.go | 14 +++++++++++--- pkg/vlc-server/server.go | 18 +++--------------- 4 files changed, 20 insertions(+), 20 deletions(-) rename pkg/instrumentation/{vlc-server.go => common.go} (69%) diff --git a/pkg/instrumentation/vlc-server.go b/pkg/instrumentation/common.go similarity index 69% rename from pkg/instrumentation/vlc-server.go rename to pkg/instrumentation/common.go index 00967947..0ab30584 100644 --- a/pkg/instrumentation/vlc-server.go +++ b/pkg/instrumentation/common.go @@ -6,8 +6,8 @@ import ( ) var ( - VlcServerHttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + HttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "vlc_server_http_duration_seconds", Help: "Duration of HTTP requests.", - }, []string{"path"}) + }, []string{"server_name", "path"}) ) diff --git a/pkg/instrumentation/tripbot.go b/pkg/instrumentation/tripbot.go index ab2146ac..88b577be 100644 --- a/pkg/instrumentation/tripbot.go +++ b/pkg/instrumentation/tripbot.go @@ -15,4 +15,8 @@ var ( Help: "The total number of chat commands", }, []string{"command"}, ) + // TripbotServerHttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + // Name: "tripbot_server_http_duration_seconds", + // Help: "Duration of HTTP requests.", + // }, []string{"path"}) ) diff --git a/pkg/server/server.go b/pkg/server/server.go index 5ba9a808..4c21ed63 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -10,8 +10,10 @@ import ( "github.com/adanalife/tripbot/pkg/chatbot" "github.com/adanalife/tripbot/pkg/config" terrors "github.com/adanalife/tripbot/pkg/errors" + "github.com/adanalife/tripbot/pkg/helpers" mytwitch "github.com/adanalife/tripbot/pkg/twitch" "github.com/adanalife/tripbot/pkg/users" + "github.com/gorilla/mux" "github.com/logrusorgru/aurora" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/crypto/acme/autocert" @@ -21,7 +23,7 @@ var certManager autocert.Manager var server *http.Server //TODO: consider adding routes to control MPD -func handle(w http.ResponseWriter, r *http.Request) { +func HomeHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": //TODO: write real healthchecks for ready vs live @@ -166,10 +168,16 @@ func Start() { var err error log.Println("Starting web server on port", config.TripbotServerPort) + r := mux.NewRouter() + + // add prometheus middleware + r.Use(helpers.PrometheusMiddleware) // make prometheus metrics available - http.Handle("/metrics", promhttp.Handler()) + r.Path("/metrics").Handler(promhttp.Handler()) + + r.HandleFunc("/", HomeHandler) + http.Handle("/", r) - http.HandleFunc("/", handle) port := fmt.Sprintf(":%s", config.TripbotServerPort) // serve http diff --git a/pkg/vlc-server/server.go b/pkg/vlc-server/server.go index f7d56c57..167830c4 100644 --- a/pkg/vlc-server/server.go +++ b/pkg/vlc-server/server.go @@ -11,11 +11,9 @@ import ( "github.com/adanalife/tripbot/pkg/config" terrors "github.com/adanalife/tripbot/pkg/errors" "github.com/adanalife/tripbot/pkg/helpers" - "github.com/adanalife/tripbot/pkg/instrumentation" onscreensServer "github.com/adanalife/tripbot/pkg/onscreens-server" "github.com/davecgh/go-spew/spew" "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -179,12 +177,13 @@ func Start() { log.Println("Starting VLC web server on host", config.VlcServerHost) r := mux.NewRouter() - r.Use(prometheusMiddleware) - r.HandleFunc("/", HomeHandler) + // add the prometheus middleware + r.Use(helpers.PrometheusMiddleware) // make prometheus metrics available r.Path("/metrics").Handler(promhttp.Handler()) + r.HandleFunc("/", HomeHandler) http.Handle("/", r) // ListenAndServe() wants a port in the format ":NUM" @@ -197,14 +196,3 @@ func Start() { terrors.Fatal(err, "couldn't start server") } } - -// prometheusMiddleware implements mux.MiddlewareFunc. -func prometheusMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - route := mux.CurrentRoute(r) - path, _ := route.GetPathTemplate() - timer := prometheus.NewTimer(instrumentation.VlcServerHttpDuration.WithLabelValues(path)) - next.ServeHTTP(w, r) - timer.ObserveDuration() - }) -} From 86ea0e92724508aa4259dc7cdcb48a8af05322f9 Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 12:47:41 -0500 Subject: [PATCH 03/10] Adding server type --- cmd/tripbot/tripbot.go | 1 + cmd/vlc-server/vlc-server.go | 2 +- pkg/config/config.go | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/tripbot/tripbot.go b/cmd/tripbot/tripbot.go index 38843160..a813e235 100644 --- a/cmd/tripbot/tripbot.go +++ b/cmd/tripbot/tripbot.go @@ -59,6 +59,7 @@ func listenForShutdown() { // startHttpServer starts a webserver, which is // used for admin tools and receiving webhooks func startHttpServer() { + config.SetServerType("tripbot") // start the HTTP server go server.Start() } diff --git a/cmd/vlc-server/vlc-server.go b/cmd/vlc-server/vlc-server.go index e21856f1..1e1e323f 100644 --- a/cmd/vlc-server/vlc-server.go +++ b/cmd/vlc-server/vlc-server.go @@ -17,7 +17,6 @@ import ( ) func main() { - // we don't yet support libvlc on darwin if helpers.RunningOnDarwin() { log.Fatal("This doesn't yet work on darwin") @@ -40,6 +39,7 @@ func main() { vlcServer.PlayRandom() // play a random video // start the webserver + config.SetServerType("vlc-server") vlcServer.Start() // listen for termination signals and gracefully shutdown diff --git a/pkg/config/config.go b/pkg/config/config.go index dd15c94d..0d9d4ce3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -31,6 +31,7 @@ const ( var ( Environment string + ServerType string // tripbot or vlc-server // ChannelName is the username of the stream ChannelName string // OutputChannel is the stream to which the bot will speak @@ -228,6 +229,10 @@ func setEnvironment() { } } +func SetServerType(server_type string) { + ServerType = server_type +} + //TODO: this should load from a config file // IgnoredUsers are users who shouldn't be in the running for miles // https://twitchinsights.net/bots From 40f15ecfbb6ef5b500c46084136c90767b935398 Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 12:52:19 -0500 Subject: [PATCH 04/10] Progress --- pkg/instrumentation/common.go | 2 +- pkg/instrumentation/tripbot.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/instrumentation/common.go b/pkg/instrumentation/common.go index 0ab30584..cc44f00f 100644 --- a/pkg/instrumentation/common.go +++ b/pkg/instrumentation/common.go @@ -9,5 +9,5 @@ var ( HttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "vlc_server_http_duration_seconds", Help: "Duration of HTTP requests.", - }, []string{"server_name", "path"}) + }, []string{"server_type", "path"}) ) diff --git a/pkg/instrumentation/tripbot.go b/pkg/instrumentation/tripbot.go index 88b577be..ab2146ac 100644 --- a/pkg/instrumentation/tripbot.go +++ b/pkg/instrumentation/tripbot.go @@ -15,8 +15,4 @@ var ( Help: "The total number of chat commands", }, []string{"command"}, ) - // TripbotServerHttpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - // Name: "tripbot_server_http_duration_seconds", - // Help: "Duration of HTTP requests.", - // }, []string{"path"}) ) From 582a185958e559d6224ad361c3ed59751ed0361d Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:19:34 -0500 Subject: [PATCH 05/10] Big refactor --- pkg/vlc-server/server.go | 332 ++++++++++++++++++++++----------------- 1 file changed, 185 insertions(+), 147 deletions(-) diff --git a/pkg/vlc-server/server.go b/pkg/vlc-server/server.go index 167830c4..d0678ae9 100644 --- a/pkg/vlc-server/server.go +++ b/pkg/vlc-server/server.go @@ -11,160 +11,183 @@ import ( "github.com/adanalife/tripbot/pkg/config" terrors "github.com/adanalife/tripbot/pkg/errors" "github.com/adanalife/tripbot/pkg/helpers" - onscreensServer "github.com/adanalife/tripbot/pkg/onscreens-server" "github.com/davecgh/go-spew/spew" "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus/promhttp" ) +// healthcheck URL, for tools to verify the stream is alive +func healthHandler(w http.ResponseWriter, r *http.Request) { + //TODO: rewrite this as a handler + healthCheck(w) +} + +func vlcCurrentHandler(w http.ResponseWriter, r *http.Request) { + // return the currently-playing file + fmt.Fprintf(w, currentlyPlaying()) +} + +func vlcPlayHandler(w http.ResponseWriter, r *http.Request) { + videoFile, ok := r.URL.Query()["video"] + if !ok || len(videoFile) > 1 { + //TODO: eventually this could just play instead of hard-requiring a param + http.Error(w, "417 expectation failed", http.StatusExpectationFailed) + return + } + + spew.Dump(videoFile) + playVideoFile(videoFile[0]) + + //TODO: better response + fmt.Fprintf(w, "OK") +} + +func vlcBackHandler(w http.ResponseWriter, r *http.Request) { + num, ok := r.URL.Query()["n"] + if !ok || len(num) > 1 { + back(1) + return + } + i, err := strconv.Atoi(num[0]) + if err != nil { + terrors.Log(err, "couldn't convert input to int") + http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) + return + } + + back(i) + + //TODO: better response + fmt.Fprintf(w, "OK") + +} + +func vlcSkipHandler(w http.ResponseWriter, r *http.Request) { + num, ok := r.URL.Query()["n"] + if !ok || len(num) > 1 { + skip(1) + return + } + i, err := strconv.Atoi(num[0]) + if err != nil { + terrors.Log(err, "couldn't convert input to int") + http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) + return + } + + skip(i) + + //TODO: better response + fmt.Fprintf(w, "OK") +} + +func vlcRandomHandler(w http.ResponseWriter, r *http.Request) { + // play a random file + err := PlayRandom() + if err != nil { + http.Error(w, "error playing random", http.StatusInternalServerError) + } + fmt.Fprintf(w, "OK") +} + +func vlcRandomHandler(w http.ResponseWriter, r *http.Request) { + // } else if strings.HasPrefix(r.URL.Path, "/onscreens/flag/show") { + base64content, ok := r.URL.Query()["duration"] + if !ok || len(base64content) > 1 { + http.Error(w, "417 expectation failed", http.StatusExpectationFailed) + return + } + durStr, err := helpers.Base64Decode(base64content[0]) + if err != nil { + terrors.Log(err, "unable to decode string") + http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) + return + } + dur, err := time.ParseDuration(durStr) + if err != nil { + http.Error(w, "unable to parse duration", http.StatusInternalServerError) + return + } + onscreensServer.ShowFlag(dur) + fmt.Fprintf(w, "OK") +} + +func onscreensGpsHideHandler(w http.ResponseWriter, r *http.Request) { + //} else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/hide") { + onscreensServer.HideGPSImage() + fmt.Fprintf(w, "OK") + +} + +func onscreensGpsShowHandler(w http.ResponseWriter, r *http.Request) { + //} else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/show") { + onscreensServer.ShowGPSImage() + fmt.Fprintf(w, "OK") + +} + +func onscreensTimewarpShowHandler(w http.ResponseWriter, r *http.Request) { + //} else if strings.HasPrefix(r.URL.Path, "/onscreens/timewarp/show") { + onscreensServer.ShowTimewarp() + fmt.Fprintf(w, "OK") + +} + +func onscreensLeaderboardShowHandler(w http.ResponseWriter, r *http.Request) { + //} else if strings.HasPrefix(r.URL.Path, "/onscreens/leaderboard/show") { + base64content, ok := r.URL.Query()["content"] + if !ok || len(base64content) > 1 { + http.Error(w, "417 expectation failed", http.StatusExpectationFailed) + return + } + spew.Dump(base64content[0]) + content, err := helpers.Base64Decode(base64content[0]) + if err != nil { + terrors.Log(err, "unable to decode string") + http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) + return + } + + onscreensServer.Leaderboard.Show(content) + fmt.Fprintf(w, "OK") + +} + +func onscreensMiddleHideHandler(w http.ResponseWriter, r *http.Request) { + onscreensServer.MiddleText.Hide() + fmt.Fprintf(w, "OK") + +} + +func onscreensMiddleShowHandler(w http.ResponseWriter, r *http.Request) { + base64content, ok := r.URL.Query()["msg"] + if !ok || len(base64content) > 1 { + http.Error(w, "417 expectation failed", http.StatusExpectationFailed) + return + } + msg, err := helpers.Base64Decode(base64content[0]) + if err != nil { + terrors.Log(err, "unable to decode string") + http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) + return + } + onscreensServer.MiddleText.Show(msg) + fmt.Fprintf(w, "OK") +} + +func faviconHandler(w http.ResponseWriter, r *http.Request) { + // // return a favicon if anyone asks for one + //} else if r.URL.Path == "/favicon.ico" { + http.ServeFile(w, r, "assets/favicon.ico") +} + //TODO: use more StatusExpectationFailed instead of http.StatusUnprocessableEntity -func HomeHandler(w http.ResponseWriter, r *http.Request) { +func catchAllHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - // healthcheck URL, for tools to verify the stream is alive - if r.URL.Path == "/health" { - healthCheck(w) - - } else if strings.HasPrefix(r.URL.Path, "/vlc/current") { - // return the currently-playing file - fmt.Fprintf(w, currentlyPlaying()) - - } else if strings.HasPrefix(r.URL.Path, "/vlc/play") { - videoFile, ok := r.URL.Query()["video"] - if !ok || len(videoFile) > 1 { - //TODO: eventually this could just play instead of hard-requiring a param - http.Error(w, "417 expectation failed", http.StatusExpectationFailed) - return - } - - spew.Dump(videoFile) - playVideoFile(videoFile[0]) - - //TODO: better response - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/vlc/back") { - num, ok := r.URL.Query()["n"] - if !ok || len(num) > 1 { - back(1) - return - } - i, err := strconv.Atoi(num[0]) - if err != nil { - terrors.Log(err, "couldn't convert input to int") - http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) - return - } - - back(i) - - //TODO: better response - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/vlc/skip") { - num, ok := r.URL.Query()["n"] - if !ok || len(num) > 1 { - skip(1) - return - } - i, err := strconv.Atoi(num[0]) - if err != nil { - terrors.Log(err, "couldn't convert input to int") - http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) - return - } - - skip(i) - - //TODO: better response - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/vlc/random") { - // play a random file - err := PlayRandom() - if err != nil { - http.Error(w, "error playing random", http.StatusInternalServerError) - } - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/flag/show") { - base64content, ok := r.URL.Query()["dur"] - if !ok || len(base64content) > 1 { - http.Error(w, "417 expectation failed", http.StatusExpectationFailed) - return - } - durStr, err := helpers.Base64Decode(base64content[0]) - if err != nil { - terrors.Log(err, "unable to decode string") - http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) - return - } - dur, err := time.ParseDuration(durStr) - if err != nil { - http.Error(w, "unable to parse duration", http.StatusInternalServerError) - return - } - onscreensServer.ShowFlag(dur) - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/hide") { - onscreensServer.HideGPSImage() - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/show") { - onscreensServer.ShowGPSImage() - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/timewarp/show") { - onscreensServer.ShowTimewarp() - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/leaderboard/show") { - base64content, ok := r.URL.Query()["content"] - if !ok || len(base64content) > 1 { - http.Error(w, "417 expectation failed", http.StatusExpectationFailed) - return - } - spew.Dump(base64content[0]) - content, err := helpers.Base64Decode(base64content[0]) - if err != nil { - terrors.Log(err, "unable to decode string") - http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) - return - } - - onscreensServer.Leaderboard.Show(content) - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/middle/hide") { - onscreensServer.MiddleText.Hide() - fmt.Fprintf(w, "OK") - - } else if strings.HasPrefix(r.URL.Path, "/onscreens/middle/show") { - base64content, ok := r.URL.Query()["msg"] - if !ok || len(base64content) > 1 { - http.Error(w, "417 expectation failed", http.StatusExpectationFailed) - return - } - msg, err := helpers.Base64Decode(base64content[0]) - if err != nil { - terrors.Log(err, "unable to decode string") - http.Error(w, "422 unprocessable entity", http.StatusUnprocessableEntity) - return - } - onscreensServer.MiddleText.Show(msg) - fmt.Fprintf(w, "OK") - - // return a favicon if anyone asks for one - } else if r.URL.Path == "/favicon.ico" { - http.ServeFile(w, r, "assets/favicon.ico") - - // some other URL was used - } else { - http.Error(w, "404 not found", http.StatusNotFound) - log.Println("someone tried hitting", r.URL.Path) - return - } + http.Error(w, "404 not found", http.StatusNotFound) + log.Println("someone tried hitting", r.URL.Path) + return // someone tried a PUT or a DELETE or something default: @@ -183,7 +206,22 @@ func Start() { // make prometheus metrics available r.Path("/metrics").Handler(promhttp.Handler()) - r.HandleFunc("/", HomeHandler) + r.HandleFunc("/health", healthHandler).Methods("GET") + r.HandleFunc("/vlc/current", vlcCurrentHandler).Methods("GET") + r.HandleFunc("/vlc/play", vlcPlayHandler).Methods("GET") + r.HandleFunc("/vlc/back", vlcBackHandler).Methods("GET") + r.HandleFunc("/vlc/skip", vlcSkipHandler).Methods("GET") + r.HandleFunc("/vlc/random", vlcRandomHandler).Methods("GET") + r.HandleFunc("/onscreens/flag/show", onscreensFlagShowHandler).Methods("GET") + r.HandleFunc("/onscreens/gps/hide", onscreensGpsHideHandler).Methods("GET") + r.HandleFunc("/onscreens/gps/show", onscreensGpsShowHandler).Methods("GET") + r.HandleFunc("/onscreens/timewarp/show", onscreensTimewarpShowHandler).Methods("GET") + r.HandleFunc("/onscreens/leaderboard/show", onscreensLeaderboardShowHandler).Methods("GET") + r.HandleFunc("/onscreens/middle/hide", onscreensMiddleHideHandler).Methods("GET") + r.HandleFunc("/onscreens/middle/show", onscreensMiddleShowHandler).Methods("GET") + r.HandleFunc("/favicon.ico", faviconHandler).Methods("GET") + // r.PathPrefix("/").Handler(catchAllHandler) + r.HandleFunc("/", catchAllHandler) http.Handle("/", r) // ListenAndServe() wants a port in the format ":NUM" From db95ab2835e049f68335f292ab75743b1f48952c Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:25:00 -0500 Subject: [PATCH 06/10] Cleanup --- pkg/vlc-server/server.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/vlc-server/server.go b/pkg/vlc-server/server.go index d0678ae9..b709c95d 100644 --- a/pkg/vlc-server/server.go +++ b/pkg/vlc-server/server.go @@ -11,6 +11,7 @@ import ( "github.com/adanalife/tripbot/pkg/config" terrors "github.com/adanalife/tripbot/pkg/errors" "github.com/adanalife/tripbot/pkg/helpers" + onscreensServer "github.com/adanalife/tripbot/pkg/onscreens-server" "github.com/davecgh/go-spew/spew" "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -90,8 +91,7 @@ func vlcRandomHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "OK") } -func vlcRandomHandler(w http.ResponseWriter, r *http.Request) { - // } else if strings.HasPrefix(r.URL.Path, "/onscreens/flag/show") { +func onscreensFlagShowHandler(w http.ResponseWriter, r *http.Request) { base64content, ok := r.URL.Query()["duration"] if !ok || len(base64content) > 1 { http.Error(w, "417 expectation failed", http.StatusExpectationFailed) @@ -113,28 +113,21 @@ func vlcRandomHandler(w http.ResponseWriter, r *http.Request) { } func onscreensGpsHideHandler(w http.ResponseWriter, r *http.Request) { - //} else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/hide") { onscreensServer.HideGPSImage() fmt.Fprintf(w, "OK") - } func onscreensGpsShowHandler(w http.ResponseWriter, r *http.Request) { - //} else if strings.HasPrefix(r.URL.Path, "/onscreens/gps/show") { onscreensServer.ShowGPSImage() fmt.Fprintf(w, "OK") - } func onscreensTimewarpShowHandler(w http.ResponseWriter, r *http.Request) { - //} else if strings.HasPrefix(r.URL.Path, "/onscreens/timewarp/show") { onscreensServer.ShowTimewarp() fmt.Fprintf(w, "OK") - } func onscreensLeaderboardShowHandler(w http.ResponseWriter, r *http.Request) { - //} else if strings.HasPrefix(r.URL.Path, "/onscreens/leaderboard/show") { base64content, ok := r.URL.Query()["content"] if !ok || len(base64content) > 1 { http.Error(w, "417 expectation failed", http.StatusExpectationFailed) @@ -150,7 +143,6 @@ func onscreensLeaderboardShowHandler(w http.ResponseWriter, r *http.Request) { onscreensServer.Leaderboard.Show(content) fmt.Fprintf(w, "OK") - } func onscreensMiddleHideHandler(w http.ResponseWriter, r *http.Request) { @@ -207,11 +199,17 @@ func Start() { r.Path("/metrics").Handler(promhttp.Handler()) r.HandleFunc("/health", healthHandler).Methods("GET") + + // vlc endpoints + //TODO: consider refactoring into a subrouter r.HandleFunc("/vlc/current", vlcCurrentHandler).Methods("GET") r.HandleFunc("/vlc/play", vlcPlayHandler).Methods("GET") r.HandleFunc("/vlc/back", vlcBackHandler).Methods("GET") r.HandleFunc("/vlc/skip", vlcSkipHandler).Methods("GET") r.HandleFunc("/vlc/random", vlcRandomHandler).Methods("GET") + + // onscreen endpoints + //TODO: consider refactoring into a subrouter r.HandleFunc("/onscreens/flag/show", onscreensFlagShowHandler).Methods("GET") r.HandleFunc("/onscreens/gps/hide", onscreensGpsHideHandler).Methods("GET") r.HandleFunc("/onscreens/gps/show", onscreensGpsShowHandler).Methods("GET") @@ -219,7 +217,11 @@ func Start() { r.HandleFunc("/onscreens/leaderboard/show", onscreensLeaderboardShowHandler).Methods("GET") r.HandleFunc("/onscreens/middle/hide", onscreensMiddleHideHandler).Methods("GET") r.HandleFunc("/onscreens/middle/show", onscreensMiddleShowHandler).Methods("GET") + + //TODO: refactor into static serving r.HandleFunc("/favicon.ico", faviconHandler).Methods("GET") + + //TODO: update to be proper catchall(?) // r.PathPrefix("/").Handler(catchAllHandler) r.HandleFunc("/", catchAllHandler) http.Handle("/", r) From 8ef53c79ba31ea5d2f96953c668b69f9de6dff49 Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:30:47 -0500 Subject: [PATCH 07/10] Rename handler --- pkg/server/server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/server/server.go b/pkg/server/server.go index 4c21ed63..a054fe17 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -23,7 +23,7 @@ var certManager autocert.Manager var server *http.Server //TODO: consider adding routes to control MPD -func HomeHandler(w http.ResponseWriter, r *http.Request) { +func catchAllHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": //TODO: write real healthchecks for ready vs live @@ -175,7 +175,9 @@ func Start() { // make prometheus metrics available r.Path("/metrics").Handler(promhttp.Handler()) - r.HandleFunc("/", HomeHandler) + //TODO: update to be proper catchall(?) + // r.PathPrefix("/").Handler(catchAllHandler) + r.HandleFunc("/", catchAllHandler) http.Handle("/", r) port := fmt.Sprintf(":%s", config.TripbotServerPort) From 1807197fb166433e112363645eefff96a352f823 Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:39:47 -0500 Subject: [PATCH 08/10] Refactoring tripbot GET requests --- pkg/server/server.go | 132 ++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/pkg/server/server.go b/pkg/server/server.go index a054fe17..0b2ac7a8 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "net/http" - "strings" "github.com/adanalife/tripbot/pkg/chatbot" "github.com/adanalife/tripbot/pkg/config" @@ -22,77 +21,78 @@ import ( var certManager autocert.Manager var server *http.Server -//TODO: consider adding routes to control MPD -func catchAllHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - //TODO: write real healthchecks for ready vs live - // healthcheck URL, for tools to verify the bot is alive - if r.URL.Path == "/health/ready" || r.URL.Path == "/health/live" { - fmt.Fprintf(w, "OK") - - // twitch issues a request here when creating a new webhook subscription - } else if strings.HasPrefix(r.URL.Path, "/webhooks/twitch") { - log.Println("got webhook challenge request at", r.URL.Path) - // exit early if we've disabled webhooks - if config.DisableTwitchWebhooks { - http.Error(w, "501 not implemented", http.StatusNotImplemented) - return - } +//TODO: write real healthchecks for ready vs live +// healthcheck URL, for tools to verify the bot is alive +func healthHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "OK") +} - challenge, ok := r.URL.Query()["hub.challenge"] - if !ok || len(challenge[0]) < 1 { - terrors.Log(nil, "something went wrong with the challenge") - log.Printf("%#v", r.URL.Query()) - http.Error(w, "404 not found", http.StatusNotFound) - return - } - log.Println("returning challenge") - fmt.Fprintf(w, string(challenge[0])) +// twitch issues a request here when creating a new webhook subscription +func webhooksTwitchHandler(w http.ResponseWriter, r *http.Request) { + log.Println("got webhook challenge request at", r.URL.Path) + // exit early if we've disabled webhooks + if config.DisableTwitchWebhooks { + http.Error(w, "501 not implemented", http.StatusNotImplemented) + return + } - // this endpoint returns private twitch access tokens - } else if r.URL.Path == "/auth/twitch" { - secret, ok := r.URL.Query()["auth"] - if !ok || !isValidSecret(secret[0]) { - http.Error(w, "404 not found", http.StatusNotFound) - return - } + challenge, ok := r.URL.Query()["hub.challenge"] + if !ok || len(challenge[0]) < 1 { + terrors.Log(nil, "something went wrong with the challenge") + log.Printf("%#v", r.URL.Query()) + http.Error(w, "404 not found", http.StatusNotFound) + return + } + log.Println("returning challenge") + fmt.Fprintf(w, string(challenge[0])) +} - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, twitchAuthJSON()) +// this endpoint returns private twitch access tokens +func authTwitchHandler(w http.ResponseWriter, r *http.Request) { + secret, ok := r.URL.Query()["auth"] + if !ok || !isValidSecret(secret[0]) { + http.Error(w, "404 not found", http.StatusNotFound) + return + } - // oauth callback URL, requests come from Twitch and have a special code - // we then use that code to generate a User Access Token - } else if r.URL.Path == "/auth/callback" { - codes, ok := r.URL.Query()["code"] + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, twitchAuthJSON()) +} - if !ok || len(codes[0]) < 1 { - msg := "no code in response from twitch" - terrors.Log(errors.New("code missing"), msg) - //TODO: better error than StatusNotFound (404) - http.Error(w, msg, http.StatusNotFound) - return - } - code := string(codes[0]) +// oauth callback URL, requests come from Twitch and have a special code +// we then use that code to generate a User Access Token +func authCallbackHandler(w http.ResponseWriter, r *http.Request) { + codes, ok := r.URL.Query()["code"] + + if !ok || len(codes[0]) < 1 { + msg := "no code in response from twitch" + terrors.Log(errors.New("code missing"), msg) + //TODO: better error than StatusNotFound (404) + http.Error(w, msg, http.StatusNotFound) + return + } + code := string(codes[0]) - log.Println(aurora.Cyan("successfully received token from twitch!")) - // use the code to generate an access token - mytwitch.GenerateUserAccessToken(code) + log.Println(aurora.Cyan("successfully received token from twitch!")) + // use the code to generate an access token + mytwitch.GenerateUserAccessToken(code) - //TODO: return a pretty HTML page here (black background, logo, etc) - fmt.Fprintf(w, "Success!") - return + //TODO: return a pretty HTML page here (black background, logo, etc) + fmt.Fprintf(w, "Success!") +} - // return a favicon if anyone asks for one - } else if r.URL.Path == "/favicon.ico" { - http.ServeFile(w, r, "assets/favicon.ico") +// return a favicon if anyone asks for one +func faviconHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "assets/favicon.ico") +} - // some other URL was used - } else { - http.Error(w, "404 not found", http.StatusNotFound) - log.Println("someone tried hitting", r.URL.Path) - return - } +//TODO: consider adding routes to control MPD +func catchAllHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + http.Error(w, "404 not found", http.StatusNotFound) + log.Println("someone tried hitting", r.URL.Path) + return case "POST": // user webhooks are received via POST at this url @@ -175,6 +175,12 @@ func Start() { // make prometheus metrics available r.Path("/metrics").Handler(promhttp.Handler()) + r.HandleFunc("/health/live", healthHandler).Methods("GET") + r.HandleFunc("/health/ready", healthHandler).Methods("GET") + r.HandleFunc("/webhooks/twitch", webhooksTwitchHandler).Methods("GET") + r.HandleFunc("/auth/twitch", authTwitchHandler).Methods("GET") + r.HandleFunc("/auth/callback", authCallbackHandler).Methods("GET") + r.HandleFunc("/favicon.ico", faviconHandler).Methods("GET") //TODO: update to be proper catchall(?) // r.PathPrefix("/").Handler(catchAllHandler) r.HandleFunc("/", catchAllHandler) From 05a9af03cb655d23845230a40d86efbbc1a474e3 Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:48:31 -0500 Subject: [PATCH 09/10] Refactoring tripbot POST endpoints --- pkg/server/server.go | 125 ++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/pkg/server/server.go b/pkg/server/server.go index 0b2ac7a8..c60267c8 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -86,6 +86,62 @@ func faviconHandler(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "assets/favicon.ico") } +// user webhooks are received via POST at this url +//TODO: we can use helix.GetWebhookTopicFromRequest() and share a webhooks URL +func webhooksTwitchUsersFollowsHandler(w http.ResponseWriter, r *http.Request) { + if config.DisableTwitchWebhooks { + http.Error(w, "501 not implemented", http.StatusNotImplemented) + return + } + + resp, err := decodeFollowWebhookResponse(r) + if err != nil { + terrors.Log(err, "error decoding follow webhook") + //TODO: better error + http.Error(w, "404 not found", http.StatusNotFound) + return + } + + for _, follower := range resp.Data.Follows { + username := follower.FromName + log.Println("got webhook for new follower:", username) + users.LoginIfNecessary(username) + // announce new follower in chat + chatbot.AnnounceNewFollower(username) + } + + fmt.Fprintf(w, "OK") +} + +// these are sent when users subscribe +func webhooksTwitchSubscriptionsEventsHandler(w http.ResponseWriter, r *http.Request) { + if config.DisableTwitchWebhooks { + http.Error(w, "501 not implemented", http.StatusNotImplemented) + return + } + + resp, err := decodeSubscriptionWebhookResponse(r) + if err != nil { + terrors.Log(err, "error decoding subscription webhook") + //TODO: better error + http.Error(w, "404 not found", http.StatusNotFound) + return + } + + for _, event := range resp.Data.Events { + username := event.Subscription.UserName + log.Println("got webhook for new sub:", username) + users.LoginIfNecessary(username) + // announce new sub in chat + chatbot.AnnounceSubscriber(event.Subscription) + } + + // update the internal subscribers list + mytwitch.GetSubscribers() + + fmt.Fprintf(w, "OK") +} + //TODO: consider adding routes to control MPD func catchAllHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -95,68 +151,10 @@ func catchAllHandler(w http.ResponseWriter, r *http.Request) { return case "POST": - // user webhooks are received via POST at this url - //TODO: we can use helix.GetWebhookTopicFromRequest() and - // share a webhooks URL - if r.URL.Path == "/webhooks/twitch/users/follows" { - - if config.DisableTwitchWebhooks { - http.Error(w, "501 not implemented", http.StatusNotImplemented) - return - } - - resp, err := decodeFollowWebhookResponse(r) - if err != nil { - terrors.Log(err, "error decoding follow webhook") - //TODO: better error - http.Error(w, "404 not found", http.StatusNotFound) - return - } - - for _, follower := range resp.Data.Follows { - username := follower.FromName - log.Println("got webhook for new follower:", username) - users.LoginIfNecessary(username) - // announce new follower in chat - chatbot.AnnounceNewFollower(username) - } - - fmt.Fprintf(w, "OK") - - // these are sent when users subscribe - } else if r.URL.Path == "/webhooks/twitch/subscriptions/events" { - - if config.DisableTwitchWebhooks { - http.Error(w, "501 not implemented", http.StatusNotImplemented) - return - } - - resp, err := decodeSubscriptionWebhookResponse(r) - if err != nil { - terrors.Log(err, "error decoding subscription webhook") - //TODO: better error - http.Error(w, "404 not found", http.StatusNotFound) - return - } - - for _, event := range resp.Data.Events { - username := event.Subscription.UserName - log.Println("got webhook for new sub:", username) - users.LoginIfNecessary(username) - // announce new sub in chat - chatbot.AnnounceSubscriber(event.Subscription) - } - - // update the internal subscribers list - mytwitch.GetSubscribers() - - fmt.Fprintf(w, "OK") - } else { - // someone tried to make a post and we dont know what to do with it - http.Error(w, "404 not found", http.StatusNotFound) - log.Println("someone tried posting to", r.URL.Path) - return - } + // someone tried to make a post and we dont know what to do with it + http.Error(w, "404 not found", http.StatusNotFound) + log.Println("someone tried posting to", r.URL.Path) + return // someone tried a PUT or a DELETE or something default: fmt.Fprintf(w, "Only GET/POST methods are supported.\n") @@ -181,6 +179,9 @@ func Start() { r.HandleFunc("/auth/twitch", authTwitchHandler).Methods("GET") r.HandleFunc("/auth/callback", authCallbackHandler).Methods("GET") r.HandleFunc("/favicon.ico", faviconHandler).Methods("GET") + + r.HandleFunc("/webhooks/twitch/users/follows", webhooksTwitchUsersFollowsHandler).Methods("POST") + r.HandleFunc("/webhooks/twitch/subscriptions/events", webhooksTwitchSubscriptionsEventsHandler).Methods("POST") //TODO: update to be proper catchall(?) // r.PathPrefix("/").Handler(catchAllHandler) r.HandleFunc("/", catchAllHandler) From 341a1985b934c1dcd2f6bad6ad368b5fdf67340e Mon Sep 17 00:00:00 2001 From: Dana Merrick Date: Sun, 6 Dec 2020 13:58:08 -0500 Subject: [PATCH 10/10] Adding graceful shutdown c.p. https://github.com/gorilla/mux#graceful-shutdown --- pkg/server/server.go | 50 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/pkg/server/server.go b/pkg/server/server.go index c60267c8..8eea09b0 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,10 +1,14 @@ package server import ( + "context" "errors" "fmt" "log" "net/http" + "os" + "os/signal" + "time" "github.com/adanalife/tripbot/pkg/chatbot" "github.com/adanalife/tripbot/pkg/config" @@ -163,7 +167,8 @@ func catchAllHandler(w http.ResponseWriter, r *http.Request) { // Start starts the web server func Start() { - var err error + var wait time.Duration + // var err error log.Println("Starting web server on port", config.TripbotServerPort) r := mux.NewRouter() @@ -185,16 +190,45 @@ func Start() { //TODO: update to be proper catchall(?) // r.PathPrefix("/").Handler(catchAllHandler) r.HandleFunc("/", catchAllHandler) - http.Handle("/", r) + // http.Handle("/", r) - port := fmt.Sprintf(":%s", config.TripbotServerPort) + addr := fmt.Sprintf("0.0.0.0:%s", config.TripbotServerPort) - // serve http - err = http.ListenAndServe(port, nil) - - if err != nil { - terrors.Fatal(err, "couldn't start server") + srv := &http.Server{ + Addr: addr, + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + terrors.Log(err, "couldn't start server") + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) } // isValidSecret returns true if the given secret matches the configured oen