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  }