-
Notifications
You must be signed in to change notification settings - Fork 45
dmsghttp
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.
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
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
The file is fetched, using dmsgget
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.
An example framework for whitelist-authenticated routes for dmsghttp is included.
One can generate keys to use with skywire-cli config gen-keys
please contact support via telegram @skywire
for rewards and technical support