-
Notifications
You must be signed in to change notification settings - Fork 43
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 dmsgweband 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