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 }