github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/server.go (about)

     1  // Package web Cozy Stack API.
     2  //
     3  // Cozy is a personal platform as a service with a focus on data.
     4  package web
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"net"
    13  	"net/http"
    14  	"os"
    15  	"path"
    16  	"time"
    17  
    18  	"github.com/cozy/cozy-stack/model/app"
    19  	"github.com/cozy/cozy-stack/model/stack"
    20  	"github.com/cozy/cozy-stack/pkg/assets"
    21  	build "github.com/cozy/cozy-stack/pkg/config"
    22  	"github.com/cozy/cozy-stack/pkg/config/config"
    23  	"github.com/cozy/cozy-stack/pkg/consts"
    24  	"github.com/cozy/cozy-stack/pkg/i18n"
    25  	"github.com/cozy/cozy-stack/pkg/logger"
    26  	"github.com/cozy/cozy-stack/pkg/utils"
    27  	"github.com/cozy/cozy-stack/web/apps"
    28  	"github.com/sirupsen/logrus"
    29  
    30  	"github.com/labstack/echo/v4"
    31  	"github.com/labstack/echo/v4/middleware"
    32  )
    33  
    34  // ReadHeaderTimeout is the amount of time allowed to read request headers for
    35  // all servers. This is activated for all handlers from all http servers
    36  // created by the stack.
    37  var ReadHeaderTimeout = 15 * time.Second
    38  
    39  var (
    40  	ErrMissingArgument = errors.New("the argument is missing")
    41  )
    42  
    43  // LoadSupportedLocales reads the po files packed in go or from the assets directory
    44  // and loads them for translations
    45  func LoadSupportedLocales() error {
    46  	// By default, use the po files packed in the binary
    47  	// but use assets from the disk is assets option is filled in config
    48  	assetsPath := config.GetConfig().Assets
    49  	if assetsPath != "" {
    50  		for _, locale := range consts.SupportedLocales {
    51  			pofile := path.Join(assetsPath, "locales", locale+".po")
    52  			po, err := os.ReadFile(pofile)
    53  			if err != nil {
    54  				return fmt.Errorf("Can't load the po file for %s", locale)
    55  			}
    56  			i18n.LoadLocale(locale, "", po)
    57  		}
    58  		return nil
    59  	}
    60  
    61  	for _, locale := range consts.SupportedLocales {
    62  		f, err := assets.Open("/locales/"+locale+".po", config.DefaultInstanceContext)
    63  		if err != nil {
    64  			return fmt.Errorf("Can't load the po file for %s", locale)
    65  		}
    66  		po, err := io.ReadAll(f)
    67  		if err != nil {
    68  			return err
    69  		}
    70  		i18n.LoadLocale(locale, "", po)
    71  	}
    72  	return nil
    73  }
    74  
    75  // ListenAndServeWithAppDir creates and setup all the necessary http endpoints
    76  // and serve the specified application on a app subdomain.
    77  //
    78  // In order to serve the application, the specified directory should provide
    79  // a manifest.webapp file that will be used to parameterize the application
    80  // permissions.
    81  func ListenAndServeWithAppDir(appsdir map[string]string, services *stack.Services) (*Servers, error) {
    82  	for slug, dir := range appsdir {
    83  		dir = utils.AbsPath(dir)
    84  		appsdir[slug] = dir
    85  		exists, err := utils.DirExists(dir)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		if !exists {
    90  			logger.WithNamespace("dev").Warnf("Directory %s does not exist", dir)
    91  		} else {
    92  			if err = checkExists(path.Join(dir, app.WebappManifestName)); err != nil {
    93  				logger.WithNamespace("dev").Warnf("The app manifest is missing: %s", err)
    94  			}
    95  			if err = checkExists(path.Join(dir, "index.html")); err != nil {
    96  				logger.WithNamespace("dev").Warnf("The index.html is missing: %s", err)
    97  			}
    98  		}
    99  	}
   100  
   101  	app.SetupAppsDir(appsdir)
   102  	return ListenAndServe(services)
   103  }
   104  
   105  func checkExists(filepath string) error {
   106  	exists, err := utils.FileExists(filepath)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	if !exists {
   111  		return fmt.Errorf("Directory %s should contain a %s file",
   112  			path.Dir(filepath), path.Base(filepath))
   113  	}
   114  	return nil
   115  }
   116  
   117  // ListenAndServe creates and setups all the necessary http endpoints and start
   118  // them.
   119  func ListenAndServe(services *stack.Services) (*Servers, error) {
   120  	e := echo.New()
   121  	e.HideBanner = true
   122  	e.HidePort = true
   123  
   124  	major, err := CreateSubdomainProxy(e, services, apps.Serve)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	if err = LoadSupportedLocales(); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if build.IsDevRelease() {
   133  		timeFormat := "time_rfc3339"
   134  		if logrus.GetLevel() == logrus.DebugLevel {
   135  			timeFormat = "time_rfc3339_nano"
   136  		}
   137  		major.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
   138  			Format: "time=${" + timeFormat + "}\tstatus=${status}\tmethod=${method}\thost=${host}\turi=${uri}\tbytes_out=${bytes_out}\n",
   139  		}))
   140  	}
   141  
   142  	admin := echo.New()
   143  	admin.HideBanner = true
   144  	admin.HidePort = true
   145  
   146  	if err = SetupAdminRoutes(admin); err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	servers := NewServers()
   151  	err = servers.Start(major, "major", config.ServerAddr())
   152  	if err != nil {
   153  		return nil, fmt.Errorf("failed to start major server: %w", err)
   154  	}
   155  
   156  	err = servers.Start(admin, "admin", config.AdminServerAddr())
   157  	if err != nil {
   158  		return nil, fmt.Errorf("failed to start admin server: %w", err)
   159  	}
   160  
   161  	return servers, nil
   162  }
   163  
   164  // Servers allow to start several [echo.Echo] servers and stop them together.
   165  //
   166  //	It also take care of several other task:
   167  //	- It sanitize the hosts format
   168  //	- It exposes the handlers on several addresses if needed
   169  //	- It forces the IPv4/IPv6 dual stack mode for `localhost` by
   170  //	  remplacing this entry by `["127.0.0.1", "::1]`
   171  type Servers struct {
   172  	serversByName   map[string]*http.Server
   173  	listenersByName map[string]net.Listener
   174  	errs            chan error
   175  }
   176  
   177  func NewServers() *Servers {
   178  	return &Servers{
   179  		serversByName:   map[string]*http.Server{},
   180  		listenersByName: map[string]net.Listener{},
   181  		errs:            make(chan error),
   182  	}
   183  }
   184  
   185  // Start the server 'e' to the given addrs.
   186  //
   187  // The 'addrs' arguments must be in the format `"host:port"`. If the host
   188  // is not a valid IPv4/IPv6/hostname or if the port not present an error is
   189  // returned.
   190  func (s *Servers) Start(handler http.Handler, name string, addr string) error {
   191  	addrs := []string{}
   192  
   193  	if len(addr) == 0 {
   194  		return fmt.Errorf("args: %w", ErrMissingArgument)
   195  	}
   196  
   197  	if len(name) == 0 {
   198  		return fmt.Errorf("name: %w", ErrMissingArgument)
   199  	}
   200  
   201  	host, port, err := net.SplitHostPort(addr)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	fmt.Fprintf(os.Stdout, "http server %s started on %q\n", name, addr)
   207  	switch host {
   208  	case "localhost":
   209  		addrs = append(addrs, net.JoinHostPort("127.0.0.1", port))
   210  		addrs = append(addrs, net.JoinHostPort("::1", port))
   211  	default:
   212  		addrs = append(addrs, net.JoinHostPort(host, port))
   213  	}
   214  
   215  	for _, addr := range addrs {
   216  		l, err := net.Listen("tcp", addr)
   217  		if err != nil {
   218  			return err
   219  		}
   220  
   221  		writer := logger.WithNamespace("stack").Writer()
   222  		logger := log.New(writer, "", 0)
   223  		server := &http.Server{
   224  			Addr:              addr,
   225  			Handler:           handler,
   226  			ReadHeaderTimeout: ReadHeaderTimeout,
   227  			ErrorLog:          logger,
   228  		}
   229  
   230  		s.serversByName[name] = server
   231  		s.listenersByName[name] = l
   232  
   233  		go func(server *http.Server) {
   234  			s.errs <- server.Serve(l)
   235  		}(server)
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // GetAddr return the address where the given server listen to.
   242  //
   243  // This endpoint is mostly used when we use dynamic port attribution
   244  // like when we don't specify a port
   245  func (s *Servers) GetAddr(name string) net.Addr {
   246  	l, ok := s.listenersByName[name]
   247  	if !ok {
   248  		return nil
   249  	}
   250  
   251  	return l.Addr()
   252  }
   253  
   254  // Wait for servers to stop or fall in error.
   255  func (s *Servers) Wait() <-chan error {
   256  	return s.errs
   257  }
   258  
   259  // Shutdown gracefully stops the servers.
   260  func (s *Servers) Shutdown(ctx context.Context) error {
   261  	shutdowners := []utils.Shutdowner{}
   262  
   263  	for _, server := range s.serversByName {
   264  		shutdowners = append(shutdowners, server)
   265  	}
   266  
   267  	g := utils.NewGroupShutdown(shutdowners...)
   268  
   269  	fmt.Print("  shutting down servers...")
   270  	if err := g.Shutdown(ctx); err != nil {
   271  		fmt.Println("failed: ", err.Error())
   272  		return err
   273  	}
   274  
   275  	fmt.Println("ok.")
   276  
   277  	return nil
   278  }