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 }