forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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/config" 17 "tangled.sh/tangled.sh/core/appview/db" 18 "tangled.sh/tangled.sh/core/appview/middleware" 19 "tangled.sh/tangled.sh/core/appview/oauth" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 "tangled.sh/tangled.sh/core/eventconsumer" 22 "tangled.sh/tangled.sh/core/idresolver" 23 "tangled.sh/tangled.sh/core/knotclient" 24 "tangled.sh/tangled.sh/core/rbac" 25 "tangled.sh/tangled.sh/core/tid" 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 registration, err := db.RegistrationByDomain(k.Db, domain) 145 if err != nil { 146 l.Error("failed to get registration for domain", "err", err) 147 fail() 148 return 149 } 150 if registration.ByDid != user.Did { 151 l.Error("unauthorized", "wantedDid", registration.ByDid, "gotDid", user.Did) 152 w.WriteHeader(http.StatusUnauthorized) 153 return 154 } 155 156 secret, err := db.GetRegistrationKey(k.Db, domain) 157 if err != nil { 158 l.Error("failed to get registration key for domain", "err", err) 159 fail() 160 return 161 } 162 163 client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 164 if err != nil { 165 l.Error("failed to create knotclient", "err", err) 166 fail() 167 return 168 } 169 170 resp, err := client.Init(user.Did) 171 if err != nil { 172 k.Pages.Notice(w, noticeId, fmt.Sprintf("Failed to make request: %s", err.Error())) 173 l.Error("failed to make init request", "err", err) 174 return 175 } 176 177 if resp.StatusCode == http.StatusConflict { 178 k.Pages.Notice(w, noticeId, "This knot is already registered") 179 l.Error("knot already registered", "statuscode", resp.StatusCode) 180 return 181 } 182 183 if resp.StatusCode != http.StatusNoContent { 184 k.Pages.Notice(w, noticeId, fmt.Sprintf("Received status %d from knot, expected %d", resp.StatusCode, http.StatusNoContent)) 185 l.Error("incorrect statuscode returned", "statuscode", resp.StatusCode, "expected", http.StatusNoContent) 186 return 187 } 188 189 // verify response mac 190 signature := resp.Header.Get("X-Signature") 191 signatureBytes, err := hex.DecodeString(signature) 192 if err != nil { 193 return 194 } 195 196 expectedMac := hmac.New(sha256.New, []byte(secret)) 197 expectedMac.Write([]byte("ok")) 198 199 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 200 k.Pages.Notice(w, noticeId, "Response signature mismatch, consider regenerating the secret and retrying.") 201 l.Error("signature mismatch", "bytes", signatureBytes) 202 return 203 } 204 205 tx, err := k.Db.BeginTx(r.Context(), nil) 206 if err != nil { 207 l.Error("failed to start tx", "err", err) 208 fail() 209 return 210 } 211 defer func() { 212 tx.Rollback() 213 err = k.Enforcer.E.LoadPolicy() 214 if err != nil { 215 l.Error("rollback failed", "err", err) 216 } 217 }() 218 219 // mark as registered 220 err = db.Register(tx, domain) 221 if err != nil { 222 l.Error("failed to register domain", "err", err) 223 fail() 224 return 225 } 226 227 // set permissions for this did as owner 228 reg, err := db.RegistrationByDomain(tx, domain) 229 if err != nil { 230 l.Error("failed get registration by domain", "err", err) 231 fail() 232 return 233 } 234 235 // add basic acls for this domain 236 err = k.Enforcer.AddKnot(domain) 237 if err != nil { 238 l.Error("failed to add knot to enforcer", "err", err) 239 fail() 240 return 241 } 242 243 // add this did as owner of this domain 244 err = k.Enforcer.AddKnotOwner(domain, reg.ByDid) 245 if err != nil { 246 l.Error("failed to add knot owner to enforcer", "err", err) 247 fail() 248 return 249 } 250 251 err = tx.Commit() 252 if err != nil { 253 l.Error("failed to commit changes", "err", err) 254 fail() 255 return 256 } 257 258 err = k.Enforcer.E.SavePolicy() 259 if err != nil { 260 l.Error("failed to update ACLs", "err", err) 261 fail() 262 return 263 } 264 265 // add this knot to knotstream 266 go k.Knotstream.AddSource( 267 context.Background(), 268 eventconsumer.NewKnotSource(domain), 269 ) 270 271 k.Pages.KnotListing(w, pages.KnotListingParams{ 272 Registration: *reg, 273 }) 274} 275 276func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 277 l := k.Logger.With("handler", "dashboard") 278 fail := func() { 279 w.WriteHeader(http.StatusInternalServerError) 280 } 281 282 domain := chi.URLParam(r, "domain") 283 if domain == "" { 284 http.Error(w, "malformed url", http.StatusBadRequest) 285 return 286 } 287 l = l.With("domain", domain) 288 289 user := k.OAuth.GetUser(r) 290 l = l.With("did", user.Did) 291 292 // dashboard is only available to owners 293 ok, err := k.Enforcer.IsKnotOwner(user.Did, domain) 294 if err != nil { 295 l.Error("failed to query enforcer", "err", err) 296 fail() 297 } 298 if !ok { 299 http.Error(w, "only owners can view dashboards", http.StatusUnauthorized) 300 return 301 } 302 303 reg, err := db.RegistrationByDomain(k.Db, domain) 304 if err != nil { 305 l.Error("failed to get registration by domain", "err", err) 306 fail() 307 return 308 } 309 310 var members []string 311 if reg.Registered != nil { 312 members, err = k.Enforcer.GetUserByRole("server:member", domain) 313 if err != nil { 314 l.Error("failed to get members list", "err", err) 315 fail() 316 return 317 } 318 } 319 320 repos, err := db.GetRepos( 321 k.Db, 322 0, 323 db.FilterEq("knot", domain), 324 db.FilterIn("did", members), 325 ) 326 if err != nil { 327 l.Error("failed to get repos list", "err", err) 328 fail() 329 return 330 } 331 // convert to map 332 repoByMember := make(map[string][]db.Repo) 333 for _, r := range repos { 334 repoByMember[r.Did] = append(repoByMember[r.Did], r) 335 } 336 337 k.Pages.Knot(w, pages.KnotParams{ 338 LoggedInUser: user, 339 Registration: reg, 340 Members: members, 341 Repos: repoByMember, 342 IsOwner: true, 343 }) 344} 345 346// list members of domain, requires auth and requires owner status 347func (k *Knots) members(w http.ResponseWriter, r *http.Request) { 348 l := k.Logger.With("handler", "members") 349 350 domain := chi.URLParam(r, "domain") 351 if domain == "" { 352 http.Error(w, "malformed url", http.StatusBadRequest) 353 return 354 } 355 l = l.With("domain", domain) 356 357 // list all members for this domain 358 memberDids, err := k.Enforcer.GetUserByRole("server:member", domain) 359 if err != nil { 360 w.Write([]byte("failed to fetch member list")) 361 return 362 } 363 364 w.Write([]byte(strings.Join(memberDids, "\n"))) 365} 366 367// add member to domain, requires auth and requires invite access 368func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 369 l := k.Logger.With("handler", "members") 370 371 domain := chi.URLParam(r, "domain") 372 if domain == "" { 373 http.Error(w, "malformed url", http.StatusBadRequest) 374 return 375 } 376 l = l.With("domain", domain) 377 378 reg, err := db.RegistrationByDomain(k.Db, domain) 379 if err != nil { 380 l.Error("failed to get registration by domain", "err", err) 381 http.Error(w, "malformed url", http.StatusBadRequest) 382 return 383 } 384 385 noticeId := fmt.Sprintf("add-member-error-%d", reg.Id) 386 l = l.With("notice-id", noticeId) 387 defaultErr := "Failed to add member. Try again later." 388 fail := func() { 389 k.Pages.Notice(w, noticeId, defaultErr) 390 } 391 392 subjectIdentifier := r.FormValue("subject") 393 if subjectIdentifier == "" { 394 http.Error(w, "malformed form", http.StatusBadRequest) 395 return 396 } 397 l = l.With("subjectIdentifier", subjectIdentifier) 398 399 subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier) 400 if err != nil { 401 l.Error("failed to resolve identity", "err", err) 402 k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 403 return 404 } 405 l = l.With("subjectDid", subjectIdentity.DID) 406 407 l.Info("adding member to knot") 408 409 // announce this relation into the firehose, store into owners' pds 410 client, err := k.OAuth.AuthorizedClient(r) 411 if err != nil { 412 l.Error("failed to create client", "err", err) 413 fail() 414 return 415 } 416 417 currentUser := k.OAuth.GetUser(r) 418 createdAt := time.Now().Format(time.RFC3339) 419 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 420 Collection: tangled.KnotMemberNSID, 421 Repo: currentUser.Did, 422 Rkey: tid.TID(), 423 Record: &lexutil.LexiconTypeDecoder{ 424 Val: &tangled.KnotMember{ 425 Subject: subjectIdentity.DID.String(), 426 Domain: domain, 427 CreatedAt: createdAt, 428 }}, 429 }) 430 // invalid record 431 if err != nil { 432 l.Error("failed to write to PDS", "err", err) 433 fail() 434 return 435 } 436 l = l.With("at-uri", resp.Uri) 437 l.Info("wrote record to PDS") 438 439 secret, err := db.GetRegistrationKey(k.Db, domain) 440 if err != nil { 441 l.Error("failed to get registration key", "err", err) 442 fail() 443 return 444 } 445 446 ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 447 if err != nil { 448 l.Error("failed to create client", "err", err) 449 fail() 450 return 451 } 452 453 ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) 454 if err != nil { 455 l.Error("failed to reach knotserver", "err", err) 456 k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.") 457 return 458 } 459 460 if ksResp.StatusCode != http.StatusNoContent { 461 l.Error("status mismatch", "got", ksResp.StatusCode, "expected", http.StatusNoContent) 462 k.Pages.Notice(w, noticeId, fmt.Sprintf("Unexpected status code from knotserver %d, expected %d", ksResp.StatusCode, http.StatusNoContent)) 463 return 464 } 465 466 err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) 467 if err != nil { 468 l.Error("failed to add member to enforcer", "err", err) 469 fail() 470 return 471 } 472 473 // success 474 k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) 475} 476 477func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 478}