Skip to content

dmsghttp

Moses Narrow edited this page Jul 15, 2023 · 2 revisions

Dmsghttp utilities

release binaries can be found here

The dmsg network allows for direct p2p connections between dmsg clients by public key.

HTTP Fileserver over dmsg has been implemented via the dmsghttp application binary provided by dmsg.

image

dmsghttp --help

	┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌┬┐┌┬┐┌─┐
	 │││││└─┐│ ┬├─┤ │  │ ├─┘
	─┴┘┴ ┴└─┘└─┘┴ ┴ ┴  ┴ ┴

Usage:
  dmsghttp 

Flags:
  -d, --dir string         local dir to serve via dmsghttp (default ".")
  -D, --dmsg-disc string   dmsg discovery url default:
                           http://dmsgd.skywire.skycoin.com
  -p, --port uint          dmsg port to serve from (default 80)
  -s, --sk string          secret key to use default:
                           0000000000000000000000000000000000000000000000000000000000000000
  -v, --version            version for dmsghttp


This is demonstrated in conjunction with dmsgget

image

dmsgget --help

	┌┬┐┌┬┐┌─┐┌─┐┌─┐┌─┐┌┬┐
	 │││││└─┐│ ┬│ ┬├┤  │
	─┴┘┴ ┴└─┘└─┘└─┘└─┘ ┴

Usage:
  dmsgget 

Flags:
  -a, --agent AGENT        identify as AGENT (default "dmsgget/unknown")
  -d, --dmsg-disc string   dmsg discovery url default:
                           http://dmsgd.skywire.skycoin.com
  -l, --loglvl string      [ debug | warn | error | fatal | panic | trace | info ]
  -o, --out string         output filepath (default ".")
  -e, --sess int           number of dmsg servers to connect to (default 1)
  -s, --sk cipher.SecKey   a random key is generated if unspecified
 (default 0000000000000000000000000000000000000000000000000000000000000000)
  -n, --stdout             output to STDOUT
  -t, --try int            download attempts (0 unlimits) (default 1)
  -v, --version            version for dmsgget
  -w, --wait int           time to wait between fetches

A file is created and served via dmsghttp

image

The file is fetched, using dmsgget

image

Integration

Here is an example website which serves on http and dmsghttp

package main

import (
	"fmt"
	"context"
	"log"
	"net/http"
	"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
)

func init() {
	rootCmd.Flags().UintVarP(&webPort, "port", "p", 80, "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:   "example",
	Short: "example http & dmsghttp server",
	Long: "example http & dmsghttp server",
	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("dmsghttp")

	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 += "<p>Hello World!</p>"
		l += "</body></html>"
		c.Writer.Write([]byte(l))
		return
	})

	r1.GET("/index.html", 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 += "<p>Hello World!</p>"
		l += "</body></html>"
		c.Writer.Write([]byte(l))
		return
	})

	// 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: 3 * 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"

the example can be run as follows, assuming golang is installed

mkdir test
cd test
nano test.go

copy the example code from above into the file, save and exit the editor.

next, sync the dependencies

go mod init ; go mod tidy ; go mod vendor

run the application (note using the default port 80 requires root)

sudo go run test.go

now, if you ran the application as root with the defaults, the example website should be accessible on 127.0.0.1

Visit 127.0.0.1/health to view the health endpoint which will display the dmsg public key where the site can be accessed over dmsghttp.

Use dmsgget or dmsgweb to access the site over dmsg

Observe the logging for the server. Notice that when the site is accessed over dmsg, the public key which is accessing the site appears in the modified logging.

image

An example framework for whitelist-authenticated routes for dmsghttp is included.

One can generate keys to use with skywire-cli config gen-keys

Clone this wiki locally