diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index 4624c5f1..fc79bb48 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -8,6 +8,8 @@ import ( "log" "net" "net/http" + "net/http/httputil" + "net/url" "os" "os/signal" "path/filepath" @@ -47,7 +49,7 @@ func (r *customResolver) Resolve(ctx context.Context, name string) (context.Cont return ctx, nil, fmt.Errorf("failed to parse IP address") } // Modify the context to include the desired port - ctx = context.WithValue(ctx, "port", strconv.Itoa(webPort)) //nolint + ctx = context.WithValue(ctx, "port", fmt.Sprintf("%v", webPort)) //nolint return ctx, ip, nil } // Use default name resolution for other domains @@ -60,9 +62,10 @@ var ( dmsgSessions int filterDomainSuffix string sk cipher.SecKey + pk cipher.PubKey dmsgWebLog *logging.Logger logLvl string - webPort int + webPort uint proxyPort uint addProxy string resolveDmsgAddr string @@ -70,25 +73,25 @@ var ( isEnvs bool ) -const envname = "DMSGWEB" +const dmsgwebenvname = "DMSGWEB" -var envfile = os.Getenv(envname) +var dmsgwebconffile = os.Getenv(dmsgwebenvname) func init() { RootCmd.Flags().StringVarP(&filterDomainSuffix, "filter", "f", ".dmsg", "domain suffix to filter") - RootCmd.Flags().UintVarP(&proxyPort, "socks", "q", scriptExecUint("${PROXYPORT:-4445}"), "port to serve the socks5 proxy") - RootCmd.Flags().StringVarP(&addProxy, "proxy", "r", scriptExecString("${ADDPROXY}"), "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)") - RootCmd.Flags().IntVarP(&webPort, "port", "p", scriptExecInt("${WEBPORT:-8080}"), "port to serve the web application") - RootCmd.Flags().StringVarP(&resolveDmsgAddr, "resolve", "t", scriptExecString("${RESOLVEPK}"), "resolve the specified dmsg address:port on the local port & disable proxy") + RootCmd.Flags().UintVarP(&proxyPort, "socks", "q", scriptExecUint("${PROXYPORT:-4445}", dmsgwebconffile), "port to serve the socks5 proxy") + RootCmd.Flags().StringVarP(&addProxy, "proxy", "r", scriptExecString("${ADDPROXY}", dmsgwebconffile), "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)") + RootCmd.Flags().UintVarP(&webPort, "port", "p", scriptExecUint("${WEBPORT:-8080}", dmsgwebconffile), "port to serve the web application") + RootCmd.Flags().StringVarP(&resolveDmsgAddr, "resolve", "t", scriptExecString("${RESOLVEPK}", dmsgwebconffile), "resolve the specified dmsg address:port on the local port & disable proxy") RootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "d", skyenv.DmsgDiscAddr, "dmsg discovery url") - RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", scriptExecInt("${DMSGSESSIONS:-1}"), "number of dmsg servers to connect to") + RootCmd.Flags().IntVarP(&dmsgSessions, "sess", "e", scriptExecInt("${DMSGSESSIONS:-1}", dmsgwebconffile), "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("DMSGWEB_SK") != "" { sk.Set(os.Getenv("DMSGWEB_SK")) //nolint } - if scriptExecString("${DMSGWEB_SK}") != "" { - sk.Set(scriptExecString("${DMSGWEB_SK}")) //nolint + if scriptExecString("${DMSGWEB_SK}", dmsgwebconffile) != "" { + sk.Set(scriptExecString("${DMSGWEB_SK}", dmsgwebconffile)) //nolint } 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") @@ -105,14 +108,14 @@ var RootCmd = &cobra.Command{ ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ │││││└─┐│ ┬│││├┤ ├┴┐ ─┴┘┴ ┴└─┘└─┘└┴┘└─┘└─┘ -DMSG resolving proxy & browser client - access websites over dmsg` + func() string { - if _, err := os.Stat(envfile); err == nil { +DMSG resolving proxy & browser client - access websites and http interfaces over dmsg` + func() string { + if _, err := os.Stat(dmsgwebconffile); err == nil { return ` -dmsgweb env file detected: ` + envfile +dmsgweb conf file detected: ` + dmsgwebconffile } return ` .conf file may also be specified with -` + envname + `=/path/to/dmsgweb.conf skywire dmsg web` +` + dmsgwebenvname + `=/path/to/dmsgweb.conf skywire dmsg web` }(), SilenceErrors: true, SilenceUsage: true, @@ -121,10 +124,16 @@ dmsgweb env file detected: ` + envfile Version: buildinfo.Version(), Run: func(cmd *cobra.Command, _ []string) { if isEnvs { + envfile := envfileLinux if runtime.GOOS == "windows" { - envfile = envfileWindows - } else { - envfile = envfileLinux + 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) @@ -187,7 +196,7 @@ dmsgweb env file detected: ` + envfile if match { port, ok := ctx.Value("port").(string) if !ok { - port = strconv.Itoa(webPort) + port = fmt.Sprintf("%v", webPort) } addr = "localhost:" + port } else { @@ -235,8 +244,10 @@ dmsgweb env file detected: ` + envfile var urlStr string if resolveDmsgAddr != "" { urlStr = fmt.Sprintf("dmsg://%s%s", resolveDmsgAddr, c.Param("path")) + if c.Request.URL.RawQuery != "" { + urlStr = fmt.Sprintf("%s?%s", urlStr, c.Request.URL.RawQuery) + } } else { - hostParts := strings.Split(c.Request.Host, ":") var dmsgp string if len(hostParts) > 1 { @@ -245,25 +256,48 @@ dmsgweb env file detected: ` + envfile dmsgp = "80" } urlStr = fmt.Sprintf("dmsg://%s:%s%s", strings.TrimRight(hostParts[0], filterDomainSuffix), dmsgp, c.Param("path")) + if c.Request.URL.RawQuery != "" { + urlStr = fmt.Sprintf("%s?%s", urlStr, c.Request.URL.RawQuery) + } } - req, err := http.NewRequest(http.MethodGet, urlStr, nil) + + fmt.Printf("Proxying request: %s %s\n", 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") return } + + for header, values := range c.Request.Header { + for _, value := range values { + req.Header.Add(header, value) + } + } + resp, err := httpC.Do(req) if err != nil { c.String(http.StatusInternalServerError, "Failed to connect to HTTP server") + fmt.Printf("Error: %v\n", err) return } defer resp.Body.Close() //nolint - c.Status(http.StatusOK) - io.Copy(c.Writer, resp.Body) //nolint + + for header, values := range resp.Header { + for _, value := range values { + c.Writer.Header().Add(header, value) + } + } + + 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) + } }) wg.Add(1) go func() { dmsgWebLog.Debug(fmt.Sprintf("Serving http on: http://127.0.0.1:%v", webPort)) - r.Run(":" + strconv.Itoa(webPort)) //nolint + r.Run(":" + fmt.Sprintf("%v", webPort)) //nolint dmsgWebLog.Debug(fmt.Sprintf("Stopped serving http on: http://127.0.0.1:%v", webPort)) wg.Done() }() @@ -271,6 +305,216 @@ dmsgweb env file detected: ` + envfile }, } +var ( + dmsgPort uint + dmsgSess int + wl string + wlkeys []cipher.PubKey + localPort uint + websrvPort uint + err error +) + +const dmsgwebsrvenvname = "DMSGWEBSRV" + +var dmsgwebsrvconffile = os.Getenv(dmsgwebsrvenvname) + +func init() { + RootCmd.AddCommand(srvCmd) + srvCmd.Flags().UintVarP(&localPort, "lport", "l", scriptExecUint("${LOCALPORT:-8086}", dmsgwebsrvconffile), "local application http interface port") + srvCmd.Flags().UintVarP(&websrvPort, "port", "p", scriptExecUint("${WEBPORT:-8081}", dmsgwebsrvconffile), "port to serve") + srvCmd.Flags().UintVarP(&dmsgPort, "dport", "d", scriptExecUint("${DMSGPORT:-80}", dmsgwebsrvconffile), "dmsg port to serve") + srvCmd.Flags().StringVarP(&wl, "wl", "w", scriptExecArray("${WHITELISTPKS[@]}", dmsgwebsrvconffile), "whitelisted keys for dmsg authenticated routes\r") + srvCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", skyenv.DmsgDiscAddr, "dmsg discovery url") + srvCmd.Flags().IntVarP(&dmsgSess, "dsess", "e", scriptExecInt("${DMSGSESSIONS:-1}", dmsgwebsrvconffile), "dmsg sessions") + if os.Getenv("DMSGWEBSRV_SK") != "" { + sk.Set(os.Getenv("DMSGWEBSRV_SK")) //nolint + } + if scriptExecString("${DMSGWEBSRV_SK}", dmsgwebsrvconffile) != "" { + sk.Set(scriptExecString("${DMSGWEBSRV_SK}", dmsgwebsrvconffile)) //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") + + srvCmd.CompletionOptions.DisableDefaultCmd = true +} + +var srvCmd = &cobra.Command{ + Use: "srv", + Short: "serve http from local port over dmsg", + Long: `DMSG web server - serve http interface from local port over dmsg` + func() string { + if _, err := os.Stat(dmsgwebsrvconffile); err == nil { + return ` + dmsenv file detected: ` + dmsgwebsrvconffile + } + return ` + .conf file may also be specified with + ` + dmsgwebsrvenvname + `=/path/to/dmsgwebsrv.conf skywire dmsg web srv` + }(), + Run: 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", "") + } + } + envfile = strings.Join(envfileslice, "\n") + } + fmt.Println(envfile) + os.Exit(0) + } + 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() + } + if wl != "" { + wlk := strings.Split(wl, ",") + for _, key := range wlk { + 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))) + } + } + + 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.WithError(err).Fatal() + } + go func() { + <-ctx.Done() + if err := lis.Close(); err != nil { + log.WithError(err).Error() + } + }() + + r1 := gin.New() + r1.Use(gin.Recovery()) + r1.Use(loggingMiddleware()) + + authRoute := r1.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{}, + } + proxy.ServeHTTP(c.Writer, c.Request) + }) + + serve := &http.Server{ + Handler: &ginHandler{Router: r1}, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + log.WithField("dmsg_addr", lis.Addr().String()).Info("Serving... ") + if err := serve.Serve(lis); err != nil && err != http.ErrServerClosed { + log.Fatalf("Serve1: %v", err) + } + wg.Done() + }() + + wg.Add(1) + go func() { + fmt.Printf("listening on http://127.0.0.1:%d using gin router\n", websrvPort) + r1.Run(fmt.Sprintf(":%d", websrvPort)) //nolint + wg.Done() + }() + + wg.Wait() +} + +func whitelistAuth(whitelistedPKs []cipher.PubKey) gin.HandlerFunc { + return func(c *gin.Context) { + remotePK, _, err := net.SplitHostPort(c.Request.RemoteAddr) + if err != nil { + c.Writer.WriteHeader(http.StatusInternalServerError) + c.Writer.Write([]byte("500 Internal Server Error")) //nolint + c.AbortWithStatus(http.StatusInternalServerError) + return + } + 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 { + c.Writer.WriteHeader(http.StatusUnauthorized) + c.Writer.Write([]byte("401 Unauthorized")) //nolint + 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 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()) @@ -383,7 +627,7 @@ func Execute() { } } -func scriptExecString(s string) string { +func scriptExecString(s, envfile string) string { if runtime.GOOS == "windows" { var variable, defaultvalue string if strings.Contains(s, ":-") { @@ -410,7 +654,29 @@ func scriptExecString(s string) string { return "" } -func scriptExecInt(s string) int { +func scriptExecArray(s, envfile string) string { + if runtime.GOOS == "windows" { + variable := s + if strings.Contains(variable, "[@]}") { + variable = strings.TrimRight(variable, "[@]}") + variable = strings.TrimRight(variable, "{") + } + out, err := script.Exec(fmt.Sprintf(`powershell -c '$SKYENV = "%s"; if ($SKYENV -ne "" -and (Test-Path $SKYENV)) { . $SKYENV }; foreach ($item in %s) { Write-Host $item }'`, envfile, variable)).Slice() + if err == nil { + if len(out) != 0 { + return "" + } + return strings.Join(out, ",") + } + } + y, err := script.Exec(fmt.Sprintf(`bash -c 'SKYENV=%s ; if [[ $SKYENV != "" ]] && [[ -f $SKYENV ]] ; then source $SKYENV ; fi ; for _i in %s ; do echo "$_i" ; done'`, envfile, s)).Slice() + if err == nil { + return strings.Join(y, ",") + } + return "" +} + +func scriptExecInt(s, envfile string) int { if runtime.GOOS == "windows" { var variable string if strings.Contains(s, ":-") { @@ -444,7 +710,7 @@ func scriptExecInt(s string) int { } return 0 } -func scriptExecUint(s string) uint { +func scriptExecUint(s, envfile string) uint { if runtime.GOOS == "windows" { var variable string if strings.Contains(s, ":-") { @@ -481,9 +747,9 @@ func scriptExecUint(s string) uint { const envfileLinux = ` ######################################################################### -# DMSGWEB CONFIG TEMPLATE -# Defaults shown -# Uncomment to change default value +#-- DMSGWEB CONFIG TEMPLATE +#-- Defaults shown +#-- Uncomment to change default value ######################################################################### #-- Set port for proxy interface @@ -501,31 +767,34 @@ const envfileLinux = ` #-- Number of dmsg servers to connect to (0 unlimits) #DMSGSESSIONS=1 +#-- Dmsg port to use +#DMSGPORT=80 + #-- Set secret key #DMSGWEB_SK='' ` -const envfileWindows = ` +const srvenvfileLinux = ` ######################################################################### -# DMSGWEB CONFIG TEMPLATE -# Defaults shown -# Uncomment to change default value +#-- DMSGWEB SRV CONFIG TEMPLATE +#-- Defaults shown +#-- Uncomment to change default value ######################################################################### -#-- Set port for proxy interface -#$PROXYPORT=4445 +#-- DMSG port to serve +#DMSGPORT=80 -#-- Configure additional proxy for dmsgvlc to use -#$ADDPROXY='127.0.0.1:1080' - -#-- Web Interface Port -#$WEBPORT=8080 +#-- Port for this application to serve http +#WEBPORT=8081 -#-- Resove a specific PK to the web port (also disables proxy) -#$RESOLVEPK='' +#-- Local Port to serve over dmsg +LOCALPORT=8086 #-- Number of dmsg servers to connect to (0 unlimits) -#$DMSGSESSIONS=1 +#DMSGSESSIONS=1 #-- Set secret key -#$DMSGWEB_SK='' +#DMSGWEBSRV_SK='' + +#-- Whitelisted keys to access the web interface +#WHITELISTPKS=('') ` diff --git a/examples/proxified/main.go b/examples/proxified/main.go index 710eb1ed..56f0975c 100644 --- a/examples/proxified/main.go +++ b/examples/proxified/main.go @@ -2,14 +2,12 @@ package main import ( "context" - "net/http" "time" + "github.com/skycoin/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire-utilities/pkg/logging" "github.com/skycoin/skywire-utilities/pkg/skyenv" - - "github.com/skycoin/skywire-utilities/pkg/cipher" "golang.org/x/net/proxy" "github.com/skycoin/dmsg/pkg/disc"