Raw
 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}