1package storage
2
3import (
4 "context"
5 "fmt"
6 "net/netip"
7 "strings"
8 "time"
9 "unicode"
10 "unicode/utf8"
11)
12
13const UserAgentMaxBytes = 256
14
15func CleanUserAgent(s string) string {
16 s = strings.Trim(s, " \t")
17 if !utf8.ValidString(s) {
18 return ""
19 }
20 for _, r := range s {
21 if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) {
22 return ""
23 }
24 }
25 if len(s) <= UserAgentMaxBytes {
26 return s
27 }
28 cut := UserAgentMaxBytes
29 for cut > 0 && !utf8.RuneStart(s[cut]) {
30 cut--
31 }
32 return s[:cut]
33}
34
35func validClientColumns(in CreateForwardSessionInput) error {
36 switch {
37 case in.UserAgent != "" && CleanUserAgent(in.UserAgent) != in.UserAgent:
38 return fmt.Errorf("user agent is not clean or over %d bytes", UserAgentMaxBytes)
39 case in.ClientID != "" && !validClientID(in.ClientID):
40 return fmt.Errorf("client id is not an OIDC client id")
41 case in.Address.IsValid() && (in.Address.Zone() != "" || in.Address.Is4In6()):
42 return fmt.Errorf("address carries a zone or is IPv4-mapped")
43 }
44 return nil
45}
46
47func validClientID(s string) bool {
48 if len(s) > 32 {
49 return false
50 }
51 for i := 0; i < len(s); i++ {
52 c := s[i]
53 if (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' {
54 return false
55 }
56 }
57 return true
58}
59
60func nullAddress(a netip.Addr) any {
61 if !a.IsValid() {
62 return nil
63 }
64 return a.String()
65}
66
67type SessionSeen struct {
68 ID string
69 At, StaleBefore time.Time
70}
71
72func (s *Store) TouchForwardSession(ctx context.Context, id string, at, staleBefore time.Time) (bool, error) {
73 changed, err := s.TouchForwardSessions(ctx, []SessionSeen{{ID: id, At: at, StaleBefore: staleBefore}})
74 return changed > 0, err
75}
76
77func (s *Store) TouchForwardSessions(ctx context.Context, seen []SessionSeen) (int64, error) {
78 const operation = "touch forward sessions"
79 if err := s.checkOpen(operation); err != nil {
80 return 0, err
81 }
82 tx, err := s.sql.BeginTx(ctx, nil)
83 if err != nil {
84 return 0, classify("begin "+operation, err)
85 }
86 defer tx.Rollback()
87 var changed int64
88 for _, one := range seen {
89 n, err := execCount(ctx, tx, operation, `
90UPDATE forward_sessions SET last_seen_at = ?
91WHERE id = ? AND revoked = 0 AND (last_seen_at IS NULL OR last_seen_at < ?)`, timeToInt(one.At), one.ID, timeToInt(one.StaleBefore))
92 if err != nil {
93 return 0, err
94 }
95 changed += n
96 }
97 return changed, classify("commit "+operation, tx.Commit())
98}