appview: refactor knot endpoints into separate router #306

merged
opened by oppi.li targeting master from push-nwslswprzvmx
Changed files
+832 -345
appview
+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(&registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 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(&registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 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(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 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(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 82 83 if err != nil { 84 if err == sql.ErrNoRows {
+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
··· 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
··· 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