web/server.go
Rama master · réplica de lectura del repositorio original.
// Package web Cozy Stack API.
//
// Cozy is a personal platform as a service with a focus on data.
package web
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"path"
"time"
"github.com/cozy/cozy-stack/model/app"
"github.com/cozy/cozy-stack/model/stack"
"github.com/cozy/cozy-stack/pkg/assets"
build "github.com/cozy/cozy-stack/pkg/config"
"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/i18n"
"github.com/cozy/cozy-stack/pkg/logger"
"github.com/cozy/cozy-stack/pkg/utils"
"github.com/cozy/cozy-stack/web/apps"
"github.com/sirupsen/logrus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// ReadHeaderTimeout is the amount of time allowed to read request headers for
// all servers. This is activated for all handlers from all http servers
// created by the stack.
var ReadHeaderTimeout = 15 * time.Second
var (
ErrMissingArgument = errors.New("the argument is missing")
)
// LoadSupportedLocales reads the po files packed in go or from the assets directory
// and loads them for translations
func LoadSupportedLocales() error {
// By default, use the po files packed in the binary
// but use assets from the disk is assets option is filled in config
assetsPath := config.GetConfig().Assets
if assetsPath != "" {
for _, locale := range consts.SupportedLocales {
pofile := path.Join(assetsPath, "locales", locale+".po")
po, err := os.ReadFile(pofile)
if err != nil {
return fmt.Errorf("Can't load the po file for %s", locale)
}
i18n.LoadLocale(locale, "", po)
}
return nil
}
for _, locale := range consts.SupportedLocales {
f, err := assets.Open("/locales/"+locale+".po", config.DefaultInstanceContext)
if err != nil {
return fmt.Errorf("Can't load the po file for %s", locale)
}
po, err := io.ReadAll(f)
if err != nil {
return err
}
i18n.LoadLocale(locale, "", po)
}
return nil
}
// ListenAndServeWithAppDir creates and setup all the necessary http endpoints
// and serve the specified application on a app subdomain.
//
// In order to serve the application, the specified directory should provide
// a manifest.webapp file that will be used to parameterize the application
// permissions.
func ListenAndServeWithAppDir(appsdir map[string]string, services *stack.Services) (*Servers, error) {
for slug, dir := range appsdir {
dir = utils.AbsPath(dir)
appsdir[slug] = dir
exists, err := utils.DirExists(dir)
if err != nil {
return nil, err
}
if !exists {
logger.WithNamespace("dev").Warnf("Directory %s does not exist", dir)
} else {
if err = checkExists(path.Join(dir, app.WebappManifestName)); err != nil {
logger.WithNamespace("dev").Warnf("The app manifest is missing: %s", err)
}
if err = checkExists(path.Join(dir, "index.html")); err != nil {
logger.WithNamespace("dev").Warnf("The index.html is missing: %s", err)
}
}
}
app.SetupAppsDir(appsdir)
return ListenAndServe(services)
}
func checkExists(filepath string) error {
exists, err := utils.FileExists(filepath)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("Directory %s should contain a %s file",
path.Dir(filepath), path.Base(filepath))
}
return nil
}
// ListenAndServe creates and setups all the necessary http endpoints and start
// them.
func ListenAndServe(services *stack.Services) (*Servers, error) {
e := echo.New()
e.HideBanner = true
e.HidePort = true
major, err := CreateSubdomainProxy(e, services, apps.Serve)
if err != nil {
return nil, err
}
if err = LoadSupportedLocales(); err != nil {
return nil, err
}
if build.IsDevRelease() {
timeFormat := "time_rfc3339"
if logrus.GetLevel() == logrus.DebugLevel {
timeFormat = "time_rfc3339_nano"
}
major.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "time=${" + timeFormat + "}\tstatus=${status}\tmethod=${method}\thost=${host}\turi=${uri}\tbytes_out=${bytes_out}\n",
}))
}
admin := echo.New()
admin.HideBanner = true
admin.HidePort = true
if err = SetupAdminRoutes(admin); err != nil {
return nil, err
}
servers := NewServers()
err = servers.Start(major, "major", config.ServerAddr())
if err != nil {
return nil, fmt.Errorf("failed to start major server: %w", err)
}
err = servers.Start(admin, "admin", config.AdminServerAddr())
if err != nil {
return nil, fmt.Errorf("failed to start admin server: %w", err)
}
return servers, nil
}
// Servers allow to start several [echo.Echo] servers and stop them together.
//
// It also take care of several other task:
// - It sanitize the hosts format
// - It exposes the handlers on several addresses if needed
// - It forces the IPv4/IPv6 dual stack mode for `localhost` by
// remplacing this entry by `["127.0.0.1", "::1]`
type Servers struct {
serversByName map[string]*http.Server
listenersByName map[string]net.Listener
errs chan error
}
func NewServers() *Servers {
return &Servers{
serversByName: map[string]*http.Server{},
listenersByName: map[string]net.Listener{},
errs: make(chan error),
}
}
// Start the server 'e' to the given addrs.
//
// The 'addrs' arguments must be in the format `"host:port"`. If the host
// is not a valid IPv4/IPv6/hostname or if the port not present an error is
// returned.
func (s *Servers) Start(handler http.Handler, name string, addr string) error {
addrs := []string{}
if len(addr) == 0 {
return fmt.Errorf("args: %w", ErrMissingArgument)
}
if len(name) == 0 {
return fmt.Errorf("name: %w", ErrMissingArgument)
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "http server %s started on %q\n", name, addr)
switch host {
case "localhost":
addrs = append(addrs, net.JoinHostPort("127.0.0.1", port))
addrs = append(addrs, net.JoinHostPort("::1", port))
default:
addrs = append(addrs, net.JoinHostPort(host, port))
}
for _, addr := range addrs {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
writer := logger.WithNamespace("stack").Writer()
logger := log.New(writer, "", 0)
server := &http.Server{
Addr: addr,
Handler: handler,
ReadHeaderTimeout: ReadHeaderTimeout,
ErrorLog: logger,
}
s.serversByName[name] = server
s.listenersByName[name] = l
go func(server *http.Server) {
s.errs <- server.Serve(l)
}(server)
}
return nil
}
// GetAddr return the address where the given server listen to.
//
// This endpoint is mostly used when we use dynamic port attribution
// like when we don't specify a port
func (s *Servers) GetAddr(name string) net.Addr {
l, ok := s.listenersByName[name]
if !ok {
return nil
}
return l.Addr()
}
// Wait for servers to stop or fall in error.
func (s *Servers) Wait() <-chan error {
return s.errs
}
// Shutdown gracefully stops the servers.
func (s *Servers) Shutdown(ctx context.Context) error {
shutdowners := []utils.Shutdowner{}
for _, server := range s.serversByName {
shutdowners = append(shutdowners, server)
}
g := utils.NewGroupShutdown(shutdowners...)
fmt.Print(" shutting down servers...")
if err := g.Shutdown(ctx); err != nil {
fmt.Println("failed: ", err.Error())
return err
}
fmt.Println("ok.")
return nil
}