forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state
2
3import (
4 "context"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "log"
10 "log/slog"
11 "net/http"
12 "strings"
13 "time"
14
15 comatproto "github.com/bluesky-social/indigo/api/atproto"
16 "github.com/bluesky-social/indigo/atproto/syntax"
17 lexutil "github.com/bluesky-social/indigo/lex/util"
18 securejoin "github.com/cyphar/filepath-securejoin"
19 "github.com/go-chi/chi/v5"
20 "tangled.sh/tangled.sh/core/api/tangled"
21 "tangled.sh/tangled.sh/core/appview"
22 "tangled.sh/tangled.sh/core/appview/db"
23 "tangled.sh/tangled.sh/core/appview/oauth"
24 "tangled.sh/tangled.sh/core/appview/pages"
25 "tangled.sh/tangled.sh/core/jetstream"
26 "tangled.sh/tangled.sh/core/rbac"
27)
28
29type State struct {
30 db *db.DB
31 oauth *oauth.OAuth
32 enforcer *rbac.Enforcer
33 tidClock syntax.TIDClock
34 pages *pages.Pages
35 resolver *appview.Resolver
36 jc *jetstream.JetstreamClient
37 config *appview.Config
38}
39
40func Make(config *appview.Config) (*State, error) {
41 d, err := db.Make(config.Core.DbPath)
42 if err != nil {
43 return nil, err
44 }
45
46 enforcer, err := rbac.NewEnforcer(config.Core.DbPath)
47 if err != nil {
48 return nil, err
49 }
50
51 clock := syntax.NewTIDClock(0)
52
53 pgs := pages.NewPages(config)
54
55 resolver := appview.NewResolver()
56
57 oauth := oauth.NewOAuth(d, config)
58
59 wrapper := db.DbWrapper{d}
60 jc, err := jetstream.NewJetstreamClient(
61 config.Jetstream.Endpoint,
62 "appview",
63 []string{
64 tangled.GraphFollowNSID,
65 tangled.FeedStarNSID,
66 tangled.PublicKeyNSID,
67 tangled.RepoArtifactNSID,
68 tangled.ActorProfileNSID,
69 },
70 nil,
71 slog.Default(),
72 wrapper,
73 false,
74 )
75 if err != nil {
76 return nil, fmt.Errorf("failed to create jetstream client: %w", err)
77 }
78 err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
79 if err != nil {
80 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
81 }
82
83 state := &State{
84 d,
85 oauth,
86 enforcer,
87 clock,
88 pgs,
89 resolver,
90 jc,
91 config,
92 }
93
94 return state, nil
95}
96
97func TID(c *syntax.TIDClock) string {
98 return c.Next().String()
99}
100
101// func (s *State) Login(w http.ResponseWriter, r *http.Request) {
102// ctx := r.Context()
103
104// switch r.Method {
105// case http.MethodGet:
106// err := s.pages.Login(w, pages.LoginParams{})
107// if err != nil {
108// log.Printf("rendering login page: %s", err)
109// }
110
111// return
112// case http.MethodPost:
113// handle := strings.TrimPrefix(r.FormValue("handle"), "@")
114// appPassword := r.FormValue("app_password")
115
116// resolved, err := s.resolver.ResolveIdent(ctx, handle)
117// if err != nil {
118// log.Println("failed to resolve handle:", err)
119// s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
120// return
121// }
122
123// atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword)
124// if err != nil {
125// s.pages.Notice(w, "login-msg", "Invalid handle or password.")
126// return
127// }
128// sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
129
130// err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
131// if err != nil {
132// s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
133// return
134// }
135
136// log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
137
138// did := resolved.DID.String()
139// defaultKnot := "knot1.tangled.sh"
140
141// go func() {
142// log.Printf("adding %s to default knot", did)
143// err = s.enforcer.AddMember(defaultKnot, did)
144// if err != nil {
145// log.Println("failed to add user to knot1.tangled.sh: ", err)
146// return
147// }
148// err = s.enforcer.E.SavePolicy()
149// if err != nil {
150// log.Println("failed to add user to knot1.tangled.sh: ", err)
151// return
152// }
153
154// secret, err := db.GetRegistrationKey(s.db, defaultKnot)
155// if err != nil {
156// log.Println("failed to get registration key for knot1.tangled.sh")
157// return
158// }
159// signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev)
160// resp, err := signedClient.AddMember(did)
161// if err != nil {
162// log.Println("failed to add user to knot1.tangled.sh: ", err)
163// return
164// }
165
166// if resp.StatusCode != http.StatusNoContent {
167// log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
168// return
169// }
170// }()
171
172// s.pages.HxRedirect(w, "/")
173// return
174// }
175// }
176
177func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
178 s.oauth.ClearSession(r, w)
179 w.Header().Set("HX-Redirect", "/login")
180 w.WriteHeader(http.StatusSeeOther)
181}
182
183func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
184 user := s.oauth.GetUser(r)
185
186 timeline, err := db.MakeTimeline(s.db)
187 if err != nil {
188 log.Println(err)
189 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
190 }
191
192 var didsToResolve []string
193 for _, ev := range timeline {
194 if ev.Repo != nil {
195 didsToResolve = append(didsToResolve, ev.Repo.Did)
196 if ev.Source != nil {
197 didsToResolve = append(didsToResolve, ev.Source.Did)
198 }
199 }
200 if ev.Follow != nil {
201 didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid)
202 }
203 if ev.Star != nil {
204 didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did)
205 }
206 }
207
208 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
209 didHandleMap := make(map[string]string)
210 for _, identity := range resolvedIds {
211 if !identity.Handle.IsInvalidHandle() {
212 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
213 } else {
214 didHandleMap[identity.DID.String()] = identity.DID.String()
215 }
216 }
217
218 s.pages.Timeline(w, pages.TimelineParams{
219 LoggedInUser: user,
220 Timeline: timeline,
221 DidHandleMap: didHandleMap,
222 })
223
224 return
225}
226
227// requires auth
228func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
229 switch r.Method {
230 case http.MethodGet:
231 // list open registrations under this did
232
233 return
234 case http.MethodPost:
235 session, err := s.oauth.Store.Get(r, appview.SessionName)
236 if err != nil || session.IsNew {
237 log.Println("unauthorized attempt to generate registration key")
238 http.Error(w, "Forbidden", http.StatusUnauthorized)
239 return
240 }
241
242 did := session.Values[appview.SessionDid].(string)
243
244 // check if domain is valid url, and strip extra bits down to just host
245 domain := r.FormValue("domain")
246 if domain == "" {
247 http.Error(w, "Invalid form", http.StatusBadRequest)
248 return
249 }
250
251 key, err := db.GenerateRegistrationKey(s.db, domain, did)
252
253 if err != nil {
254 log.Println(err)
255 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
256 return
257 }
258
259 w.Write([]byte(key))
260 }
261}
262
263func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
264 user := chi.URLParam(r, "user")
265 user = strings.TrimPrefix(user, "@")
266
267 if user == "" {
268 w.WriteHeader(http.StatusBadRequest)
269 return
270 }
271
272 id, err := s.resolver.ResolveIdent(r.Context(), user)
273 if err != nil {
274 w.WriteHeader(http.StatusInternalServerError)
275 return
276 }
277
278 pubKeys, err := db.GetPublicKeys(s.db, id.DID.String())
279 if err != nil {
280 w.WriteHeader(http.StatusNotFound)
281 return
282 }
283
284 if len(pubKeys) == 0 {
285 w.WriteHeader(http.StatusNotFound)
286 return
287 }
288
289 for _, k := range pubKeys {
290 key := strings.TrimRight(k.Key, "\n")
291 w.Write([]byte(fmt.Sprintln(key)))
292 }
293}
294
295// create a signed request and check if a node responds to that
296func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
297 user := s.oauth.GetUser(r)
298
299 domain := chi.URLParam(r, "domain")
300 if domain == "" {
301 http.Error(w, "malformed url", http.StatusBadRequest)
302 return
303 }
304 log.Println("checking ", domain)
305
306 secret, err := db.GetRegistrationKey(s.db, domain)
307 if err != nil {
308 log.Printf("no key found for domain %s: %s\n", domain, err)
309 return
310 }
311
312 client, err := NewSignedClient(domain, secret, s.config.Core.Dev)
313 if err != nil {
314 log.Println("failed to create client to ", domain)
315 }
316
317 resp, err := client.Init(user.Did)
318 if err != nil {
319 w.Write([]byte("no dice"))
320 log.Println("domain was unreachable after 5 seconds")
321 return
322 }
323
324 if resp.StatusCode == http.StatusConflict {
325 log.Println("status conflict", resp.StatusCode)
326 w.Write([]byte("already registered, sorry!"))
327 return
328 }
329
330 if resp.StatusCode != http.StatusNoContent {
331 log.Println("status nok", resp.StatusCode)
332 w.Write([]byte("no dice"))
333 return
334 }
335
336 // verify response mac
337 signature := resp.Header.Get("X-Signature")
338 signatureBytes, err := hex.DecodeString(signature)
339 if err != nil {
340 return
341 }
342
343 expectedMac := hmac.New(sha256.New, []byte(secret))
344 expectedMac.Write([]byte("ok"))
345
346 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
347 log.Printf("response body signature mismatch: %x\n", signatureBytes)
348 return
349 }
350
351 tx, err := s.db.BeginTx(r.Context(), nil)
352 if err != nil {
353 log.Println("failed to start tx", err)
354 http.Error(w, err.Error(), http.StatusInternalServerError)
355 return
356 }
357 defer func() {
358 tx.Rollback()
359 err = s.enforcer.E.LoadPolicy()
360 if err != nil {
361 log.Println("failed to rollback policies")
362 }
363 }()
364
365 // mark as registered
366 err = db.Register(tx, domain)
367 if err != nil {
368 log.Println("failed to register domain", err)
369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
371 }
372
373 // set permissions for this did as owner
374 reg, err := db.RegistrationByDomain(tx, domain)
375 if err != nil {
376 log.Println("failed to register domain", err)
377 http.Error(w, err.Error(), http.StatusInternalServerError)
378 return
379 }
380
381 // add basic acls for this domain
382 err = s.enforcer.AddDomain(domain)
383 if err != nil {
384 log.Println("failed to setup owner of domain", err)
385 http.Error(w, err.Error(), http.StatusInternalServerError)
386 return
387 }
388
389 // add this did as owner of this domain
390 err = s.enforcer.AddOwner(domain, reg.ByDid)
391 if err != nil {
392 log.Println("failed to setup owner of domain", err)
393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
395 }
396
397 err = tx.Commit()
398 if err != nil {
399 log.Println("failed to commit changes", err)
400 http.Error(w, err.Error(), http.StatusInternalServerError)
401 return
402 }
403
404 err = s.enforcer.E.SavePolicy()
405 if err != nil {
406 log.Println("failed to update ACLs", err)
407 http.Error(w, err.Error(), http.StatusInternalServerError)
408 return
409 }
410
411 w.Write([]byte("check success"))
412}
413
414func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
415 domain := chi.URLParam(r, "domain")
416 if domain == "" {
417 http.Error(w, "malformed url", http.StatusBadRequest)
418 return
419 }
420
421 user := s.oauth.GetUser(r)
422 reg, err := db.RegistrationByDomain(s.db, domain)
423 if err != nil {
424 w.Write([]byte("failed to pull up registration info"))
425 return
426 }
427
428 var members []string
429 if reg.Registered != nil {
430 members, err = s.enforcer.GetUserByRole("server:member", domain)
431 if err != nil {
432 w.Write([]byte("failed to fetch member list"))
433 return
434 }
435 }
436
437 var didsToResolve []string
438 for _, m := range members {
439 didsToResolve = append(didsToResolve, m)
440 }
441 didsToResolve = append(didsToResolve, reg.ByDid)
442 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
443 didHandleMap := make(map[string]string)
444 for _, identity := range resolvedIds {
445 if !identity.Handle.IsInvalidHandle() {
446 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
447 } else {
448 didHandleMap[identity.DID.String()] = identity.DID.String()
449 }
450 }
451
452 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
453 isOwner := err == nil && ok
454
455 p := pages.KnotParams{
456 LoggedInUser: user,
457 DidHandleMap: didHandleMap,
458 Registration: reg,
459 Members: members,
460 IsOwner: isOwner,
461 }
462
463 s.pages.Knot(w, p)
464}
465
466// get knots registered by this user
467func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
468 // for now, this is just pubkeys
469 user := s.oauth.GetUser(r)
470 registrations, err := db.RegistrationsByDid(s.db, user.Did)
471 if err != nil {
472 log.Println(err)
473 }
474
475 s.pages.Knots(w, pages.KnotsParams{
476 LoggedInUser: user,
477 Registrations: registrations,
478 })
479}
480
481// list members of domain, requires auth and requires owner status
482func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
483 domain := chi.URLParam(r, "domain")
484 if domain == "" {
485 http.Error(w, "malformed url", http.StatusBadRequest)
486 return
487 }
488
489 // list all members for this domain
490 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
491 if err != nil {
492 w.Write([]byte("failed to fetch member list"))
493 return
494 }
495
496 w.Write([]byte(strings.Join(memberDids, "\n")))
497 return
498}
499
500// add member to domain, requires auth and requires invite access
501func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
502 domain := chi.URLParam(r, "domain")
503 if domain == "" {
504 http.Error(w, "malformed url", http.StatusBadRequest)
505 return
506 }
507
508 subjectIdentifier := r.FormValue("subject")
509 if subjectIdentifier == "" {
510 http.Error(w, "malformed form", http.StatusBadRequest)
511 return
512 }
513
514 subjectIdentity, err := s.resolver.ResolveIdent(r.Context(), subjectIdentifier)
515 if err != nil {
516 w.Write([]byte("failed to resolve member did to a handle"))
517 return
518 }
519 log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
520
521 // announce this relation into the firehose, store into owners' pds
522 client, err := s.oauth.AuthorizedClient(r)
523 if err != nil {
524 http.Error(w, "failed to authorize client", http.StatusInternalServerError)
525 return
526 }
527 currentUser := s.oauth.GetUser(r)
528 createdAt := time.Now().Format(time.RFC3339)
529 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
530 Collection: tangled.KnotMemberNSID,
531 Repo: currentUser.Did,
532 Rkey: appview.TID(),
533 Record: &lexutil.LexiconTypeDecoder{
534 Val: &tangled.KnotMember{
535 Subject: subjectIdentity.DID.String(),
536 Domain: domain,
537 CreatedAt: createdAt,
538 }},
539 })
540
541 // invalid record
542 if err != nil {
543 log.Printf("failed to create record: %s", err)
544 return
545 }
546 log.Println("created atproto record: ", resp.Uri)
547
548 secret, err := db.GetRegistrationKey(s.db, domain)
549 if err != nil {
550 log.Printf("no key found for domain %s: %s\n", domain, err)
551 return
552 }
553
554 ksClient, err := NewSignedClient(domain, secret, s.config.Core.Dev)
555 if err != nil {
556 log.Println("failed to create client to ", domain)
557 return
558 }
559
560 ksResp, err := ksClient.AddMember(subjectIdentity.DID.String())
561 if err != nil {
562 log.Printf("failed to make request to %s: %s", domain, err)
563 return
564 }
565
566 if ksResp.StatusCode != http.StatusNoContent {
567 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
568 return
569 }
570
571 err = s.enforcer.AddMember(domain, subjectIdentity.DID.String())
572 if err != nil {
573 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
574 return
575 }
576
577 w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String())))
578}
579
580func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
581}
582
583func validateRepoName(name string) error {
584 // check for path traversal attempts
585 if name == "." || name == ".." ||
586 strings.Contains(name, "/") || strings.Contains(name, "\\") {
587 return fmt.Errorf("Repository name contains invalid path characters")
588 }
589
590 // check for sequences that could be used for traversal when normalized
591 if strings.Contains(name, "./") || strings.Contains(name, "../") ||
592 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
593 return fmt.Errorf("Repository name contains invalid path sequence")
594 }
595
596 // then continue with character validation
597 for _, char := range name {
598 if !((char >= 'a' && char <= 'z') ||
599 (char >= 'A' && char <= 'Z') ||
600 (char >= '0' && char <= '9') ||
601 char == '-' || char == '_' || char == '.') {
602 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
603 }
604 }
605
606 // additional check to prevent multiple sequential dots
607 if strings.Contains(name, "..") {
608 return fmt.Errorf("Repository name cannot contain sequential dots")
609 }
610
611 // if all checks pass
612 return nil
613}
614
615func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
616 switch r.Method {
617 case http.MethodGet:
618 user := s.oauth.GetUser(r)
619 knots, err := s.enforcer.GetDomainsForUser(user.Did)
620 if err != nil {
621 s.pages.Notice(w, "repo", "Invalid user account.")
622 return
623 }
624
625 s.pages.NewRepo(w, pages.NewRepoParams{
626 LoggedInUser: user,
627 Knots: knots,
628 })
629
630 case http.MethodPost:
631 user := s.oauth.GetUser(r)
632
633 domain := r.FormValue("domain")
634 if domain == "" {
635 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
636 return
637 }
638
639 repoName := r.FormValue("name")
640 if repoName == "" {
641 s.pages.Notice(w, "repo", "Repository name cannot be empty.")
642 return
643 }
644
645 if err := validateRepoName(repoName); err != nil {
646 s.pages.Notice(w, "repo", err.Error())
647 return
648 }
649
650 defaultBranch := r.FormValue("branch")
651 if defaultBranch == "" {
652 defaultBranch = "main"
653 }
654
655 description := r.FormValue("description")
656
657 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
658 if err != nil || !ok {
659 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
660 return
661 }
662
663 existingRepo, err := db.GetRepo(s.db, user.Did, repoName)
664 if err == nil && existingRepo != nil {
665 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
666 return
667 }
668
669 secret, err := db.GetRegistrationKey(s.db, domain)
670 if err != nil {
671 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
672 return
673 }
674
675 client, err := NewSignedClient(domain, secret, s.config.Core.Dev)
676 if err != nil {
677 s.pages.Notice(w, "repo", "Failed to connect to knot server.")
678 return
679 }
680
681 rkey := appview.TID()
682 repo := &db.Repo{
683 Did: user.Did,
684 Name: repoName,
685 Knot: domain,
686 Rkey: rkey,
687 Description: description,
688 }
689
690 xrpcClient, err := s.oauth.AuthorizedClient(r)
691 if err != nil {
692 s.pages.Notice(w, "repo", "Failed to write record to PDS.")
693 return
694 }
695
696 createdAt := time.Now().Format(time.RFC3339)
697 atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
698 Collection: tangled.RepoNSID,
699 Repo: user.Did,
700 Rkey: rkey,
701 Record: &lexutil.LexiconTypeDecoder{
702 Val: &tangled.Repo{
703 Knot: repo.Knot,
704 Name: repoName,
705 CreatedAt: createdAt,
706 Owner: user.Did,
707 }},
708 })
709 if err != nil {
710 log.Printf("failed to create record: %s", err)
711 s.pages.Notice(w, "repo", "Failed to announce repository creation.")
712 return
713 }
714 log.Println("created repo record: ", atresp.Uri)
715
716 tx, err := s.db.BeginTx(r.Context(), nil)
717 if err != nil {
718 log.Println(err)
719 s.pages.Notice(w, "repo", "Failed to save repository information.")
720 return
721 }
722 defer func() {
723 tx.Rollback()
724 err = s.enforcer.E.LoadPolicy()
725 if err != nil {
726 log.Println("failed to rollback policies")
727 }
728 }()
729
730 resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
731 if err != nil {
732 s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
733 return
734 }
735
736 switch resp.StatusCode {
737 case http.StatusConflict:
738 s.pages.Notice(w, "repo", "A repository with that name already exists.")
739 return
740 case http.StatusInternalServerError:
741 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
742 case http.StatusNoContent:
743 // continue
744 }
745
746 repo.AtUri = atresp.Uri
747 err = db.AddRepo(tx, repo)
748 if err != nil {
749 log.Println(err)
750 s.pages.Notice(w, "repo", "Failed to save repository information.")
751 return
752 }
753
754 // acls
755 p, _ := securejoin.SecureJoin(user.Did, repoName)
756 err = s.enforcer.AddRepo(user.Did, domain, p)
757 if err != nil {
758 log.Println(err)
759 s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
760 return
761 }
762
763 err = tx.Commit()
764 if err != nil {
765 log.Println("failed to commit changes", err)
766 http.Error(w, err.Error(), http.StatusInternalServerError)
767 return
768 }
769
770 err = s.enforcer.E.SavePolicy()
771 if err != nil {
772 log.Println("failed to update ACLs", err)
773 http.Error(w, err.Error(), http.StatusInternalServerError)
774 return
775 }
776
777 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
778 return
779 }
780}