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  }