this repo has no description
1package knots 2 3import ( 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 31type 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 42func (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 65func (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 81func (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 125func (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 264func (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 350func (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 372func (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 481func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 482}