-
Notifications
You must be signed in to change notification settings - Fork 45
dmsghttp dmsgweb streaming media
This guide requires skywire
and golang
installed
skywire cli config gen-keys > dmsgvlc.key
skywire cli config gen-keys > dmsgweb.key
package main
import (
"fmt"
"context"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"sync"
"net"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
cc "github.com/ivanpirog/coloredcobra"
"github.com/skycoin/skywire-utilities/pkg/logging"
"github.com/skycoin/skywire-utilities/pkg/cipher"
"github.com/skycoin/skywire-utilities/pkg/cmdutil"
"github.com/skycoin/dmsg/pkg/disc"
dmsg "github.com/skycoin/dmsg/pkg/dmsg"
"github.com/skycoin/skywire-utilities/pkg/skyenv"
)
func main() {
Execute()
}
var (
startTime = time.Now()
runTime time.Duration
sk cipher.SecKey
pk cipher.PubKey
dmsgDisc string
dmsgPort uint
wl string
wlkeys []cipher.PubKey
webPort uint
vlcPort uint
vlcPath string
)
func init() {
rootCmd.Flags().UintVarP(&vlcPort, "vport", "v", 8079, "vlc port to connect to")
rootCmd.Flags().StringVarP(&vlcPath, "vpath", "x", "/music", "vlc path configured")
rootCmd.Flags().UintVarP(&webPort, "port", "p", 8081, "port to serve")
rootCmd.Flags().UintVarP(&dmsgPort, "dport", "d", 80, "dmsg port to serve")
rootCmd.Flags().StringVarP(&wl, "wl", "w", "", "whitelisted keys for dmsg authenticated routes")
rootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", skyenv.DmsgDiscAddr, "dmsg discovery url")
pk, _ = sk.PubKey()
rootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r")
rootCmd.CompletionOptions.DisableDefaultCmd = true
var helpflag bool
rootCmd.SetUsageTemplate(help)
rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use)
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
rootCmd.PersistentFlags().MarkHidden("help") //nolint
}
var rootCmd = &cobra.Command{
Use: "dmsgvlc",
Short: "dmsg vlc",
Long: "dmsg streaming media with vlc",
Run: func(_ *cobra.Command, _ []string) {
Server()
},
}
func Execute() {
cc.Init(&cc.Config{
RootCmd: rootCmd,
Headings: cc.HiBlue + cc.Bold,
Commands: cc.HiBlue + cc.Bold,
CmdShortDescr: cc.HiBlue,
Example: cc.HiBlue + cc.Italic,
ExecName: cc.HiBlue + cc.Bold,
Flags: cc.HiBlue + cc.Bold,
FlagsDescr: cc.HiBlue,
NoExtraNewlines: true,
NoBottomNewline: true,
})
if err := rootCmd.Execute(); err != nil {
log.Fatal("Failed to execute command: ", err)
}
}
func Server() {
wg := new(sync.WaitGroup)
wg.Add(1)
log := logging.MustGetLogger("dmsgvlc")
ctx, cancel := cmdutil.SignalContext(context.Background(), log)
defer cancel()
pk, err := sk.PubKey()
if err != nil {
pk, sk = cipher.GenerateKeyPair()
}
if wl != "" {
wlk := strings.Split(wl, ",")
for _, key := range wlk {
var pk1 cipher.PubKey
err := pk1.Set(key)
if err == nil {
wlkeys = append(wlkeys, pk1)
}
}
}
if len(wlkeys) > 0 {
if len(wlkeys) == 1 {
log.Info(fmt.Sprintf("%d key whitelisted", len(wlkeys)))
} else {
log.Info(fmt.Sprintf("%d keys whitelisted", len(wlkeys)))
}
}
dmsgclient := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig())
defer func() {
if err := dmsgclient.Close(); err != nil {
log.WithError(err).Error()
}
}()
go dmsgclient.Serve(context.Background())
select {
case <-ctx.Done():
log.WithError(ctx.Err()).Warn()
return
case <-dmsgclient.Ready():
}
lis, err := dmsgclient.Listen(uint16(dmsgPort))
if err != nil {
log.WithError(err).Fatal()
}
go func() {
<-ctx.Done()
if err := lis.Close(); err != nil {
log.WithError(err).Error()
}
}()
r1 := gin.New()
// Disable Gin's default logger middleware
r1.Use(gin.Recovery())
r1.Use(loggingMiddleware())
r1.GET("/", func(c *gin.Context) {
c.Writer.Header().Set("Server", "")
c.Writer.WriteHeader(http.StatusOK)
// l := "<!doctype html><html lang=en><head><title>Example Website</title></head><body style='background-color:black;color:white;'>\n<style type='text/css'>\npre {\n font-family:Courier New;\n font-size:10pt;\n}\n.af_line {\n color: gray;\n text-decoration: none;\n}\n.column {\n float: left;\n width: 30%;\n padding: 10px;\n}\n.row:after {\n content: '';\n display: table;\n clear: both;\n}\n</style>\n<pre>"
l := "<!DOCTYPE html><html><head><meta name='viewport' content='initial-scale=1'></head><body style='background-color:black;color:white;'><audio controls><source src='"+vlcPath+"' type='audio/mpeg'><source src='"+vlcPath+"' type='audio/ogg'><source src='"+vlcPath+"' type='audio/wav'>Your browser does not support the audio element.</audio></body></html>"
// l += "</body></html>"
c.Writer.Write([]byte(l))
return
})
r1.GET(vlcPath, func(c *gin.Context) {
targetURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%v/", vlcPort))
proxy := httputil.NewSingleHostReverseProxy(targetURL)
proxy.ServeHTTP(c.Writer, c.Request)
})
// only whitelisted public keys can access authRoute(s)
authRoute := r1.Group("/")
if len(wlkeys) > 0 {
authRoute.Use(whitelistAuth(wlkeys))
}
authRoute.GET("/auth", func(c *gin.Context) {
//override the behavior of `public fallback` for this endpoint when no keys are whitelisted
if len(wlkeys) == 0 {
c.Writer.WriteHeader(http.StatusNotFound)
return
}
c.Writer.WriteHeader(http.StatusOK)
l := "<!doctype html><html lang=en><head><title>Example Website</title></head><body style='background-color:black;color:white;'>\n<style type='text/css'>\npre {\n font-family:Courier New;\n font-size:10pt;\n}\n.af_line {\n color: gray;\n text-decoration: none;\n}\n.column {\n float: left;\n width: 30%;\n padding: 10px;\n}\n.row:after {\n content: '';\n display: table;\n clear: both;\n}\n</style>\n<pre>"
l += "<p>Hello World!</p>"
l += "</body></html>"
c.Writer.Write([]byte(l))
})
r1.GET("/health", func(c *gin.Context) {
runTime = time.Since(startTime)
c.JSON(http.StatusOK, gin.H{
"frontend_start_time": startTime,
"frontend_run_time": runTime.String(),
"dmsg_address": fmt.Sprintf("%s:%d", pk.String(), dmsgPort),
})
})
// Start the server using the custom Gin handler
serve := &http.Server{
Handler: &GinHandler{Router: r1},
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
// Start serving
go func() {
log.WithField("dmsg_addr", lis.Addr().String()).Info("Serving...")
if err := serve.Serve(lis); err != nil && err != http.ErrServerClosed {
log.Fatalf("Serve: %v", err)
}
wg.Done()
}()
go func() {
fmt.Printf("listening on http://127.0.0.1:%d using gin router\n", webPort)
r1.Run(fmt.Sprintf(":%d", webPort))
wg.Done()
}()
wg.Wait()
}
func whitelistAuth(whitelistedPKs []cipher.PubKey) gin.HandlerFunc {
return func(c *gin.Context) {
// Get the remote PK.
remotePK, _, err := net.SplitHostPort(c.Request.RemoteAddr)
if err != nil {
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.Write([]byte("500 Internal Server Error"))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// Check if the remote PK is whitelisted.
whitelisted := false
if len(whitelistedPKs) == 0 {
whitelisted = true
} else {
for _, whitelistedPK := range whitelistedPKs {
if remotePK == whitelistedPK.String() {
whitelisted = true
break
}
}
}
if whitelisted {
c.Next()
} else {
// Otherwise, return a 401 Unauthorized error.
c.Writer.WriteHeader(http.StatusUnauthorized)
c.Writer.Write([]byte("401 Unauthorized"))
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
type GinHandler struct {
Router *gin.Engine
}
func (h *GinHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Router.ServeHTTP(w, r)
}
func loggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
if latency > time.Minute {
latency = latency.Truncate(time.Second)
}
statusCode := c.Writer.Status()
method := c.Request.Method
path := c.Request.URL.Path
// Get the background color based on the status code
statusCodeBackgroundColor := getBackgroundColor(statusCode)
// Get the method color
methodColor := getMethodColor(method)
fmt.Printf("[EXAMPLE] %s |%s %3d %s| %13v | %15s | %72s |%s %-7s %s %s\n",
time.Now().Format("2006/01/02 - 15:04:05"),
statusCodeBackgroundColor,
statusCode,
resetColor(),
latency,
c.ClientIP(),
c.Request.RemoteAddr,
methodColor,
method,
resetColor(),
path,
)
}
}
func getBackgroundColor(statusCode int) string {
switch {
case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices:
return green
case statusCode >= http.StatusMultipleChoices && statusCode < http.StatusBadRequest:
return white
case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError:
return yellow
default:
return red
}
}
func getMethodColor(method string) string {
switch method {
case http.MethodGet:
return blue
case http.MethodPost:
return cyan
case http.MethodPut:
return yellow
case http.MethodDelete:
return red
case http.MethodPatch:
return green
case http.MethodHead:
return magenta
case http.MethodOptions:
return white
default:
return reset
}
}
func resetColor() string {
return reset
}
type consoleColorModeValue int
var consoleColorMode = autoColor
const (
autoColor consoleColorModeValue = iota
disableColor
forceColor
)
const (
green = "\033[97;42m"
white = "\033[90;47m"
yellow = "\033[90;43m"
red = "\033[97;41m"
blue = "\033[97;44m"
magenta = "\033[97;45m"
cyan = "\033[97;46m"
reset = "\033[0m"
)
var (
err error
)
const help = "Usage:\r\n" +
" {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" +
"{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" +
"Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " +
"{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" +
"Flags:\r\n" +
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" +
"Global Flags:\r\n" +
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n"
$ go mod init ; go mod tidy ; go mod vendor
go run main.go -s $(tail -n1 dmsgvlc.key)
Media > Stream (or CTRL+S
)
Add media to stream. It's recommended to create a playlist and then select the playlist in this dialog
Click stream
after adding media or playlist
Click Next
Select HTTP
from the dropdown menu, then click Add
Use port 8079
and the path /music
; click Next
Click Next
Finally, click Stream
NOTE: this does not work using the resolving proxy of dmsgweb
configured as socks5 proxy in your web browser and entering the dmsg address in the address bar - which is the standard configuration of dmsgweb
Instead, you must use dmsgweb
to resolve the dmsg address of the dmsgvlc
application to a local port; in this example, port 8082
skywire dmsg web -t $(head -n1 dmsgvlc.key) $(tail -n1 dmsgweb.key) -p 8082
Access the port that dmsgweb
is serving locally in a web browser:
http://127.0.0.1:8082
You should see an audio widget. Click it to start the audio stream.
The same will work on any two machines, or when dmsgweb
is not run on the same machine as the dmsgvlc
program. Simply manually copy the public key - the first line of dmsgvlc.key
- instead of copying the whole file to another machine. Provide the public key as an argument for the -t
flag of skywire dmsg web
As long as any two clients are able to access the dmsg network, they can connect to each other.
It's also possible to simply open the network stream in VLC, instead of accessing it in a web browser
Media > Open Network Stream (or CTRL+N
)
enter http://127.0.0.1:8082/music
and click Play
Sometimes you may get the error failed to connect to http server
Two things may have happen in this instance. Either dmsgvlc
has become disconnected from dmsg, or dmsgweb
has encountered an error.
In this instance, first try restarting dmsgweb
and attempt again to access the interface. If that does not work, try restarting dmsgvlc
.
please contact support via telegram @skywire
for rewards and technical support