diff --git a/cmd/dmsgweb/README.md b/cmd/dmsgweb/README.md index ac418b38..c5fe77fa 100644 --- a/cmd/dmsgweb/README.md +++ b/cmd/dmsgweb/README.md @@ -2,31 +2,79 @@ ``` +$ go run cmd/dmsgweb/dmsgweb.go --help -┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ - │││││└─┐│ ┬│││├┤ ├┴┐ -─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ -DMSG resolving proxy & browser client - access websites over dmsg + ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ + │││││└─┐│ ┬│││├┤ ├┴┐ + ─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ +DMSG resolving proxy & browser client - access websites and http interfaces over dmsg +.conf file may also be specified with +DMSGWEB=/path/to/dmsgweb.conf skywire dmsg web Usage: -web + dmsgweb Available Commands: -completion Generate the autocompletion script for the specified shell -gen-keys generate public / secret keypair + completion Generate the autocompletion script for the specified shell + srv Serve HTTP or raw TCP from local port over DMSG Flags: --d, --dmsg-disc string dmsg discovery url default: - http://dmsgd.skywire.skycoin.com --f, --filter string domain suffix to filter (default ".dmsg") --l, --loglvl string [ debug | warn | error | fatal | panic | trace | info ] --p, --port string port to serve the web application (default "8080") --r, --proxy string configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080) --t, --resolve string resolve the specified dmsg address:port on the local port & disable proxy --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) --q, --socks string port to serve the socks5 proxy (default "4445") --v, --version version for web + -r, --addproxy string configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080) + + -D, --dmsg-disc string dmsg discovery url + (default "http://dmsgd.skywire.skycoin.com") + -z, --envs show example .conf file + + -f, --filter string domain suffix to filter + (default ".dmsg") + -l, --loglvl string [ debug | warn | error | fatal | panic | trace | info ] + + -p, --port uints port(s) to serve the web application + (default [8080]) + -x, --proxy string connect to dmsg via proxy (i.e. '127.0.0.1:1080') + + -t, --resolve strings resolve the specified dmsg address:port on the local port & disable proxy + + -c, --rt bools proxy local port as raw TCP + (default [false]) + -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) + -q, --socks uint port to serve the socks5 proxy + (default 4445) + -v, --version version for dmsgweb + +``` + +``` +$ go run cmd/dmsgweb/dmsgweb.go srv --help +DMSG web server - serve HTTP or raw TCP interface from local port over DMSG + .conf file may also be specified with DMSGWEBSRV=/path/to/dmsgwebsrv.conf skywire dmsg web srv + +Usage: + dmsgweb srv [flags] + +Flags: + -D, --dmsg-disc string DMSG discovery URL + (default "http://dmsgd.skywire.skycoin.com") + -d, --dport uints DMSG port(s) to serve + (default [80]) + -e, --dsess int DMSG sessions + (default 1) + -z, --envs show example .conf file + + -l, --loglvl string [ debug | warn | error | fatal | panic | trace | info ] + + -p, --lport uints local application interface port(s) + (default [8086]) + -x, --proxy string connect to DMSG via proxy (e.g., '127.0.0.1:1080') + + -c, --rt bools proxy local port as raw TCP, comma separated + (default [false]) + -s, --sk cipher.SecKey a random key is generated if unspecified + (default 0000000000000000000000000000000000000000000000000000000000000000) + -w, --wl strings whitelisted keys for DMSG authenticated routes + ``` diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index 2eb6e971..7db2a4c1 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -5,19 +5,17 @@ import ( "context" "fmt" "io" - "log" "net" "net/http" "os" "os/signal" "path/filepath" "regexp" - "runtime" "strconv" "strings" + "sync" "syscall" - "github.com/bitfield/script" "github.com/chen3feng/safecast" "github.com/confiant-inc/go-socks5" "github.com/gin-gonic/gin" @@ -36,7 +34,6 @@ import ( type customResolver struct{} func (r *customResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { - // Handle custom name resolution for .dmsg domains regexPattern := `\.` + filterDomainSuffix + `(:[0-9]+)?$` match, _ := regexp.MatchString(regexPattern, name) //nolint:errcheck if match { @@ -44,37 +41,44 @@ func (r *customResolver) Resolve(ctx context.Context, name string) (context.Cont if ip == nil { return ctx, nil, fmt.Errorf("failed to parse IP address") } - // Modify the context to include the desired port ctx = context.WithValue(ctx, "port", fmt.Sprintf("%v", webPort)) //nolint return ctx, ip, nil } - // Use default name resolution for other domains return ctx, nil, nil } -const dmsgwebenvname = "DMSGWEB" +const dwenv = "DMSGWEB" -var dmsgwebconffile = os.Getenv(dmsgwebenvname) +var dwcfg = os.Getenv(dwenv) func init() { - RootCmd.Flags().StringVarP(&filterDomainSuffix, "filter", "f", ".dmsg", "domain suffix to filter") - RootCmd.Flags().UintVarP(&proxyPort, "socks", "q", scriptExecUint("${PROXYPORT:-4445}", dmsgwebconffile), "port to serve the socks5 proxy") - RootCmd.Flags().StringVarP(&addProxy, "addproxy", "r", scriptExecString("${ADDPROXY}", dmsgwebconffile), "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)") - RootCmd.Flags().UintSliceVarP(&webPort, "port", "p", scriptExecUintSlice("${WEBPORT[@]:-8080}", dmsgwebconffile), "port(s) to serve the web application") - RootCmd.Flags().StringSliceVarP(&resolveDmsgAddr, "resolve", "t", scriptExecStringSlice("${RESOLVEPK[@]}", dmsgwebconffile), "resolve the specified dmsg address:port on the local port & disable proxy") - RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsgDisc, "dmsg discovery url(s)") - RootCmd.Flags().StringVarP(&proxyAddr, "proxy", "x", "", "connect to dmsg via proxy (i.e. '127.0.0.1:1080')") - RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", scriptExecInt("${DMSGSESSIONS:-1}", dmsgwebconffile), "number of dmsg servers to connect to") - RootCmd.Flags().BoolSliceVarP(&rawTCP, "rt", "c", scriptExecBoolSlice("${RAWTCP[@]:-false}", dmsgwebconffile), "proxy local port as raw TCP") - RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m") + dmsgDisc = dmsg.DiscAddr(false) + webPort = scriptExecUintSlice("${WEBPORT[@]:-8080}", dwcfg) + proxyPort = scriptExecUint("${PROXYPORT:-4445}", dwcfg) + addProxy = scriptExecString("${ADDPROXY}", dwcfg) + resolveDmsgAddr = scriptExecStringSlice("${RESOLVEPK[@]}", dwcfg) + dmsgSess = scriptExecInt("${DMSGSESSIONS:-1}", dwcfg) + rawTCP = scriptExecBoolSlice("${RAWTCP[@]:-false}", dwcfg) if os.Getenv("DMSGWEBSK") != "" { sk.Set(os.Getenv("DMSGWEBSK")) //nolint } - if scriptExecString("${DMSGWEBSK}", dmsgwebconffile) != "" { - sk.Set(scriptExecString("${DMSGWEBSK}", dmsgwebconffile)) //nolint + if scriptExecString("${DMSGWEBSK}", dwcfg) != "" { + sk.Set(scriptExecString("${DMSGWEBSK}", dwcfg)) //nolint } + pk, _ = sk.PubKey() //nolint + + RootCmd.Flags().StringVarP(&filterDomainSuffix, "filter", "f", ".dmsg", "domain suffix to filter\033[0m\n\r") + RootCmd.Flags().UintVarP(&proxyPort, "socks", "q", proxyPort, "port to serve the socks5 proxy\033[0m\n\r") + RootCmd.Flags().StringVarP(&addProxy, "addproxy", "r", addProxy, "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)\033[0m\n\r") + RootCmd.Flags().UintSliceVarP(&webPort, "port", "p", webPort, "port(s) to serve the web application\033[0m\n\r") + RootCmd.Flags().StringSliceVarP(&resolveDmsgAddr, "resolve", "t", resolveDmsgAddr, "resolve the specified dmsg address:port on the local port & disable proxy\033[0m\n\r") + RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsgDisc, "dmsg discovery url\033[0m\n\r") + RootCmd.Flags().StringVarP(&proxyAddr, "proxy", "x", "", "connect to dmsg via proxy (i.e. '127.0.0.1:1080')\033[0m\n\r") + RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", dmsgSess, "number of dmsg servers to connect to\033[0m\n\r") + RootCmd.Flags().BoolSliceVarP(&rawTCP, "rt", "c", rawTCP, "proxy local port as raw TCP\033[0m\n\r") + RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "debug", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") - RootCmd.Flags().BoolVarP(&isEnvs, "envs", "z", false, "show example .conf file") + RootCmd.Flags().BoolVarP(&isEnvs, "envs", "z", false, "show example .conf file\033[0m\n\r") } @@ -89,65 +93,44 @@ var RootCmd = &cobra.Command{ │││││└─┐│ ┬│││├┤ ├┴┐ ─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ DMSG resolving proxy & browser client - access websites and http interfaces over dmsg` + func() string { - if _, err := os.Stat(dmsgwebconffile); err == nil { + if _, err := os.Stat(dwcfg); err == nil { return ` -dmsgweb conf file detected: ` + dmsgwebconffile +dmsgweb conf file detected: ` + dwcfg } return ` .conf file may also be specified with -` + dmsgwebenvname + `=/path/to/dmsgweb.conf skywire dmsg web` +` + dwenv + `=/path/to/dmsgweb.conf skywire dmsg web` }(), SilenceErrors: true, SilenceUsage: true, DisableSuggestions: true, DisableFlagsInUseLine: true, Version: buildinfo.Version(), - Run: func(_ *cobra.Command, _ []string) { + PreRun: func(_ *cobra.Command, _ []string) { if isEnvs { - envfile := envfileLinux - if runtime.GOOS == "windows" { - envfileslice, _ := script.Echo(envfile).Slice() //nolint - for i := range envfileslice { - efs, _ := script.Echo(envfileslice[i]).Reject("##").Reject("#-").Reject("# ").Replace("#", "#$").String() //nolint - if efs != "" && efs != "\n" { - envfileslice[i] = strings.ReplaceAll(efs, "\n", "") - } - } - envfile = strings.Join(envfileslice, "\n") - } - fmt.Println(envfile) - os.Exit(0) - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) //nolint - go func() { - <-c - os.Exit(1) - }() - if dmsgWebLog == nil { - dmsgWebLog = logging.MustGetLogger("dmsgweb") + printEnvs(srvenvfileLinux) } if logLvl != "" { if lvl, err := logging.LevelFromString(logLvl); err == nil { logging.SetLevel(lvl) } } + dLog = logging.MustGetLogger("dmsgweb") if dmsgDisc == "" { - dmsgWebLog.Fatal("Dmsg Discovery URL not specified") + dLog.Fatal("Dmsg Discovery URL not specified") } if len(resolveDmsgAddr) > 0 && len(webPort) != len(resolveDmsgAddr) { - dmsgWebLog.Fatal("-resolve --t flag cannot contain a different number of elements than -port -p flag") + dLog.Fatal("-resolve --t flag cannot contain a different number of elements than -port -p flag") } if len(resolveDmsgAddr) == 0 && len(webPort) > 1 { - dmsgWebLog.Fatal("-port --p flag cannot specify multiple ports without specifying multiple dmsg address:port(s) to -resolve --t flag") + dLog.Fatal("-port --p flag cannot specify multiple ports without specifying multiple dmsg address:port(s) to -resolve --t flag") } seenResolveDmsgAddr := make(map[string]bool) for _, item := range resolveDmsgAddr { if seenResolveDmsgAddr[item] { - dmsgWebLog.Fatal("-resolve --t flag cannot contain duplicates") + dLog.Fatal("-resolve --t flag cannot contain duplicates") } seenResolveDmsgAddr[item] = true } @@ -155,7 +138,7 @@ dmsgweb conf file detected: ` + dmsgwebconffile seenWebPort := make(map[uint]bool) for _, item := range webPort { if seenWebPort[item] { - dmsgWebLog.Fatal("-port --p flag cannot contain duplicates") + dLog.Fatal("-port --p flag cannot contain duplicates") } seenWebPort[item] = true } @@ -167,47 +150,68 @@ dmsgweb conf file detected: ` + dmsgwebconffile } else if len(rawTCP) > len(resolveDmsgAddr) { rawTCP = rawTCP[:len(resolveDmsgAddr)] } - + if len(webPort) == 0 { + dLog.Fatal("webPort is empty. Ensure at least one port is specified.") + } if filterDomainSuffix == "" { - dmsgWebLog.Fatal("domain suffix to filter cannot be an empty string") + dLog.Fatal("domain suffix to filter cannot be an empty string") } - ctx, cancel := cmdutil.SignalContext(context.Background(), dmsgWebLog) + }, + Run: func(_ *cobra.Command, _ []string) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) //nolint + go func() { + <-c + os.Exit(0) + }() + + ctx, cancel := cmdutil.SignalContext(context.Background(), dLog) defer cancel() - pk, err := sk.PubKey() + pk, err = sk.PubKey() if err != nil { pk, sk = cipher.GenerateKeyPair() } - dmsgWebLog.Info("dmsg client pk: ", pk.String()) + dLog.Info("dmsg client pk: ", pk.String()) if len(resolveDmsgAddr) > 0 { dialPK = make([]cipher.PubKey, len(resolveDmsgAddr)) - dmsgPorts = make([]uint, dmsgSessions) + dmsgPorts = make([]uint, len(resolveDmsgAddr)) + for i, dmsgaddr := range resolveDmsgAddr { - dmsgWebLog.Info("dmsg address to dial: ", dmsgaddr) - dmsgAddr = strings.Split(dmsgaddr, ":") - var setpk cipher.PubKey - err := setpk.Set(dmsgAddr[0]) - if err != nil { - log.Fatalf("failed to parse dmsg
: : %v", err) + dLog.Info("dmsg address to dial: ", dmsgaddr) + + // Split the address into public key and port + parts := strings.Split(dmsgaddr, ":") + if len(parts) < 1 || parts[0] == "" { + dLog.Fatal("Invalid dmsg address format. Expected [:]") } - dialPK[i] = setpk - if len(dmsgAddr) > 1 { - dport, err := strconv.ParseUint(dmsgAddr[1], 10, 64) + + // Parse the public key + var pk cipher.PubKey + if err := pk.Set(parts[0]); err != nil { + dLog.WithError(err).Fatal("Failed to parse public key from dmsg address") + } + dialPK[i] = pk + dLog.Info("Parsed public key: ", pk) + + // Parse the port or use the default (80) + port := uint(80) // Default port + if len(parts) > 1 && parts[1] != "" { + parsedPort, err := strconv.ParseUint(parts[1], 10, 16) // Ports are 16-bit unsigned integers if err != nil { - log.Fatalf("Failed to parse dmsg port: %v", err) + dLog.WithError(err).Fatal("Failed to parse dmsg port") } - dmsgPorts[i] = uint(dport) - } else { - dmsgPorts[i] = uint(80) + port = uint(parsedPort) } + dmsgPorts[i] = port + dLog.Info("Parsed port: ", port) } } if proxyAddr != "" { - // Use SOCKS5 proxy dialer if specified dialer, err = proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) if err != nil { - log.Fatalf("Error creating SOCKS5 dialer: %v", err) + dLog.WithError(err).Fatal("Error creating SOCKS5 dialer") } transport := &http.Transport{ Dial: dialer.Dial, @@ -218,9 +222,9 @@ dmsgweb conf file detected: ` + dmsgwebconffile ctx = context.WithValue(context.Background(), "socks5_proxy", proxyAddr) //nolint } - dmsgC, closeDmsg, err := cli.StartDmsg(ctx, dmsgWebLog, pk, sk, &httpC, dmsgDisc, dmsgSessions) + dmsgC, closeDmsg, err = cli.StartDmsg(ctx, dLog, pk, sk, &http.Client{}, dmsgDisc, dmsgSessions) if err != nil { - dmsgWebLog.WithError(err).Fatal("failed to start dmsg") + dLog.WithError(err).Fatal("failed to start dmsg") } defer closeDmsg() @@ -234,7 +238,6 @@ dmsgweb conf file detected: ` + dmsgwebconffile httpC = http.Client{Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgC)} if len(resolveDmsgAddr) == 0 { - // Create a SOCKS5 server with custom name resolution conf := &socks5.Config{ Resolver: &customResolver{}, Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { @@ -252,7 +255,6 @@ dmsgweb conf file detected: ` + dmsgwebconffile addr = "localhost:" + port } else { if addProxy != "" { - // Fallback to another SOCKS5 proxy dialer, err := proxy.SOCKS5("tcp", addProxy, nil, proxy.Direct) if err != nil { return nil, err @@ -260,44 +262,48 @@ dmsgweb conf file detected: ` + dmsgwebconffile return dialer.Dial(network, addr) } } - dmsgWebLog.Debug("Dialing address:", addr) + dLog.Debug("Dialing address:", addr) return net.Dial(network, addr) }, } - // Start the SOCKS5 server socksAddr := fmt.Sprintf("127.0.0.1:%v", proxyPort) - log.Printf("SOCKS5 proxy server started on %s", socksAddr) + dLog.Debug("SOCKS5 proxy server started on", socksAddr) server, err := socks5.New(conf) if err != nil { - log.Fatalf("Failed to create SOCKS5 server: %v", err) + dLog.WithError(err).Fatal("Failed to create SOCKS5 server") } wg.Add(1) go func() { - dmsgWebLog.Debug("Serving SOCKS5 proxy on " + socksAddr) + dLog.Debug("Serving SOCKS5 proxy on " + socksAddr) err := server.ListenAndServe("tcp", socksAddr) if err != nil { - log.Fatalf("Failed to start SOCKS5 server: %v", err) + dLog.WithError(err).Fatal("Failed to start SOCKS5 server") } - defer server.Close() - dmsgWebLog.Debug("Stopped serving SOCKS5 proxy on " + socksAddr) + defer server.Close() //nolint + dLog.Debug("Stopped serving SOCKS5 proxy on " + socksAddr) }() } if len(resolveDmsgAddr) == 0 && len(webPort) == 1 { if rawTCP[0] { + dLog.Debug("proxyTCPConn(-1)") proxyTCPConn(-1) } else { + dLog.Debug("proxyHTTPConn(-1)") proxyHTTPConn(-1) } } else { for i := range resolveDmsgAddr { + wg.Add(1) if rawTCP[i] { - proxyTCPConn(i) + dLog.Debug("proxyTCPConn(" + fmt.Sprintf("%v", i) + ")") + go proxyTCPConn(i) } else { - proxyHTTPConn(i) + dLog.Debug("proxyHTTPConn(" + fmt.Sprintf("%v", i) + ")") + go proxyHTTPConn(i) } } } @@ -305,6 +311,69 @@ dmsgweb conf file detected: ` + dmsgwebconffile }, } +func proxyTCPConn(n int) { + var thiswebport uint + if n == -1 { + thiswebport = webPort[0] + } else { + thiswebport = webPort[n] + } + listener, err := net.Listen("tcp", fmt.Sprintf(":%v", thiswebport)) + if err != nil { + dLog.WithError(err).Fatal(fmt.Sprintf("Failed to start TCP listener on port: %v", thiswebport)) + } + defer listener.Close() //nolint + dLog.Debug("Serving TCP on 127.0.0.1:", thiswebport) + if dmsgC == nil { + dLog.Fatal("dmsgC is nil") + } + + for { + conn, err := listener.Accept() + if err != nil { + dLog.WithError(err).Warn("Failed to accept connection") + continue + } + + go func(conn net.Conn, n int, dmsgC *dmsg.Client) { + defer conn.Close() //nolint + dp, ok := safecast.To[uint16](dmsgPorts[n]) + if !ok { + dLog.Fatal("uint16 overflow when converting dmsg port") + } + dLog.Debug(fmt.Sprintf("Dialing %v:%v", dialPK[n].String(), dp)) + dmsgConn, err := dmsgC.DialStream(context.Background(), dmsg.Addr{PK: dialPK[n], Port: dp}) //nolint + if err != nil { + dLog.WithError(err).Warn(fmt.Sprintf("Failed to dial dmsg address %v port %v", dialPK[n].String(), dmsgPorts[n])) + return + } + + defer dmsgConn.Close() //nolint + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _, err := io.Copy(dmsgConn, conn) + if err != nil { + dLog.WithError(err).Warn("Error on io.Copy(dmsgConn, conn)") + } + }() + + go func() { + defer wg.Done() + _, err := io.Copy(conn, dmsgConn) + if err != nil { + dLog.WithError(err).Warn("Error on io.Copy(conn, dmsgConn)") + } + }() + + wg.Wait() + }(conn, n, dmsgC) + } +} + func proxyHTTPConn(n int) { r := gin.New() @@ -333,10 +402,11 @@ func proxyHTTPConn(n int) { } } - fmt.Printf("Proxying request: %s %s\n", c.Request.Method, urlStr) + dLog.Debug(fmt.Sprintf("Proxying request: %s %s", c.Request.Method, urlStr)) req, err := http.NewRequest(c.Request.Method, urlStr, c.Request.Body) if err != nil { c.String(http.StatusInternalServerError, "Failed to create HTTP request") + dLog.WithError(err).Warn("Failed to create HTTP request") return } @@ -349,7 +419,7 @@ func proxyHTTPConn(n int) { resp, err := httpC.Do(req) if err != nil { c.String(http.StatusInternalServerError, "Failed to connect to HTTP server") - fmt.Printf("Error: %v\n", err) + dLog.WithError(err).Warn("Failed to connect to HTTP server") return } defer resp.Body.Close() //nolint @@ -363,7 +433,7 @@ func proxyHTTPConn(n int) { c.Status(resp.StatusCode) if _, err := io.Copy(c.Writer, resp.Body); err != nil { c.String(http.StatusInternalServerError, "Failed to copy response body") - fmt.Printf("Error copying response body: %v\n", err) + dLog.WithError(err).Warn("Failed to copy response body") } }) wg.Add(1) @@ -374,66 +444,12 @@ func proxyHTTPConn(n int) { } else { thiswebport = webPort[n] } - dmsgWebLog.Debug(fmt.Sprintf("Serving http on: http://127.0.0.1:%v", thiswebport)) + dLog.Debug(fmt.Sprintf("Serving http on: http://127.0.0.1:%v", thiswebport)) r.Run(":" + fmt.Sprintf("%v", thiswebport)) //nolint - dmsgWebLog.Debug(fmt.Sprintf("Stopped serving http on: http://127.0.0.1:%v", thiswebport)) + dLog.Debug(fmt.Sprintf("Stopped serving http on: http://127.0.0.1:%v", thiswebport)) wg.Done() }() } -func proxyTCPConn(n int) { - var thiswebport uint - if n == -1 { - thiswebport = webPort[0] - } else { - thiswebport = webPort[n] - } - listener, err := net.Listen("tcp", fmt.Sprintf(":%v", thiswebport)) - if err != nil { - dmsgWebLog.Fatalf("Failed to start TCP listener on port %v: %v", thiswebport, err) - } - defer listener.Close() //nolint - log.Printf("Serving TCP on 127.0.0.1:%v", thiswebport) - - for { - conn, err := listener.Accept() - if err != nil { - log.Printf("Failed to accept connection: %v", err) - continue - } - - wg.Add(1) - go func(conn net.Conn, n int) { - defer wg.Done() - defer conn.Close() //nolint - dp, ok := safecast.To[uint16](dmsgPorts[n]) - if !ok { - dmsgWebLog.Fatal("uint16 overflow when converting dmsg port") - } - dmsgConn, err := dmsgC.DialStream(context.Background(), dmsg.Addr{PK: dialPK[n], Port: dp}) //nolint - if err != nil { - log.Printf("Failed to dial dmsg address %v:%v %v", dialPK[n].String(), dmsgPorts[n], err) - return - } - defer dmsgConn.Close() //nolint - - go func() { - _, err := io.Copy(dmsgConn, conn) - if err != nil { - log.Printf("Error copying data to dmsg server: %v", err) - } - dmsgConn.Close() //nolint - }() - - go func() { - _, err := io.Copy(conn, dmsgConn) - if err != nil { - log.Printf("Error copying data from dmsg server: %v", err) - } - conn.Close() //nolint - }() - }(conn, n) - } -} const envfileLinux = ` ######################################################################### diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index 314716c1..84f0a916 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -10,13 +10,9 @@ import ( "net/http/httputil" "net/url" "os" - "runtime" - "strings" "sync" "time" - "github.com/bitfield/script" - "github.com/chen3feng/safecast" "github.com/gin-gonic/gin" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" @@ -28,250 +24,255 @@ import ( dmsg "github.com/skycoin/dmsg/pkg/dmsg" ) -const dmsgwebsrvenvname = "DMSGWEBSRV" +const dwsenv = "DMSGWEBSRV" -var dmsgwebsrvconffile = os.Getenv(dmsgwebsrvenvname) +var dwscfg = os.Getenv(dwsenv) func init() { - RootCmd.AddCommand(srvCmd) - srvCmd.Flags().UintSliceVarP(&localPort, "lport", "l", scriptExecUintSlice("${LOCALPORT[@]:-8086}", dmsgwebsrvconffile), "local application http interface port(s)") - srvCmd.Flags().UintSliceVarP(&dmsgPort, "dport", "d", scriptExecUintSlice("${DMSGPORT[@]:-80}", dmsgwebsrvconffile), "dmsg port(s) to serve") - srvCmd.Flags().StringSliceVarP(&wl, "wl", "w", scriptExecStringSlice("${WHITELISTPKS[@]}", dmsgwebsrvconffile), "whitelisted keys for dmsg authenticated routes\r") - srvCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsgDisc, "dmsg discovery url(s)") - srvCmd.Flags().StringVarP(&proxyAddr, "proxy", "x", proxyAddr, "connect to dmsg via proxy (i.e. '127.0.0.1:1080')") - srvCmd.Flags().IntVarP(&dmsgSess, "dsess", "e", scriptExecInt("${DMSGSESSIONS:-1}", dmsgwebsrvconffile), "dmsg sessions") - srvCmd.Flags().BoolSliceVarP(&rawTCP, "rt", "c", scriptExecBoolSlice("${RAWTCP[@]:-false}", dmsgwebsrvconffile), "proxy local port as raw TCP") + dmsgPort = scriptExecUintSlice("${DMSGPORT[@]:-80}", dwscfg) + dmsgSess = scriptExecInt("${DMSGSESSIONS:-1}", dwscfg) + wl = scriptExecStringSlice("${WHITELISTPKS[@]}", dwscfg) + localPort = scriptExecUintSlice("${LOCALPORT[@]:-8086}", dwscfg) + rawTCP = scriptExecBoolSlice("${RAWTCP[@]:-false}", dwscfg) if os.Getenv("DMSGWEBSRVSK") != "" { sk.Set(os.Getenv("DMSGWEBSRVSK")) //nolint } - if scriptExecString("${DMSGWEBSRVSK}", dmsgwebsrvconffile) != "" { - sk.Set(scriptExecString("${DMSGWEBSRVSK}", dmsgwebsrvconffile)) //nolint + if scriptExecString("${DMSGWEBSRVSK}", dwscfg) != "" { + sk.Set(scriptExecString("${DMSGWEBSRVSK}", dwscfg)) //nolint } pk, _ = sk.PubKey() //nolint - srvCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") - srvCmd.Flags().BoolVarP(&isEnvs, "envs", "z", false, "show example .conf file") + RootCmd.AddCommand(srvCmd) + srvCmd.Flags().UintSliceVarP(&localPort, "lport", "p", localPort, "local application interface port(s)\033[0m\n\r") + srvCmd.Flags().UintSliceVarP(&dmsgPort, "dport", "d", dmsgPort, "DMSG port(s) to serve\033[0m\n\r") + srvCmd.Flags().StringSliceVarP(&wl, "wl", "w", wl, "whitelisted keys for DMSG authenticated routes\033[0m\n\r") + srvCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsgDisc, "DMSG discovery URL\033[0m\n\r") + srvCmd.Flags().StringVarP(&proxyAddr, "proxy", "x", proxyAddr, "connect to DMSG via proxy (e.g., '127.0.0.1:1080')\033[0m\n\r") + srvCmd.Flags().IntVarP(&dmsgSess, "dsess", "e", dmsgSess, "DMSG sessions\033[0m\n\r") + srvCmd.Flags().BoolSliceVarP(&rawTCP, "rt", "c", rawTCP, "proxy local port as raw TCP, comma separated\033[0m\n\r") + srvCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "debug", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") + srvCmd.Flags().BoolVarP(&isEnvs, "envs", "z", false, "show example .conf file\033[0m\n\r") + srvCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\033[0m\n\r") srvCmd.CompletionOptions.DisableDefaultCmd = true } var srvCmd = &cobra.Command{ Use: "srv", - Short: "serve http or raw TCP from local port over dmsg", - Long: `DMSG web server - serve http or raw TCP interface from local port over dmsg` + func() string { - if _, err := os.Stat(dmsgwebsrvconffile); err == nil { - return ` - dmsenv file detected: ` + dmsgwebsrvconffile + Short: "Serve HTTP or raw TCP from local port over DMSG", + Long: `DMSG web server - serve HTTP or raw TCP interface from local port over DMSG` + func() string { + if _, err := os.Stat(dwscfg); err == nil { + return "\n\t.dmsenv file detected: " + dwscfg } - return ` - .conf file may also be specified with - ` + dmsgwebsrvenvname + `=/path/to/dmsgwebsrv.conf skywire dmsg web srv` + return "\n\t.conf file may also be specified with " + dwsenv + `=/path/to/dmsgwebsrv.conf skywire dmsg web srv` }(), - Run: func(_ *cobra.Command, _ []string) { + PreRun: func(_ *cobra.Command, _ []string) { if isEnvs { - envfile := srvenvfileLinux - if runtime.GOOS == "windows" { - envfileslice, _ := script.Echo(envfile).Slice() //nolint - for i := range envfileslice { - efs, _ := script.Echo(envfileslice[i]).Reject("##").Reject("#-").Reject("# ").Replace("#", "#$").String() //nolint - if efs != "" && efs != "\n" { - envfileslice[i] = strings.ReplaceAll(efs, "\n", "") - } + printEnvs(srvenvfileLinux) + } + if logLvl != "" { + if lvl, err := logging.LevelFromString(logLvl); err == nil { + logging.SetLevel(lvl) + } + } + dLog = logging.MustGetLogger("dmsgwebsrv") + if len(localPort) != len(dmsgPort) || len(localPort) != len(rawTCP) { + dLog.Fatal("The number of local ports, DMSG ports, and raw TCP flags must be the same") + } + pk, err = sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + dLog.Debugf("DMSG client public key: %v", pk.String()) + + if len(wl) > 0 { + for _, key := range wl { + var pk cipher.PubKey + if err := pk.Set(key); err == nil { + wlkeys = append(wlkeys, pk) } - envfile = strings.Join(envfileslice, "\n") } - fmt.Println(envfile) - os.Exit(0) + dLog.Infof("%d keys whitelisted", len(wlkeys)) } + if proxyAddr != "" { + var err error + dialer, err = proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) + if err != nil { + dLog.Fatalf("Error creating SOCKS5 dialer: %v", err) + } + httpClient = &http.Client{Transport: &http.Transport{Dial: dialer.Dial}} + } + }, + Run: func(_ *cobra.Command, _ []string) { server() }, } func server() { - log := logging.MustGetLogger("dmsgwebsrv") - if len(localPort) != len(dmsgPort) { - log.Fatal(fmt.Sprintf("the same number of local ports as dmsg ports must be specified ; local ports: %v ; dmsg ports: %v", len(localPort), len(dmsgPort))) - } - - seenLocalPort := make(map[uint]bool) - for _, item := range localPort { - if seenLocalPort[item] { - log.Fatal("-lport --l flag cannot contain duplicates") - } - seenLocalPort[item] = true - } - - seenDmsgPort := make(map[uint]bool) - for _, item := range dmsgPort { - if seenDmsgPort[item] { - log.Fatal("-dport --d flag cannot contain duplicates") - } - seenDmsgPort[item] = true - } - - ctx, cancel := cmdutil.SignalContext(context.Background(), log) + ctx, cancel := cmdutil.SignalContext(context.Background(), dLog) defer cancel() - pk, err = sk.PubKey() - if err != nil { - pk, sk = cipher.GenerateKeyPair() - } - log.Infof("dmsg client pk: %v", pk.String()) - - if len(wl) > 0 { - for _, key := range wl { - var pk0 cipher.PubKey - err := pk0.Set(key) - if err == nil { - wlkeys = append(wlkeys, pk0) - } - } - } - 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))) - } - } - if proxyAddr != "" { - // Use SOCKS5 proxy dialer if specified - dialer, err = proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) - if err != nil { - log.Fatalf("Error creating SOCKS5 dialer: %v", err) - } - transport := &http.Transport{ - Dial: dialer.Dial, - } - httpClient = &http.Client{ - Transport: transport, - } - ctx = context.WithValue(context.Background(), "socks5_proxy", proxyAddr) //nolint - } - - dmsgC := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig()) + dmsgClient := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, dLog), dmsg.DefaultConfig()) defer func() { - if err := dmsgC.Close(); err != nil { - log.WithError(err).Error() + if err := dmsgClient.Close(); err != nil { + dLog.WithError(err).Error() } }() - - go dmsgC.Serve(context.Background()) + go dmsgClient.Serve(ctx) select { case <-ctx.Done(): - log.WithError(ctx.Err()).Warn() + dLog.WithError(ctx.Err()).Warn() return - - case <-dmsgC.Ready(): + case <-dmsgClient.Ready(): } - var listN []net.Listener - - for _, dport := range dmsgPort { - dp, ok := safecast.To[uint16](dport) - if !ok { - log.Fatal("uint16 overflow when converting dmsg port") - } - lis, err := dmsgC.Listen(dp) + wg := sync.WaitGroup{} + for i := range localPort { + lis, err := dmsgClient.Listen(uint16(dmsgPort[i])) //nolint if err != nil { - log.Fatalf("Error listening on port %d: %v", dport, err) + dLog.Fatalf("Error listening on DMSG port %d: %v", dmsgPort[i], err) } - - listN = append(listN, lis) - - dport := dp - go func(l net.Listener, port uint16) { - <-ctx.Done() - if err := l.Close(); err != nil { - log.Printf("Error closing listener on port %d: %v", port, err) - log.WithError(err).Error() - } - }(lis, dport) - } - - wg := new(sync.WaitGroup) - - for i, lpt := range localPort { wg.Add(1) - go func(localPort uint, rtcp bool, lis net.Listener) { + go func(ctx context.Context, localPort uint, rawTCP bool, listener net.Listener) { defer wg.Done() - if rtcp { - proxyTCPConnections(localPort, lis, log) + defer listener.Close() //nolint + + if rawTCP { + proxyTCPConnections(ctx, localPort, listener) } else { - proxyHTTPConnections(localPort, lis, log) + proxyHTTPConnections(ctx, localPort, listener) } - }(lpt, rawTCP[i], listN[i]) + }(ctx, localPort[i], rawTCP[i], lis) } - wg.Wait() } -func proxyHTTPConnections(localPort uint, lis net.Listener, log *logging.Logger) { - r1 := gin.New() - r1.Use(gin.Recovery()) - r1.Use(loggingMiddleware()) +func proxyHTTPConnections(ctx context.Context, localPort uint, listener net.Listener) { + router := gin.New() + router.Use(gin.Recovery()) + router.Use(loggingMiddleware()) - authRoute := r1.Group("/") + authRoute := router.Group("/") if len(wlkeys) > 0 { authRoute.Use(whitelistAuth(wlkeys)) } authRoute.Any("/*path", func(c *gin.Context) { - targetURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%v%s?%s", localPort, c.Request.URL.Path, c.Request.URL.RawQuery)) //nolint - proxy := httputil.ReverseProxy{ - Director: func(req *http.Request) { - req.URL = targetURL - req.Host = targetURL.Host - req.Method = c.Request.Method - }, - Transport: &http.Transport{}, - } + targetURL := fmt.Sprintf("http://127.0.0.1:%d%s?%s", localPort, c.Request.URL.Path, c.Request.URL.RawQuery) + proxy := httputil.ReverseProxy{Director: func(req *http.Request) { + req.URL, _ = url.Parse(targetURL) //nolint + req.Host = req.URL.Host + }} proxy.ServeHTTP(c.Writer, c.Request) }) - serve := &http.Server{ - Handler: &ginHandler{Router: r1}, + + server := &http.Server{ + Handler: router, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, } - log.Printf("Serving HTTP on dmsg port %v with DMSG listener %s", localPort, lis.Addr().String()) - if err := serve.Serve(lis); err != nil && err != http.ErrServerClosed { - log.Fatalf("Serve: %v", err) + + // Graceful shutdown on context cancellation + go func() { + <-ctx.Done() + if err := server.Shutdown(context.Background()); err != nil { + dLog.Errorf("HTTP server shutdown error: %v", err) + } + }() + + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + dLog.Fatalf("HTTP server error: %v", err) } } -func proxyTCPConnections(localPort uint, lis net.Listener, log *logging.Logger) { +func proxyTCPConnections(ctx context.Context, localPort uint, listener net.Listener) { + // To track active connections for cleanup + var connWg sync.WaitGroup + connChan := make(chan net.Conn) + activeConns := make(map[net.Conn]struct{}) + connMutex := &sync.Mutex{} // Protect access to activeConns + + // Goroutine to accept new connections + go func() { + defer close(connChan) + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + // Listener closed due to context cancellation + return + default: + dLog.Errorf("Error accepting connection: %v", err) + return + } + } + connChan <- conn + } + }() + for { - conn, err := lis.Accept() - if err != nil { - log.Printf("Error accepting connection: %v", err) + select { + case <-ctx.Done(): + dLog.Info("Shutting down TCP proxy connections...") + listener.Close() //nolint + + connMutex.Lock() + for conn := range activeConns { + conn.Close() //nolint + } + connMutex.Unlock() + + connWg.Wait() return - } - go handleTCPConnection(conn, localPort, log) - } -} + case conn, ok := <-connChan: + if !ok { + return + } -func handleTCPConnection(dmsgConn net.Conn, localPort uint, log *logging.Logger) { - defer dmsgConn.Close() //nolint + connMutex.Lock() + activeConns[conn] = struct{}{} + connMutex.Unlock() - localConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) - if err != nil { - log.Printf("Error connecting to local port %d: %v", localPort, err) - return - } - defer localConn.Close() //nolint + connWg.Add(1) + go func(dmsgConn net.Conn) { + defer connWg.Done() + defer dmsgConn.Close() //nolint - copyConn := func(dst net.Conn, src net.Conn) { - _, err := io.Copy(dst, src) - if err != nil { - log.Printf("Error during copy: %v", err) + localConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) + if err != nil { + dLog.Errorf("Error connecting to local port %d: %v", localPort, err) + + connMutex.Lock() + delete(activeConns, dmsgConn) + connMutex.Unlock() + + return + } + defer localConn.Close() //nolint + + go func() { + _, err1 := io.Copy(dmsgConn, localConn) + if err1 != nil { + dLog.WithError(err1).Warn("Error on io.Copy(dmsgConn, localConn)") + } + }() + _, err2 := io.Copy(localConn, dmsgConn) + if err2 != nil { + dLog.WithError(err2).Warn("Error on io.Copy(localConn, dmsgConn)") + } + + connMutex.Lock() + delete(activeConns, dmsgConn) + connMutex.Unlock() + }(conn) } } - - go copyConn(dmsgConn, localConn) - go copyConn(localConn, dmsgConn) } const srvenvfileLinux = ` diff --git a/cmd/dmsgweb/commands/root.go b/cmd/dmsgweb/commands/root.go index 31671b66..0f8b6c32 100644 --- a/cmd/dmsgweb/commands/root.go +++ b/cmd/dmsgweb/commands/root.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "os" "runtime" "strconv" "strings" @@ -23,8 +24,10 @@ import ( ) var ( + dLog *logging.Logger httpC http.Client dmsgC *dmsg.Client + closeDmsg func() dmsgDisc = dmsg.DiscAddr(false) proxyAddr string dmsgSessions int @@ -85,6 +88,22 @@ func startDmsg(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey, dmsgDisc } } */ + +func printEnvs(envfile string) { + if runtime.GOOS == "windows" { + envfileslice, _ := script.Echo(envfile).Slice() //nolint + for i := range envfileslice { + efs, _ := script.Echo(envfileslice[i]).Reject("##").Reject("#-").Reject("# ").Replace("#", "#$").String() //nolint + if efs != "" && efs != "\n" { + envfileslice[i] = strings.ReplaceAll(efs, "\n", "") + } + } + envfile = strings.Join(envfileslice, "\n") + } + fmt.Println(envfile) + os.Exit(0) +} + //TODO: these functions are more or less duplicated in several places - need to standardize and put in it's own library import in "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/..." func scriptExecString(s, envfile string) string { diff --git a/examples/dmsghttp-client/dmsghttp-client.go b/examples/dmsghttp-client/dmsghttp-client.go new file mode 100644 index 00000000..05efc83b --- /dev/null +++ b/examples/dmsghttp-client/dmsghttp-client.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + + "github.com/skycoin/dmsg/internal/cli" + "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsghttp" +) + +func main() { + dLog := logging.MustGetLogger("dmsghttp-client") + dmsgDisc := dmsg.DiscAddr(false) + parsedURL, err := url.Parse(os.Args[1]) + if err != nil { + dLog.Fatalf("Failed to parse URL: %v", err) + } + pk, sk := cipher.GenerateKeyPair() + ctx := context.Background() + dmsgClient, closeDmsg, err := cli.StartDmsg(ctx, dLog, pk, sk, &http.Client{}, dmsgDisc, 1) + if err != nil { + dLog.Fatalf("Failed to start DMSG client: %v", err) + } + dLog.Println("started dmsg client") + defer closeDmsg() + if dmsgClient == nil { + dLog.Fatal("DMSG client initialization failed. Exiting.") + } + httpClient := &http.Client{ + Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgClient), + } + resp, err := httpClient.Get(parsedURL.String()) + if err != nil { + dLog.Fatalf("Failed to perform GET request: %v", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + dLog.Fatalf("Failed to read response body: %v", err) + } + fmt.Println(string(body)) +} diff --git a/examples/dmsghttp/README.md b/examples/dmsghttp/README.md new file mode 100644 index 00000000..06089b03 --- /dev/null +++ b/examples/dmsghttp/README.md @@ -0,0 +1,72 @@ +## example hello world via HTTP over DMSG + +### Generate keys: + +``` +go run ../gen-keys/gen-keys.go | tee dmsgtest.keys +``` +OR +``` +go run ../gen-keys/gen-keys.go > dmsgtest.keys +``` + + +### Start application using secret key + + +``` +$ go run dmsghttp.go -s $(tail -n1 dmsgtest.key) +[2025-01-28T15:45:26.218738525-06:00] DEBUG disc.NewHTTP [dmsghttp]: Created HTTP client. addr="http://dmsgd.skywire.skycoin.com" +[2025-01-28T15:45:26.218798474-06:00] DEBUG [dmsg_client]: Discovering dmsg servers... +[2025-01-28T15:45:26.494061772-06:00] DEBUG [dmsg_client]: Dialing session... remote_pk=0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb +[2025-01-28T15:45:27.047394122-06:00] DEBUG [dmsg_client]: Serving session. remote_pk=0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb +[2025-01-28T15:45:27.047406948-06:00] INFO [dmsghttp]: Serving Hello World on DMSG address 03f45df9890955214bbfe2e06487741489266a60c487365989b9723680b45e0f6e:80 +[2025-01-28T15:46:02.992699297-06:00] INFO [dmsghttp]: Received request from 0352b141c5a423a3788ad7202745b083e2240dea5ce304742a272ec4a80ece6c1c:49153 + +``` + +### Get using dmsgcurl + + +``` +$ go run ../../cmd/dmsgcurl/dmsgcurl.go -l debug dmsg://$(head -n1 dmsgtest.key):80 +[2025-01-28T15:46:00.785331607-06:00] DEBUG disc.NewHTTP [dmsgcurl]: Created HTTP client. addr="http://dmsgd.skywire.skycoin.com" +[2025-01-28T15:46:00.785375317-06:00] DEBUG [dmsgcurl]: Connecting to dmsg network... dmsg_disc="http://dmsgd.skywire.skycoin.com" public_key="0352b141c5a423a3788ad7202745b083e2240dea5ce304742a272ec4a80ece6c1c" +[2025-01-28T15:46:00.785475179-06:00] DEBUG [dmsg_client]: Discovering dmsg servers... +[2025-01-28T15:46:01.053319739-06:00] DEBUG [dmsg_client]: Dialing session... remote_pk=02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd +[2025-01-28T15:46:01.611696253-06:00] DEBUG [dmsg_client]: Serving session. remote_pk=02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd +[2025-01-28T15:46:01.611738628-06:00] DEBUG [dmsgcurl]: Dmsg network ready. +[2025-01-28T15:46:02.018101877-06:00] DEBUG [dmsg_client]: Dialing session... remote_pk=0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb +[2025-01-28T15:46:02.431323641-06:00] DEBUG [dmsg_client]: Updating entry. entry= version: 0.0.1 + sequence: 0 + registered at: 1738100761468088283 + static public key: 0352b141c5a423a3788ad7202745b083e2240dea5ce304742a272ec4a80ece6c1c + signature: 4a761f8e84f021683957f272832a007890de622414d155db49bf0cdac944477d6160f846e07f68d6d78c93209ebe593b3dc4c48d1ef729f7ee7c52fb79d295a200 + entry is registered as client. Related info: + delegated servers: + 02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd + 0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb + + +[2025-01-28T15:46:02.569096852-06:00] DEBUG [dmsg_client]: Serving session. remote_pk=0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb +Hello, World![2025-01-28T15:46:03.127242853-06:00] DEBUG [dmsg_client]: Stopped serving client! +[2025-01-28T15:46:03.127263963-06:00] DEBUG [dmsg_client]: Stopped accepting streams. error="session shutdown" session=02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd +[2025-01-28T15:46:03.12746417-06:00] DEBUG [dmsg_client]: Session closed. error= +[2025-01-28T15:46:03.127561533-06:00] DEBUG [dmsg_client]: Stopped accepting streams. error="session shutdown" session=0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb +[2025-01-28T15:46:03.127602412-06:00] DEBUG [dmsg_client]: Session closed. error= +[2025-01-28T15:46:03.127623574-06:00] DEBUG [dmsg_client]: All sessions closed. +[2025-01-28T15:46:03.395174115-06:00] DEBUG [dmsg_client]: Deleting entry. entry= version: 0.0.1 + sequence: 1 + registered at: 1738100762431422654 + static public key: 0352b141c5a423a3788ad7202745b083e2240dea5ce304742a272ec4a80ece6c1c + signature: 40a80e385094c38a420d9229d517172a3859f28b79057b5b79cf2600d3021f9d7af57fa26d0f5c9c0643fa45d07bad8169860b842851543d866455a8f170cf6701 + entry is registered as client. Related info: + delegated servers: + 02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd + 0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb + + +[2025-01-28T15:46:03.533465754-06:00] DEBUG [dmsg_client]: Entry Deleted successfully. +[2025-01-28T15:46:03.533508976-06:00] DEBUG [dmsgcurl]: Disconnected from dmsg network. error= + +``` diff --git a/examples/dmsghttp/dmsghttp.go b/examples/dmsghttp/dmsghttp.go new file mode 100644 index 00000000..ec01e75c --- /dev/null +++ b/examples/dmsghttp/dmsghttp.go @@ -0,0 +1,153 @@ +// Example Hello World HTTP over DMSG +package main + +import ( + "context" + "log" + "net/http" + "os" + "strings" + "time" + + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +var ( + sk cipher.SecKey + dmsgDisc string + dmsgPort uint +) + +func init() { + RootCmd.Flags().UintVarP(&dmsgPort, "port", "p", 80, "DMSG port to serve from") + RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsg.DiscAddr(false), "DMSG discovery URL") + if os.Getenv("DMSGHTTP_SK") != "" { + sk.Set(os.Getenv("DMSGHTTP_SK")) //nolint + } + RootCmd.Flags().VarP(&sk, "sk", "s", "A random key is generated if unspecified\n\r") +} + +// RootCmd contains the root DMSG HTTP command +var RootCmd = &cobra.Command{ + Use: func() string { + return strings.Split(os.Args[0], " ")[0] + }(), + Short: "DMSG HTTP Hello World server", + Long: "DMSG HTTP Hello World server", + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Run: func(_ *cobra.Command, _ []string) { + log := logging.MustGetLogger("dmsghttp") + if dmsgDisc == "" { + log.Fatal("DMSG discovery URL not specified") + } + + ctx, cancel := cmdutil.SignalContext(context.Background(), log) + defer cancel() + + // Generate keys if not provided + pk, err := sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + + // Initialize the DMSG client + c := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig()) + defer func() { + if err := c.Close(); err != nil { + log.WithError(err).Error("Failed to close DMSG client") + } + }() + go c.Serve(context.Background()) + + // Wait for the DMSG client to be ready + select { + case <-ctx.Done(): + log.WithError(ctx.Err()).Warn() + return + case <-c.Ready(): + } + + // Listen on the specified DMSG port + lis, err := c.Listen(uint16(dmsgPort)) + if err != nil { + log.WithError(err).Fatal("Failed to listen on DMSG port") + } + defer lis.Close() + + log.Infof("Serving Hello World on DMSG address %s", lis.Addr()) + + // Set up HTTP server to respond with "Hello, World!" + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Infof("Received request from %s", r.RemoteAddr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello, World!")) + }) + + // Start the HTTP server + server := &http.Server{ + ReadHeaderTimeout: 3 * time.Second, + } + + // Graceful shutdown handler + go func() { + <-ctx.Done() + log.Info("Shutdown signal received, shutting down HTTP server...") + server.Shutdown(context.Background()) + log.Info("HTTP server successfully shut down") + }() + + // Start serving HTTP requests + log.Fatal(server.Serve(lis)) + }, +} + +// Execute executes the root CLI command +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} + +func init() { + var helpflag bool + RootCmd.SetUsageTemplate(help) + RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for dmsghttp-cli") + RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + +func main() { + 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, + }) + Execute() +} + +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" diff --git a/examples/dmsgtcp/README.md b/examples/dmsgtcp/README.md new file mode 100644 index 00000000..44cf8d62 --- /dev/null +++ b/examples/dmsgtcp/README.md @@ -0,0 +1 @@ +example hello world via TCP over DMSG diff --git a/examples/dmsgtcp/dmsgtcp.go b/examples/dmsgtcp/dmsgtcp.go new file mode 100644 index 00000000..1a0592f2 --- /dev/null +++ b/examples/dmsgtcp/dmsgtcp.go @@ -0,0 +1,165 @@ +// Example hello world TCP over DMSG +package main + +import ( + "context" + "log" + "net" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/internal/cli" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +var ( + sk cipher.SecKey + dmsgDisc string + dmsgPort uint +) + +func init() { + RootCmd.Flags().UintVarP(&dmsgPort, "port", "p", 80, "DMSG port to serve from") + RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsg.DiscAddr(false), "DMSG discovery URL") + if os.Getenv("DMSGTCP_SK") != "" { + sk.Set(os.Getenv("DMSGTCP_SK")) //nolint + } + RootCmd.Flags().VarP(&sk, "sk", "s", "A random key is generated if unspecified\n\r") +} + +// RootCmd contains the root DMSG TCP command +var RootCmd = &cobra.Command{ + Use: func() string { + return strings.Split(os.Args[0], " ")[0] + }(), + Short: "DMSG TCP Hello World server", + Long: "DMSG TCP Hello World server", + Run: func(_ *cobra.Command, _ []string) { + log := logging.MustGetLogger("dmsgtcp") + if dmsgDisc == "" { + log.Fatal("DMSG discovery URL not specified") + } + + // Create the context and cancel function + ctx, cancel := cmdutil.SignalContext(context.Background(), log) + defer cancel() + + // Generate keys if not provided + pk, err := sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + + // Initialize the DMSG client + dmsgC, closeDmsg, err := cli.StartDmsg(ctx, log, pk, sk, &http.Client{}, dmsgDisc, 1) + if err != nil { + log.WithError(err).Fatal("failed to start dmsg") + } + defer closeDmsg() + + go func() { + <-ctx.Done() + cancel() + closeDmsg() + os.Exit(0) + }() + + // Listen on the specified DMSG port + lis, err := dmsgC.Listen(uint16(dmsgPort)) + if err != nil { + log.WithError(err).Fatal("Failed to listen on DMSG port") + } + defer lis.Close() + + log.Infof("Serving Hello World TCP on DMSG address %s", lis.Addr()) + + // Handle system interrupt (Ctrl + C) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to wait for shutdown signals + go func() { + <-signalChan + log.Info("Received shutdown signal.") + lis.Close() + closeDmsg() + cancel() // Cancel context to terminate DMSG client and server + }() + + // Accept TCP connections and respond with "Hello, World!" + for { + conn, err := lis.Accept() + if err != nil { + // If the server was closed or a signal was received, break out + if ctx.Err() != nil { + log.Info("Server shutting down...") + return + } + log.WithError(err).Error("Failed to accept connection") + continue + } + go handleConnection(conn, log) + } + }, +} + +func handleConnection(conn net.Conn, log *logging.Logger) { + defer conn.Close() + log.Infof("Received connection from %s", conn.RemoteAddr()) + + // Write "Hello, World!" message to the connection + _, err := conn.Write([]byte("Hello, World!\n")) + if err != nil { + log.WithError(err).Error("Failed to write response") + } +} + +// Execute executes the root CLI command +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} + +func init() { + var helpflag bool + RootCmd.SetUsageTemplate(help) + RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for dmsgpty-cli") + RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + +func main() { + 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, + }) + Execute() +} + +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" diff --git a/examples/dmsgweb/commands/dmsgweb.go b/examples/dmsgweb/commands/dmsgweb.go new file mode 100644 index 00000000..03e2a2e9 --- /dev/null +++ b/examples/dmsgweb/commands/dmsgweb.go @@ -0,0 +1,200 @@ +// Package commands cmd/dmsgweb/commands/dmsgweb.go +package commands + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/signal" + "regexp" + "strings" + "syscall" + + "github.com/confiant-inc/go-socks5" + "github.com/gin-gonic/gin" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + "golang.org/x/net/proxy" + + "github.com/skycoin/dmsg/pkg/dmsghttp" +) + +func init() { + RootCmd.AddCommand(genKeysCmd) + RootCmd.Flags().StringVarP(&filterDomainSuffix, "filter", "f", ".dmsg", "domain suffix to filter") + RootCmd.Flags().StringVarP(&proxyPort, "socks", "q", "4445", "port to serve the socks5 proxy") + RootCmd.Flags().StringVarP(&addProxy, "proxy", "r", "", "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)") + RootCmd.Flags().StringVarP(&webPort, "port", "p", "8080", "port to serve the web application") + RootCmd.Flags().StringVarP(&resolveDmsgAddr, "resolve", "t", "", "resolve the specified dmsg address:port on the local port & disable proxy") + RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsgDisc, "dmsg discovery url") + RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", 1, "number of dmsg servers to connect to") + RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m") + if os.Getenv("DMSGGET_SK") != "" { + sk.Set(os.Getenv("DMSGGET_SK")) //nolint + } + RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") +} + +// RootCmd contains the root command for dmsgweb +var RootCmd = &cobra.Command{ + Use: "web", + Short: "DMSG resolving proxy & browser client", + Long: ` + ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ + │││││└─┐│ ┬│││├┤ ├┴┐ + ─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ + ` + "DMSG resolving proxy & browser client - access websites over dmsg", + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Version: buildinfo.Version(), + Run: func(cmd *cobra.Command, _ []string) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) //nolint + go func() { + <-c + os.Exit(1) + }() + if dmsgWebLog == nil { + dmsgWebLog = logging.MustGetLogger("dmsgweb") + } + if logLvl != "" { + if lvl, err := logging.LevelFromString(logLvl); err == nil { + logging.SetLevel(lvl) + } + } + + if filterDomainSuffix == "" { + dmsgWebLog.Fatal("domain suffix to filter cannot be an empty string") + } + ctx, cancel := cmdutil.SignalContext(context.Background(), dmsgWebLog) + defer cancel() + + pk, err := sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + + dmsgC, closeDmsg, err := startDmsg(ctx, pk, sk) + if err != nil { + dmsgWebLog.WithError(err).Fatal("failed to start dmsg") + } + defer closeDmsg() + + go func() { + <-ctx.Done() + cancel() + closeDmsg() + os.Exit(0) //this should not be necessary + }() + + httpC = http.Client{Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgC)} + + if resolveDmsgAddr == "" { + // Create a SOCKS5 server with custom name resolution + conf := &socks5.Config{ + Resolver: &customResolver{}, + Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + regexPattern := `\` + filterDomainSuffix + `(:[0-9]+)?$` + match, _ := regexp.MatchString(regexPattern, host) //nolint:errcheck + if match { + port, ok := ctx.Value("port").(string) + if !ok { + port = webPort + } + addr = "localhost:" + port + } else { + if addProxy != "" { + // Fallback to another SOCKS5 proxy + dialer, err := proxy.SOCKS5("tcp", addProxy, nil, proxy.Direct) + if err != nil { + return nil, err + } + return dialer.Dial(network, addr) + } + } + dmsgWebLog.Debug("Dialing address:", addr) + return net.Dial(network, addr) + }, + } + + // Start the SOCKS5 server + socksAddr := "127.0.0.1:" + proxyPort + log.Printf("SOCKS5 proxy server started on %s", socksAddr) + + server, err := socks5.New(conf) + if err != nil { + log.Fatalf("Failed to create SOCKS5 server: %v", err) + } + + wg.Add(1) + go func() { + dmsgWebLog.Debug("Serving SOCKS5 proxy on " + socksAddr) + err := server.ListenAndServe("tcp", socksAddr) + if err != nil { + log.Fatalf("Failed to start SOCKS5 server: %v", err) + } + defer server.Close() + dmsgWebLog.Debug("Stopped serving SOCKS5 proxy on " + socksAddr) + }() + } + r := gin.New() + + r.Use(gin.Recovery()) + + r.Use(loggingMiddleware()) + + r.Any("/*path", func(c *gin.Context) { + var urlStr string + if resolveDmsgAddr != "" { + urlStr = fmt.Sprintf("dmsg://%s%s", resolveDmsgAddr, c.Param("path")) + } else { + + hostParts := strings.Split(c.Request.Host, ":") + var dmsgp string + if len(hostParts) > 1 { + dmsgp = hostParts[1] + } else { + dmsgp = "80" + } + urlStr = fmt.Sprintf("dmsg://%s:%s%s", strings.TrimRight(hostParts[0], filterDomainSuffix), dmsgp, c.Param("path")) + } + + req, err := http.NewRequest(http.MethodGet, urlStr, nil) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to create HTTP request") + return + } + + resp, err := httpC.Do(req) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to connect to HTTP server") + return + } + defer resp.Body.Close() //nolint + + c.Status(http.StatusOK) + io.Copy(c.Writer, resp.Body) //nolint + }) + wg.Add(1) + go func() { + dmsgWebLog.Debug("Serving http on " + webPort) + r.Run(":" + webPort) //nolint + dmsgWebLog.Debug("Stopped serving http on " + webPort) + wg.Done() + }() + wg.Wait() + }, +} diff --git a/examples/dmsgweb/commands/root.go b/examples/dmsgweb/commands/root.go new file mode 100644 index 00000000..894fe977 --- /dev/null +++ b/examples/dmsgweb/commands/root.go @@ -0,0 +1,179 @@ +// Package commands cmd/dmsgweb/commands/dmsgweb.go +package commands + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + "os" + "regexp" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +// RootCmd contains commands that interact with the config of local skywire-visor +var genKeysCmd = &cobra.Command{ + Use: "gen-keys", + Short: "generate public / secret keypair", + Run: func(cmd *cobra.Command, args []string) { + pk, sk := cipher.GenerateKeyPair() + fmt.Println(pk) + fmt.Println(sk) + }, +} + +type customResolver struct{} + +func (r *customResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { + // Handle custom name resolution for .dmsg domains + regexPattern := `\.` + filterDomainSuffix + `(:[0-9]+)?$` + match, _ := regexp.MatchString(regexPattern, name) //nolint:errcheck + if match { + ip := net.ParseIP("127.0.0.1") + if ip == nil { + return ctx, nil, fmt.Errorf("failed to parse IP address") + } + // Modify the context to include the desired port + ctx = context.WithValue(ctx, "port", webPort) //nolint + return ctx, ip, nil + } + // Use default name resolution for other domains + return ctx, nil, nil +} + +var ( + httpC http.Client + dmsgDisc = dmsg.DiscAddr(false) + dmsgSessions int + filterDomainSuffix string + sk cipher.SecKey + dmsgWebLog *logging.Logger + logLvl string + webPort string + proxyPort string + addProxy string + resolveDmsgAddr string + wg sync.WaitGroup +) + +func startDmsg(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey) (dmsgC *dmsg.Client, stop func(), err error) { + dmsgC = dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, dmsgWebLog), &dmsg.Config{MinSessions: dmsgSessions}) + go dmsgC.Serve(context.Background()) + + stop = func() { + err := dmsgC.Close() + dmsgWebLog.WithError(err).Debug("Disconnected from dmsg network.") + fmt.Printf("\n") + } + dmsgWebLog.WithField("public_key", pk.String()).WithField("dmsg_disc", dmsgDisc). + Debug("Connecting to dmsg network...") + + select { + case <-ctx.Done(): + stop() + os.Exit(0) + return nil, nil, ctx.Err() + + case <-dmsgC.Ready(): + dmsgWebLog.Debug("Dmsg network ready.") + return dmsgC, stop, nil + } +} + +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) + // Print the logging in a custom format which includes the publickeyfrom c.Request.RemoteAddr ex.: + // [DMSGHTTP] 2023/05/18 - 19:43:15 | 200 | 10.80885ms | | 02b5ee5333aa6b7f5fc623b7d5f35f505cb7f974e98a70751cf41962f84c8c4637:49153 | GET /node-info.json + fmt.Printf("[DMSGHTTP] %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 +} + +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" +) + +// Execute executes root CLI command. +func Execute() { + if err := RootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} diff --git a/examples/dmsgweb/dmsgweb.go b/examples/dmsgweb/dmsgweb.go new file mode 100644 index 00000000..58198978 --- /dev/null +++ b/examples/dmsgweb/dmsgweb.go @@ -0,0 +1,44 @@ +// Package main cmd/dmsgweb/dmsgweb.go +package main + +import ( + cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/examples/dmsgweb/commands" +) + +func init() { + var helpflag bool + commands.RootCmd.SetUsageTemplate(help) + commands.RootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for dmsgweb") + commands.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + commands.RootCmd.PersistentFlags().MarkHidden("help") //nolint +} + +func main() { + cc.Init(&cc.Config{ + RootCmd: commands.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, + //FlagsDataType: cc.HiBlue, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + commands.Execute() +} + +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" diff --git a/examples/gen-keys/README.md b/examples/gen-keys/README.md new file mode 100644 index 00000000..2c6c673e --- /dev/null +++ b/examples/gen-keys/README.md @@ -0,0 +1,11 @@ +## public / secret key pair generation + +Returns: +* Public Key +* Secret Key + +``` +$ go run gen-keys.go +03620454a0ba368051defa4baac2fd92a434449292d2dd2aff8b81041403a9d3d1 +54ce2ff7310f8ab33e01d9b0f5be7f79a62ba92d812a113b1a8a993c95f162ac +``` diff --git a/examples/gen-keys/gen-keys.go b/examples/gen-keys/gen-keys.go new file mode 100644 index 00000000..cf7175af --- /dev/null +++ b/examples/gen-keys/gen-keys.go @@ -0,0 +1,13 @@ +// example keypair generation +package main + +import ( + "fmt" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" +) + +func main() { + pk, sk := cipher.GenerateKeyPair() + fmt.Printf("%s\n%s\n", pk, sk) +} diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 00000000..7e79b8ee --- /dev/null +++ b/examples/http/README.md @@ -0,0 +1 @@ +example hello world via HTTP diff --git a/examples/http/http.go b/examples/http/http.go new file mode 100644 index 00000000..90cc98d5 --- /dev/null +++ b/examples/http/http.go @@ -0,0 +1,34 @@ +// example hello world HTTP +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "os" +) + +func main() { + // Define the HTTP handler + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received request: %s %s", r.Method, r.URL.Path) + fmt.Fprintf(w, "Hello, World!\n") + }) + + // Use the specified port from command-line arguments + address := os.Args[1] + listener, err := net.Listen("tcp", address) + if err != nil { + log.Fatal("Failed to start HTTP server:", err) + return + } + defer listener.Close() + + log.Println("HTTP server started on", address) + // Start the HTTP server + err = http.Serve(listener, nil) + if err != nil { + log.Fatal("HTTP server stopped with error:", err) + } +} diff --git a/examples/tcp-multi-proxy-dmsg/tcp-multi-proxy-dmsg.go b/examples/tcp-multi-proxy-dmsg/tcp-multi-proxy-dmsg.go new file mode 100644 index 00000000..b7a60352 --- /dev/null +++ b/examples/tcp-multi-proxy-dmsg/tcp-multi-proxy-dmsg.go @@ -0,0 +1,174 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "sync" + + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +func main() { + cc.Init(&cc.Config{ + RootCmd: srvCmd, + 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, + }) + srvCmd.Execute() +} + +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" + +var ( + localPorts []uint + dmsgPorts []uint + dmsgDisc string + dmsgSess int + sk cipher.SecKey +) + +func init() { + srvCmd.Flags().UintSliceVarP(&localPorts, "lport", "l", nil, "local application HTTP interface port(s) (comma-separated)") + srvCmd.Flags().UintSliceVarP(&dmsgPorts, "dport", "d", nil, "DMSG port(s) to serve (comma-separated)") + srvCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsg.DiscAddr(false), "DMSG discovery URL") + srvCmd.Flags().IntVarP(&dmsgSess, "dsess", "e", 1, "DMSG sessions") + srvCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") + + srvCmd.CompletionOptions.DisableDefaultCmd = true + var helpFlag bool + srvCmd.SetUsageTemplate(help) + srvCmd.PersistentFlags().BoolVarP(&helpFlag, "help", "h", false, "help for dmsgweb") + srvCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + srvCmd.PersistentFlags().MarkHidden("help") //nolint +} + +var srvCmd = &cobra.Command{ + Use: "srv", + Short: "Serve raw TCP from local ports over DMSG", + Long: `DMSG web server - serve HTTP or raw TCP interface from local ports over DMSG`, + Run: func(_ *cobra.Command, _ []string) { + server() + }, +} + +func server() { + log := logging.MustGetLogger("dmsgwebsrv") + ctx, cancel := cmdutil.SignalContext(context.Background(), log) + defer cancel() + + if len(localPorts) != len(dmsgPorts) { + log.Fatalf("The number of local ports (%d) must match the number of DMSG ports (%d).", len(localPorts), len(dmsgPorts)) + } + + pk, err := sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + log.Infof("DMSG client public key: %v", pk.String()) + + dmsgC := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig()) + defer func() { + if err := dmsgC.Close(); err != nil { + log.WithError(err).Error("Error closing DMSG client") + } + }() + go dmsgC.Serve(ctx) + + select { + case <-ctx.Done(): + log.WithError(ctx.Err()).Warn() + return + case <-dmsgC.Ready(): + log.Info("DMSG client is ready.") + } + + wg := new(sync.WaitGroup) + for i, localPort := range localPorts { + dmsgPort := dmsgPorts[i] + wg.Add(1) + + go func(localPort, dmsgPort uint) { + defer wg.Done() + proxyPort(ctx, dmsgC, localPort, dmsgPort, log) + }(localPort, dmsgPort) + } + + wg.Wait() +} + +func proxyPort(ctx context.Context, dmsgC *dmsg.Client, localPort, dmsgPort uint, log *logging.Logger) { + listener, err := dmsgC.Listen(uint16(dmsgPort)) + if err != nil { + log.Fatalf("Error listening on DMSG port %d: %v", dmsgPort, err) + } + defer listener.Close() + + log.Infof("Started proxying local port %d to DMSG port %d", localPort, dmsgPort) + + go func() { + <-ctx.Done() + listener.Close() + }() + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Error accepting connection on DMSG port %d: %v", dmsgPort, err) + return + } + + go handleTCPConnection(conn, localPort, log) + } +} + +func handleTCPConnection(dmsgConn net.Conn, localPort uint, log *logging.Logger) { + defer dmsgConn.Close() + + localConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) + if err != nil { + log.Printf("Failed to connect to local port %d: %v", localPort, err) + return + } + defer localConn.Close() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + io.Copy(dmsgConn, localConn) + }() + + go func() { + defer wg.Done() + io.Copy(localConn, dmsgConn) + }() + + wg.Wait() + log.Printf("Closed connection between local port %d and DMSG connection", localPort) +} diff --git a/examples/tcp-proxy-dmsg/tcp-proxy-dmsg.go b/examples/tcp-proxy-dmsg/tcp-proxy-dmsg.go new file mode 100644 index 00000000..e707329a --- /dev/null +++ b/examples/tcp-proxy-dmsg/tcp-proxy-dmsg.go @@ -0,0 +1,256 @@ +package main + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "sync" + + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +func main() { + cc.Init(&cc.Config{ + RootCmd: srvCmd, + 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, + }) + srvCmd.Execute() +} + +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" + +var ( + httpC http.Client + dmsgC *dmsg.Client + closeDmsg func() + dmsgDisc string + dmsgSessions int + dmsgAddr []string + dialPK []cipher.PubKey + filterDomainSuffix string + sk cipher.SecKey + pk cipher.PubKey + dmsgWebLog *logging.Logger + logLvl string + webPort []uint + proxyPort uint + addProxy string + resolveDmsgAddr []string + wg sync.WaitGroup + isEnvs bool + dmsgPort uint + dmsgPorts []uint + dmsgSess int + wl []string + wlkeys []cipher.PubKey + localPort uint + err error + rawTCP []bool + RootCmd = srvCmd +) + +func init() { + srvCmd.Flags().UintVarP(&localPort, "lport", "l", 8086, "local application http interface port(s)") + srvCmd.Flags().UintVarP(&dmsgPort, "dport", "d", 8086, "dmsg port(s) to serve") + srvCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", dmsg.DiscAddr(false), "dmsg discovery url") + srvCmd.Flags().IntVarP(&dmsgSess, "dsess", "e", 1, "dmsg sessions") + srvCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") + + srvCmd.CompletionOptions.DisableDefaultCmd = true + var helpflag bool + srvCmd.SetUsageTemplate(help) + srvCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for dmsgweb") + srvCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + srvCmd.PersistentFlags().MarkHidden("help") //nolint +} + +var srvCmd = &cobra.Command{ + Use: "srv", + Short: "serve raw TCP from local port over dmsg", + Long: `DMSG web server - serve http or raw TCP interface from local port over dmsg`, + Run: func(_ *cobra.Command, _ []string) { + server() + }, +} + +func server() { + log := logging.MustGetLogger("dmsgwebsrv") + + ctx, cancel := cmdutil.SignalContext(context.Background(), log) + + defer cancel() + pk, err = sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + log.Infof("dmsg client pk: %v", pk.String()) + + dmsgC := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig()) + defer func() { + if err := dmsgC.Close(); err != nil { + log.WithError(err).Error() + } + }() + + go dmsgC.Serve(context.Background()) + + select { + case <-ctx.Done(): + log.WithError(ctx.Err()).Warn() + return + + case <-dmsgC.Ready(): + } + + lis, err := dmsgC.Listen(uint16(dmsgPort)) + if err != nil { + log.Fatalf("Error listening on port %d: %v", dmsgPort, err) + } + + go func(l net.Listener, port uint) { + <-ctx.Done() + if err := l.Close(); err != nil { + log.Printf("Error closing listener on port %d: %v", port, err) + log.WithError(err).Error() + } + }(lis, dmsgPort) + + wg := new(sync.WaitGroup) + + wg.Add(1) + go func(localPort uint, lis net.Listener) { + defer wg.Done() + proxyTCPConnections(localPort, lis, log) + }(localPort, lis) + + wg.Wait() +} + +func proxyTCPConnections(localPort uint, lis net.Listener, log *logging.Logger) { + for { + conn, err := lis.Accept() + if err != nil { + log.Printf("Error accepting connection: %v", err) + return + } + + go handleTCPConnection(conn, localPort, log) + } +} + +func handleTCPConnection(dmsgConn net.Conn, localPort uint, log *logging.Logger) { + defer dmsgConn.Close() // Ensure the dmsg connection is closed. + + localConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) + if err != nil { + log.Printf("Failed to dial server %s: %v", fmt.Sprintf("127.0.0.1:%d", localPort), err) + return + } + defer localConn.Close() // Ensure the local connection is closed. + + var wg sync.WaitGroup + wg.Add(2) + + // Log data copied from localConn to dmsgConn + go func() { + defer wg.Done() + reader := io.TeeReader(localConn, logWriter("local -> dmsg", log)) + _, err := io.Copy(dmsgConn, reader) + if err != nil && !isClosedConnErr(err) { + log.Printf("Error copying from local to dmsg: %v", err) + } + }() + + // Log data copied from dmsgConn to localConn + go func() { + defer wg.Done() + reader := io.TeeReader(dmsgConn, logWriter("dmsg -> local", log)) + _, err := io.Copy(localConn, reader) + if err != nil && !isClosedConnErr(err) { + log.Printf("Error copying from dmsg to local: %v", err) + } + }() + + wg.Wait() + dmsgConn.Close() + localConn.Close() + log.Printf("Closed connection between DMSG and local port %d", localPort) +} + +// logWriter creates a writer that logs the copied data with a prefix. +func logWriter(direction string, log *logging.Logger) io.Writer { + return &logWriterImpl{ + prefix: direction, + log: log, + } +} + +// logWriterImpl is an implementation of io.Writer that logs data as it is written. +type logWriterImpl struct { + prefix string + log *logging.Logger +} + +func (lw *logWriterImpl) Write(p []byte) (int, error) { + lw.log.Printf("[%s] %s", lw.prefix, string(p)) // Log the data as a string. + return len(p), nil +} + +// isClosedConnErr checks if the error indicates a closed connection. +func isClosedConnErr(err error) bool { + if err == io.EOF { + return true + } + netErr, ok := err.(net.Error) + return ok && netErr.Timeout() // Check for timeout error indicating closed connection +} + +func startDmsg(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey) (dmsgC *dmsg.Client, stop func(), err error) { + dmsgC = dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, dmsgWebLog), &dmsg.Config{MinSessions: dmsgSessions}) + go dmsgC.Serve(context.Background()) + + stop = func() { + err := dmsgC.Close() + dmsgWebLog.WithError(err).Debug("Disconnected from dmsg network.") + fmt.Printf("\n") + } + dmsgWebLog.WithField("public_key", pk.String()).WithField("dmsg_disc", dmsgDisc). + Debug("Connecting to dmsg network...") + + select { + case <-ctx.Done(): + stop() + os.Exit(0) + return nil, nil, ctx.Err() + + case <-dmsgC.Ready(): + dmsgWebLog.Debug("Dmsg network ready.") + return dmsgC, stop, nil + } +} diff --git a/examples/tcp-proxy/tcp-proxy.go b/examples/tcp-proxy/tcp-proxy.go new file mode 100644 index 00000000..d126ae45 --- /dev/null +++ b/examples/tcp-proxy/tcp-proxy.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "io" + "log" + "net" + "os" + "strconv" + "sync" +) + +func main() { + if len(os.Args) < 3 { + log.Fatalf("requires two arguments; usage: tcp-proxy ") + } + sourcePort, err := strconv.Atoi(os.Args[2]) + if err != nil { + log.Fatalf("Failed to parse tcp source port string \"%v\" to int: %v", sourcePort, err) + } + targetPort, err := strconv.Atoi(os.Args[1]) + if err != nil { + log.Fatalf("Failed to parse tcp target port string \"%v\" to int: %v", targetPort, err) + } + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", sourcePort)) + if err != nil { + log.Fatalf("Failed to start TCP listener on port %d: %v", sourcePort, err) + } + defer listener.Close() + log.Printf("TCP proxy started: Listening on port %d and forwarding to port %d", sourcePort, targetPort) + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Failed to accept connection: %v", err) + continue + } + + go handleConnection(conn, targetPort) + } +} + +func handleConnection(conn net.Conn, targetPort int) { + defer conn.Close() + + targetAddr := fmt.Sprintf("localhost:%d", targetPort) + target, err := net.Dial("tcp", targetAddr) + if err != nil { + log.Printf("Failed to dial target server %s: %v", targetAddr, err) + return + } + defer target.Close() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + _, err := io.Copy(target, conn) + if err != nil && !isClosedConnErr(err) { + log.Printf("Error copying from client to target: %v", err) + } + target.Close() + wg.Done() + }() + + go func() { + _, err := io.Copy(conn, target) + if err != nil && !isClosedConnErr(err) { + log.Printf("Error copying from target to client: %v", err) + } + conn.Close() + wg.Done() + }() + + wg.Wait() +} + +func isClosedConnErr(err error) bool { + if err == io.EOF { + return true + } + netErr, ok := err.(net.Error) + return ok && netErr.Timeout() +} diff --git a/examples/tcp-reverse-proxy-dmsg/tcp-reverse-proxy-dmsg.go b/examples/tcp-reverse-proxy-dmsg/tcp-reverse-proxy-dmsg.go new file mode 100644 index 00000000..1bbd240f --- /dev/null +++ b/examples/tcp-reverse-proxy-dmsg/tcp-reverse-proxy-dmsg.go @@ -0,0 +1,249 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cmdutil" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +func main() { + 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, + }) + RootCmd.Execute() +} + +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" + +var ( + httpC http.Client + dmsgC *dmsg.Client + closeDmsg func() + dmsgDisc string + dmsgSessions int + dmsgAddr []string + dialPK cipher.PubKey + sk cipher.SecKey + pk cipher.PubKey + dmsgWebLog *logging.Logger + logLvl string + webPort uint + resolveDmsgAddr string + wg sync.WaitGroup + dmsgPort uint + dmsgSess int + err error +) + +func init() { + RootCmd.Flags().UintVarP(&webPort, "port", "p", 8080, "port to serve the web application") + RootCmd.Flags().StringVarP(&resolveDmsgAddr, "resolve", "t", "", "resolve the specified dmsg address:port on the local port & disable proxy") + RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "d", dmsg.DiscAddr(false), "dmsg discovery url") + RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", 1, "number of dmsg servers to connect to") + RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m") + RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r") +} + +// RootCmd contains the root command for dmsgweb +var RootCmd = &cobra.Command{ + Use: func() string { + return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] + }(), + Short: "DMSG reverse tcp proxy", + Long: "DMSG reverse tcp proxy", + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, _ []string) { + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) //nolint + go func() { + <-c + os.Exit(1) + }() + if dmsgWebLog == nil { + dmsgWebLog = logging.MustGetLogger("dmsgweb") + } + if logLvl != "" { + if lvl, err := logging.LevelFromString(logLvl); err == nil { + logging.SetLevel(lvl) + } + } + + ctx, cancel := cmdutil.SignalContext(context.Background(), dmsgWebLog) + defer cancel() + + pk, err := sk.PubKey() + if err != nil { + pk, sk = cipher.GenerateKeyPair() + } + dmsgWebLog.Info("dmsg client pk: ", pk.String()) + + dmsgWebLog.Info("dmsg address to dial: ", resolveDmsgAddr) + dmsgAddr = strings.Split(resolveDmsgAddr, ":") + var setpk cipher.PubKey + err = setpk.Set(dmsgAddr[0]) + if err != nil { + log.Fatalf("failed to parse dmsg
: : %v", err) + } + dialPK = setpk + if len(dmsgAddr) > 1 { + dport, err := strconv.ParseUint(dmsgAddr[1], 10, 64) + if err != nil { + log.Fatalf("Failed to parse dmsg port: %v", err) + } + dmsgPort = uint(dport) + } else { + dmsgPort = uint(80) + } + + dmsgC, closeDmsg, err = startDmsg(ctx, pk, sk) + if err != nil { + dmsgWebLog.WithError(err).Fatal("failed to start dmsg") + } + defer closeDmsg() + + go func() { + <-ctx.Done() + cancel() + closeDmsg() + os.Exit(0) + }() + + proxyTCPConn() + wg.Wait() + }, +} + +func proxyTCPConn() { + listener, err := net.Listen("tcp", fmt.Sprintf(":%v", webPort)) + if err != nil { + dmsgWebLog.Fatalf("Failed to start TCP listener on port %v: %v", webPort, err) + } + defer listener.Close() //nolint + log.Printf("Serving TCP on 127.0.0.1:%v", webPort) + if dmsgC == nil { + log.Fatal("dmsgC is nil") + } + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Failed to accept connection: %v", err) + continue + } + + wg.Add(1) + go func(conn net.Conn) { + defer wg.Done() + + log.Println(fmt.Sprintf("Dialing dmsg address: %v ; port: %v", dialPK.String(), dmsgPort)) + dmsgConn, err := dmsgC.DialStream(context.Background(), dmsg.Addr{PK: dialPK, Port: uint16(dmsgPort)}) + if err != nil { + log.Printf("Failed to dial dmsg address %v:%v %v", dialPK.String(), dmsgPort, err) + return + } + // Log data copied to the dmsg connection (from the client connection) + go func() { + defer dmsgConn.Close() + reader := io.TeeReader(conn, logWriter("client -> dmsg", dmsgWebLog)) + _, err := io.Copy(dmsgConn, reader) + if err != nil { + log.Printf("Error copying data to dmsg client: %v", err) + } + }() + + // Log data copied from the dmsg connection (to the client connection) + go func() { + defer conn.Close() //nolint + reader := io.TeeReader(dmsgConn, logWriter("dmsg -> client", dmsgWebLog)) + _, err := io.Copy(conn, reader) + if err != nil { + log.Printf("Error copying data from dmsg client: %v", err) + } + }() + }(conn) + wg.Wait() + } +} + +// logWriter creates a writer that logs the copied data with a prefix. +func logWriter(direction string, log *logging.Logger) io.Writer { + return &logWriterImpl{ + prefix: direction, + log: log, + } +} + +// logWriterImpl is an implementation of io.Writer that logs data as it is written. +type logWriterImpl struct { + prefix string + log *logging.Logger +} + +func (lw *logWriterImpl) Write(p []byte) (int, error) { + lw.log.Printf("[%s] %s", lw.prefix, string(p)) // Log the data as a string. + return len(p), nil +} + +func startDmsg(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey) (dmsgC *dmsg.Client, stop func(), err error) { + dmsgC = dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, dmsgWebLog), &dmsg.Config{MinSessions: dmsgSessions}) + go dmsgC.Serve(context.Background()) + + stop = func() { + err := dmsgC.Close() + dmsgWebLog.WithError(err).Debug("Disconnected from dmsg network.") + fmt.Printf("\n") + } + dmsgWebLog.WithField("public_key", pk.String()).WithField("dmsg_disc", dmsgDisc). + Debug("Connecting to dmsg network...") + + select { + case <-ctx.Done(): + stop() + os.Exit(0) + return nil, nil, ctx.Err() + + case <-dmsgC.Ready(): + dmsgWebLog.Debug("Dmsg network ready.") + return dmsgC, stop, nil + } +} diff --git a/examples/tcp/README.md b/examples/tcp/README.md new file mode 100644 index 00000000..e7f52381 --- /dev/null +++ b/examples/tcp/README.md @@ -0,0 +1 @@ +example hello world via TCP diff --git a/examples/tcp/tcp.go b/examples/tcp/tcp.go new file mode 100644 index 00000000..c9569f1c --- /dev/null +++ b/examples/tcp/tcp.go @@ -0,0 +1,41 @@ +// example hello world TCP +package main + +import ( + "log" + "net" + "os" +) + +func main() { + // Start a TCP server listening on port 8000 + listener, err := net.Listen("tcp", os.Args[1]) //":8000") + if err != nil { + log.Fatal("Failed to start server:", err) + return + } + defer listener.Close() + log.Println("TCP server started on port", os.Args[1]) + + // Accept and handle incoming connections + for { + conn, err := listener.Accept() + if err != nil { + log.Println("Failed to accept connection:", err) + continue + } + go handleConnection(conn) + } +} + +func handleConnection(conn net.Conn) { + defer conn.Close() + log.Println("Handling Connection") + // Send a greeting message to the client + message := "Hello, World!\n" + _, err := conn.Write([]byte(message)) + if err != nil { + log.Println("Error writing response:", err) + return + } +}