github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/apps/apps.go (about)

     1  // Package apps is the HTTP frontend of the application package. It
     2  // exposes the HTTP api install, update or uninstall applications.
     3  package apps
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/cozy/cozy-stack/model/account"
    20  	"github.com/cozy/cozy-stack/model/app"
    21  	"github.com/cozy/cozy-stack/model/instance"
    22  	"github.com/cozy/cozy-stack/model/job"
    23  	"github.com/cozy/cozy-stack/model/oauth"
    24  	"github.com/cozy/cozy-stack/model/permission"
    25  	"github.com/cozy/cozy-stack/model/session"
    26  	"github.com/cozy/cozy-stack/pkg/appfs"
    27  	build "github.com/cozy/cozy-stack/pkg/config"
    28  	"github.com/cozy/cozy-stack/pkg/consts"
    29  	"github.com/cozy/cozy-stack/pkg/couchdb"
    30  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    31  	"github.com/cozy/cozy-stack/pkg/limits"
    32  	"github.com/cozy/cozy-stack/pkg/logger"
    33  	"github.com/cozy/cozy-stack/pkg/registry"
    34  	"github.com/cozy/cozy-stack/web/jobs"
    35  	"github.com/cozy/cozy-stack/web/middlewares"
    36  	"github.com/labstack/echo/v4"
    37  )
    38  
    39  // JSMimeType is the content-type for javascript
    40  const JSMimeType = "application/javascript"
    41  
    42  const typeTextEventStream = "text/event-stream"
    43  
    44  type AppLog struct {
    45  	Time  time.Time `json:"timestamp"`
    46  	Level string    `json:"level"`
    47  	Msg   string    `json:"msg"`
    48  }
    49  
    50  type apiApp struct {
    51  	app.Manifest
    52  }
    53  
    54  func (man *apiApp) MarshalJSON() ([]byte, error) {
    55  	return json.Marshal(man.Manifest)
    56  }
    57  
    58  // Links is part of the Manifest interface
    59  func (man *apiApp) Links() *jsonapi.LinksList {
    60  	var route string
    61  	links := jsonapi.LinksList{}
    62  	switch a := man.Manifest.(type) {
    63  	case (*app.WebappManifest):
    64  		route = "/apps/"
    65  		if a.Icon() != "" {
    66  			links.Icon = "/apps/" + a.Slug() + "/icon/" + a.Version()
    67  		}
    68  		if (a.State() == app.Ready || a.State() == app.Installed) &&
    69  			a.Instance != nil {
    70  			links.Related = a.Instance.SubDomain(a.Slug()).String()
    71  		}
    72  	case (*app.KonnManifest):
    73  		route = "/konnectors/"
    74  		if a.Icon() != "" {
    75  			links.Icon = "/konnectors/" + a.Slug() + "/icon/" + a.Version()
    76  		}
    77  		links.Perms = "/permissions/konnectors/" + a.Slug()
    78  	}
    79  	if route != "" {
    80  		links.Self = route + man.Manifest.Slug()
    81  	}
    82  	return &links
    83  }
    84  
    85  // Relationships is part of the Manifest interface
    86  func (man *apiApp) Relationships() jsonapi.RelationshipMap {
    87  	return jsonapi.RelationshipMap{}
    88  }
    89  
    90  // Included is part of the Manifest interface
    91  func (man *apiApp) Included() []jsonapi.Object {
    92  	return []jsonapi.Object{}
    93  }
    94  
    95  // apiApp is a jsonapi.Object
    96  var _ jsonapi.Object = (*apiApp)(nil)
    97  
    98  func getHandler(appType consts.AppType) echo.HandlerFunc {
    99  	return func(c echo.Context) error {
   100  		instance := middlewares.GetInstance(c)
   101  		slug := c.Param("slug")
   102  		man, err := app.GetBySlug(instance, slug, appType)
   103  		if err != nil {
   104  			return wrapAppsError(err)
   105  		}
   106  		if err := middlewares.Allow(c, permission.GET, man); err != nil {
   107  			return err
   108  		}
   109  
   110  		registries := instance.Registries()
   111  		copier := app.Copier(man.AppType(), instance)
   112  		man = app.DoLazyUpdate(instance, man, copier, registries)
   113  
   114  		if appType == consts.WebappType {
   115  			// TODO: Check is this line is really necessary
   116  			man.(*app.WebappManifest).Instance = instance
   117  		}
   118  		return jsonapi.Data(c, http.StatusOK, &apiApp{man}, nil)
   119  	}
   120  }
   121  
   122  func downloadHandler(appType consts.AppType) echo.HandlerFunc {
   123  	return func(c echo.Context) error {
   124  		inst := middlewares.GetInstance(c)
   125  		slug := c.Param("slug")
   126  		man, err := app.GetBySlug(inst, slug, appType)
   127  		if err != nil {
   128  			return wrapAppsError(err)
   129  		}
   130  		if err := middlewares.Allow(c, permission.GET, man); err != nil {
   131  			return err
   132  		}
   133  
   134  		version := c.Param("version")
   135  		if version == "" {
   136  			version = man.Version()
   137  		}
   138  
   139  		source := man.Source()
   140  		if strings.HasPrefix(source, "registry://") {
   141  			registries := inst.Registries()
   142  			v, err := registry.GetVersion(slug, version, registries)
   143  			if err != nil {
   144  				return wrapAppsError(err)
   145  			}
   146  			return c.Redirect(http.StatusSeeOther, v.URL)
   147  		}
   148  
   149  		if version != man.Version() {
   150  			err := errors.New("code for this version is not available")
   151  			return jsonapi.PreconditionFailed("version", err)
   152  		}
   153  
   154  		var fs appfs.FileServer
   155  		switch appType {
   156  		case consts.WebappType:
   157  			man := man.(*app.WebappManifest)
   158  			if man.FromAppsDir {
   159  				fs = app.FSForAppDir(slug)
   160  			} else {
   161  				fs = app.AppsFileServer(inst)
   162  			}
   163  		case consts.KonnectorType:
   164  			fs = app.KonnectorsFileServer(inst)
   165  		}
   166  
   167  		return fs.ServeCodeTarball(c.Response(), c.Request(), slug, version, man.Checksum())
   168  	}
   169  }
   170  
   171  // installHandler handles all POST /:slug request and tries to install
   172  // or update the application with the given Source.
   173  func installHandler(installerType consts.AppType) echo.HandlerFunc {
   174  	return func(c echo.Context) error {
   175  		instance := middlewares.GetInstance(c)
   176  		slug := c.Param("slug")
   177  		source := c.QueryParam("Source")
   178  		if source == "" {
   179  			source = "registry://" + slug + "/stable"
   180  		}
   181  		if err := middlewares.AllowInstallApp(c, installerType, source, permission.POST); err != nil {
   182  			return err
   183  		}
   184  
   185  		var overridenParameters map[string]interface{}
   186  		if p := c.QueryParam("Parameters"); p != "" {
   187  			if err := json.Unmarshal([]byte(p), &overridenParameters); err != nil {
   188  				return echo.NewHTTPError(http.StatusBadRequest)
   189  			}
   190  		}
   191  
   192  		var w http.ResponseWriter
   193  		isEventStream := c.Request().Header.Get(echo.HeaderAccept) == typeTextEventStream
   194  		if isEventStream {
   195  			w = c.Response().Writer
   196  			w.Header().Set(echo.HeaderContentType, typeTextEventStream)
   197  			w.WriteHeader(200)
   198  		}
   199  
   200  		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
   201  			&app.InstallerOptions{
   202  				Operation:   app.Install,
   203  				Type:        installerType,
   204  				SourceURL:   source,
   205  				Slug:        slug,
   206  				Deactivated: c.QueryParam("Deactivated") == "true",
   207  				Registries:  instance.Registries(),
   208  
   209  				OverridenParameters: overridenParameters,
   210  			},
   211  		)
   212  		if err != nil {
   213  			if isEventStream {
   214  				var b []byte
   215  				if b, err = json.Marshal(err.Error()); err == nil {
   216  					writeStream(w, "error", string(b))
   217  				}
   218  			}
   219  			return wrapAppsError(err)
   220  		}
   221  
   222  		go inst.Run()
   223  		return pollInstaller(c, instance, isEventStream, w, slug, inst)
   224  	}
   225  }
   226  
   227  // logsHandler handles all POST /:slug/logs requests and forwards the log lines
   228  // sent as a JSON array to the server logger.
   229  func logsHandler(appType consts.AppType) echo.HandlerFunc {
   230  	return func(c echo.Context) error {
   231  		inst := middlewares.GetInstance(c)
   232  
   233  		slug := c.Param("slug")
   234  		if err := middlewares.AllowMaximal(c); err != nil {
   235  			// If logs are not sent by the flagship app, check that it's sent by
   236  			// a konnector or an app with the logs permission and get its slug
   237  			// from the permission.
   238  			pdoc, err := middlewares.GetPermission(c)
   239  			if err != nil {
   240  				return err
   241  			}
   242  
   243  			if err := middlewares.AllowWholeType(c, permission.POST, consts.AppLogs); err != nil {
   244  				return err
   245  			}
   246  
   247  			if appType == consts.KonnectorType && pdoc.Type == permission.TypeKonnector {
   248  				slug = strings.TrimPrefix(pdoc.SourceID, consts.Konnectors+"/")
   249  			} else if appType == consts.WebappType && pdoc.Type == permission.TypeWebapp {
   250  				slug = strings.TrimPrefix(pdoc.SourceID, consts.Apps+"/")
   251  			} else {
   252  				return middlewares.ErrForbidden
   253  			}
   254  		}
   255  
   256  		clientSide := false
   257  		if appType == consts.KonnectorType {
   258  			man, err := app.GetKonnectorBySlug(inst, slug)
   259  			if err != nil {
   260  				return wrapAppsError(err)
   261  			}
   262  			clientSide = man.ClientSide()
   263  		}
   264  
   265  		var logs []AppLog
   266  		if err := json.NewDecoder(c.Request().Body).Decode(&logs); err != nil {
   267  			return jsonapi.BadJSON()
   268  		}
   269  
   270  		l := logger.WithDomain(inst.Domain).WithNamespace("jobs").
   271  			WithField("slug", slug).
   272  			WithField("job_id", c.QueryParam("job_id"))
   273  
   274  		if clientSide {
   275  			l = l.WithField("worker_id", "client")
   276  		}
   277  		for _, log := range logs {
   278  			level, err := logger.ParseLevel(log.Level)
   279  			if err != nil {
   280  				return jsonapi.InvalidAttribute("level", err)
   281  			}
   282  
   283  			l := l.WithTime(log.Time)
   284  
   285  			if v := c.QueryParam("version"); v != "" {
   286  				l = l.WithField("version", v)
   287  			}
   288  
   289  			l.Log(level, log.Msg)
   290  		}
   291  
   292  		return c.NoContent(http.StatusNoContent)
   293  	}
   294  }
   295  
   296  // updateHandler handles all POST /:slug request and tries to install
   297  // or update the application with the given Source.
   298  func updateHandler(installerType consts.AppType) echo.HandlerFunc {
   299  	return func(c echo.Context) error {
   300  		instance := middlewares.GetInstance(c)
   301  		slug := c.Param("slug")
   302  		source := c.QueryParam("Source")
   303  		if err := middlewares.AllowInstallApp(c, installerType, source, permission.POST); err != nil {
   304  			return err
   305  		}
   306  
   307  		var overridenParameters map[string]interface{}
   308  		if p := c.QueryParam("Parameters"); p != "" {
   309  			if err := json.Unmarshal([]byte(p), &overridenParameters); err != nil {
   310  				return echo.NewHTTPError(http.StatusBadRequest)
   311  			}
   312  		}
   313  
   314  		var w http.ResponseWriter
   315  		isEventStream := c.Request().Header.Get(echo.HeaderAccept) == typeTextEventStream
   316  		if isEventStream {
   317  			w = c.Response().Writer
   318  			w.Header().Set(echo.HeaderContentType, typeTextEventStream)
   319  			w.WriteHeader(200)
   320  		}
   321  
   322  		permissionsAcked, _ := strconv.ParseBool(c.QueryParam("PermissionsAcked"))
   323  		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
   324  			&app.InstallerOptions{
   325  				Operation:  app.Update,
   326  				Type:       installerType,
   327  				SourceURL:  source,
   328  				Slug:       slug,
   329  				Registries: instance.Registries(),
   330  
   331  				PermissionsAcked:    permissionsAcked,
   332  				OverridenParameters: overridenParameters,
   333  			},
   334  		)
   335  		if err != nil {
   336  			if isEventStream {
   337  				var b []byte
   338  				if b, err = json.Marshal(err.Error()); err == nil {
   339  					writeStream(w, "error", string(b))
   340  				}
   341  				return nil
   342  			}
   343  			return wrapAppsError(err)
   344  		}
   345  
   346  		go inst.Run()
   347  		return pollInstaller(c, instance, isEventStream, w, slug, inst)
   348  	}
   349  }
   350  
   351  // deleteHandler handles all DELETE /:slug used to delete an application with
   352  // the specified slug.
   353  func deleteHandler(installerType consts.AppType) echo.HandlerFunc {
   354  	return func(c echo.Context) error {
   355  		instance := middlewares.GetInstance(c)
   356  		slug := c.Param("slug")
   357  		source := "registry://" + slug
   358  		if err := middlewares.AllowInstallApp(c, installerType, source, permission.DELETE); err != nil {
   359  			return err
   360  		}
   361  
   362  		// Check if there is a mobile client attached to this app
   363  		if installerType == consts.WebappType {
   364  			oauthClient, err := oauth.FindClientBySoftwareID(instance, "registry://"+slug)
   365  			if err == nil && oauthClient != nil {
   366  				return wrapAppsError(app.ErrLinkedAppExists)
   367  			}
   368  		}
   369  
   370  		// Delete accounts locally and remotely for banking konnectors
   371  		if installerType == consts.KonnectorType {
   372  			toDelete, err := findAccountsToDelete(instance, slug)
   373  			if err != nil {
   374  				return wrapAppsError(err)
   375  			}
   376  			if len(toDelete) > 0 {
   377  				man, err := app.GetKonnectorBySlug(instance, slug)
   378  				if err != nil {
   379  					return wrapAppsError(err)
   380  				}
   381  				deleteKonnectorWithAccounts(instance, man, toDelete)
   382  				return jsonapi.Data(c, http.StatusAccepted, &apiApp{man}, nil)
   383  			}
   384  		}
   385  
   386  		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
   387  			&app.InstallerOptions{
   388  				Operation:  app.Delete,
   389  				Type:       installerType,
   390  				Slug:       slug,
   391  				Registries: instance.Registries(),
   392  			},
   393  		)
   394  		if err != nil {
   395  			return wrapAppsError(err)
   396  		}
   397  		man, err := inst.RunSync()
   398  		if err != nil {
   399  			return wrapAppsError(err)
   400  		}
   401  		return jsonapi.Data(c, http.StatusOK, &apiApp{man}, nil)
   402  	}
   403  }
   404  
   405  func findAccountsToDelete(instance *instance.Instance, slug string) ([]account.CleanEntry, error) {
   406  	jobsSystem := job.System()
   407  	triggers, err := jobsSystem.GetAllTriggers(instance)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  
   412  	var toDelete []account.CleanEntry
   413  	for _, t := range triggers {
   414  		if !t.Infos().IsKonnectorTrigger() {
   415  			continue
   416  		}
   417  
   418  		var msg struct {
   419  			Account string `json:"account"`
   420  			Slug    string `json:"konnector"`
   421  		}
   422  		err := t.Infos().Message.Unmarshal(&msg)
   423  		if err == nil && msg.Slug == slug && msg.Account != "" {
   424  			// XXX we can have several triggers for the same account (e.g. cron + webhook)
   425  			hasEntry := false
   426  			for i, entry := range toDelete {
   427  				if entry.Account.ID() == msg.Account {
   428  					toDelete[i].Triggers = append(entry.Triggers, t)
   429  					hasEntry = true
   430  					break
   431  				}
   432  			}
   433  			if !hasEntry {
   434  				acc := &account.Account{}
   435  				if err := couchdb.GetDoc(instance, consts.Accounts, msg.Account, acc); err == nil {
   436  					entry := account.CleanEntry{
   437  						Account:  acc,
   438  						Triggers: []job.Trigger{t},
   439  					}
   440  					toDelete = append(toDelete, entry)
   441  				}
   442  			}
   443  		}
   444  	}
   445  	return toDelete, nil
   446  }
   447  
   448  func deleteKonnectorWithAccounts(instance *instance.Instance, man *app.KonnManifest, toDelete []account.CleanEntry) {
   449  	go func() {
   450  		defer func() {
   451  			if r := recover(); r != nil {
   452  				var err error
   453  				switch r := r.(type) {
   454  				case error:
   455  					err = r
   456  				default:
   457  					err = fmt.Errorf("%v", r)
   458  				}
   459  				stack := make([]byte, 4<<10) // 4 KB
   460  				length := runtime.Stack(stack, false)
   461  				log := instance.Logger().WithNamespace("konnectors").WithField("panic", true)
   462  				log.Errorf("PANIC RECOVER %s: %s", err.Error(), stack[:length])
   463  			}
   464  		}()
   465  
   466  		slug := man.Slug()
   467  		for i := range toDelete {
   468  			toDelete[i].ManifestOnDelete = man.OnDeleteAccount() != ""
   469  			toDelete[i].Slug = slug
   470  		}
   471  
   472  		log := instance.Logger().WithNamespace("konnectors")
   473  		if err := account.CleanAndWait(instance, toDelete); err != nil {
   474  			log.Errorf("Cannot clean accounts: %v", err)
   475  			return
   476  		}
   477  		inst, err := app.NewInstaller(instance, app.Copier(consts.KonnectorType, instance),
   478  			&app.InstallerOptions{
   479  				Operation:  app.Delete,
   480  				Type:       consts.KonnectorType,
   481  				Slug:       slug,
   482  				Registries: instance.Registries(),
   483  			},
   484  		)
   485  		if err != nil {
   486  			log.Errorf("Cannot uninstall the konnector: %v", err)
   487  			return
   488  		}
   489  		_, err = inst.RunSync()
   490  		if err != nil {
   491  			log.Errorf("Cannot uninstall the konnector: %v", err)
   492  		}
   493  	}()
   494  }
   495  
   496  func pollInstaller(c echo.Context, instance *instance.Instance, isEventStream bool, w http.ResponseWriter, slug string, inst *app.Installer) error {
   497  	if !isEventStream {
   498  		man, _, err := inst.Poll()
   499  		if err != nil {
   500  			return wrapAppsError(err)
   501  		}
   502  		go func() {
   503  			for {
   504  				_, done, err := inst.Poll()
   505  				if done || err != nil {
   506  					break
   507  				}
   508  			}
   509  		}()
   510  		return jsonapi.Data(c, http.StatusAccepted, &apiApp{man}, nil)
   511  	}
   512  
   513  	manc := inst.ManifestChannel()
   514  	ticker := time.NewTicker(10 * time.Second)
   515  	defer ticker.Stop()
   516  	for {
   517  		select {
   518  		case man := <-manc:
   519  			if err := man.Error(); err != nil {
   520  				var b []byte
   521  				if b, err = json.Marshal(err.Error()); err == nil {
   522  					writeStream(w, "error", string(b))
   523  				}
   524  				return nil
   525  			}
   526  			buf := new(bytes.Buffer)
   527  			if err := jsonapi.WriteData(buf, &apiApp{man}, nil); err == nil {
   528  				writeStream(w, "state", strings.TrimSuffix(buf.String(), "\n"))
   529  			}
   530  			if s := man.State(); s == app.Ready || s == app.Installed || s == app.Errored {
   531  				return nil
   532  			}
   533  
   534  		case <-ticker.C:
   535  			_, _ = w.Write([]byte(": still working\r\n"))
   536  		}
   537  		if f, ok := w.(http.Flusher); ok {
   538  			f.Flush()
   539  		}
   540  	}
   541  }
   542  
   543  func writeStream(w http.ResponseWriter, event string, b string) {
   544  	s := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", event, b)
   545  	_, err := w.Write([]byte(s))
   546  	if err != nil {
   547  		return
   548  	}
   549  	if f, ok := w.(http.Flusher); ok {
   550  		f.Flush()
   551  	}
   552  }
   553  
   554  // listWebappsHandler handles all GET / requests which can be used to list
   555  // installed applications.
   556  func listWebappsHandler(c echo.Context) error {
   557  	instance := middlewares.GetInstance(c)
   558  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Apps); err != nil {
   559  		return err
   560  	}
   561  
   562  	// Adding the startKey if it is given in the request
   563  	startKey := c.QueryParam("start_key")
   564  
   565  	// Same for the limit
   566  	var limit int
   567  	if l := c.QueryParam("limit"); l != "" {
   568  		if converted, err := strconv.Atoi(l); err == nil {
   569  			limit = converted
   570  		}
   571  	}
   572  
   573  	docs, next, err := app.ListWebappsWithPagination(instance, limit, startKey)
   574  	if err != nil {
   575  		return wrapAppsError(err)
   576  	}
   577  	objs := make([]jsonapi.Object, len(docs))
   578  	for i, d := range docs {
   579  		d.Instance = instance
   580  		objs[i] = &apiApp{d}
   581  	}
   582  
   583  	// Generating links list for the next apps
   584  	links := generateLinksList(c, next, limit, next)
   585  
   586  	return jsonapi.DataList(c, http.StatusOK, objs, links)
   587  }
   588  
   589  // listKonnectorsHandler handles all GET / requests which can be used to list
   590  // installed applications.
   591  func listKonnectorsHandler(c echo.Context) error {
   592  	instance := middlewares.GetInstance(c)
   593  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Konnectors); err != nil {
   594  		return err
   595  	}
   596  
   597  	// Adding the startKey if it is given in the request
   598  	var startKey string
   599  	if sk := c.QueryParam("start_key"); sk != "" {
   600  		startKey = sk
   601  	}
   602  
   603  	// Same for the limit
   604  	var limit int
   605  	if l := c.QueryParam("limit"); l != "" {
   606  		if converted, err := strconv.Atoi(l); err == nil {
   607  			limit = converted
   608  		}
   609  	}
   610  	docs, next, err := app.ListKonnectorsWithPagination(instance, limit, startKey)
   611  	if err != nil {
   612  		return wrapAppsError(err)
   613  	}
   614  	objs := make([]jsonapi.Object, len(docs))
   615  	for i, d := range docs {
   616  		objs[i] = &apiApp{d}
   617  	}
   618  
   619  	// Generating links list for the next konnectors
   620  	links := generateLinksList(c, next, limit, next)
   621  
   622  	return jsonapi.DataList(c, http.StatusOK, objs, links)
   623  }
   624  
   625  func generateLinksList(c echo.Context, next string, limit int, nextID string) *jsonapi.LinksList {
   626  	links := &jsonapi.LinksList{}
   627  	if next != "" { // Do not generate the next URL if there are no next konnectors
   628  		nextURL := &url.URL{
   629  			Scheme: c.Scheme(),
   630  			Host:   c.Request().Host,
   631  			Path:   c.Path(),
   632  		}
   633  		values := nextURL.Query()
   634  		values.Set("start_key", nextID)
   635  		values.Set("limit", strconv.Itoa(limit))
   636  		nextURL.RawQuery = values.Encode()
   637  
   638  		links.Next = nextURL.String()
   639  	}
   640  	return links
   641  }
   642  
   643  // iconHandler gives the icon of an application
   644  func iconHandler(appType consts.AppType) echo.HandlerFunc {
   645  	return func(c echo.Context) error {
   646  		instance := middlewares.GetInstance(c)
   647  		slug := c.Param("slug")
   648  		version := c.Param("version")
   649  		a, err := app.GetBySlug(instance, slug, appType)
   650  		if err != nil {
   651  			if errors.Is(err, app.ErrNotFound) {
   652  				return jsonapi.NotFound(err)
   653  			}
   654  			return err
   655  		}
   656  
   657  		if !middlewares.IsLoggedIn(c) {
   658  			if err := middlewares.Allow(c, permission.GET, a); err != nil {
   659  				return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
   660  			}
   661  		}
   662  
   663  		if version != "" {
   664  			// The maximum cache-control recommanded is one year :
   665  			// https://www.ietf.org/rfc/rfc2616.txt
   666  			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
   667  		}
   668  
   669  		var fs appfs.FileServer
   670  		var filepath string
   671  		switch appType {
   672  		case consts.WebappType:
   673  			a := a.(*app.WebappManifest)
   674  			filepath = path.Join("/", a.Icon())
   675  			if a.FromAppsDir {
   676  				fs = app.FSForAppDir(slug)
   677  			} else {
   678  				fs = app.AppsFileServer(instance)
   679  			}
   680  		case consts.KonnectorType:
   681  			filepath = path.Join("/", a.Icon())
   682  			fs = app.KonnectorsFileServer(instance)
   683  		}
   684  
   685  		err = fs.ServeFileContent(c.Response(), c.Request(),
   686  			a.Slug(), a.Version(), a.Checksum(), filepath)
   687  		if os.IsNotExist(err) {
   688  			return echo.NewHTTPError(http.StatusNotFound, err)
   689  		}
   690  		return err
   691  	}
   692  }
   693  
   694  func createTrigger(c echo.Context) error {
   695  	inst := middlewares.GetInstance(c)
   696  	slug := c.Param("slug")
   697  	man, err := app.GetBySlug(inst, slug, consts.KonnectorType)
   698  	if err != nil {
   699  		return wrapAppsError(err)
   700  	}
   701  	var createdByApp string
   702  	if claims := c.Get("claims"); claims != nil {
   703  		cl := claims.(permission.Claims)
   704  		if cl.Subject != "" {
   705  			createdByApp = cl.Subject
   706  		}
   707  	}
   708  	t, err := man.(*app.KonnManifest).BuildTrigger(inst, c.QueryParam("AccountID"), createdByApp)
   709  	if err != nil {
   710  		return wrapAppsError(err)
   711  	}
   712  	if err = middlewares.Allow(c, permission.POST, t); err != nil {
   713  		return err
   714  	}
   715  	sched := job.System()
   716  	if err = sched.AddTrigger(t); err != nil {
   717  		return wrapAppsError(err)
   718  	}
   719  
   720  	if c.QueryParam("ExecNow") == "true" {
   721  		req := t.Infos().JobRequest()
   722  		req.Manual = true
   723  		_, _ = sched.PushJob(inst, req)
   724  	}
   725  
   726  	return jsonapi.Data(c, http.StatusCreated, jobs.NewAPITrigger(t.Infos(), inst), nil)
   727  }
   728  
   729  type apiOpenParams struct {
   730  	slug   string
   731  	cookie string
   732  	params serveParams
   733  }
   734  
   735  func (o *apiOpenParams) ID() string         { return o.slug }
   736  func (o *apiOpenParams) Rev() string        { return "" }
   737  func (o *apiOpenParams) DocType() string    { return consts.AppsOpenParameters }
   738  func (o *apiOpenParams) SetID(id string)    {}
   739  func (o *apiOpenParams) SetRev(rev string)  {}
   740  func (o *apiOpenParams) Clone() couchdb.Doc { return o }
   741  func (o *apiOpenParams) MarshalJSON() ([]byte, error) {
   742  	data := map[string]interface{}{}
   743  	data["Cookie"] = o.cookie
   744  	data["Token"] = o.params.Token
   745  	data["Domain"] = o.params.Domain()
   746  	data["SubDomain"] = o.params.SubDomain
   747  	data["Tracking"] = strconv.FormatBool(o.params.Tracking)
   748  	data["Locale"] = o.params.Locale()
   749  	data["AppEditor"] = o.params.AppEditor()
   750  	data["AppName"] = o.params.AppName()
   751  	data["AppNamePrefix"] = o.params.AppNamePrefix()
   752  	data["AppSlug"] = o.params.AppSlug()
   753  	data["IconPath"] = o.params.IconPath()
   754  	data["Flags"], _ = o.params.Flags()
   755  	data["Capabilities"], _ = o.params.Capabilities()
   756  	data["CozyBar"], _ = o.params.CozyBar()
   757  	data["CozyFonts"] = o.params.CozyFonts()
   758  	data["CozyClientJS"], _ = o.params.CozyClientJS()
   759  	data["ThemeCSS"] = o.params.ThemeCSS()
   760  	data["Favicon"] = o.params.Favicon()
   761  	data["DefaultWallpaper"] = o.params.DefaultWallpaper()
   762  	data["Warnings"], _ = o.params.Warnings()
   763  	return json.Marshal(data)
   764  }
   765  
   766  func (o *apiOpenParams) Relationships() jsonapi.RelationshipMap { return nil }
   767  func (o *apiOpenParams) Included() []jsonapi.Object             { return nil }
   768  func (o *apiOpenParams) Links() *jsonapi.LinksList {
   769  	return &jsonapi.LinksList{Self: "/apps/" + o.slug + "/open"}
   770  }
   771  
   772  // openWebapp handles GET /apps/:slug/open requests and returns the data useful
   773  // for the flagship app to open the given the webapp in a webview.
   774  func openWebapp(c echo.Context) error {
   775  	if err := middlewares.AllowMaximal(c); err != nil {
   776  		return wrapAppsError(err)
   777  	}
   778  
   779  	inst := middlewares.GetInstance(c)
   780  	slug := c.Param("slug")
   781  	webapp, err := app.GetWebappBySlug(inst, slug)
   782  	if err != nil {
   783  		return wrapAppsError(err)
   784  	}
   785  
   786  	var cookie *http.Cookie
   787  	sess, err := session.FromCookie(c, inst)
   788  	if err == nil {
   789  		cookie, err = c.Cookie(session.CookieName(inst))
   790  		if err != nil {
   791  			return wrapAppsError(err)
   792  		}
   793  		cookie.MaxAge = 0
   794  		cookie.Path = "/"
   795  		cookie.Domain = session.CookieDomain(inst)
   796  		cookie.Secure = !build.IsDevRelease()
   797  		cookie.HttpOnly = true
   798  		cookie.SameSite = http.SameSiteLaxMode
   799  	} else {
   800  		sess, err = session.New(inst, session.NormalRun)
   801  		if err != nil {
   802  			return wrapAppsError(err)
   803  		}
   804  		cookie, err = sess.ToCookie()
   805  		if err != nil {
   806  			return wrapAppsError(err)
   807  		}
   808  	}
   809  
   810  	isLoggedIn := true
   811  	params := buildServeParams(c, inst, webapp, isLoggedIn, sess.ID())
   812  	obj := &apiOpenParams{
   813  		slug:   slug,
   814  		cookie: cookie.String(),
   815  		params: params,
   816  	}
   817  	return jsonapi.Data(c, http.StatusOK, obj, nil)
   818  }
   819  
   820  // WebappsRoutes sets the routing for the web apps service
   821  func WebappsRoutes(router *echo.Group) {
   822  	router.GET("/", listWebappsHandler)
   823  	router.GET("/:slug", getHandler(consts.WebappType))
   824  	router.POST("/:slug", installHandler(consts.WebappType))
   825  	router.PUT("/:slug", updateHandler(consts.WebappType))
   826  	router.DELETE("/:slug", deleteHandler(consts.WebappType))
   827  	router.GET("/:slug/icon", iconHandler(consts.WebappType))
   828  	router.GET("/:slug/icon/:version", iconHandler(consts.WebappType))
   829  	router.GET("/:slug/open", openWebapp)
   830  	router.GET("/:slug/download", downloadHandler(consts.WebappType))
   831  	router.GET("/:slug/download/:version", downloadHandler(consts.WebappType))
   832  	router.POST("/:slug/logs", logsHandler(consts.WebappType))
   833  }
   834  
   835  // KonnectorRoutes sets the routing for the konnectors service
   836  func KonnectorRoutes(router *echo.Group) {
   837  	router.GET("/", listKonnectorsHandler)
   838  	router.GET("/:slug", getHandler(consts.KonnectorType))
   839  	router.POST("/:slug", installHandler(consts.KonnectorType))
   840  	router.PUT("/:slug", updateHandler(consts.KonnectorType))
   841  	router.DELETE("/:slug", deleteHandler(consts.KonnectorType))
   842  	router.GET("/:slug/icon", iconHandler(consts.KonnectorType))
   843  	router.GET("/:slug/icon/:version", iconHandler(consts.KonnectorType))
   844  	router.POST("/:slug/trigger", createTrigger)
   845  	router.GET("/:slug/download", downloadHandler(consts.KonnectorType))
   846  	router.GET("/:slug/download/:version", downloadHandler(consts.KonnectorType))
   847  	router.POST("/:slug/logs", logsHandler(consts.KonnectorType))
   848  }
   849  
   850  func wrapAppsError(err error) error {
   851  	switch err {
   852  	case app.ErrInvalidSlugName:
   853  		return jsonapi.InvalidParameter("slug", err)
   854  	case app.ErrAlreadyExists:
   855  		return jsonapi.Conflict(err)
   856  	case app.ErrNotFound:
   857  		return jsonapi.NotFound(err)
   858  	case app.ErrNotSupportedSource:
   859  		return jsonapi.InvalidParameter("Source", err)
   860  	case app.ErrManifestNotReachable:
   861  		return jsonapi.NotFound(err)
   862  	case app.ErrSourceNotReachable:
   863  		return jsonapi.BadRequest(err)
   864  	case app.ErrBadManifest:
   865  		return jsonapi.BadRequest(err)
   866  	case app.ErrMissingSource:
   867  		return jsonapi.BadRequest(err)
   868  	case app.ErrLinkedAppExists:
   869  		return jsonapi.BadRequest(err)
   870  	case limits.ErrRateLimitReached,
   871  		limits.ErrRateLimitExceeded:
   872  		return jsonapi.BadRequest(err)
   873  	}
   874  	if _, ok := err.(*url.Error); ok {
   875  		return jsonapi.InvalidParameter("Source", err)
   876  	}
   877  	return err
   878  }