oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package calls
2
3import (
4 "bytes"
5 "context"
6 "database/sql"
7 "errors"
8 "fmt"
9 "net/http"
10 "strconv"
11
12 "amadan.net/oficina/home/connect"
13 "amadan.net/oficina/home/connect/linkstore"
14 "amadan.net/oficina/home/handoff"
15 "amadan.net/rastrillo/rastrillo/csrf"
16 "amadan.net/rastrillo/rastrillo/migrate"
17 "amadan.net/rastrillo/rastrillo/sessions"
18)
19
20var errHomeIdentity = errors.New("calls: Home identity refused")
21
22// errHomeRemoved is the definite subset of errHomeIdentity: the Home
23// link is refused, the person row is gone, or the person is no longer a
24// member. A database that could not be read is errHomeIdentity alone,
25// and never ends a session.
26var errHomeRemoved = fmt.Errorf("%w: removed", errHomeIdentity)
27
28type homeIntegration struct {
29 bridge *connect.Bridge
30 routes *connect.Routes
31 sessions *sessions.Sessions
32}
33
34func (a *app) wireHome() error {
35 h := a.cfg.home
36 if h == nil {
37 return nil
38 }
39 if _, err := migrate.Apply(context.Background(), a.db, migrate.Merge(migrate.MustFromFS(handoff.Migrations, "oficina_handoff"), migrate.MustFromFS(linkstore.Migrations, linkstore.SchemaNamespace))); err != nil {
40 return errHomeIdentity
41 }
42 sess, err := sessions.New(sessions.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, SigninPath: signinPath, Logger: a.logger})
43 if err != nil || sess.CookieName() != a.auth.SessionCookie() {
44 return errHomeIdentity
45 }
46 read := func(r *http.Request) (*connect.Session, error) {
47 s, ok := sess.From(r)
48 if !ok {
49 return nil, nil
50 }
51 return &connect.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime, At: s.At}, nil
52 }
53 b, err := connect.New(connect.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, Issuer: h.Issuer, ConsumerID: h.ConsumerID, Namespace: "oficina/calls", SessionCookie: sess.CookieName(), Keys: h.Keys, DEK: h.DEK, ReadSession: read,
54 MintHomeSession: func(w http.ResponseWriter, r *http.Request, s connect.Session) error {
55 return sess.SignIn(w, r, sessions.Session{Subject: s.Subject, Method: s.Method, AuthTime: s.AuthTime})
56 },
57 LookupLocalSession: func(ctx context.Context, s connect.Session) (connect.Person, error) {
58 if s.Method != "magiclink" && s.Method != "keymail" {
59 return connect.Person{}, errHomeIdentity
60 }
61 p, err := a.store.existingHomePerson(ctx, s.Subject)
62 if err != nil || !a.isMember(p) {
63 return connect.Person{}, errHomeIdentity
64 }
65 return connect.Person{ID: p.ID, Label: p.Email}, nil
66 },
67 LookupPerson: func(ctx context.Context, q linkstore.Queryer, id int64) (string, error) {
68 p, err := a.store.homePersonByID(ctx, q, id)
69 if err != nil {
70 return "", err
71 }
72 if !a.isMember(p) {
73 return "", sql.ErrNoRows
74 }
75 return p.Email, nil
76 }, Render: a.renderHome})
77 if err != nil {
78 return errHomeIdentity
79 }
80 routes, err := connect.NewRoutes(b, connect.RoutesConfig{LocalLandingPath: "/continue", Namespace: "oficina/calls", Origin: a.cfg.Origin, Issuer: h.Issuer, FormPolicy: homeFormPolicy(h.Issuer), SessionCookie: sess.CookieName(), DEK: h.DEK, ReadSession: read, SignOut: sess.SignOut, LocalSignin: a.localSignin, Protect: csrf.Protect(a.cfg.Origin), Render: a.renderHome})
81 if err != nil {
82 return errHomeIdentity
83 }
84 a.home = &homeIntegration{b, routes, sess}
85 return nil
86}
87
88func (a *app) personForSession(ctx context.Context, s sessions.Session) (Person, error) {
89 if s.Method == "guest" {
90 id, err := strconv.ParseInt(s.Subject, 10, 64)
91 if err != nil {
92 return Person{}, errHomeIdentity
93 }
94 p, ok, err := a.store.personByID(ctx, id)
95 if err != nil || !ok || !p.Anonymous {
96 return Person{}, errHomeIdentity
97 }
98 return p, nil
99 }
100 if s.Method == connect.HomeMethod {
101 if a.home == nil {
102 return Person{}, errHomeIdentity
103 }
104 linked, err := a.home.bridge.Resolve(ctx, s.Subject)
105 if errors.Is(err, linkstore.ErrRefused) {
106 return Person{}, errHomeRemoved
107 }
108 if err != nil {
109 return Person{}, errHomeIdentity
110 }
111 p, err := a.store.homePersonByID(ctx, a.db.Writer(), linked.ID)
112 if errors.Is(err, sql.ErrNoRows) {
113 return Person{}, errHomeRemoved
114 }
115 if err != nil {
116 return Person{}, errHomeIdentity
117 }
118 if !a.isMember(p) {
119 return Person{}, errHomeRemoved
120 }
121 return p, nil
122 }
123 if s.Method != "magiclink" && s.Method != "keymail" {
124 return Person{}, errHomeIdentity
125 }
126 return a.store.ensurePerson(ctx, s.Subject)
127}
128
129// signin is GET /signin. A session that no longer resolves to a person
130// who may be here is ended first and told so: with Home configured,
131// connect's Signin skips straight to the destination for any live
132// session cookie, and the destination would refuse it again — the loop
133// Astra found in Docs (#65, finding 1) and Sheets (#39, finding 2).
134func (a *app) signin(w http.ResponseWriter, r *http.Request) {
135 if id, ok := a.auth.SessionFrom(r); ok {
136 if _, err := a.lookupPerson(r, sessions.Session{Subject: id.Address, Method: string(id.Method)}); errors.Is(err, errHomeRemoved) {
137 a.endRemovedSession(w, r)
138 return
139 }
140 }
141 if a.home != nil {
142 if _, pending := a.readJoinCookie(r); !pending {
143 a.home.sessions.Middleware(http.HandlerFunc(a.home.routes.Signin)).ServeHTTP(w, r)
144 return
145 }
146 }
147 a.localSignin(w, r)
148}
149
150// endRemovedSession ends this request's session and sends a page request
151// to the sign-in form with the reason; anything else gets the 403 a
152// fetch reads as signed out (room.js's readRefusal).
153func (a *app) endRemovedSession(w http.ResponseWriter, r *http.Request) {
154 a.nativeAuth.sessions.SignOut(w, r)
155 if r.Method != http.MethodGet && r.Method != http.MethodHead {
156 plainError(w, http.StatusForbidden)
157 return
158 }
159 w.Header().Set("Cache-Control", "no-store")
160 http.Redirect(w, r, a.signinAgain(r, "err=removed"), http.StatusSeeOther)
161}
162
163func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label string, status int) {
164 home := connect.Page(state, label)
165 if state == "signedout" {
166 // Where Sign out lands: the Oficina door, in the door's words
167 // (connect's name the hub); its Sign in goes to connect's action.
168 a.renderSignedOut(w, r, home.Action, status)
169 return
170 }
171 var body bytes.Buffer
172 if err := pages["home"].ExecuteTemplate(&body, "layout", a.page(r, titled(home.Title), map[string]any{"Home": home})); err != nil {
173 a.ErrorPage(w, r, 500, "")
174 return
175 }
176 w.Header().Set("Content-Type", "text/html; charset=utf-8")
177 w.Header().Set("Cache-Control", "no-store")
178 w.WriteHeader(status)
179 _, _ = w.Write(body.Bytes())
180}
181func (a *app) showHomeConnection(r *http.Request) bool {
182 s, ok := sessions.Current(r)
183 return a.home != nil && ok && (s.Method == "magiclink" || s.Method == "keymail")
184}
185