github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/ui/server.go (about)

     1  /*
     2   * Copyright (C) 2019 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package ui
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"net/http"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/mysteriumnetwork/node/ui/versionmanager"
    28  
    29  	"github.com/gin-contrib/cors"
    30  	"github.com/gin-gonic/gin"
    31  	godvpnweb "github.com/mysteriumnetwork/go-dvpn-web/v2"
    32  	"github.com/mysteriumnetwork/node/requests"
    33  	"github.com/mysteriumnetwork/node/ui/discovery"
    34  	"github.com/rs/zerolog/log"
    35  )
    36  
    37  // Server represents our web UI server
    38  type Server struct {
    39  	servers         []*http.Server
    40  	discovery       discovery.LANDiscovery
    41  	reverseProxy    gin.HandlerFunc
    42  	uiVersionConfig versionmanager.NodeUIVersionConfig
    43  }
    44  
    45  type jwtAuthenticator interface {
    46  	ValidateToken(token string) (bool, error)
    47  }
    48  
    49  var corsConfig = cors.Config{
    50  	AllowMethods: []string{
    51  		"GET",
    52  		"HEAD",
    53  		"POST",
    54  		"PUT",
    55  		"DELETE",
    56  		"CONNECT",
    57  		"OPTIONS",
    58  		"TRACE",
    59  		"PATCH",
    60  	},
    61  	AllowHeaders: []string{
    62  		"Origin",
    63  		"Content-Length",
    64  		"Content-Type",
    65  		"Cache-Control",
    66  		"X-XSRF-TOKEN",
    67  		"X-CSRF-TOKEN",
    68  	},
    69  	AllowCredentials: true,
    70  	AllowOriginFunc: func(origin string) bool {
    71  		return true
    72  	},
    73  }
    74  
    75  // NewServer creates a new instance of the server for the given port
    76  // you can chain addresses with ',' i.e. "192.168.0.1,127.0.0.1"
    77  func NewServer(
    78  	bindAddress string,
    79  	port int,
    80  	tequilapiAddress string,
    81  	tequilapiPort int,
    82  	authenticator jwtAuthenticator,
    83  	httpClient *requests.HTTPClient,
    84  	uiVersionConfig versionmanager.NodeUIVersionConfig,
    85  ) *Server {
    86  	gin.SetMode(gin.ReleaseMode)
    87  	reverseProxy := ReverseTequilapiProxy(tequilapiAddress, tequilapiPort, authenticator)
    88  
    89  	var r *gin.Engine
    90  	version, err := uiVersionConfig.Version()
    91  
    92  	var assets http.FileSystem = godvpnweb.Assets
    93  	if err != nil || version == versionmanager.BundledVersionName {
    94  		log.Warn().Err(err).Msg("could not read node ui version config, falling back to bundled version")
    95  	} else {
    96  		assets = http.Dir(uiVersionConfig.UIBuildPath(version))
    97  	}
    98  
    99  	r = ginEngine(reverseProxy, assets)
   100  
   101  	addrs := strings.Split(bindAddress, ",")
   102  
   103  	var srvs []*http.Server
   104  	for _, addr := range addrs {
   105  		s := &http.Server{
   106  			Addr:    fmt.Sprintf("%v:%v", addr, port),
   107  			Handler: r,
   108  		}
   109  		srvs = append(srvs, s)
   110  	}
   111  
   112  	return &Server{
   113  		servers:         srvs,
   114  		discovery:       discovery.NewLANDiscoveryService(port, httpClient),
   115  		reverseProxy:    reverseProxy,
   116  		uiVersionConfig: uiVersionConfig,
   117  	}
   118  }
   119  
   120  func ginEngine(reverseProxy gin.HandlerFunc, dir http.FileSystem) *gin.Engine {
   121  	gin.SetMode(gin.ReleaseMode)
   122  	r := gin.New()
   123  	r.Use(gin.Recovery())
   124  	r.NoRoute(reverseProxy)
   125  	r.Use(cors.New(corsConfig))
   126  
   127  	r.StaticFS("/", dir)
   128  
   129  	return r
   130  }
   131  
   132  // SwitchUI switch nodeUI version
   133  func (s *Server) SwitchUI(path string) {
   134  	var assets http.FileSystem = http.Dir(path)
   135  	if path == versionmanager.BundledVersionName {
   136  		assets = godvpnweb.Assets
   137  	}
   138  	for i := range s.servers {
   139  		s.servers[i].Handler = ginEngine(s.reverseProxy, assets)
   140  	}
   141  }
   142  
   143  // Serve starts servers
   144  func (s *Server) Serve() {
   145  	go func() {
   146  		err := s.discovery.Start()
   147  		if err != nil {
   148  			log.Error().Err(err).Msg("Failed to start local discovery service")
   149  		}
   150  	}()
   151  
   152  	for _, srv := range s.servers {
   153  		go startListen(srv)
   154  	}
   155  }
   156  
   157  func startListen(s *http.Server) {
   158  	log.Info().Msgf("UI starting on: %s", s.Addr)
   159  	err := s.ListenAndServe()
   160  	if err != http.ErrServerClosed {
   161  		log.Err(err).Msg("UI server crashed")
   162  	}
   163  }
   164  
   165  // Stop stops servers
   166  func (s *Server) Stop() {
   167  	err := s.discovery.Stop()
   168  	if err != nil {
   169  		log.Error().Err(err).Msg("Failed to stop local discovery service")
   170  	}
   171  
   172  	// give the server a few seconds to shut down properly in case a request is waiting somewhere
   173  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   174  	defer cancel()
   175  	for _, srv := range s.servers {
   176  		err = srv.Shutdown(ctx)
   177  		log.Info().Err(err).Msg("Server stopped")
   178  	}
   179  }