oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "context"
5 "encoding/base64"
6 "net/http"
7 "strings"
8 "time"
9
10 "amadan.net/oficina/home/internal/home/directorydata"
11 "amadan.net/oficina/home/roster"
12)
13
14const rosterPath = "/roster"
15
16type rosterReadSource interface {
17 Read(context.Context, directorydata.RosterRequest, time.Time) (roster.Page, int, error)
18}
19type rosterHandler struct {
20 runtime rosterRuntime
21 directory string
22 source rosterReadSource
23 now func() time.Time
24}
25
26func newRosterHandler(cfg Runtime, source rosterReadSource) (*rosterHandler, error) {
27 if cfg.role != "directory" || !cfg.roster.valid(cfg.role, cfg.keys) || !directorydata.ValidOrigin(cfg.origins.Directory) || (cfg.roster.mode == "serve") != (source != nil) {
28 return nil, errExchangeConfig
29 }
30 return &rosterHandler{runtime: cfg.roster, directory: cfg.origins.Directory, source: source, now: time.Now}, nil
31}
32func rosterFailure(w http.ResponseWriter, status int) {
33 code := "unavailable"
34 switch status {
35 case 400:
36 code = "bad_request"
37 case 401:
38 code = "unauthorized"
39 case 404:
40 code = "not_found"
41 case 405:
42 code = "method_not_allowed"
43 default:
44 status = 503
45 }
46 w.WriteHeader(status)
47 _, _ = w.Write([]byte(`{"error":"` + code + `"}`))
48}
49func rosterQuery(r *http.Request, directory string) (directorydata.RosterRequest, bool) {
50 var req directorydata.RosterRequest
51 if r.URL.EscapedPath() != rosterPath || r.URL.IsAbs() || r.URL.Fragment != "" || r.ContentLength != 0 || len(r.TransferEncoding) != 0 || len(r.RequestURI) > 4096 || len(directory)+len(rosterPath)+1+len(r.URL.RawQuery) > 4096 {
52 return req, false
53 }
54 fields := strings.Split(r.URL.RawQuery, "&")
55 if len(fields) != 2 && len(fields) != 3 {
56 return req, false
57 }
58 seen := map[string]bool{}
59 for _, field := range fields {
60 name, value, ok := strings.Cut(field, "=")
61 if !ok || value == "" || seen[name] {
62 return req, false
63 }
64 seen[name] = true
65 switch name {
66 case "team":
67 if len(value) > 64 {
68 return req, false
69 }
70 for _, c := range value {
71 if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9') {
72 return req, false
73 }
74 }
75 req.Team = value
76 case "fetch":
77 if !roster.ValidChallenge(value) {
78 return req, false
79 }
80 req.Fetch = value
81 case "cursor":
82 if len(value) > roster.CursorLimit {
83 return req, false
84 }
85 b, e := base64.RawURLEncoding.Strict().DecodeString(value)
86 if e != nil || base64.RawURLEncoding.EncodeToString(b) != value {
87 return req, false
88 }
89 req.Cursor = value
90 default:
91 return req, false
92 }
93 }
94 return req, seen["team"] && seen["fetch"]
95}
96func rosterBearer(headers http.Header) (string, bool) {
97 var values []string
98 for name, v := range headers {
99 if strings.EqualFold(name, "Authorization") {
100 values = append(values, v...)
101 }
102 }
103 if len(values) != 1 || !strings.HasPrefix(values[0], "Bearer ") {
104 return "", false
105 }
106 token := strings.TrimPrefix(values[0], "Bearer ")
107 return token, roster.ValidChallenge(token)
108}
109func (h *rosterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
110 w.Header().Set("Cache-Control", "no-store, no-transform")
111 w.Header().Set("Content-Type", "application/json")
112 w.Header().Set("X-Content-Type-Options", "nosniff")
113 if r.Host != strings.TrimPrefix(h.directory, "https://") {
114 rosterFailure(w, 404)
115 return
116 }
117 if r.Method != http.MethodGet {
118 w.Header().Set("Allow", "GET")
119 rosterFailure(w, 405)
120 return
121 }
122 if h.runtime.mode != "serve" {
123 rosterFailure(w, 503)
124 return
125 }
126 req, ok := rosterQuery(r, h.directory)
127 if !ok {
128 rosterFailure(w, 400)
129 return
130 }
131 req.Token, ok = rosterBearer(r.Header)
132 if !ok {
133 rosterFailure(w, 401)
134 return
135 }
136 now := h.now()
137 if now.Unix() <= 0 {
138 rosterFailure(w, 503)
139 return
140 }
141 ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
142 defer cancel()
143 page, status, err := h.source.Read(ctx, req, now)
144 if err != nil {
145 if !(status == 400 && err == directorydata.ErrRosterBadRequest || status == 401 && err == directorydata.ErrRosterUnauthorized || status == 404 && err == directorydata.ErrRosterNotFound || status == 503 && err == directorydata.ErrUnavailable) {
146 status = 503
147 }
148 rosterFailure(w, status)
149 return
150 }
151 if !(status == 200 && page.Type == "page" || status == 409 && page.Type == "stale-cursor") {
152 rosterFailure(w, 503)
153 return
154 }
155 wire, err := roster.SignPage(h.runtime.signer, h.runtime.keyID, page)
156 if err != nil || len(wire) > roster.BodyLimit {
157 rosterFailure(w, 503)
158 return
159 }
160 w.WriteHeader(status)
161 _, _ = w.Write(wire)
162}
163