diff --git a/.gitlab-ci-server.yml b/.gitlab-ci-server.yml index d65460f0c6ea4cf82804f08bd30f778b6fe447fe..ae740df6a4b316896efe47cb4c11592593d4e5c1 100644 --- a/.gitlab-ci-server.yml +++ b/.gitlab-ci-server.yml @@ -2,7 +2,7 @@ server: stage: build - image: golang:1.13 + image: golang:1.14 before_script: - go mod download diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a0352759cb91b9e22d95da2e98123df1c7ba65..59b7c224cd787c95dcd55a4acf221c3b1c7c0d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### v1.0.0 <small>_June 20, 2020_</small> +* Server: Added TLS support +* Server: Added namespace support +* Server: Added monitoring (prometheus metric exporter) +* Display: Improved player + - Made players clickable to unmute and acknowledge mature audiences warning. + - Fixed chat sometimes showing when it is not supposed to be + - Minor fixes + ### v0.9.15 <small>_June 16, 2020_</small> * Server: Fixed chat * Server: Fixed `volume=0` not working @@ -102,4 +111,4 @@ ### v0.9.0 <small>_August 21, 2019_</small> -* Initial Release \ No newline at end of file +* Initial Release diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000000000000000000000000000000000000..e0bf7db58fff4f185117799386dfe755b7124414 --- /dev/null +++ b/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow +EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d +7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B +5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr +BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 +NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l +Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc +6MF9+Yw1Yy0t +-----END CERTIFICATE----- diff --git a/cmd/stream-server/main.go b/cmd/stream-server/main.go index ca793306eca7419549ada13238a7c4f9483d8fdc..18817dae579aa9168f9cc2a4d8b7b3d49580a60a 100644 --- a/cmd/stream-server/main.go +++ b/cmd/stream-server/main.go @@ -15,11 +15,15 @@ import ( const docURL = "https://stream-server.h-da.io" func main() { + app := cli.NewApp() var port int - var flagBrowser bool - var flagLocal bool + var tlsport int + var tlsCert, tlsKey string + + var listenAddr string + var flagUseHostNamespace bool var flagHelp bool app.Usage = docURL @@ -29,20 +33,37 @@ func main() { app.Flags = []cli.Flag{ cli.IntFlag{ - Name: "port, p", + Name: "port", Value: 8080, Usage: "http port to listen on", Destination: &port, }, - cli.BoolFlag{ - Name: "browser, b", - Usage: "automatically open the default browser", - Destination: &flagBrowser, + cli.IntFlag{ + Name: "tlsport", + Value: 8443, + Usage: "https port to listen on", + Destination: &tlsport, + }, + cli.StringFlag{ + Name: "listen", + Value: "0.0.0.0", + Usage: "address to listen on", + Destination: &listenAddr, + }, + cli.StringFlag{ + Name: "tlscert", + Usage: "TLS cert", + Destination: &tlsCert, + }, + cli.StringFlag{ + Name: "tlskey", + Usage: "TLS key", + Destination: &tlsKey, }, cli.BoolFlag{ - Name: "local, l", - Usage: "only listen on 127.0.0.1 (see doc)", - Destination: &flagLocal, + Name: "use-host-namespace", + Usage: "use the Host header as namespace", + Destination: &flagUseHostNamespace, }, cli.BoolFlag{ Name: "help, h", @@ -61,36 +82,56 @@ func main() { return fmt.Errorf("Unknown arguments: %v", c.Args()) } - localhost := fmt.Sprintf("127.0.0.1:%d", port) - all := fmt.Sprintf("0.0.0.0:%d", port) - outbound := localhost - - if outboundIP, err := util.GetOutboundIP(); err == nil { - outbound = fmt.Sprintf("%s:%d", outboundIP, port) - } + withTLS := tlsCert != "" && tlsKey != "" - var listenAddr string - if flagLocal { - listenAddr = localhost + fmt.Print("Serving on\n") + if listenAddr == "0.0.0.0" { + fmt.Printf(" http://%s:%d\n", "localhost", port) + if withTLS { + fmt.Printf(" https://%s:%d\n", "localhost", tlsport) + } - fmt.Printf("Serving on http://%s\n", localhost) + if outboundIP, err := util.GetOutboundIP(); err == nil { + fmt.Printf(" http://%s:%d\n", outboundIP, port) + if withTLS { + fmt.Printf(" https://%s:%d\n", outboundIP, tlsport) + } + } } else { - listenAddr = all - - fmt.Printf("Serving on\n http://%s\n http://%s\n", localhost, outbound) - } - - if flagBrowser { - err := util.OpenBrowser(fmt.Sprintf("http://%s", localhost)) - if err != nil { - fmt.Printf("Could not automatically open browser: %v\n", err) + fmt.Printf(" http://%s:%d\n", listenAddr, port) + if withTLS { + fmt.Printf(" https://%s:%d\n", listenAddr, tlsport) } } fmt.Printf("Read the quickstart at %s/quickstart to get started.\n", docURL) fmt.Printf("Stop with Ctrl-C or close this terminal.\n") - return server.ListenAndServe(listenAddr) + router := server.GetRouter(flagUseHostNamespace) + + for { + errs := make(chan error) + + go func() { + if err := server.ServeHTTP(fmt.Sprintf("%s:%d", listenAddr, port), router); err != nil { + errs <- err + } + }() + + if withTLS { + go func() { + if err := server.ServeHTTPS(fmt.Sprintf("%s:%d", listenAddr, tlsport), router, tlsCert, tlsKey); err != nil { + errs <- err + } + }() + } + + select { + case err := <-errs: + return err + } + } + } err := app.Run(os.Args) diff --git a/docs/android-integration.md b/docs/android-integration.md index 30766fc26311ca5fb0f053d52a2226c3b7171750..df0ff34e810a1964c7b69570b267f45a41e4e8ba 100644 --- a/docs/android-integration.md +++ b/docs/android-integration.md @@ -14,4 +14,4 @@ This is a very simple helper to call the Stream Server. See the [reference](refe <!-- Insert StreamServerClient.java --> ``` java linenums="1" --8<-- "docs/assets/StreamServerClient.java" -``` \ No newline at end of file +``` diff --git a/docs/download-and-run.md b/docs/download-and-run.md index 72e78926ae7049201503c1ad04c5766e53dcdd92..36f6f84aa278196a7a8095d4b41d3e66e9cc649e 100644 --- a/docs/download-and-run.md +++ b/docs/download-and-run.md @@ -59,6 +59,5 @@ Darwin (macOS) | 64 bit | [stream-server](/bin/~~~${VERSION}~~~/darwin-x86_64/s * listen tcp 0.0.0.0:8080: bind: address already in use - > Something is already using the port. This could be another Stream Server instance or some other application (like a webserver). > Close / disable the other service or change the port via the [command-line options](options.md). diff --git a/docs/options.md b/docs/options.md index b63fbdbc2b3b50596a48b517fdf90c09c7f247f2..a56760ae32bfeec3e338faca51d57c371f89bb5f 100644 --- a/docs/options.md +++ b/docs/options.md @@ -17,24 +17,35 @@ USAGE: stream-server [global options] [arguments...] VERSION: - v0.9.9 + v1.0.0 GLOBAL OPTIONS: - --port value, -p value http port to listen on (default: 8080) - --browser, -b automatically open the default browser - --local, -l only listen on 127.0.0.1 (see doc) - --help, -h show help - --version, -v print the version + --port value http port to listen on (default: 8080) + --tlsport value https port to listen on (default: 8443) + --listen value address to listen on (default: "0.0.0.0") + --tlscert value TLS cert + --tlskey value TLS key + --use-host-namespace use the Host header as namespace + --help, -h show help + --version, -v print the version ``` By default stream-server listens on all ip addresses on port 8080. This means that it is exposed to the local network (LAN / WLAN) which is needed if the app is on a phone. Note that the phone must be in the same network as the computer / laptop. -When using the Android Emulator on the same device as the server this exposure is not necessary. Use the `--local` flag to only listen on `127.0.0.1` aka `localhost`. The emulator must then connect to `10.0.2.2` as described here: +When using the Android Emulator on the same device as the server this exposure is not necessary. Use the `--listen 127.0.0.1` flag to only listen on `127.0.0.1` aka `localhost`. The emulator must then connect to `10.0.2.2` as described here: > Also note that the address 127.0.0.1 on your development machine corresponds to the emulator's own loopback interface. If you want to access services running on your development machine loopback interface (a.k.a. 127.0.0.1 on your machine), you should use the special address 10.0.2.2 instead. > > [Android Studio - User guide](https://developer.android.com/studio/run/emulator-networking#networkaddresses) +## Advanced configuration + +- HTTPS can be enable by providing a TLS cert and key. If both config values are present a additional HTTPS server is spawned on the tlsport. +- By default the stream-server has only one namespace, meaning that all incoming requests reference the same display. + With `--use-host-namespace` the _Host_ header is used as a namespace. This means that each different host has a different display. + This can be used to dynamically host different displays on one stream-server instance. + New displays are created on demand. + ## Environment Variables The `TWITCH_CLIENT_ID` environment variable can be set. This is only necessary during development to access the Twitch API. All official release builds ship with a default client id but you can still supply your own if you want. diff --git a/docs/quickstart.md b/docs/quickstart.md index ee35a11db723dc8de2b318cc136a1b3f30f9ceb4..6a6ee0599153d7cc65601824842a4f9d30d5038a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -5,8 +5,8 @@ $ ./stream-server ``` ``` Serving on - http://127.0.0.1:8080 - http://192.168.0.66:8080 + http://localhost:8080 + http://10.0.2.15:8080 Read the quickstart at https://stream-server.h-da.io/quickstart to get started. Stop with Ctrl-C or close this terminal. ``` @@ -15,10 +15,10 @@ Stop with Ctrl-C or close this terminal. In the examples of this documentation only the local address is used. If you want to connect from your phone or the emulator you need to use the network address (`192.168.0.66` in this case). Also we assume you use the default port `8080`. -1. Open <http://127.0.0.1:8080> in a browser. Leave this tab / window open while you do the other steps. +1. Open <http://localhost:8080> in a browser. Leave this tab / window open while you do the other steps. 2. Get the current state of the display: - <pre><code><http://127.0.0.1:8080/display></code></pre> + <pre><code><http://localhost:8080/display></code></pre> ``` java StreamServerClient client = new StreamServerClient("10.0.2.2:8080"); @@ -38,7 +38,7 @@ Stop with Ctrl-C or close this terminal. 3. Find some stream on twitch: - <pre><code><http://127.0.0.1:8080/twitch/getTopStreams></code></pre> + <pre><code><http://localhost:8080/twitch/getTopStreams></code></pre> ``` java List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null); // no filters ``` @@ -66,9 +66,9 @@ Stop with Ctrl-C or close this terminal. 4. Start playback of your selected stream with chat and muted volume: - <pre><code><http://127.0.0.1:8080/display?large_channel={channel name here}&volume=0&show_chat=true></code></pre> + <pre><code><http://localhost:8080/display?large_channel=CHANNEL-NAME&volume=0&show_chat=true></code></pre> ``` java - client.display("channel name here", null, 0.0f, null, true); + client.display("CHANNEL-NAME", null, 0.0f, null, true); ``` !!! example @@ -84,4 +84,4 @@ Stop with Ctrl-C or close this terminal. The tab / window from step 1 should now start playing that stream with the provided settings. -5. Read the [Reference](reference.md). \ No newline at end of file +5. Read the [Reference](reference.md). diff --git a/docs/reference.md b/docs/reference.md index f3d3fc2f9cb2dc715b69c165ab5dbf9211a2a617..8dc4c2468805b5353ccf323db1ef6f87c1781845 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -21,14 +21,14 @@ Returns [`DisplayState`](#displaystate). ??? example "Examples" * Only set the large_channel to riotgames - <pre><code><http://127.0.0.1:8080/display?large_channel=riotgames></code></pre> + <pre><code><http://localhost:8080/display?large_channel=riotgames></code></pre> ``` java /* JSONObject newState = */ client.display("riotgames", null, null, null, null); ``` * Set the large_channel to monstercat, disable the small_channel, set the volume to 0.8 and hide the chat. Notice the small_scale is not changed (because of its absence) - <pre><code><http://127.0.0.1:8080/display?large_channel=monstercat&small_channel=&volume=0.8&show_chat=false></code></pre> + <pre><code><http://localhost:8080/display?large_channel=monstercat&small_channel=&volume=0.8&show_chat=false></code></pre> ``` java /* JSONObject newState = */ client.display("monstercat", "", 0.8f, null, false); ``` @@ -47,7 +47,7 @@ Returns [`Game`](#game) array. ??? example "Examples" * Get top games - <pre><code><http://127.0.0.1:8080/twitch/getTopGames></code></pre> + <pre><code><http://localhost:8080/twitch/getTopGames></code></pre> ``` java List<JSONObject> topGames = client.twitchGetTopGames(); ``` @@ -67,13 +67,13 @@ Returns [`Game`](#game) array. ??? example "Examples" * Search for the "game" Talk Shows & Podcasts - <pre><code><http://127.0.0.1:8080/twitch/searchGames?query=talk%20show></code></pre> + <pre><code><http://localhost:8080/twitch/searchGames?query=talk%20show></code></pre> ``` java List<JSONObject> foundGames = client.twitchSearchGames("talk show"); ``` * Search for ove (will show games starting with "ove" like Overwatch) - <pre><code><http://127.0.0.1:8080/twitch/searchGames?query=ove></code></pre> + <pre><code><http://localhost:8080/twitch/searchGames?query=ove></code></pre> ``` java List<JSONObject> foundGames = client.twitchSearchGames("ove"); ``` @@ -99,25 +99,25 @@ Returns [`Stream`](#stream) array. ??? example "Examples" * Get the unfiltered top streams - <pre><code><http://127.0.0.1:8080/twitch/getTopStreams></code></pre> + <pre><code><http://localhost:8080/twitch/getTopStreams></code></pre> ``` java List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null); ``` * German top streams - <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?language=de></code></pre> + <pre><code><http://localhost:8080/twitch/getTopStreams?language=de></code></pre> ``` java List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, "de"); ``` * English top streams in Talk Shows and Podcasts - <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?language=en&game=Talk%20Shows%20%26%20Podcasts></code></pre> + <pre><code><http://localhost:8080/twitch/getTopStreams?language=en&game=Talk%20Shows%20%26%20Podcasts></code></pre> ``` java List<JSONObject> topStreams = client.twitchGetTopStreams(null, "Talk Shows and Podcasts", "en"); ``` * Tip: If your app implements a favorite list you can very easily query these channels and see who is streaming and other information - <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?channels=xqcow,dafran,kitboga></code></pre> + <pre><code><http://localhost:8080/twitch/getTopStreams?channels=xqcow,dafran,kitboga></code></pre> ``` java String[] favoriteChannels = {"xqcow", "dafran", "kitboga"}; @@ -136,7 +136,7 @@ Returns [`Stream`](#stream) array. ??? example "Examples" * Get featured streams - <pre><code><http://127.0.0.1:8080/twitch/getFeaturedStreams></code></pre> + <pre><code><http://localhost:8080/twitch/getFeaturedStreams></code></pre> ``` java List<JSONObject> featuredStreams = client.twitchGetFeaturedStreams(); ``` @@ -188,4 +188,4 @@ Returns [`Stream`](#stream) array. * The image at the `preview_img_url` url updates itself every couple of seconds. If the app uses preview images it could also. * `started_at` is an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp. * `video_height`: `#!json 1080` = `HD`, `#!json < 1080` = `SD`, `#!json > 1080` = `UHD`. - * `mature`: If the stream is meant for mature audiences (set by the streamer themselves as a guideline). \ No newline at end of file + * `mature`: If the stream is meant for mature audiences (set by the streamer themselves as a guideline). diff --git a/go.mod b/go.mod index 804b29a772cc234f3acbd0fd3156c16cf944632f..d46a6f86ba36c802503281d87c4bc8455aea3070 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,12 @@ go 1.13 require ( github.com/JamesStewy/sse v0.3.0 + github.com/dyson/certman v0.2.1 + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/hashicorp/go-multierror v1.0.0 github.com/markbates/pkger v0.12.8 + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.7.0 github.com/urfave/cli v1.21.0 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 0090024b07b3f1585d458193eb4cce51ce54fcce..99c8fce771d0f21848866b87185322be8250dbb4 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,55 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/JamesStewy/sse v0.3.0 h1:1CABzcQydehMc54Vypki6b5+/WMmGZVWi7owRvDKo7g= github.com/JamesStewy/sse v0.3.0/go.mod h1:i60+CezIhaOZYviCbCRqXCDB7cVCWvUqlD2AYnxPVRE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dyson/certman v0.2.1 h1:+EJdgffbfwIkBvnyx97mfS3slfn2o4UN/LWGm+mWVrQ= +github.com/dyson/certman v0.2.1/go.mod h1:Z2ho3wmP4oCGON+c/RF+FJVsMb9zYZVsupp0c1a+SlQ= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -17,17 +57,77 @@ 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/markbates/pkger v0.12.8 h1:4mEUzWb1HzRnxPwUevBX8g8ntsQ4rWw2R8CRB2QdZVI= github.com/markbates/pkger v0.12.8/go.mod h1:C7e5A6bnWZT+nXkUwkvysGW7sxl/IGd63HEa6N/JY8s= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE= github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/server/display.go b/internal/server/display.go index 57f037edaae443283047f132cdea238e403d5c23..1ef0d73fe24ea3b1211cf555ea1060f7f923eba3 100644 --- a/internal/server/display.go +++ b/internal/server/display.go @@ -2,7 +2,7 @@ package server import ( "encoding/json" - "log" + "fmt" "net/http" "strconv" "sync" @@ -22,26 +22,42 @@ type displayState struct { ShowChat bool `json:"show_chat"` } -// state is the current state of the display. -// It is initialized with default values. -var state = displayState{ - LargeChannel: "", - SmallChannel: "", - Volume: 0.5, - SmallScale: 0.3, - ShowChat: false, +// Display struct defines the Display itself +type Display struct { + State displayState + clientsMu sync.Mutex + // clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients. + // Must acquire clientsMu before accessing the map. + // Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever. + // This is basically a *set* in go. + clients map[*sse.Client]bool + // the prefix used for logging + loggingPrefix string } -// clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients. -// Must acquire clientsMu before accessing the map. -// Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever. -// This is basically a *set* in go. -var ( - clientsMu sync.Mutex - clients map[*sse.Client]bool = make(map[*sse.Client]bool) -) +// NewDisplay creates a new Display with default values +func NewDisplay(namespace string) *Display { + var loggingPrefix string + if namespace == "default" { + loggingPrefix = "display" + } else { + loggingPrefix = fmt.Sprintf("display[%s]", namespace) + } + + return &Display{ + State: displayState{ + LargeChannel: "", + SmallChannel: "", + Volume: 0.5, + SmallScale: 0.3, + ShowChat: false, + }, + clients: make(map[*sse.Client]bool), + loggingPrefix: loggingPrefix, + } +} -// displayStateHandler updated the state based on the query string. +// stateHandler updated the state based on the query string. // For example // /display?large_channel=asdf&small_channel=null&volume=&small_scale=0.25&show_chat=true // will @@ -52,9 +68,9 @@ var ( // - set show_chat to true // // If the floats (volume and small_scale) or the boolean (show_chat) could not be parsed, no changes are made at all and an error gets returned even if other fields could be parsed. -func displayStateHandler(w http.ResponseWriter, r *http.Request) { +func (d *Display) stateHandler(w http.ResponseWriter, r *http.Request) { // the new state we will replace the current state with IF everything parses correctly. - newState := state + newState := d.State // we save the error messages of the 5 parsing steps here. var errs error @@ -117,18 +133,18 @@ func displayStateHandler(w http.ResponseWriter, r *http.Request) { if errs != nil { // we had errors http.Error(w, errs.Error(), http.StatusBadRequest) - log.Printf("Error(s) while parsing /display query: %v\n", errs) + logger.Printf("%s: Error(s) while parsing /display query: %v\n", d.loggingPrefix, errs) return } - if state != newState { - state = newState + if d.State != newState { + d.State = newState - body, err := json.Marshal(state) + body, err := json.Marshal(d.State) if err != nil { return } - log.Printf("Updated display (%s):\n%s\n", r.URL.RawQuery, string(body)) + logger.Printf("%s: Updated (%s):\n%s\n", d.loggingPrefix, r.URL.RawQuery, string(body)) // craft the SSE message msg := sse.Msg{ @@ -142,11 +158,11 @@ func displayStateHandler(w http.ResponseWriter, r *http.Request) { // of clients to send to (while holding clientsMu) and start // sending only after giving up the lock again. var sendingTo []*sse.Client - clientsMu.Lock() - for client := range clients { + d.clientsMu.Lock() + for client := range d.clients { sendingTo = append(sendingTo, client) } - clientsMu.Unlock() + d.clientsMu.Unlock() // now broadcast it to all clients for _, client := range sendingTo { @@ -154,11 +170,11 @@ func displayStateHandler(w http.ResponseWriter, r *http.Request) { } } - util.ServeIndentedJSON(w, r, &state) + util.ServeIndentedJSON(w, r, d.State) } -// displayEventsHandler serves a SSE (Server-sent events) endpoint that the website(s) can connect to. It publishes the state when it gets changed. -func displayEventsHandler(w http.ResponseWriter, r *http.Request) { +// eventsHandler serves a SSE (Server-sent events) endpoint that the website(s) can connect to. It publishes the state when it gets changed. +func (d *Display) eventsHandler(w http.ResponseWriter, r *http.Request) { client, err := sse.ClientInit(w) // return error if unable to initialize sse connection @@ -169,18 +185,18 @@ func displayEventsHandler(w http.ResponseWriter, r *http.Request) { // Add client to broadcast set. Need to serialize access to clients as // we're running concurrently with other requests. - clientsMu.Lock() - clients[client] = true - clientsMu.Unlock() + d.clientsMu.Lock() + d.clients[client] = true + d.clientsMu.Unlock() // remove client from broadcast set on exit defer func() { - clientsMu.Lock() - delete(clients, client) - clientsMu.Unlock() + d.clientsMu.Lock() + delete(d.clients, client) + d.clientsMu.Unlock() }() - body, err := json.Marshal(state) + body, err := json.Marshal(d.State) if err != nil { return } diff --git a/internal/server/monitoring.go b/internal/server/monitoring.go new file mode 100644 index 0000000000000000000000000000000000000000..f18de21cbd2c655e3f319c95671b991209d2d7bc --- /dev/null +++ b/internal/server/monitoring.go @@ -0,0 +1,49 @@ +package server + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + namespacesMetric = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "namespaces", + Help: "Number of namespaces", + }) + + displayListernersMetric = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "display_listeners", + Help: "Active clients listening to /display", + }) + + counterMetric = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "requests_total", + Help: "Count the total number of requests.", + }, + []string{"path", "code"}, + ) + + durationMetric = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "requests_duration_seconds", + Help: "A histogram of duration for requests.", + Buckets: []float64{.25, .5, 1, 2.5, 5, 10}, + }, + []string{"path"}, + ) +) + +func init() { + prometheus.MustRegister(namespacesMetric, displayListernersMetric, counterMetric, durationMetric) +} + +func monitoringMiddleware(handler string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return promhttp.InstrumentHandlerDuration( + durationMetric.MustCurryWith(prometheus.Labels{"path": handler}), + promhttp.InstrumentHandlerCounter(counterMetric.MustCurryWith(prometheus.Labels{"path": handler}), next)) + } +} diff --git a/internal/server/router.go b/internal/server/router.go new file mode 100644 index 0000000000000000000000000000000000000000..eb33edb7d3d2cff29e45d18c51aac3bad67d6f6f --- /dev/null +++ b/internal/server/router.go @@ -0,0 +1,64 @@ +package server + +import ( + "net/http" + "sync" + + "github.com/go-chi/chi" + "github.com/markbates/pkger" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + displaysMu sync.Mutex + displays map[string]*Display = make(map[string]*Display) +) + +// GetRouter gets the router for the server +func GetRouter(useHostNamespace bool) *chi.Mux { + + r := chi.NewRouter() + + r.Route("/twitch", func(r chi.Router) { + r.With(monitoringMiddleware("/twitch/getTopGames")).Get("/getTopGames", twitchGetTopGamesHandler) + r.With(monitoringMiddleware("/twitch/searchGames")).Get("/searchGames", twitchSearchGamesHandler) + r.With(monitoringMiddleware("/twitch/getTopStreams")).Get("/getTopStreams", twitchGetTopStreamsHandler) + r.With(monitoringMiddleware("/twitch/getFeaturedStreams")).Get("/getFeaturedStreams", twitchGetFeaturedStreamsHandler) + }) + + r.Get("/display", func(w http.ResponseWriter, r *http.Request) { + namespace := "default" + if useHostNamespace { + namespace = r.Host + } + + displaysMu.Lock() + display, ok := displays[namespace] + if !ok { + display = NewDisplay(namespace) + displays[namespace] = display + } + displaysMu.Unlock() + + // We had to create a namespace + // Increase metric outside mutex for performance + if !ok { + namespacesMetric.Inc() + } + + if r.Header.Get("Accept") == "text/event-stream" { + displayListernersMetric.Inc() + defer displayListernersMetric.Dec() + monitoringMiddleware("/display(events)")(http.HandlerFunc(display.eventsHandler)).ServeHTTP(w, r) + } else { + monitoringMiddleware("/display(state)")(http.HandlerFunc(display.stateHandler)).ServeHTTP(w, r) + } + }) + + // Export Prometheus metrics + r.Handle("/metrics", promhttp.Handler()) + + r.With(monitoringMiddleware("/")).Mount("/", http.FileServer(pkger.Dir("/internal/static"))) + + return r +} diff --git a/internal/server/server.go b/internal/server/server.go index 238afbfa184740433a3e6ed50ba46e4319943d5d..184bcb3d71c717a6adb4b17b12ddd8a9e28d8db5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,32 +1,45 @@ package server import ( + "crypto/tls" + "log" "net/http" + "os" - "github.com/go-chi/chi" - "github.com/markbates/pkger" + "github.com/dyson/certman" ) -// ListenAndServe will listen and serve on the provided listenAddr -func ListenAndServe(listenAddr string) error { - r := chi.NewRouter() +var logger = log.New(os.Stdout, "", log.LstdFlags) - r.Route("/twitch", func(r chi.Router) { - r.Get("/getTopGames", twitchGetTopGamesHandler) - r.Get("/searchGames", twitchSearchGamesHandler) - r.Get("/getTopStreams", twitchGetTopStreamsHandler) - r.Get("/getFeaturedStreams", twitchGetFeaturedStreamsHandler) - }) +// ServeHTTP serves http requests on addr with handler +func ServeHTTP(addr string, handler http.Handler) error { + server := &http.Server{ + Addr: addr, + Handler: handler, + } - r.Get("/display", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Accept") == "text/event-stream" { - displayEventsHandler(w, r) - } else { - displayStateHandler(w, r) - } - }) + return server.ListenAndServe() +} + +// ServeHTTPS serves https requests on addr with handler +func ServeHTTPS(addr string, handler http.Handler, certFile, keyFile string) error { + certManager, err := certman.New(certFile, keyFile) + if err != nil { + return err + } + certManager.Logger(logger) + + if err := certManager.Watch(); err != nil { + return err + } - r.Mount("/", http.FileServer(pkger.Dir("/internal/static"))) + server := &http.Server{ + Addr: addr, + Handler: handler, + TLSConfig: &tls.Config{ + GetCertificate: certManager.GetCertificate, + }, + } - return http.ListenAndServe(listenAddr, r) + return server.ListenAndServeTLS("", "") } diff --git a/internal/static/index.html b/internal/static/index.html index 738b40cd0cc9bb6b5882067eb6a1e7520fc2bf23..4ba6cdb663cb313a8239e08bc3cf6b9a908b45af 100644 --- a/internal/static/index.html +++ b/internal/static/index.html @@ -25,7 +25,8 @@ <a href="https://stream-server.h-da.io/reference">Reference</a> </p> <small> - <a href="javascript:toggleFullscreen()">toggle fullscreen</a> + <a href="javascript:toggleFullscreen()">toggle fullscreen</a> or press + <kbd>F11</kbd> </small> </div> diff --git a/internal/static/main.js b/internal/static/main.js index d9263cca6c1f63554a23d0e9b104a50a54630ced..7c8c49bd1a5c5db5c0b3a5c371474a09dff0f713 100644 --- a/internal/static/main.js +++ b/internal/static/main.js @@ -1,5 +1,11 @@ "use strict"; +// When the chat loads faster than the stream, the stream would be out to the left of the window. +// This fixes the scrolling... +window.onscroll = () => { + window.scrollTo(0, 0); +}; + let large_player; let small_player; @@ -10,6 +16,8 @@ function newLargePlayer(channel) { height: "", controls: false, muted: false, + theme: "dark", + allowfullscreen: false, }); } @@ -20,6 +28,8 @@ function newSmallPlayer(channel) { height: "", controls: false, muted: true, + theme: "dark", + allowfullscreen: false, }); } @@ -162,12 +172,3 @@ function toggleFullscreen() { } } } - -large_player_elem.addEventListener("mousedown", (event) => { - if (event.detail > 1) { - // double click - toggleFullscreen(); - - event.preventDefault(); - } -}); diff --git a/internal/static/style.css b/internal/static/style.css index b590bd51a5bccdf37728704aa6f2751cadbf5f52..194749f80c95fd4fe482054e6674ed8a8e6e7ef4 100644 --- a/internal/static/style.css +++ b/internal/static/style.css @@ -51,7 +51,15 @@ body.with-chat { /* the iframes always grow to the parents size */ flex-grow: 1; - pointer-events: none; + /* pointer-events: none; */ +} + +#small-player > iframe { + z-index: 2000; +} + +#large-player > iframe { + z-index: 1000; } .hidden > iframe { @@ -81,7 +89,7 @@ body.with-chat { } #chat { - width: 21.25rem; + flex: 0 0 21.25rem; } a:link, diff --git a/k3s.yaml b/k3s.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4426680bf1cd2394c8a2a952bde2a0223da0ba78 --- /dev/null +++ b/k3s.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: + - cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJWekNCL3FBREFnRUNBZ0VBTUFvR0NDcUdTTTQ5QkFNQ01DTXhJVEFmQmdOVkJBTU1HR3N6Y3kxelpYSjIKWlhJdFkyRkFNVFU1TWpZeE1qY3lNakFlRncweU1EQTJNakF3TURJMU1qSmFGdzB6TURBMk1UZ3dNREkxTWpKYQpNQ014SVRBZkJnTlZCQU1NR0dzemN5MXpaWEoyWlhJdFkyRkFNVFU1TWpZeE1qY3lNakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkFGUTdlUWhjK3IxQkdxQjB5WThFdXNJbmtacXdQcjB5cGUxN2FlTUF3RnEKdjhzc3lOTHFrQUI1ZlVvbFBOR2VqeENPd2pxaElkbG9tWkJTQnZScTdJQ2pJekFoTUE0R0ExVWREd0VCL3dRRQpBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUJORTVXbHZ5VlprClArNlB0WUJUUFFFQzRFY3k0NjNQdk1zNnhBU29Cb0JtQWlFQXVqY1JsamtDd1BEMjJ4dmVZQVhQVzZmRDBvQ1UKY0pldll4UHNXT2tYMHZvPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://127.0.0.1:6443 + name: default +contexts: + - context: + cluster: default + user: default + name: default +current-context: default +kind: Config +preferences: {} +users: + - name: default + user: + password: 77f4f381bb04e6e873e5eb29282592bc + username: admin diff --git a/key.pem b/key.pem new file mode 100644 index 0000000000000000000000000000000000000000..104fb099f1cee6f401f45843bd0793acd353a2c3 --- /dev/null +++ b/key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 +AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q +EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== +-----END EC PRIVATE KEY-----