tinyrelayfile
tinyrelay/internal/daemon/session.go/

internal/daemon/session.go raw
1package daemon
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "database/sql"
8 "encoding/hex"
9 "errors"
10 "net/http"
11 "net/url"
12 "strings"
13 "time"
14)
15
16const sessionCookie = "tiny_session"
17
18// requestURL binds signatures to the original external request, including a
19// tenant path stripped by the router. Forwarded headers are never trusted.
20func (t *Tenant) requestURL(r *http.Request) string {
21 scheme := "http"
22 if r.TLS != nil {
23 scheme = "https"
24 } else if base, err := url.Parse(t.app.cfg.PublicURL); err == nil && base.Scheme == "https" {
25 scheme = "https"
26 }
27 requestURI := r.RequestURI
28 if requestURI == "" {
29 requestURI = r.URL.RequestURI()
30 }
31 if strings.HasPrefix(requestURI, "http://") || strings.HasPrefix(requestURI, "https://") {
32 if u, err := url.Parse(requestURI); err == nil {
33 requestURI = u.RequestURI()
34 }
35 }
36 return scheme + "://" + r.Host + requestURI
37}
38
39func (t *Tenant) initSessions(ctx context.Context) error {
40 _, err := t.store.DB().ExecContext(ctx, `CREATE TABLE IF NOT EXISTS browser_sessions(token_hash TEXT PRIMARY KEY,pubkey TEXT NOT NULL,expires_at INTEGER NOT NULL)`)
41 return err
42}
43
44func sessionHash(token string) string {
45 sum := sha256.Sum256([]byte(token))
46 return hex.EncodeToString(sum[:])
47}
48
49func (t *Tenant) cookieActor(r *http.Request) (string, error) {
50 cookie, err := r.Cookie(sessionCookie)
51 if errors.Is(err, http.ErrNoCookie) {
52 return "", nil
53 }
54 if err != nil {
55 return "", err
56 }
57 var actor string
58 err = t.store.DB().QueryRowContext(r.Context(), "SELECT pubkey FROM browser_sessions WHERE token_hash=? AND expires_at>?", sessionHash(cookie.Value), time.Now().Unix()).Scan(&actor)
59 if errors.Is(err, sql.ErrNoRows) {
60 return "", nil
61 }
62 if err != nil {
63 return "", err
64 }
65 banned, err := t.community.IsBanned(r.Context(), actor)
66 if banned {
67 return "", errors.New("blocked: this pubkey is banned")
68 }
69 return actor, err
70}
71
72func (t *Tenant) sessionHTTP(w http.ResponseWriter, r *http.Request) {
73 if r.Method != http.MethodPost {
74 w.Header().Set("Allow", "POST")
75 w.WriteHeader(http.StatusMethodNotAllowed)
76 return
77 }
78 base, err := url.Parse(t.requestURL(r))
79 if err != nil {
80 http.Error(w, "invalid relay URL", 500)
81 return
82 }
83 // Cookie-only requests may change session state only from this origin.
84 if origin := r.Header.Get("Origin"); origin != "" && origin != base.Scheme+"://"+base.Host {
85 http.Error(w, "foreign origin", http.StatusForbidden)
86 return
87 }
88 cookiePath := strings.TrimSuffix(base.Path, r.URL.Path) + "/"
89 cookie := &http.Cookie{Name: sessionCookie, Path: cookiePath, HttpOnly: true, Secure: base.Scheme == "https", SameSite: http.SameSiteStrictMode}
90 if r.URL.Path == "/session/logout" {
91 if r.Header.Get("Origin") == "" && r.Header.Get("Authorization") == "" {
92 http.Error(w, "origin or signature required", http.StatusForbidden)
93 return
94 }
95 if r.Header.Get("Origin") == "" {
96 if _, err := t.resolveUIActor(r); err != nil {
97 http.Error(w, "invalid logout signature", http.StatusUnauthorized)
98 return
99 }
100 }
101 if prior, err := r.Cookie(sessionCookie); err == nil {
102 if _, err := t.store.DB().ExecContext(r.Context(), "DELETE FROM browser_sessions WHERE token_hash=?", sessionHash(prior.Value)); err != nil {
103 http.Error(w, "logout failed", 500)
104 return
105 }
106 }
107 cookie.MaxAge = -1
108 http.SetCookie(w, cookie)
109 writeJSON(w, 200, map[string]any{"ok": true})
110 return
111 }
112 body, err := t.requestBody(w, r)
113 if err != nil {
114 http.Error(w, err.Error(), 400)
115 return
116 }
117 session, err := t.session(r, body)
118 if err != nil {
119 http.Error(w, err.Error(), 401)
120 return
121 }
122 actor := first(session.PubKeys)
123 banned, err := t.community.IsBanned(r.Context(), actor)
124 if err != nil {
125 http.Error(w, "login failed", 500)
126 return
127 }
128 if banned {
129 http.Error(w, "blocked: this pubkey is banned", 403)
130 return
131 }
132 var token [32]byte
133 if _, err := rand.Read(token[:]); err != nil {
134 http.Error(w, "login failed", 500)
135 return
136 }
137 cookie.Value = hex.EncodeToString(token[:])
138 cookie.MaxAge = int((24 * time.Hour) / time.Second)
139 cookie.Expires = time.Now().Add(24 * time.Hour)
140 if _, err := t.store.DB().ExecContext(r.Context(), "INSERT INTO browser_sessions(token_hash,pubkey,expires_at) VALUES(?,?,?)", sessionHash(cookie.Value), actor, cookie.Expires.Unix()); err != nil {
141 http.Error(w, "login failed", 500)
142 return
143 }
144 http.SetCookie(w, cookie)
145 if actor == t.Policy().Owner {
146 if err := t.records.Heartbeat(r.Context(), actor, time.Now().Unix()); err != nil {
147 t.app.telemetry.Logger().Error("owner heartbeat", "error", err)
148 }
149 }
150 writeJSON(w, 200, map[string]any{"pubkey": actor})
151}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs