github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/middlewares/instance.go (about) 1 package middlewares 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "net/url" 8 "strings" 9 10 "github.com/cozy/cozy-stack/model/feature" 11 "github.com/cozy/cozy-stack/model/instance" 12 "github.com/cozy/cozy-stack/model/instance/lifecycle" 13 "github.com/cozy/cozy-stack/model/move" 14 "github.com/cozy/cozy-stack/model/oauth" 15 "github.com/cozy/cozy-stack/model/permission" 16 "github.com/cozy/cozy-stack/pkg/assets" 17 "github.com/cozy/cozy-stack/pkg/consts" 18 "github.com/cozy/cozy-stack/pkg/jsonapi" 19 "github.com/labstack/echo/v4" 20 "golang.org/x/net/idna" 21 ) 22 23 // NeedInstance is an echo middleware which will display an error 24 // if there is no instance. 25 func NeedInstance(next echo.HandlerFunc) echo.HandlerFunc { 26 return func(c echo.Context) error { 27 if c.Get("instance") != nil { 28 return next(c) 29 } 30 host, err := idna.ToUnicode(c.Request().Host) 31 if err != nil { 32 return err 33 } 34 i, err := lifecycle.GetInstance(host) 35 if err != nil { 36 var errHTTP *echo.HTTPError 37 switch err { 38 case instance.ErrNotFound, instance.ErrIllegalDomain: 39 err = instance.ErrNotFound 40 errHTTP = echo.NewHTTPError(http.StatusNotFound, err) 41 default: 42 errHTTP = echo.NewHTTPError(http.StatusInternalServerError, err) 43 } 44 errHTTP.Internal = err 45 return errHTTP 46 } 47 c.Set("instance", i.WithContextualDomain(host)) 48 return next(c) 49 } 50 } 51 52 // CheckInstanceDeleting is a middleware that blocks the routing access for 53 // instances with the deleting flag set. 54 func CheckInstanceDeleting(next echo.HandlerFunc) echo.HandlerFunc { 55 return func(c echo.Context) error { 56 i := GetInstance(c) 57 if i.Deleting { 58 err := instance.ErrNotFound 59 errHTTP := echo.NewHTTPError(http.StatusNotFound, err) 60 errHTTP.Internal = err 61 return errHTTP 62 } 63 return next(c) 64 } 65 } 66 67 // CheckInstanceBlocked is a middleware that blocks the routing access (for 68 // instance if the term-of-services have not been signed and have reach its 69 // deadline) 70 func CheckInstanceBlocked(next echo.HandlerFunc) echo.HandlerFunc { 71 return func(c echo.Context) error { 72 i := GetInstance(c) 73 if _, ok := GetCLIPermission(c); ok { 74 return next(c) 75 } 76 if i.CheckInstanceBlocked() { 77 return handleBlockedInstance(c, i, next) 78 } 79 return next(c) 80 } 81 } 82 83 func handleBlockedInstance(c echo.Context, i *instance.Instance, next echo.HandlerFunc) error { 84 returnCode := http.StatusServiceUnavailable 85 contentType := AcceptedContentType(c) 86 87 if c.Request().URL.Path == "/robots.txt" { 88 if f, ok := assets.Get("/robots.txt", i.ContextName); ok { 89 _, err := io.Copy(c.Response(), f.Reader()) 90 return err 91 } 92 } 93 94 // Standard checks 95 if i.BlockingReason == instance.BlockedLoginFailed.Code { 96 return c.Render(returnCode, "instance_blocked.html", echo.Map{ 97 "Domain": i.ContextualDomain(), 98 "ContextName": i.ContextName, 99 "Locale": i.Locale, 100 "Title": i.TemplateTitle(), 101 "Favicon": Favicon(i), 102 "Reason": i.Translate(instance.BlockedLoginFailed.Message), 103 "SupportEmail": i.SupportEmailAddress(), 104 }) 105 } 106 107 // Allow konnectors to be run for the delete accounts hook just before 108 // moving a Cozy. 109 if move.GetStore().AllowDeleteAccounts(i) { 110 perms, err := GetPermission(c) 111 if err == nil && perms.Type == permission.TypeKonnector { 112 return next(c) 113 } 114 } 115 116 if i.BlockingReason == instance.BlockedImporting.Code || 117 i.BlockingReason == instance.BlockedMoving.Code { 118 // Allow requests to the importing page 119 if strings.HasPrefix(c.Request().URL.Path, "/move/") { 120 return next(c) 121 } 122 switch contentType { 123 case jsonapi.ContentType, echo.MIMEApplicationJSON: 124 reason := i.Translate(instance.BlockedPaymentFailed.Message) 125 return c.JSON(returnCode, echo.Map{"error": reason}) 126 default: 127 return c.Redirect(http.StatusFound, i.PageURL("/move/importing", nil)) 128 } 129 } 130 131 if url, _ := i.ManagerURL(instance.ManagerBlockedURL); url != "" && IsLoggedIn(c) { 132 switch contentType { 133 case jsonapi.ContentType, echo.MIMEApplicationJSON: 134 warnings := warningOrBlocked(i, returnCode) 135 return c.JSON(returnCode, warnings) 136 default: 137 return c.Redirect(http.StatusFound, url) 138 } 139 } 140 141 // Fallback by trying to determine the blocking reason 142 reason := i.BlockingReason 143 if reason == instance.BlockedPaymentFailed.Code { 144 returnCode = http.StatusPaymentRequired 145 reason = i.Translate(instance.BlockedPaymentFailed.Message) 146 } 147 148 switch contentType { 149 case jsonapi.ContentType, echo.MIMEApplicationJSON: 150 warnings := warningOrBlocked(i, returnCode) 151 return c.JSON(returnCode, warnings) 152 default: 153 return c.Render(returnCode, "instance_blocked.html", echo.Map{ 154 "Domain": i.ContextualDomain(), 155 "ContextName": i.ContextName, 156 "Locale": i.Locale, 157 "Title": i.TemplateTitle(), 158 "Favicon": Favicon(i), 159 "Reason": reason, 160 "SupportEmail": i.SupportEmailAddress(), 161 }) 162 } 163 } 164 165 func warningOrBlocked(i *instance.Instance, returnCode int) []*jsonapi.Error { 166 warnings := ListWarnings(i) 167 if len(warnings) == 0 { 168 warnings = []*jsonapi.Error{ 169 { 170 Status: returnCode, 171 Title: "Blocked", 172 Code: instance.BlockedUnknown.Code, 173 Detail: i.Translate(instance.BlockedUnknown.Message), 174 }, 175 } 176 } 177 return warnings 178 } 179 180 // CheckOnboardingNotFinished checks if there is the instance needs to complete 181 // its onboarding 182 func CheckOnboardingNotFinished(next echo.HandlerFunc) echo.HandlerFunc { 183 return func(c echo.Context) error { 184 i := GetInstance(c) 185 if !i.OnboardingFinished { 186 return RenderNeedOnboarding(c, i) 187 } 188 return next(c) 189 } 190 } 191 192 // RenderNeedOnboarding renders the page that tells the user that they have to 193 // confirm their email address and choose a password before using their Cozy. 194 func RenderNeedOnboarding(c echo.Context, inst *instance.Instance) error { 195 return c.Render(http.StatusOK, "need_onboarding.html", echo.Map{ 196 "Domain": inst.ContextualDomain(), 197 "ContextName": inst.ContextName, 198 "Locale": inst.Locale, 199 "Title": inst.TemplateTitle(), 200 "Favicon": Favicon(inst), 201 "SupportEmail": inst.SupportEmailAddress(), 202 "UUID": inst.UUID, 203 }) 204 } 205 206 // CheckTOSDeadlineExpired checks if there is not signed ToS and the deadline is 207 // exceeded 208 func CheckTOSDeadlineExpired(next echo.HandlerFunc) echo.HandlerFunc { 209 return func(c echo.Context) error { 210 i := GetInstance(c) 211 if _, ok := GetCLIPermission(c); ok { 212 return next(c) 213 } 214 215 redirect, _ := i.ManagerURL(instance.ManagerTOSURL) 216 217 // Skip check if the instance does not have a ManagerURL or a 218 // registerToken 219 if len(i.RegisterToken) > 0 || redirect == "" { 220 return next(c) 221 } 222 223 notSigned, deadline := i.CheckTOSNotSignedAndDeadline() 224 if notSigned && deadline == instance.TOSBlocked { 225 switch AcceptedContentType(c) { 226 case jsonapi.ContentType, echo.MIMEApplicationJSON: 227 return c.JSON(http.StatusPaymentRequired, ListWarnings(i)) 228 default: 229 return c.Redirect(http.StatusFound, redirect) 230 } 231 } 232 return next(c) 233 } 234 } 235 236 // CheckOAuthClientsLimitExceeded checks if there are more OAuth clients 237 // connected by the user than what their plan allows 238 func CheckOAuthClientsLimitExceeded(c echo.Context) (bool, error) { 239 i := GetInstance(c) 240 if _, ok := GetCLIPermission(c); ok { 241 return false, nil 242 } 243 244 slug := c.Get("slug").(string) 245 if slug == consts.SettingsSlug { 246 return false, nil 247 } 248 249 flags, err := feature.GetFlags(i) 250 if err != nil { 251 return true, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("Could not get flags: %w", err)) 252 } 253 254 if clientsLimit, ok := flags.M["cozy.oauthclients.max"].(float64); ok && clientsLimit >= 0 { 255 _, exceeded := oauth.CheckOAuthClientsLimitReached(i, int(clientsLimit)) 256 if exceeded { 257 reqURL := c.Request().URL 258 subdomain := i.SubDomain(slug) 259 subdomain.Path = reqURL.Path 260 subdomain.RawQuery = reqURL.RawQuery 261 subdomain.Fragment = reqURL.Fragment 262 q := url.Values{"redirect": {subdomain.String()}} 263 264 return true, c.Redirect(http.StatusSeeOther, i.PageURL("/settings/clients/limit-exceeded", q)) 265 } 266 } 267 268 return false, nil 269 } 270 271 // GetInstance will return the instance linked to the given echo 272 // context or panic if none exists 273 func GetInstance(c echo.Context) *instance.Instance { 274 return c.Get("instance").(*instance.Instance) 275 } 276 277 // GetInstanceSafe will return the instance linked to the given echo 278 // context 279 func GetInstanceSafe(c echo.Context) (*instance.Instance, bool) { 280 i := c.Get("instance") 281 if i == nil { 282 return nil, false 283 } 284 inst, ok := i.(*instance.Instance) 285 return inst, ok 286 }