github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/routing.go (about) 1 //go:generate statik -f -src=../assets -dest=. -externals=../assets/.externals 2 3 package web 4 5 import ( 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/cozy/cozy-stack/model/instance/lifecycle" 11 "github.com/cozy/cozy-stack/model/stack" 12 build "github.com/cozy/cozy-stack/pkg/config" 13 "github.com/cozy/cozy-stack/pkg/config/config" 14 "github.com/cozy/cozy-stack/pkg/jsonapi" 15 "github.com/cozy/cozy-stack/pkg/metrics" 16 "github.com/cozy/cozy-stack/web/accounts" 17 "github.com/cozy/cozy-stack/web/apps" 18 "github.com/cozy/cozy-stack/web/auth" 19 "github.com/cozy/cozy-stack/web/bitwarden" 20 "github.com/cozy/cozy-stack/web/compat" 21 "github.com/cozy/cozy-stack/web/conncheck" 22 "github.com/cozy/cozy-stack/web/contacts" 23 "github.com/cozy/cozy-stack/web/data" 24 "github.com/cozy/cozy-stack/web/errors" 25 "github.com/cozy/cozy-stack/web/files" 26 "github.com/cozy/cozy-stack/web/instances" 27 "github.com/cozy/cozy-stack/web/intents" 28 "github.com/cozy/cozy-stack/web/jobs" 29 "github.com/cozy/cozy-stack/web/middlewares" 30 "github.com/cozy/cozy-stack/web/move" 31 "github.com/cozy/cozy-stack/web/notes" 32 "github.com/cozy/cozy-stack/web/notifications" 33 "github.com/cozy/cozy-stack/web/oauth" 34 "github.com/cozy/cozy-stack/web/office" 35 "github.com/cozy/cozy-stack/web/oidc" 36 "github.com/cozy/cozy-stack/web/permissions" 37 "github.com/cozy/cozy-stack/web/public" 38 "github.com/cozy/cozy-stack/web/realtime" 39 "github.com/cozy/cozy-stack/web/registry" 40 "github.com/cozy/cozy-stack/web/remote" 41 "github.com/cozy/cozy-stack/web/settings" 42 "github.com/cozy/cozy-stack/web/sharings" 43 "github.com/cozy/cozy-stack/web/shortcuts" 44 "github.com/cozy/cozy-stack/web/statik" 45 "github.com/cozy/cozy-stack/web/status" 46 "github.com/cozy/cozy-stack/web/swift" 47 "github.com/cozy/cozy-stack/web/tools" 48 "github.com/cozy/cozy-stack/web/version" 49 "github.com/cozy/cozy-stack/web/wellknown" 50 "github.com/labstack/echo/v4" 51 "github.com/labstack/echo/v4/middleware" 52 "github.com/prometheus/client_golang/prometheus" 53 "golang.org/x/net/idna" 54 ) 55 56 const ( 57 // cspScriptSrcAllowList is an allowlist for default allowed domains in CSP. 58 cspScriptSrcAllowList = "https://matomo.cozycloud.cc https://errors.cozycloud.cc https://api.pwnedpasswords.com" 59 60 // cspImgSrcAllowList is an allowlist of images domains that are allowed in 61 // CSP. 62 cspImgSrcAllowList = "https://matomo.cozycloud.cc https://*.tile.openstreetmap.org https://*.tile.osm.org" 63 64 // cspFrameSrcAllowList is an allowlist of custom protocols that are allowed 65 // in the CSP. We are using iframes on these custom protocols to open 66 // deeplinks to them and have a fallback if the mobile apps are not 67 // available. 68 cspFrameSrcAllowList = "cozydrive: cozybanks:" 69 ) 70 71 var hstsMaxAge = 365 * 24 * time.Hour // 1 year 72 73 // SetupAppsHandler adds all the necessary middlewares for the application 74 // handler. 75 func SetupAppsHandler(appsHandler echo.HandlerFunc) echo.HandlerFunc { 76 mws := []echo.MiddlewareFunc{ 77 middlewares.LoadSession, 78 middlewares.CheckUserAgent, 79 middlewares.Accept(middlewares.AcceptOptions{ 80 DefaultContentTypeOffer: echo.MIMETextHTML, 81 }), 82 middlewares.CheckInstanceBlocked, 83 middlewares.CheckInstanceDeleting, 84 middlewares.CheckTOSDeadlineExpired, 85 } 86 87 if !config.GetConfig().CSPDisabled { 88 // Add CSP exceptions for loading the OnlyOffice editor (script + frame) 89 perContext := config.GetConfig().CSPPerContext 90 scriptSrc := cspScriptSrcAllowList 91 frameSrc := cspFrameSrcAllowList 92 for ctxName, office := range config.GetConfig().Office { 93 oo := office.OnlyOfficeURL 94 if oo == "" { 95 continue 96 } 97 if !strings.HasSuffix(oo, "/") { 98 oo += "/" 99 } 100 if ctxName == config.DefaultInstanceContext { 101 scriptSrc = oo + " " + scriptSrc 102 frameSrc = oo + " " + frameSrc 103 } else { 104 cfg := perContext[ctxName] 105 if cfg == nil { 106 cfg = make(map[string]string) 107 } 108 cfg["script"] = oo + " " + cfg["script"] 109 cfg["frame"] = oo + " " + cfg["frame"] 110 perContext[ctxName] = cfg 111 } 112 } 113 114 // Add CSO exception for starting a move from settings 115 formAction := config.GetConfig().Move.URL 116 117 secure := middlewares.Secure(&middlewares.SecureConfig{ 118 HSTSMaxAge: hstsMaxAge, 119 CSPDefaultSrc: []middlewares.CSPSource{middlewares.CSPSrcSelf, middlewares.CSPSrcParent, middlewares.CSPSrcWS}, 120 CSPStyleSrc: []middlewares.CSPSource{middlewares.CSPUnsafeInline}, 121 CSPFontSrc: []middlewares.CSPSource{middlewares.CSPSrcData}, 122 CSPImgSrc: []middlewares.CSPSource{middlewares.CSPSrcData, middlewares.CSPSrcBlob}, 123 CSPObjectSrc: []middlewares.CSPSource{middlewares.CSPSrcNone}, 124 CSPFrameSrc: []middlewares.CSPSource{middlewares.CSPSrcSiblings}, 125 CSPFrameAncestors: []middlewares.CSPSource{middlewares.CSPSrcSelf}, 126 CSPBaseURI: []middlewares.CSPSource{middlewares.CSPSrcSelf}, 127 CSPFormAction: []middlewares.CSPSource{middlewares.CSPSrcParent}, 128 129 CSPDefaultSrcAllowList: config.GetConfig().CSPAllowList["default"], 130 CSPImgSrcAllowList: config.GetConfig().CSPAllowList["img"] + " " + cspImgSrcAllowList, 131 CSPScriptSrcAllowList: config.GetConfig().CSPAllowList["script"] + " " + scriptSrc, 132 CSPConnectSrcAllowList: config.GetConfig().CSPAllowList["connect"] + " " + cspScriptSrcAllowList, 133 CSPStyleSrcAllowList: config.GetConfig().CSPAllowList["style"], 134 CSPFontSrcAllowList: config.GetConfig().CSPAllowList["font"], 135 CSPMediaSrcAllowList: config.GetConfig().CSPAllowList["media"], 136 CSPFrameSrcAllowList: config.GetConfig().CSPAllowList["frame"] + " " + frameSrc, 137 CSPFormActionAllowList: config.GetConfig().CSPAllowList["form"] + " " + formAction, 138 139 CSPPerContext: perContext, 140 }) 141 mws = append([]echo.MiddlewareFunc{secure}, mws...) 142 } 143 144 return middlewares.Compose(appsHandler, mws...) 145 } 146 147 // SetupAssets add assets routing and handling to the given router. It also 148 // adds a Renderer to render templates. 149 func SetupAssets(router *echo.Echo, assetsPath string) (err error) { 150 var r statik.AssetRenderer 151 if assetsPath != "" { 152 r, err = statik.NewDirRenderer(assetsPath) 153 } else { 154 r, err = statik.NewRenderer() 155 } 156 if err != nil { 157 return err 158 } 159 middlewares.BuildTemplates() 160 apps.BuildTemplates() 161 162 router.Renderer = r 163 router.HEAD("/assets/*", echo.WrapHandler(r)) 164 router.GET("/assets/*", echo.WrapHandler(r)) 165 router.GET("/favicon.ico", echo.WrapHandler(r)) 166 router.GET("/robots.txt", echo.WrapHandler(r)) 167 router.GET("/security.txt", echo.WrapHandler(r)) 168 return nil 169 } 170 171 // SetupRoutes sets the routing for HTTP endpoints 172 func SetupRoutes(router *echo.Echo, services *stack.Services) error { 173 router.Use(timersMiddleware) 174 175 if !config.GetConfig().CSPDisabled { 176 secure := middlewares.Secure(&middlewares.SecureConfig{ 177 HSTSMaxAge: hstsMaxAge, 178 CSPDefaultSrc: []middlewares.CSPSource{middlewares.CSPSrcNone}, 179 CSPFrameSrc: []middlewares.CSPSource{middlewares.CSPSrcNone}, 180 CSPFrameAncestors: []middlewares.CSPSource{middlewares.CSPSrcNone}, 181 CSPBaseURI: []middlewares.CSPSource{middlewares.CSPSrcNone}, 182 }) 183 router.Use(secure) 184 } 185 186 router.Use(middlewares.CORS(middlewares.CORSOptions{ 187 BlockList: []string{"/auth/"}, 188 })) 189 190 // non-authentified HTML routes for authentication (login, OAuth, ...) 191 { 192 mws := []echo.MiddlewareFunc{ 193 middlewares.NeedInstance, 194 middlewares.LoadSession, 195 middlewares.Accept(middlewares.AcceptOptions{ 196 DefaultContentTypeOffer: echo.MIMETextHTML, 197 }), 198 middlewares.CheckUserAgent, 199 middlewares.CheckInstanceBlocked, 200 middlewares.CheckInstanceDeleting, 201 } 202 203 router.GET("/", auth.Home, mws...) 204 auth.Routes(router.Group("/auth", mws...)) 205 public.Routes(router.Group("/public", mws...)) 206 wellknown.Routes(router.Group("/.well-known", mws...)) 207 } 208 209 // authentified JSON API routes 210 { 211 mwsNotBlocked := []echo.MiddlewareFunc{ 212 middlewares.NeedInstance, 213 middlewares.LoadSession, 214 middlewares.Accept(middlewares.AcceptOptions{ 215 DefaultContentTypeOffer: jsonapi.ContentType, 216 }), 217 } 218 mws := append(mwsNotBlocked, 219 middlewares.CheckInstanceBlocked, 220 middlewares.CheckTOSDeadlineExpired, 221 ) 222 registry.Routes(router.Group("/registry", mws...)) 223 data.Routes(router.Group("/data", mws...)) 224 files.Routes(router.Group("/files", mws...)) 225 contacts.Routes(router.Group("/contacts", mws...)) 226 intents.Routes(router.Group("/intents", mws...)) 227 jobs.NewHTTPHandler(services.Emailer).Register(router.Group("/jobs", mws...)) 228 notifications.Routes(router.Group("/notifications", mws...)) 229 move.Routes(router.Group("/move", mws...)) 230 permissions.Routes(router.Group("/permissions", mws...)) 231 realtime.Routes(router.Group("/realtime", mws...)) 232 notes.Routes(router.Group("/notes", mws...)) 233 office.Routes(router.Group("/office", mws...)) 234 remote.Routes(router.Group("/remote", mws...)) 235 sharings.Routes(router.Group("/sharings", mws...)) 236 bitwarden.Routes(router.Group("/bitwarden", mws...)) 237 shortcuts.Routes(router.Group("/shortcuts", mws...)) 238 239 // The settings routes needs not to be blocked 240 apps.WebappsRoutes(router.Group("/apps", mwsNotBlocked...)) 241 apps.KonnectorRoutes(router.Group("/konnectors", mwsNotBlocked...)) 242 243 // TODO: An init refacto will soon be required 244 settings.NewHTTPHandler(services.Settings).Register(router.Group("/settings", mwsNotBlocked...)) 245 246 compat.Routes(router.Group("/compat", mwsNotBlocked...)) 247 248 // Careful, the normal middlewares NeedInstance and LoadSession are not 249 // applied to these groups since they should not be used for oauth 250 // redirection. 251 accounts.Routes(router.Group("/accounts")) 252 oidc.Routes(router.Group("/oidc")) 253 } 254 255 // other non-authentified routes 256 { 257 conncheck.Routes(router.Group("/connection_check")) 258 status.Routes(router.Group("/status")) 259 version.Routes(router.Group("/version")) 260 } 261 262 // dev routes 263 if build.IsDevRelease() { 264 router.GET("/dev/mails/:name", devMailsHandler, middlewares.NeedInstance) 265 router.GET("/dev/templates/:name", devTemplatesHandler) 266 } 267 268 setupRecover(router) 269 router.HTTPErrorHandler = errors.ErrorHandler 270 return nil 271 } 272 273 func timersMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 274 return func(c echo.Context) error { 275 timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { 276 status := strconv.Itoa(c.Response().Status) 277 metrics.HTTPTotalDurations. 278 WithLabelValues(c.Request().Method, status). 279 Observe(v) 280 })) 281 defer timer.ObserveDuration() 282 return next(c) 283 } 284 } 285 286 // SetupAdminRoutes sets the routing for the administration HTTP endpoints 287 func SetupAdminRoutes(router *echo.Echo) error { 288 var mws []echo.MiddlewareFunc 289 if build.IsDevRelease() { 290 mws = append(mws, middleware.LoggerWithConfig(middleware.LoggerConfig{ 291 Format: "time=${time_rfc3339}\tstatus=${status}\tmethod=${method}\thost=${host}\turi=${uri}\tbytes_out=${bytes_out}\n", 292 })) 293 } else { 294 mws = append(mws, middlewares.BasicAuth(config.GetConfig().AdminSecretFileName)) 295 } 296 297 instances.Routes(router.Group("/instances", mws...)) 298 apps.AdminRoutes(router.Group("/konnectors", mws...)) 299 version.Routes(router.Group("/version", mws...)) 300 metrics.Routes(router.Group("/metrics", mws...)) 301 oauth.Routes(router.Group("/oauth", mws...)) 302 oidc.AdminRoutes(router.Group("/oidc", mws...)) 303 realtime.Routes(router.Group("/realtime", mws...)) 304 swift.Routes(router.Group("/swift", mws...)) 305 tools.Routes(router.Group("/tools", mws...)) 306 conncheck.Routes(router.Group("/connection_check", mws...)) 307 308 setupRecover(router) 309 310 router.HTTPErrorHandler = errors.ErrorHandler 311 return nil 312 } 313 314 // CreateSubdomainProxy returns a new web server that will handle that apps 315 // proxy routing if the host of the request match an application, and route to 316 // the given router otherwise. 317 func CreateSubdomainProxy(router *echo.Echo, services *stack.Services, appsHandler echo.HandlerFunc) (*echo.Echo, error) { 318 if err := SetupAssets(router, config.GetConfig().Assets); err != nil { 319 return nil, err 320 } 321 322 if err := SetupRoutes(router, services); err != nil { 323 return nil, err 324 } 325 326 appsHandler = SetupAppsHandler(appsHandler) 327 328 main := echo.New() 329 main.HideBanner = true 330 main.HidePort = true 331 main.Renderer = router.Renderer 332 main.Any("/*", firstRouting(router, appsHandler)) 333 334 main.HTTPErrorHandler = errors.HTMLErrorHandler 335 return main, nil 336 } 337 338 // firstRouting receives the requests and use the domain to decide if we should 339 // use the API router, serve an app, or use delegated authentication. 340 func firstRouting(router *echo.Echo, appsHandler echo.HandlerFunc) echo.HandlerFunc { 341 return func(c echo.Context) error { 342 host, err := idna.ToUnicode(c.Request().Host) 343 if err != nil { 344 return err 345 } 346 if contextName, ok := oidc.FindLoginDomain(host); ok { 347 return oidc.LoginDomainHandler(c, contextName) 348 } 349 350 if parent, slug, _ := config.SplitCozyHost(host); slug != "" { 351 if i, err := lifecycle.GetInstance(parent); err == nil { 352 c.Set("instance", i.WithContextualDomain(parent)) 353 c.Set("slug", slug) 354 return appsHandler(c) 355 } 356 } 357 358 router.ServeHTTP(c.Response(), c.Request()) 359 return nil 360 } 361 } 362 363 // setupRecover sets a recovering strategy of panics happening in handlers 364 func setupRecover(router *echo.Echo) { 365 if !build.IsDevRelease() { 366 recoverMiddleware := middlewares.RecoverWithConfig(middlewares.RecoverConfig{ 367 StackSize: 10 << 10, // 10KB 368 }) 369 router.Use(recoverMiddleware) 370 } 371 }