From 1d6e4270ba89e68dda3b32fdf42276648890c4b2 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 23 Aug 2021 02:45:44 +0200 Subject: [PATCH] Add minimal web interface --- cmd/dogtamer.go | 12 ++++ config/config.go | 98 ++++++++++++++++++++++++++++ config/config_test.go | 126 ++++++++++++++++++++++++++++++++++++ dogtamer.example.toml | 10 +++ go.mod | 1 + go.sum | 2 + server/templates/index.html | 8 +++ server/web.go | 68 +++++++++++++++++++ 8 files changed, 325 insertions(+) create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 dogtamer.example.toml create mode 100644 server/templates/index.html create mode 100644 server/web.go diff --git a/cmd/dogtamer.go b/cmd/dogtamer.go index 1a1d91b..ec9f1d8 100644 --- a/cmd/dogtamer.go +++ b/cmd/dogtamer.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "net/http" "os" "os/signal" @@ -57,10 +58,21 @@ func main() { func ActionServe(c *cli.Context) error { ctx, cancel := context.WithCancel(context.Background()) + // Setup rtmp server s := server.NewRTMPServer(ctx, ":5566") logger := setupServerLogger() s.Logger = logger + // Setup web server + ws := server.NewWebServer(ctx, s) + go func() { + s.Logger.Info("Starting web server") + err := ws.Serve() + if err != nil && err != http.ErrServerClosed { + s.Logger.Infow("Web server shut down with error", "err", err) + } + }() + // Listen for SIGINT sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..05b9b72 --- /dev/null +++ b/config/config.go @@ -0,0 +1,98 @@ +package config + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/pelletier/go-toml" +) + +var ErrNotFound = errors.New("no config file found") + +type Config struct { + ListenAddr string `toml:"ListenAddr"` + LogLevel string `toml:"LogLevel"` +} + +type InvalidValueError struct { + Key string +} + +func (ive *InvalidValueError) Error() string { + return fmt.Sprintf("invalid value: config key %s has invalid value", ive.Key) +} + +func FromReader(r io.Reader) (*Config, error) { + var c Config + // Set some defaults + c.ListenAddr = ":5566" + c.LogLevel = "INFO" + + decoder := toml.NewDecoder(r) + if err := decoder.Decode(&c); err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + + return &c, c.Verify() +} + +func (c *Config) Verify() error { + // Check that LogLevel is valid + switch c.LogLevel { + case "DEBUG", "INFO", "WARN", "ERROR": + default: + return &InvalidValueError{Key: "LogLevel"} + } + return nil +} + +func (c *Config) UpdateFromEnv() error { + if loglevel, found := os.LookupEnv("DOGTAMER_LOGLEVEL"); found { + c.LogLevel = loglevel + } + if listenAddr, found := os.LookupEnv("DOGTAMER_LISTENADDR"); found { + c.ListenAddr = listenAddr + } + + return c.Verify() +} + +func FromFile(path string) (*Config, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("Error opening config file: %w", err) + } + + return FromReader(f) +} + +func FromDefaultLocations(path string) (*Config, error) { + defaultLocations := []string{ + "dogtamer.toml", + "/etc/dogtamer.toml", + } + baseConfigDir, err := os.UserConfigDir() + if err == nil { + userConfigFile := filepath.Join(baseConfigDir, "dogtamer", "dogtamer.toml") + defaultLocations = append(defaultLocations, userConfigFile) + } + + for _, fname := range defaultLocations { + if _, err := os.Stat(fname); os.IsNotExist(err) { + continue + } + + cfg, err := FromFile(fname) + if err != nil { + continue + + } + + return cfg, cfg.UpdateFromEnv() + } + + return nil, ErrNotFound +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..81a4aec --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,126 @@ +package config_test + +import ( + "os" + "strings" + "testing" + + "github.uio.no/torjus/dogtamer/config" +) + +func TestConfig(t *testing.T) { + t.Run("TestEmpty", func(t *testing.T) { + emptyConfig := "" + sr := strings.NewReader(emptyConfig) + + c, err := config.FromReader(sr) + if err != nil { + t.Fatalf("Error reading empty config: %s", err) + } + + // Ensure proper defaults are set + if c.ListenAddr != ":5566" { + t.Errorf("Unexpected ListenAddr: %s", c.ListenAddr) + } + if c.LogLevel != "INFO" { + t.Errorf("Unexpected LogLevel: %s", c.LogLevel) + } + }) + + t.Run("TestWithValidValues", func(t *testing.T) { + configString := ` + # Random comment + LogLevel = "DEBUG" + + ListenAddr = ":5555" + ` + sr := strings.NewReader(configString) + + c, err := config.FromReader(sr) + if err != nil { + t.Fatalf("Error reading config: %s", err) + } + + if c.ListenAddr != ":5555" { + t.Errorf("Unexpected ListenAddr: %s", c.ListenAddr) + } + + if c.LogLevel != "DEBUG" { + t.Errorf("Unexpected LogLevel: %s", c.LogLevel) + } + }) + t.Run("TestWithInValidValues", func(t *testing.T) { + configString := ` + # Random comment + LogLevel = "INVALID" + + ListenAddr = ":5555" + ` + sr := strings.NewReader(configString) + + _, err := config.FromReader(sr) + ive, ok := err.(*config.InvalidValueError) + if !ok { + t.Fatalf("Error is of wrong type: %T", ive) + } + if ive.Key != "LogLevel" { + t.Errorf("Error has wrong key: %s", ive.Key) + } + }) + t.Run("TestFromEnv", func(t *testing.T) { + os.Clearenv() + + envValues := map[string]string{ + "DOGTAMER_LOGLEVEL": "DEBUG", + "DOGTAMER_LISTENADDR": ":3333", + } + + for k, v := range envValues { + if err := os.Setenv(k, v); err != nil { + t.Fatalf("Unable to set env values: %s", err) + } + } + + emptyCfg := "" + sr := strings.NewReader(emptyCfg) + + c, err := config.FromReader(sr) + if err != nil { + t.Fatalf("Error parsing config string: %s", err) + } + if err := c.UpdateFromEnv(); err != nil { + t.Errorf("Error updating config with environment") + } + + if c.ListenAddr != envValues["DOGTAMER_LISTENADDR"] { + t.Errorf("ListenAddr has wrong value: %s", c.ListenAddr) + } + if c.LogLevel != envValues["DOGTAMER_LOGLEVEL"] { + t.Errorf("LogLevel has wrong value: %s", c.LogLevel) + } + }) + t.Run("TestFromEnvInvalid", func(t *testing.T) { + os.Clearenv() + + envValues := map[string]string{ + "DOGTAMER_LOGLEVEL": "TEST", + "DOGTAMER_LISTENADDR": ":3333", + } + + for k, v := range envValues { + if err := os.Setenv(k, v); err != nil { + t.Fatalf("Unable to set env values: %s", err) + } + } + emptyCfg := "" + sr := strings.NewReader(emptyCfg) + + c, err := config.FromReader(sr) + if err != nil { + t.Fatalf("Error parsing config string: %s", err) + } + if err := c.UpdateFromEnv(); err == nil { + t.Errorf("No error when parsing invalid env values") + } + }) +} diff --git a/dogtamer.example.toml b/dogtamer.example.toml new file mode 100644 index 0000000..3c03129 --- /dev/null +++ b/dogtamer.example.toml @@ -0,0 +1,10 @@ +# dogtamer example config file + +# Address to listen to for incoming connections +# Default: ":5566" +ListenAddr = ":5566" + +# Log level +# Default: INFO +# Possible values: "DEBUG", "INFO", "WARN", "ERROR" +LogLevel = "INFO" diff --git a/go.mod b/go.mod index e28e752..aae0ddc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require github.com/urfave/cli/v2 v2.3.0 require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect diff --git a/go.sum b/go.sum index 7d26cfe..17ecdd4 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM= github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/server/templates/index.html b/server/templates/index.html new file mode 100644 index 0000000..ff26ad2 --- /dev/null +++ b/server/templates/index.html @@ -0,0 +1,8 @@ + + +
    + {{ range .Streams }} +
  1. {{ .URL }}
  2. + {{ end }} +
+ \ No newline at end of file diff --git a/server/web.go b/server/web.go new file mode 100644 index 0000000..ae0cc71 --- /dev/null +++ b/server/web.go @@ -0,0 +1,68 @@ +package server + +import ( + "context" + "fmt" + "html/template" + "net/http" + "time" + + "go.uber.org/zap" +) + +type WebServer struct { + Logger *zap.SugaredLogger + ctx context.Context + rtmpServer *RTMPServer + httpServer *http.Server +} + +func NewWebServer(ctx context.Context, rs *RTMPServer) *WebServer { + return &WebServer{ + ctx: ctx, + rtmpServer: rs, + Logger: zap.NewNop().Sugar(), + } +} + +func (ws *WebServer) Serve() error { + ws.httpServer = &http.Server{ + Addr: ":8077", + Handler: http.HandlerFunc(ws.IndexHandler), + } + + go func() { + <-ws.ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = ws.httpServer.Shutdown(shutdownCtx) + }() + + return ws.httpServer.ListenAndServe() +} + +func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) { + data := struct { + Streams []struct { + Name string + URL template.URL + } + }{} + + for _, s := range ws.rtmpServer.streams { + stream := struct { + Name string + URL template.URL + }{ + Name: s.Name, + URL: template.URL(fmt.Sprintf("rtmp://localhost:5566/view/%s", s.Name)), + } + data.Streams = append(data.Streams, stream) + } + + tmpl := template.Must(template.ParseFiles("server/templates/index.html")) + + w.Header().Add("Content-Type", "text/html") + + tmpl.Execute(w, data) +}