Signed-off-by: oppiliappan me@oppi.li
+5
-4
appview/db/registration.go
+5
-4
appview/db/registration.go
···
10
)
11
12
type Registration struct {
13
Domain string
14
ByDid string
15
Created *time.Time
···
36
var registrations []Registration
37
38
rows, err := e.Query(`
39
-
select domain, did, created, registered from registrations
40
where did = ?
41
`, did)
42
if err != nil {
···
47
var createdAt *string
48
var registeredAt *string
49
var registration Registration
50
-
err = rows.Scan(®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
51
52
if err != nil {
53
log.Println(err)
···
75
var registration Registration
76
77
err := e.QueryRow(`
78
-
select domain, did, created, registered from registrations
79
where domain = ?
80
-
`, domain).Scan(®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
81
82
if err != nil {
83
if err == sql.ErrNoRows {
···
10
)
11
12
type Registration struct {
13
+
Id int64
14
Domain string
15
ByDid string
16
Created *time.Time
···
37
var registrations []Registration
38
39
rows, err := e.Query(`
40
+
select id, domain, did, created, registered from registrations
41
where did = ?
42
`, did)
43
if err != nil {
···
48
var createdAt *string
49
var registeredAt *string
50
var registration Registration
51
+
err = rows.Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
52
53
if err != nil {
54
log.Println(err)
···
76
var registration Registration
77
78
err := e.QueryRow(`
79
+
select id, domain, did, created, registered from registrations
80
where domain = ?
81
+
`, domain).Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
82
83
if err != nil {
84
if err == sql.ErrNoRows {
+482
appview/knots/knots.go
+482
appview/knots/knots.go
···
···
1
+
package knots
2
+
3
+
import (
4
+
"context"
5
+
"crypto/hmac"
6
+
"crypto/sha256"
7
+
"encoding/hex"
8
+
"fmt"
9
+
"log/slog"
10
+
"net/http"
11
+
"strings"
12
+
"time"
13
+
14
+
"github.com/go-chi/chi/v5"
15
+
"tangled.sh/tangled.sh/core/api/tangled"
16
+
"tangled.sh/tangled.sh/core/appview"
17
+
"tangled.sh/tangled.sh/core/appview/config"
18
+
"tangled.sh/tangled.sh/core/appview/db"
19
+
"tangled.sh/tangled.sh/core/appview/idresolver"
20
+
"tangled.sh/tangled.sh/core/appview/middleware"
21
+
"tangled.sh/tangled.sh/core/appview/oauth"
22
+
"tangled.sh/tangled.sh/core/appview/pages"
23
+
"tangled.sh/tangled.sh/core/eventconsumer"
24
+
"tangled.sh/tangled.sh/core/knotclient"
25
+
"tangled.sh/tangled.sh/core/rbac"
26
+
27
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
28
+
lexutil "github.com/bluesky-social/indigo/lex/util"
29
+
)
30
+
31
+
type Knots struct {
32
+
Db *db.DB
33
+
OAuth *oauth.OAuth
34
+
Pages *pages.Pages
35
+
Config *config.Config
36
+
Enforcer *rbac.Enforcer
37
+
IdResolver *idresolver.Resolver
38
+
Logger *slog.Logger
39
+
Knotstream *eventconsumer.Consumer
40
+
}
41
+
42
+
func (k *Knots) Router(mw *middleware.Middleware) http.Handler {
43
+
r := chi.NewRouter()
44
+
45
+
r.Use(middleware.AuthMiddleware(k.OAuth))
46
+
47
+
r.Get("/", k.index)
48
+
r.Post("/key", k.generateKey)
49
+
50
+
r.Route("/{domain}", func(r chi.Router) {
51
+
r.Post("/init", k.init)
52
+
r.Get("/", k.dashboard)
53
+
r.Route("/member", func(r chi.Router) {
54
+
r.Use(mw.KnotOwner())
55
+
r.Get("/", k.members)
56
+
r.Put("/", k.addMember)
57
+
r.Delete("/", k.removeMember)
58
+
})
59
+
})
60
+
61
+
return r
62
+
}
63
+
64
+
// get knots registered by this user
65
+
func (k *Knots) index(w http.ResponseWriter, r *http.Request) {
66
+
l := k.Logger.With("handler", "index")
67
+
68
+
user := k.OAuth.GetUser(r)
69
+
registrations, err := db.RegistrationsByDid(k.Db, user.Did)
70
+
if err != nil {
71
+
l.Error("failed to get registrations by did", "err", err)
72
+
}
73
+
74
+
k.Pages.Knots(w, pages.KnotsParams{
75
+
LoggedInUser: user,
76
+
Registrations: registrations,
77
+
})
78
+
}
79
+
80
+
// requires auth
81
+
func (k *Knots) generateKey(w http.ResponseWriter, r *http.Request) {
82
+
l := k.Logger.With("handler", "generateKey")
83
+
84
+
user := k.OAuth.GetUser(r)
85
+
did := user.Did
86
+
l = l.With("did", did)
87
+
88
+
// check if domain is valid url, and strip extra bits down to just host
89
+
domain := r.FormValue("domain")
90
+
if domain == "" {
91
+
l.Error("empty domain")
92
+
http.Error(w, "Invalid form", http.StatusBadRequest)
93
+
return
94
+
}
95
+
l = l.With("domain", domain)
96
+
97
+
noticeId := "registration-error"
98
+
fail := func() {
99
+
k.Pages.Notice(w, noticeId, "Failed to generate registration key.")
100
+
}
101
+
102
+
key, err := db.GenerateRegistrationKey(k.Db, domain, did)
103
+
if err != nil {
104
+
l.Error("failed to generate registration key", "err", err)
105
+
fail()
106
+
return
107
+
}
108
+
109
+
allRegs, err := db.RegistrationsByDid(k.Db, did)
110
+
if err != nil {
111
+
l.Error("failed to generate registration key", "err", err)
112
+
fail()
113
+
return
114
+
}
115
+
116
+
k.Pages.KnotListingFull(w, pages.KnotListingFullParams{
117
+
Registrations: allRegs,
118
+
})
119
+
k.Pages.KnotSecret(w, pages.KnotSecretParams{
120
+
Secret: key,
121
+
})
122
+
}
123
+
124
+
// create a signed request and check if a node responds to that
125
+
func (k *Knots) init(w http.ResponseWriter, r *http.Request) {
126
+
l := k.Logger.With("handler", "init")
127
+
user := k.OAuth.GetUser(r)
128
+
129
+
noticeId := "operation-error"
130
+
defaultErr := "Failed to initialize knot. Try again later."
131
+
fail := func() {
132
+
k.Pages.Notice(w, noticeId, defaultErr)
133
+
}
134
+
135
+
domain := chi.URLParam(r, "domain")
136
+
if domain == "" {
137
+
http.Error(w, "malformed url", http.StatusBadRequest)
138
+
return
139
+
}
140
+
l = l.With("domain", domain)
141
+
142
+
l.Info("checking domain")
143
+
144
+
secret, err := db.GetRegistrationKey(k.Db, domain)
145
+
if err != nil {
146
+
l.Error("failed to get registration key for domain", "err", err)
147
+
fail()
148
+
return
149
+
}
150
+
151
+
client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev)
152
+
if err != nil {
153
+
l.Error("failed to create knotclient", "err", err)
154
+
fail()
155
+
return
156
+
}
157
+
158
+
resp, err := client.Init(user.Did)
159
+
if err != nil {
160
+
k.Pages.Notice(w, noticeId, fmt.Sprintf("Failed to make request: %s", err.Error()))
161
+
l.Error("failed to make init request", "err", err)
162
+
return
163
+
}
164
+
165
+
if resp.StatusCode == http.StatusConflict {
166
+
k.Pages.Notice(w, noticeId, "This knot is already registered")
167
+
l.Error("knot already registered", "statuscode", resp.StatusCode)
168
+
return
169
+
}
170
+
171
+
if resp.StatusCode != http.StatusNoContent {
172
+
k.Pages.Notice(w, noticeId, fmt.Sprintf("Received status %d from knot, expected %d", resp.StatusCode, http.StatusNoContent))
173
+
l.Error("incorrect statuscode returned", "statuscode", resp.StatusCode, "expected", http.StatusNoContent)
174
+
return
175
+
}
176
+
177
+
// verify response mac
178
+
signature := resp.Header.Get("X-Signature")
179
+
signatureBytes, err := hex.DecodeString(signature)
180
+
if err != nil {
181
+
return
182
+
}
183
+
184
+
expectedMac := hmac.New(sha256.New, []byte(secret))
185
+
expectedMac.Write([]byte("ok"))
186
+
187
+
if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
188
+
k.Pages.Notice(w, noticeId, "Response signature mismatch, consider regenerating the secret and retrying.")
189
+
l.Error("signature mismatch", "bytes", signatureBytes)
190
+
return
191
+
}
192
+
193
+
tx, err := k.Db.BeginTx(r.Context(), nil)
194
+
if err != nil {
195
+
l.Error("failed to start tx", "err", err)
196
+
fail()
197
+
return
198
+
}
199
+
defer func() {
200
+
tx.Rollback()
201
+
err = k.Enforcer.E.LoadPolicy()
202
+
if err != nil {
203
+
l.Error("rollback failed", "err", err)
204
+
}
205
+
}()
206
+
207
+
// mark as registered
208
+
err = db.Register(tx, domain)
209
+
if err != nil {
210
+
l.Error("failed to register domain", "err", err)
211
+
fail()
212
+
return
213
+
}
214
+
215
+
// set permissions for this did as owner
216
+
reg, err := db.RegistrationByDomain(tx, domain)
217
+
if err != nil {
218
+
l.Error("failed get registration by domain", "err", err)
219
+
fail()
220
+
return
221
+
}
222
+
223
+
// add basic acls for this domain
224
+
err = k.Enforcer.AddKnot(domain)
225
+
if err != nil {
226
+
l.Error("failed to add knot to enforcer", "err", err)
227
+
fail()
228
+
return
229
+
}
230
+
231
+
// add this did as owner of this domain
232
+
err = k.Enforcer.AddKnotOwner(domain, reg.ByDid)
233
+
if err != nil {
234
+
l.Error("failed to add knot owner to enforcer", "err", err)
235
+
fail()
236
+
return
237
+
}
238
+
239
+
err = tx.Commit()
240
+
if err != nil {
241
+
l.Error("failed to commit changes", "err", err)
242
+
fail()
243
+
return
244
+
}
245
+
246
+
err = k.Enforcer.E.SavePolicy()
247
+
if err != nil {
248
+
l.Error("failed to update ACLs", "err", err)
249
+
fail()
250
+
return
251
+
}
252
+
253
+
// add this knot to knotstream
254
+
go k.Knotstream.AddSource(
255
+
context.Background(),
256
+
eventconsumer.NewKnotSource(domain),
257
+
)
258
+
259
+
k.Pages.KnotListing(w, pages.KnotListingParams{
260
+
Registration: *reg,
261
+
})
262
+
}
263
+
264
+
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
265
+
l := k.Logger.With("handler", "dashboard")
266
+
fail := func() {
267
+
w.WriteHeader(http.StatusInternalServerError)
268
+
}
269
+
270
+
domain := chi.URLParam(r, "domain")
271
+
if domain == "" {
272
+
http.Error(w, "malformed url", http.StatusBadRequest)
273
+
return
274
+
}
275
+
l = l.With("domain", domain)
276
+
277
+
user := k.OAuth.GetUser(r)
278
+
l = l.With("did", user.Did)
279
+
280
+
// dashboard is only available to owners
281
+
ok, err := k.Enforcer.IsKnotOwner(user.Did, domain)
282
+
if err != nil {
283
+
l.Error("failed to query enforcer", "err", err)
284
+
fail()
285
+
}
286
+
if !ok {
287
+
http.Error(w, "only owners can view dashboards", http.StatusUnauthorized)
288
+
return
289
+
}
290
+
291
+
reg, err := db.RegistrationByDomain(k.Db, domain)
292
+
if err != nil {
293
+
l.Error("failed to get registration by domain", "err", err)
294
+
fail()
295
+
return
296
+
}
297
+
298
+
var members []string
299
+
if reg.Registered != nil {
300
+
members, err = k.Enforcer.GetUserByRole("server:member", domain)
301
+
if err != nil {
302
+
l.Error("failed to get members list", "err", err)
303
+
fail()
304
+
return
305
+
}
306
+
}
307
+
308
+
repos, err := db.GetRepos(
309
+
k.Db,
310
+
db.FilterEq("knot", domain),
311
+
db.FilterIn("did", members),
312
+
)
313
+
if err != nil {
314
+
l.Error("failed to get repos list", "err", err)
315
+
fail()
316
+
return
317
+
}
318
+
// convert to map
319
+
repoByMember := make(map[string][]db.Repo)
320
+
for _, r := range repos {
321
+
repoByMember[r.Did] = append(repoByMember[r.Did], r)
322
+
}
323
+
324
+
var didsToResolve []string
325
+
for _, m := range members {
326
+
didsToResolve = append(didsToResolve, m)
327
+
}
328
+
didsToResolve = append(didsToResolve, reg.ByDid)
329
+
resolvedIds := k.IdResolver.ResolveIdents(r.Context(), didsToResolve)
330
+
didHandleMap := make(map[string]string)
331
+
for _, identity := range resolvedIds {
332
+
if !identity.Handle.IsInvalidHandle() {
333
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
334
+
} else {
335
+
didHandleMap[identity.DID.String()] = identity.DID.String()
336
+
}
337
+
}
338
+
339
+
k.Pages.Knot(w, pages.KnotParams{
340
+
LoggedInUser: user,
341
+
DidHandleMap: didHandleMap,
342
+
Registration: reg,
343
+
Members: members,
344
+
Repos: repoByMember,
345
+
IsOwner: true,
346
+
})
347
+
}
348
+
349
+
// list members of domain, requires auth and requires owner status
350
+
func (k *Knots) members(w http.ResponseWriter, r *http.Request) {
351
+
l := k.Logger.With("handler", "members")
352
+
353
+
domain := chi.URLParam(r, "domain")
354
+
if domain == "" {
355
+
http.Error(w, "malformed url", http.StatusBadRequest)
356
+
return
357
+
}
358
+
l = l.With("domain", domain)
359
+
360
+
// list all members for this domain
361
+
memberDids, err := k.Enforcer.GetUserByRole("server:member", domain)
362
+
if err != nil {
363
+
w.Write([]byte("failed to fetch member list"))
364
+
return
365
+
}
366
+
367
+
w.Write([]byte(strings.Join(memberDids, "\n")))
368
+
return
369
+
}
370
+
371
+
// add member to domain, requires auth and requires invite access
372
+
func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
373
+
l := k.Logger.With("handler", "members")
374
+
375
+
domain := chi.URLParam(r, "domain")
376
+
if domain == "" {
377
+
http.Error(w, "malformed url", http.StatusBadRequest)
378
+
return
379
+
}
380
+
l = l.With("domain", domain)
381
+
382
+
reg, err := db.RegistrationByDomain(k.Db, domain)
383
+
if err != nil {
384
+
l.Error("failed to get registration by domain", "err", err)
385
+
http.Error(w, "malformed url", http.StatusBadRequest)
386
+
return
387
+
}
388
+
389
+
noticeId := fmt.Sprintf("add-member-error-%d", reg.Id)
390
+
l = l.With("notice-id", noticeId)
391
+
defaultErr := "Failed to add member. Try again later."
392
+
fail := func() {
393
+
k.Pages.Notice(w, noticeId, defaultErr)
394
+
}
395
+
396
+
subjectIdentifier := r.FormValue("subject")
397
+
if subjectIdentifier == "" {
398
+
http.Error(w, "malformed form", http.StatusBadRequest)
399
+
return
400
+
}
401
+
l = l.With("subjectIdentifier", subjectIdentifier)
402
+
403
+
subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier)
404
+
if err != nil {
405
+
l.Error("failed to resolve identity", "err", err)
406
+
k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
407
+
return
408
+
}
409
+
l = l.With("subjectDid", subjectIdentity.DID)
410
+
411
+
l.Info("adding member to knot")
412
+
413
+
// announce this relation into the firehose, store into owners' pds
414
+
client, err := k.OAuth.AuthorizedClient(r)
415
+
if err != nil {
416
+
l.Error("failed to create client", "err", err)
417
+
fail()
418
+
return
419
+
}
420
+
421
+
currentUser := k.OAuth.GetUser(r)
422
+
createdAt := time.Now().Format(time.RFC3339)
423
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
424
+
Collection: tangled.KnotMemberNSID,
425
+
Repo: currentUser.Did,
426
+
Rkey: appview.TID(),
427
+
Record: &lexutil.LexiconTypeDecoder{
428
+
Val: &tangled.KnotMember{
429
+
Subject: subjectIdentity.DID.String(),
430
+
Domain: domain,
431
+
CreatedAt: createdAt,
432
+
}},
433
+
})
434
+
// invalid record
435
+
if err != nil {
436
+
l.Error("failed to write to PDS", "err", err)
437
+
fail()
438
+
return
439
+
}
440
+
l = l.With("at-uri", resp.Uri)
441
+
l.Info("wrote record to PDS")
442
+
443
+
secret, err := db.GetRegistrationKey(k.Db, domain)
444
+
if err != nil {
445
+
l.Error("failed to get registration key", "err", err)
446
+
fail()
447
+
return
448
+
}
449
+
450
+
ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev)
451
+
if err != nil {
452
+
l.Error("failed to create client", "err", err)
453
+
fail()
454
+
return
455
+
}
456
+
457
+
ksResp, err := ksClient.AddMember(subjectIdentity.DID.String())
458
+
if err != nil {
459
+
l.Error("failed to reach knotserver", "err", err)
460
+
k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.")
461
+
return
462
+
}
463
+
464
+
if ksResp.StatusCode != http.StatusNoContent {
465
+
l.Error("status mismatch", "got", ksResp.StatusCode, "expected", http.StatusNoContent)
466
+
k.Pages.Notice(w, noticeId, fmt.Sprintf("Unexpected status code from knotserver %d, expected %d", ksResp.StatusCode, http.StatusNoContent))
467
+
return
468
+
}
469
+
470
+
err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String())
471
+
if err != nil {
472
+
l.Error("failed to add member to enforcer", "err", err)
473
+
fail()
474
+
return
475
+
}
476
+
477
+
// success
478
+
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
479
+
}
480
+
481
+
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
482
+
}
+19
-18
appview/state/router.go
+19
-18
appview/state/router.go
···
7
"github.com/go-chi/chi/v5"
8
"github.com/gorilla/sessions"
9
"tangled.sh/tangled.sh/core/appview/issues"
10
"tangled.sh/tangled.sh/core/appview/middleware"
11
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
12
"tangled.sh/tangled.sh/core/appview/pipelines"
···
101
102
r.Get("/", s.Timeline)
103
104
-
r.Route("/knots", func(r chi.Router) {
105
-
r.Use(middleware.AuthMiddleware(s.oauth))
106
-
r.Get("/", s.Knots)
107
-
r.Post("/key", s.RegistrationKey)
108
-
109
-
r.Route("/{domain}", func(r chi.Router) {
110
-
r.Post("/init", s.InitKnotServer)
111
-
r.Get("/", s.KnotServerInfo)
112
-
r.Route("/member", func(r chi.Router) {
113
-
r.Use(mw.KnotOwner())
114
-
r.Get("/", s.ListMembers)
115
-
r.Put("/", s.AddMember)
116
-
r.Delete("/", s.RemoveMember)
117
-
})
118
-
})
119
-
})
120
-
121
r.Route("/repo", func(r chi.Router) {
122
r.Route("/new", func(r chi.Router) {
123
r.Use(middleware.AuthMiddleware(s.oauth))
···
151
})
152
153
r.Mount("/settings", s.SettingsRouter())
154
r.Mount("/spindles", s.SpindlesRouter())
155
r.Mount("/", s.OAuthRouter())
156
···
195
return spindles.Router()
196
}
197
198
func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
199
issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog)
200
return issues.Router(mw)
201
-
202
}
203
204
func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {
···
7
"github.com/go-chi/chi/v5"
8
"github.com/gorilla/sessions"
9
"tangled.sh/tangled.sh/core/appview/issues"
10
+
"tangled.sh/tangled.sh/core/appview/knots"
11
"tangled.sh/tangled.sh/core/appview/middleware"
12
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
13
"tangled.sh/tangled.sh/core/appview/pipelines"
···
102
103
r.Get("/", s.Timeline)
104
105
r.Route("/repo", func(r chi.Router) {
106
r.Route("/new", func(r chi.Router) {
107
r.Use(middleware.AuthMiddleware(s.oauth))
···
135
})
136
137
r.Mount("/settings", s.SettingsRouter())
138
+
r.Mount("/knots", s.KnotsRouter(mw))
139
r.Mount("/spindles", s.SpindlesRouter())
140
r.Mount("/", s.OAuthRouter())
141
···
180
return spindles.Router()
181
}
182
183
+
func (s *State) KnotsRouter(mw *middleware.Middleware) http.Handler {
184
+
logger := log.New("knots")
185
+
186
+
knots := &knots.Knots{
187
+
Db: s.db,
188
+
OAuth: s.oauth,
189
+
Pages: s.pages,
190
+
Config: s.config,
191
+
Enforcer: s.enforcer,
192
+
IdResolver: s.idResolver,
193
+
Knotstream: s.knotstream,
194
+
Logger: logger,
195
+
}
196
+
197
+
return knots.Router(mw)
198
+
}
199
+
200
func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
201
issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog)
202
return issues.Router(mw)
203
}
204
205
func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {
+326
-323
appview/state/state.go
+326
-323
appview/state/state.go
···
2
3
import (
4
"context"
5
-
"crypto/hmac"
6
-
"crypto/sha256"
7
-
"encoding/hex"
8
"fmt"
9
"log"
10
"log/slog"
···
202
}
203
204
// requires auth
205
-
func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
206
-
switch r.Method {
207
-
case http.MethodGet:
208
-
// list open registrations under this did
209
-
210
-
return
211
-
case http.MethodPost:
212
-
session, err := s.oauth.Stores().Get(r, oauth.SessionName)
213
-
if err != nil || session.IsNew {
214
-
log.Println("unauthorized attempt to generate registration key")
215
-
http.Error(w, "Forbidden", http.StatusUnauthorized)
216
-
return
217
-
}
218
-
219
-
did := session.Values[oauth.SessionDid].(string)
220
-
221
-
// check if domain is valid url, and strip extra bits down to just host
222
-
domain := r.FormValue("domain")
223
-
if domain == "" {
224
-
http.Error(w, "Invalid form", http.StatusBadRequest)
225
-
return
226
-
}
227
-
228
-
key, err := db.GenerateRegistrationKey(s.db, domain, did)
229
-
230
-
if err != nil {
231
-
log.Println(err)
232
-
http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
233
-
return
234
-
}
235
-
236
-
w.Write([]byte(key))
237
-
}
238
-
}
239
240
func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
241
user := chi.URLParam(r, "user")
···
270
}
271
272
// create a signed request and check if a node responds to that
273
-
func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
274
-
user := s.oauth.GetUser(r)
275
-
276
-
domain := chi.URLParam(r, "domain")
277
-
if domain == "" {
278
-
http.Error(w, "malformed url", http.StatusBadRequest)
279
-
return
280
-
}
281
-
log.Println("checking ", domain)
282
-
283
-
secret, err := db.GetRegistrationKey(s.db, domain)
284
-
if err != nil {
285
-
log.Printf("no key found for domain %s: %s\n", domain, err)
286
-
return
287
-
}
288
-
289
-
client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
290
-
if err != nil {
291
-
log.Println("failed to create client to ", domain)
292
-
}
293
-
294
-
resp, err := client.Init(user.Did)
295
-
if err != nil {
296
-
w.Write([]byte("no dice"))
297
-
log.Println("domain was unreachable after 5 seconds")
298
-
return
299
-
}
300
-
301
-
if resp.StatusCode == http.StatusConflict {
302
-
log.Println("status conflict", resp.StatusCode)
303
-
w.Write([]byte("already registered, sorry!"))
304
-
return
305
-
}
306
-
307
-
if resp.StatusCode != http.StatusNoContent {
308
-
log.Println("status nok", resp.StatusCode)
309
-
w.Write([]byte("no dice"))
310
-
return
311
-
}
312
-
313
-
// verify response mac
314
-
signature := resp.Header.Get("X-Signature")
315
-
signatureBytes, err := hex.DecodeString(signature)
316
-
if err != nil {
317
-
return
318
-
}
319
-
320
-
expectedMac := hmac.New(sha256.New, []byte(secret))
321
-
expectedMac.Write([]byte("ok"))
322
-
323
-
if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
324
-
log.Printf("response body signature mismatch: %x\n", signatureBytes)
325
-
return
326
-
}
327
-
328
-
tx, err := s.db.BeginTx(r.Context(), nil)
329
-
if err != nil {
330
-
log.Println("failed to start tx", err)
331
-
http.Error(w, err.Error(), http.StatusInternalServerError)
332
-
return
333
-
}
334
-
defer func() {
335
-
tx.Rollback()
336
-
err = s.enforcer.E.LoadPolicy()
337
-
if err != nil {
338
-
log.Println("failed to rollback policies")
339
-
}
340
-
}()
341
-
342
-
// mark as registered
343
-
err = db.Register(tx, domain)
344
-
if err != nil {
345
-
log.Println("failed to register domain", err)
346
-
http.Error(w, err.Error(), http.StatusInternalServerError)
347
-
return
348
-
}
349
-
350
-
// set permissions for this did as owner
351
-
reg, err := db.RegistrationByDomain(tx, domain)
352
-
if err != nil {
353
-
log.Println("failed to register domain", err)
354
-
http.Error(w, err.Error(), http.StatusInternalServerError)
355
-
return
356
-
}
357
-
358
-
// add basic acls for this domain
359
-
err = s.enforcer.AddKnot(domain)
360
-
if err != nil {
361
-
log.Println("failed to setup owner of domain", err)
362
-
http.Error(w, err.Error(), http.StatusInternalServerError)
363
-
return
364
-
}
365
-
366
-
// add this did as owner of this domain
367
-
err = s.enforcer.AddKnotOwner(domain, reg.ByDid)
368
-
if err != nil {
369
-
log.Println("failed to setup owner of domain", err)
370
-
http.Error(w, err.Error(), http.StatusInternalServerError)
371
-
return
372
-
}
373
-
374
-
err = tx.Commit()
375
-
if err != nil {
376
-
log.Println("failed to commit changes", err)
377
-
http.Error(w, err.Error(), http.StatusInternalServerError)
378
-
return
379
-
}
380
-
381
-
err = s.enforcer.E.SavePolicy()
382
-
if err != nil {
383
-
log.Println("failed to update ACLs", err)
384
-
http.Error(w, err.Error(), http.StatusInternalServerError)
385
-
return
386
-
}
387
-
388
-
// add this knot to knotstream
389
-
go s.knotstream.AddSource(
390
-
context.Background(),
391
-
eventconsumer.NewKnotSource(domain),
392
-
)
393
-
394
-
w.Write([]byte("check success"))
395
-
}
396
-
397
-
func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
398
-
domain := chi.URLParam(r, "domain")
399
-
if domain == "" {
400
-
http.Error(w, "malformed url", http.StatusBadRequest)
401
-
return
402
-
}
403
-
404
-
user := s.oauth.GetUser(r)
405
-
reg, err := db.RegistrationByDomain(s.db, domain)
406
-
if err != nil {
407
-
w.Write([]byte("failed to pull up registration info"))
408
-
return
409
-
}
410
-
411
-
var members []string
412
-
if reg.Registered != nil {
413
-
members, err = s.enforcer.GetUserByRole("server:member", domain)
414
-
if err != nil {
415
-
w.Write([]byte("failed to fetch member list"))
416
-
return
417
-
}
418
-
}
419
-
420
-
var didsToResolve []string
421
-
for _, m := range members {
422
-
didsToResolve = append(didsToResolve, m)
423
-
}
424
-
didsToResolve = append(didsToResolve, reg.ByDid)
425
-
resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve)
426
-
didHandleMap := make(map[string]string)
427
-
for _, identity := range resolvedIds {
428
-
if !identity.Handle.IsInvalidHandle() {
429
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
430
-
} else {
431
-
didHandleMap[identity.DID.String()] = identity.DID.String()
432
-
}
433
-
}
434
-
435
-
ok, err := s.enforcer.IsKnotOwner(user.Did, domain)
436
-
isOwner := err == nil && ok
437
-
438
-
p := pages.KnotParams{
439
-
LoggedInUser: user,
440
-
DidHandleMap: didHandleMap,
441
-
Registration: reg,
442
-
Members: members,
443
-
IsOwner: isOwner,
444
-
}
445
-
446
-
s.pages.Knot(w, p)
447
-
}
448
449
// get knots registered by this user
450
-
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
451
-
// for now, this is just pubkeys
452
-
user := s.oauth.GetUser(r)
453
-
registrations, err := db.RegistrationsByDid(s.db, user.Did)
454
-
if err != nil {
455
-
log.Println(err)
456
-
}
457
-
458
-
s.pages.Knots(w, pages.KnotsParams{
459
-
LoggedInUser: user,
460
-
Registrations: registrations,
461
-
})
462
-
}
463
464
// list members of domain, requires auth and requires owner status
465
-
func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
466
-
domain := chi.URLParam(r, "domain")
467
-
if domain == "" {
468
-
http.Error(w, "malformed url", http.StatusBadRequest)
469
-
return
470
-
}
471
-
472
-
// list all members for this domain
473
-
memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
474
-
if err != nil {
475
-
w.Write([]byte("failed to fetch member list"))
476
-
return
477
-
}
478
-
479
-
w.Write([]byte(strings.Join(memberDids, "\n")))
480
-
return
481
-
}
482
483
// add member to domain, requires auth and requires invite access
484
-
func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
485
-
domain := chi.URLParam(r, "domain")
486
-
if domain == "" {
487
-
http.Error(w, "malformed url", http.StatusBadRequest)
488
-
return
489
-
}
490
-
491
-
subjectIdentifier := r.FormValue("subject")
492
-
if subjectIdentifier == "" {
493
-
http.Error(w, "malformed form", http.StatusBadRequest)
494
-
return
495
-
}
496
-
497
-
subjectIdentity, err := s.idResolver.ResolveIdent(r.Context(), subjectIdentifier)
498
-
if err != nil {
499
-
w.Write([]byte("failed to resolve member did to a handle"))
500
-
return
501
-
}
502
-
log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
503
-
504
-
// announce this relation into the firehose, store into owners' pds
505
-
client, err := s.oauth.AuthorizedClient(r)
506
-
if err != nil {
507
-
http.Error(w, "failed to authorize client", http.StatusInternalServerError)
508
-
return
509
-
}
510
-
currentUser := s.oauth.GetUser(r)
511
-
createdAt := time.Now().Format(time.RFC3339)
512
-
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
513
-
Collection: tangled.KnotMemberNSID,
514
-
Repo: currentUser.Did,
515
-
Rkey: appview.TID(),
516
-
Record: &lexutil.LexiconTypeDecoder{
517
-
Val: &tangled.KnotMember{
518
-
Subject: subjectIdentity.DID.String(),
519
-
Domain: domain,
520
-
CreatedAt: createdAt,
521
-
}},
522
-
})
523
-
524
-
// invalid record
525
-
if err != nil {
526
-
log.Printf("failed to create record: %s", err)
527
-
return
528
-
}
529
-
log.Println("created atproto record: ", resp.Uri)
530
-
531
-
secret, err := db.GetRegistrationKey(s.db, domain)
532
-
if err != nil {
533
-
log.Printf("no key found for domain %s: %s\n", domain, err)
534
-
return
535
-
}
536
-
537
-
ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
538
-
if err != nil {
539
-
log.Println("failed to create client to ", domain)
540
-
return
541
-
}
542
-
543
-
ksResp, err := ksClient.AddMember(subjectIdentity.DID.String())
544
-
if err != nil {
545
-
log.Printf("failed to make request to %s: %s", domain, err)
546
-
return
547
-
}
548
-
549
-
if ksResp.StatusCode != http.StatusNoContent {
550
-
w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
551
-
return
552
-
}
553
-
554
-
err = s.enforcer.AddKnotMember(domain, subjectIdentity.DID.String())
555
-
if err != nil {
556
-
w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
557
-
return
558
-
}
559
-
560
-
w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String())))
561
-
}
562
-
563
-
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
564
-
}
565
566
func validateRepoName(name string) error {
567
// check for path traversal attempts
···
2
3
import (
4
"context"
5
"fmt"
6
"log"
7
"log/slog"
···
199
}
200
201
// requires auth
202
+
// func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
203
+
// switch r.Method {
204
+
// case http.MethodGet:
205
+
// // list open registrations under this did
206
+
//
207
+
// return
208
+
// case http.MethodPost:
209
+
// session, err := s.oauth.Stores().Get(r, oauth.SessionName)
210
+
// if err != nil || session.IsNew {
211
+
// log.Println("unauthorized attempt to generate registration key")
212
+
// http.Error(w, "Forbidden", http.StatusUnauthorized)
213
+
// return
214
+
// }
215
+
//
216
+
// did := session.Values[oauth.SessionDid].(string)
217
+
//
218
+
// // check if domain is valid url, and strip extra bits down to just host
219
+
// domain := r.FormValue("domain")
220
+
// if domain == "" {
221
+
// http.Error(w, "Invalid form", http.StatusBadRequest)
222
+
// return
223
+
// }
224
+
//
225
+
// key, err := db.GenerateRegistrationKey(s.db, domain, did)
226
+
//
227
+
// if err != nil {
228
+
// log.Println(err)
229
+
// http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
230
+
// return
231
+
// }
232
+
//
233
+
// w.Write([]byte(key))
234
+
// }
235
+
// }
236
237
func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
238
user := chi.URLParam(r, "user")
···
267
}
268
269
// create a signed request and check if a node responds to that
270
+
// func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
271
+
// user := s.oauth.GetUser(r)
272
+
//
273
+
// noticeId := "operation-error"
274
+
// defaultErr := "Failed to register spindle. Try again later."
275
+
// fail := func() {
276
+
// s.pages.Notice(w, noticeId, defaultErr)
277
+
// }
278
+
//
279
+
// domain := chi.URLParam(r, "domain")
280
+
// if domain == "" {
281
+
// http.Error(w, "malformed url", http.StatusBadRequest)
282
+
// return
283
+
// }
284
+
// log.Println("checking ", domain)
285
+
//
286
+
// secret, err := db.GetRegistrationKey(s.db, domain)
287
+
// if err != nil {
288
+
// log.Printf("no key found for domain %s: %s\n", domain, err)
289
+
// return
290
+
// }
291
+
//
292
+
// client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
293
+
// if err != nil {
294
+
// log.Println("failed to create client to ", domain)
295
+
// }
296
+
//
297
+
// resp, err := client.Init(user.Did)
298
+
// if err != nil {
299
+
// w.Write([]byte("no dice"))
300
+
// log.Println("domain was unreachable after 5 seconds")
301
+
// return
302
+
// }
303
+
//
304
+
// if resp.StatusCode == http.StatusConflict {
305
+
// log.Println("status conflict", resp.StatusCode)
306
+
// w.Write([]byte("already registered, sorry!"))
307
+
// return
308
+
// }
309
+
//
310
+
// if resp.StatusCode != http.StatusNoContent {
311
+
// log.Println("status nok", resp.StatusCode)
312
+
// w.Write([]byte("no dice"))
313
+
// return
314
+
// }
315
+
//
316
+
// // verify response mac
317
+
// signature := resp.Header.Get("X-Signature")
318
+
// signatureBytes, err := hex.DecodeString(signature)
319
+
// if err != nil {
320
+
// return
321
+
// }
322
+
//
323
+
// expectedMac := hmac.New(sha256.New, []byte(secret))
324
+
// expectedMac.Write([]byte("ok"))
325
+
//
326
+
// if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
327
+
// log.Printf("response body signature mismatch: %x\n", signatureBytes)
328
+
// return
329
+
// }
330
+
//
331
+
// tx, err := s.db.BeginTx(r.Context(), nil)
332
+
// if err != nil {
333
+
// log.Println("failed to start tx", err)
334
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
335
+
// return
336
+
// }
337
+
// defer func() {
338
+
// tx.Rollback()
339
+
// err = s.enforcer.E.LoadPolicy()
340
+
// if err != nil {
341
+
// log.Println("failed to rollback policies")
342
+
// }
343
+
// }()
344
+
//
345
+
// // mark as registered
346
+
// err = db.Register(tx, domain)
347
+
// if err != nil {
348
+
// log.Println("failed to register domain", err)
349
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
350
+
// return
351
+
// }
352
+
//
353
+
// // set permissions for this did as owner
354
+
// reg, err := db.RegistrationByDomain(tx, domain)
355
+
// if err != nil {
356
+
// log.Println("failed to register domain", err)
357
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
358
+
// return
359
+
// }
360
+
//
361
+
// // add basic acls for this domain
362
+
// err = s.enforcer.AddKnot(domain)
363
+
// if err != nil {
364
+
// log.Println("failed to setup owner of domain", err)
365
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
366
+
// return
367
+
// }
368
+
//
369
+
// // add this did as owner of this domain
370
+
// err = s.enforcer.AddKnotOwner(domain, reg.ByDid)
371
+
// if err != nil {
372
+
// log.Println("failed to setup owner of domain", err)
373
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
374
+
// return
375
+
// }
376
+
//
377
+
// err = tx.Commit()
378
+
// if err != nil {
379
+
// log.Println("failed to commit changes", err)
380
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
381
+
// return
382
+
// }
383
+
//
384
+
// err = s.enforcer.E.SavePolicy()
385
+
// if err != nil {
386
+
// log.Println("failed to update ACLs", err)
387
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
388
+
// return
389
+
// }
390
+
//
391
+
// // add this knot to knotstream
392
+
// go s.knotstream.AddSource(
393
+
// context.Background(),
394
+
// eventconsumer.NewKnotSource(domain),
395
+
// )
396
+
//
397
+
// w.Write([]byte("check success"))
398
+
// }
399
+
400
+
// func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
401
+
// domain := chi.URLParam(r, "domain")
402
+
// if domain == "" {
403
+
// http.Error(w, "malformed url", http.StatusBadRequest)
404
+
// return
405
+
// }
406
+
//
407
+
// user := s.oauth.GetUser(r)
408
+
// reg, err := db.RegistrationByDomain(s.db, domain)
409
+
// if err != nil {
410
+
// w.Write([]byte("failed to pull up registration info"))
411
+
// return
412
+
// }
413
+
//
414
+
// var members []string
415
+
// if reg.Registered != nil {
416
+
// members, err = s.enforcer.GetUserByRole("server:member", domain)
417
+
// if err != nil {
418
+
// w.Write([]byte("failed to fetch member list"))
419
+
// return
420
+
// }
421
+
// }
422
+
//
423
+
// var didsToResolve []string
424
+
// for _, m := range members {
425
+
// didsToResolve = append(didsToResolve, m)
426
+
// }
427
+
// didsToResolve = append(didsToResolve, reg.ByDid)
428
+
// resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve)
429
+
// didHandleMap := make(map[string]string)
430
+
// for _, identity := range resolvedIds {
431
+
// if !identity.Handle.IsInvalidHandle() {
432
+
// didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
433
+
// } else {
434
+
// didHandleMap[identity.DID.String()] = identity.DID.String()
435
+
// }
436
+
// }
437
+
//
438
+
// ok, err := s.enforcer.IsKnotOwner(user.Did, domain)
439
+
// isOwner := err == nil && ok
440
+
//
441
+
// p := pages.KnotParams{
442
+
// LoggedInUser: user,
443
+
// DidHandleMap: didHandleMap,
444
+
// Registration: reg,
445
+
// Members: members,
446
+
// IsOwner: isOwner,
447
+
// }
448
+
//
449
+
// s.pages.Knot(w, p)
450
+
// }
451
452
// get knots registered by this user
453
+
// func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
454
+
// // for now, this is just pubkeys
455
+
// user := s.oauth.GetUser(r)
456
+
// registrations, err := db.RegistrationsByDid(s.db, user.Did)
457
+
// if err != nil {
458
+
// log.Println(err)
459
+
// }
460
+
//
461
+
// s.pages.Knots(w, pages.KnotsParams{
462
+
// LoggedInUser: user,
463
+
// Registrations: registrations,
464
+
// })
465
+
// }
466
467
// list members of domain, requires auth and requires owner status
468
+
// func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
469
+
// domain := chi.URLParam(r, "domain")
470
+
// if domain == "" {
471
+
// http.Error(w, "malformed url", http.StatusBadRequest)
472
+
// return
473
+
// }
474
+
//
475
+
// // list all members for this domain
476
+
// memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
477
+
// if err != nil {
478
+
// w.Write([]byte("failed to fetch member list"))
479
+
// return
480
+
// }
481
+
//
482
+
// w.Write([]byte(strings.Join(memberDids, "\n")))
483
+
// return
484
+
// }
485
486
// add member to domain, requires auth and requires invite access
487
+
// func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
488
+
// domain := chi.URLParam(r, "domain")
489
+
// if domain == "" {
490
+
// http.Error(w, "malformed url", http.StatusBadRequest)
491
+
// return
492
+
// }
493
+
//
494
+
// subjectIdentifier := r.FormValue("subject")
495
+
// if subjectIdentifier == "" {
496
+
// http.Error(w, "malformed form", http.StatusBadRequest)
497
+
// return
498
+
// }
499
+
//
500
+
// subjectIdentity, err := s.idResolver.ResolveIdent(r.Context(), subjectIdentifier)
501
+
// if err != nil {
502
+
// w.Write([]byte("failed to resolve member did to a handle"))
503
+
// return
504
+
// }
505
+
// log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
506
+
//
507
+
// // announce this relation into the firehose, store into owners' pds
508
+
// client, err := s.oauth.AuthorizedClient(r)
509
+
// if err != nil {
510
+
// http.Error(w, "failed to authorize client", http.StatusInternalServerError)
511
+
// return
512
+
// }
513
+
// currentUser := s.oauth.GetUser(r)
514
+
// createdAt := time.Now().Format(time.RFC3339)
515
+
// resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
516
+
// Collection: tangled.KnotMemberNSID,
517
+
// Repo: currentUser.Did,
518
+
// Rkey: appview.TID(),
519
+
// Record: &lexutil.LexiconTypeDecoder{
520
+
// Val: &tangled.KnotMember{
521
+
// Subject: subjectIdentity.DID.String(),
522
+
// Domain: domain,
523
+
// CreatedAt: createdAt,
524
+
// }},
525
+
// })
526
+
//
527
+
// // invalid record
528
+
// if err != nil {
529
+
// log.Printf("failed to create record: %s", err)
530
+
// return
531
+
// }
532
+
// log.Println("created atproto record: ", resp.Uri)
533
+
//
534
+
// secret, err := db.GetRegistrationKey(s.db, domain)
535
+
// if err != nil {
536
+
// log.Printf("no key found for domain %s: %s\n", domain, err)
537
+
// return
538
+
// }
539
+
//
540
+
// ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
541
+
// if err != nil {
542
+
// log.Println("failed to create client to ", domain)
543
+
// return
544
+
// }
545
+
//
546
+
// ksResp, err := ksClient.AddMember(subjectIdentity.DID.String())
547
+
// if err != nil {
548
+
// log.Printf("failed to make request to %s: %s", domain, err)
549
+
// return
550
+
// }
551
+
//
552
+
// if ksResp.StatusCode != http.StatusNoContent {
553
+
// w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
554
+
// return
555
+
// }
556
+
//
557
+
// err = s.enforcer.AddKnotMember(domain, subjectIdentity.DID.String())
558
+
// if err != nil {
559
+
// w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
560
+
// return
561
+
// }
562
+
//
563
+
// w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String())))
564
+
// }
565
+
566
+
// func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
567
+
// }
568
569
func validateRepoName(name string) error {
570
// check for path traversal attempts