at main 13 kB view raw
1package api 2 3import ( 4 "encoding/json" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "reflect" 9 "regexp" 10 "slices" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/go-chi/chi/v5" 16 "github.com/go-chi/chi/v5/middleware" 17 "github.com/go-chi/cors" 18 "github.com/go-chi/httprate" 19 _ "github.com/lib/pq" 20 did "github.com/whyrusleeping/go-did" 21 22 "tangled.sh/seiso.moe/aletheia.directory/ent" 23 "tangled.sh/seiso.moe/aletheia.directory/ent/operation" 24 "tangled.sh/seiso.moe/aletheia.directory/ent/syncstatus" 25 "tangled.sh/seiso.moe/aletheia.directory/pkg/plc" 26) 27 28var ( 29 validDIDPattern = regexp.MustCompile(`[a-z0-9:]{24}`) 30) 31 32type Server struct { 33 client *ent.Client 34 logger *slog.Logger 35 port int 36} 37 38type DidDocService struct { 39 ID string `json:"id"` 40 Type string `json:"type"` 41 ServiceEndpoint string `json:"serviceEndpoint"` 42} 43 44type DidDocument struct { 45 Context []string `json:"@context"` 46 ID string `json:"id"` 47 AlsoKnownAs []string `json:"alsoKnownAs"` 48 VerificationMethod []VerificationMethod `json:"verificationMethod"` 49 Service []DidDocService `json:"service"` 50} 51 52type VerificationMethod struct { 53 ID string `json:"id"` 54 Type string `json:"type"` 55 Controller string `json:"controller"` 56 PublicKeyMultibase string `json:"publicKeyMultibase"` 57} 58 59type OperationResponse struct { 60 DID string `json:"did"` 61 Operation plc.PLCOperation `json:"operation"` 62 CID string `json:"cid"` 63 Nullified bool `json:"nullified"` 64 CreatedAt time.Time `json:"createdAt"` 65} 66 67type KeyAndContext struct { 68 Context *string 69 Type string 70 PublicKeyMultiBase string 71} 72 73func NewServer(client *ent.Client, logger *slog.Logger, port int) *Server { 74 return &Server{ 75 client: client, 76 logger: logger, 77 port: port, 78 } 79} 80 81func (s *Server) Start() error { 82 r := chi.NewRouter() 83 r.Use(middleware.Logger) 84 85 r.Use(cors.Handler(cors.Options{ 86 AllowedOrigins: []string{"*"}, 87 AllowedMethods: []string{"GET"}, 88 AllowCredentials: false, 89 })) 90 91 // probally overkill but meh 92 r.Use(httprate.LimitByIP(500, 1*time.Minute)) 93 94 // unofficial endpoints 95 r.Get("/_health", s.handleHealth) 96 97 // official endpoints 98 r.Get("/{did}", s.handleDid) 99 r.Get("/{did}/log", s.handleLog) 100 r.Get("/{did}/log/audit", s.handleAudit) 101 r.Get("/{did}/log/last", s.handleLastOp) 102 r.Get("/{did}/data", s.handlePlcData) 103 r.Get("/export", s.handleExport) 104 105 s.logger.Info("starting API server", "port", s.port) 106 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r) 107} 108 109func validDID(did string) bool { 110 return validDIDPattern.MatchString(did) 111} 112 113func formatKeyAndContext(key string) KeyAndContext { 114 keyInfo, err := did.PubKeyFromDIDString(key) 115 if err != nil { 116 return KeyAndContext{ 117 Context: nil, 118 Type: "Multikey", 119 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 120 } 121 } 122 123 switch keyInfo.Type { 124 case did.KeyTypeSecp256k1: 125 str := "https://w3id.org/security/suites/secp256k1-2019/v1" 126 return KeyAndContext{ 127 Context: &str, 128 Type: "MultiKey", 129 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 130 } 131 case did.KeyTypeP256: 132 str := "https://w3id.org/security/suites/secp256k1-2019/v1" 133 return KeyAndContext{ 134 Context: &str, 135 Type: "MultiKey", 136 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 137 } 138 default: 139 str := "https://w3id.org/security/suites/ecdsa-2019/v1" 140 return KeyAndContext{ 141 Context: &str, 142 Type: "MultiKey", 143 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 144 } 145 } 146} 147 148func formatDidDoc(did string, operation plc.PLCOperation) DidDocument { 149 context := []string{ 150 "https://www.w3.org/ns/did/v1", 151 "https://w3id.org/security/multikey/v1", 152 } 153 154 verificationMethods := make([]VerificationMethod, 0, len(operation.VerificationMethods)) 155 for k, v := range operation.VerificationMethods { 156 info := formatKeyAndContext(v) 157 if info.Context != nil && !slices.Contains(context, *info.Context) { 158 context = append(context, *info.Context) 159 } 160 verificationMethods = append(verificationMethods, VerificationMethod{ 161 ID: fmt.Sprintf("%s#%s", did, k), 162 Type: info.Type, 163 Controller: did, 164 PublicKeyMultibase: info.PublicKeyMultiBase, 165 }) 166 } 167 168 services := make([]DidDocService, 0, len(operation.Services)) 169 for k, v := range operation.Services { 170 services = append(services, DidDocService{ 171 ID: fmt.Sprintf("#%s", k), 172 Type: v.Type, 173 ServiceEndpoint: v.Endpoint, 174 }) 175 } 176 177 return DidDocument{ 178 Context: context, 179 ID: did, 180 AlsoKnownAs: operation.AlsoKnownAs, 181 VerificationMethod: verificationMethods, 182 Service: services, 183 } 184} 185 186func (s *Server) handleDid(w http.ResponseWriter, r *http.Request) { 187 ctx := r.Context() 188 did := r.PathValue("did") 189 190 if !validDID(did) { 191 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 192 return 193 } 194 195 n, err := s.client.Operation.Query(). 196 Where(operation.Did(did)). 197 Where(operation.NullifiedEQ(false)). 198 Order(ent.Desc(operation.FieldCreatedAt)). 199 First(ctx) 200 201 if err != nil { 202 if ent.IsNotFound(err) { 203 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 204 return 205 } 206 s.logger.Error("database query failed", "error", err) 207 w.WriteHeader(http.StatusInternalServerError) 208 return 209 } 210 211 if n.Operation.GetType() == plc.OperationTypeTombstone { 212 s.writeErrorResponse(w, http.StatusOK, fmt.Sprintf("DID not available: %s", did)) 213 return 214 } 215 216 didDoc := formatDidDoc(did, n.Operation) 217 s.writeJSONResponse(w, http.StatusOK, didDoc) 218} 219 220func (s *Server) handlePlcData(w http.ResponseWriter, r *http.Request) { 221 ctx := r.Context() 222 did := r.PathValue("did") 223 224 if !validDID(did) { 225 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 226 return 227 } 228 229 n, err := s.client.Operation.Query(). 230 Where(operation.Did(did)). 231 Where(operation.NullifiedEQ(false)). 232 Order(ent.Desc(operation.FieldCreatedAt)). 233 First(ctx) 234 235 if err != nil { 236 if ent.IsNotFound(err) { 237 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 238 return 239 } 240 s.logger.Error("database query failed", "error", err) 241 w.WriteHeader(http.StatusInternalServerError) 242 return 243 } 244 245 if n.Operation.GetType() == plc.OperationTypeTombstone { 246 s.writeErrorResponse(w, http.StatusOK, fmt.Sprintf("DID not available: %s", did)) 247 return 248 } 249 250 resp := struct { 251 did string 252 VerificationMethods map[string]string 253 RotationKeys []string 254 AlsoKnownAs []string 255 Services map[string]plc.ServiceEndpoint 256 }{ 257 did: did, 258 VerificationMethods: n.Operation.VerificationMethods, 259 RotationKeys: n.Operation.RotationKeys, 260 AlsoKnownAs: n.Operation.AlsoKnownAs, 261 Services: n.Operation.Services, 262 } 263 s.writeJSONResponse(w, http.StatusOK, resp) 264} 265 266func (s *Server) handleLog(w http.ResponseWriter, r *http.Request) { 267 ctx := r.Context() 268 did := r.PathValue("did") 269 270 if !validDID(did) { 271 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 272 return 273 } 274 275 n, err := s.client.Operation.Query(). 276 Where(operation.Did(did)). 277 Where(operation.NullifiedEQ(false)). 278 Order(ent.Asc(operation.FieldCreatedAt)). 279 All(ctx) 280 281 if err != nil { 282 if ent.IsNotFound(err) { 283 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 284 return 285 } 286 s.logger.Error("database query failed", "error", err) 287 w.WriteHeader(http.StatusInternalServerError) 288 return 289 } 290 291 ops := make([]any, 0, len(n)) 292 for _, v := range n { 293 ops = append(ops, v.Operation) 294 } 295 296 s.writeJSONResponse(w, http.StatusOK, ops) 297} 298 299func (s *Server) handleAudit(w http.ResponseWriter, r *http.Request) { 300 ctx := r.Context() 301 did := r.PathValue("did") 302 303 if !validDID(did) { 304 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 305 return 306 } 307 308 n, err := s.client.Operation.Query(). 309 Where(operation.Did(did)). 310 Where(operation.NullifiedEQ(false)). 311 Order(ent.Asc(operation.FieldCreatedAt)). 312 All(ctx) 313 314 if err != nil { 315 if ent.IsNotFound(err) { 316 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 317 return 318 } 319 s.logger.Error("database query failed", "error", err) 320 w.WriteHeader(http.StatusInternalServerError) 321 return 322 } 323 324 responses := make([]OperationResponse, len(n)) 325 for i, op := range n { 326 responses[i] = OperationResponse{ 327 DID: op.Did, 328 Operation: op.Operation, 329 CID: op.Cid, 330 Nullified: op.Nullified, 331 CreatedAt: op.CreatedAt, 332 } 333 } 334 335 s.writeJSONResponse(w, http.StatusOK, responses) 336} 337 338func (s *Server) handleLastOp(w http.ResponseWriter, r *http.Request) { 339 ctx := r.Context() 340 did := r.PathValue("did") 341 342 if !validDID(did) { 343 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 344 return 345 } 346 347 n, err := s.client.Operation.Query(). 348 Where(operation.Did(did)). 349 Where(operation.NullifiedEQ(false)). 350 Order(ent.Desc(operation.FieldCreatedAt)). 351 First(ctx) 352 353 if err != nil { 354 if ent.IsNotFound(err) { 355 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 356 return 357 } 358 s.logger.Error("database query failed", "error", err) 359 w.WriteHeader(http.StatusInternalServerError) 360 return 361 } 362 363 s.writeJSONResponse(w, http.StatusOK, n.Operation) 364} 365 366func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { 367 ctx := r.Context() 368 369 countStr := r.FormValue("count") 370 var count int 371 if countStr == "" { 372 count = 10 373 } else { 374 var err error 375 count, err = strconv.Atoi(r.FormValue("count")) 376 if err != nil { 377 s.writeErrorResponse(w, http.StatusBadRequest, "invalid count parameter") 378 return 379 } 380 } 381 count = min(1000, count) 382 383 after, err := time.Parse(time.RFC3339Nano, r.FormValue("after")) 384 if err != nil { 385 after = time.Time{} 386 } 387 388 ops, err := s.client.Operation.Query(). 389 Select( 390 operation.FieldDid, 391 operation.FieldOperation, 392 operation.FieldCid, 393 operation.FieldNullified, 394 operation.FieldCreatedAt, 395 ). 396 Where(operation.CreatedAtGT(after)). 397 Order(ent.Asc(operation.FieldCreatedAt)). 398 Limit(count). 399 All(ctx) 400 if err != nil { 401 s.logger.Error("failed to query operations", "error", err) 402 w.WriteHeader(http.StatusInternalServerError) 403 return 404 } 405 406 responses := make([]OperationResponse, len(ops)) 407 for i, op := range ops { 408 responses[i] = OperationResponse{ 409 DID: op.Did, 410 Operation: op.Operation, 411 CID: op.Cid, 412 Nullified: op.Nullified, 413 CreatedAt: op.CreatedAt, 414 } 415 } 416 417 s.writeJSONLResponse(w, http.StatusOK, responses) 418} 419 420func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { 421 ctx := r.Context() 422 423 syncStatus, err := s.client.SyncStatus.Query(). 424 Where(syncstatus.Key("last_mirror_sync")). 425 First(ctx) 426 427 if err != nil { 428 if ent.IsNotFound(err) { 429 s.writeJSONResponse(w, http.StatusOK, map[string]any{ 430 "status": "starting", 431 "message": "No sync data available yet", 432 }) 433 return 434 } 435 s.logger.Error("failed to query sync status", "error", err) 436 w.WriteHeader(http.StatusInternalServerError) 437 return 438 } 439 440 s.writeJSONResponse(w, http.StatusOK, map[string]any{ 441 "status": "healthy", 442 "last_updated_at": syncStatus.LastSyncTime.Format(time.RFC3339), 443 }) 444} 445 446func (s *Server) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) { 447 s.writeJSONResponse(w, statusCode, map[string]any{"message": message}) 448} 449 450func (s *Server) writeJSONResponse(w http.ResponseWriter, statusCode int, data any) { 451 w.Header().Set("Content-Type", "application/json") 452 w.WriteHeader(statusCode) 453 454 b, err := json.Marshal(data) 455 if err != nil { 456 s.logger.Error("failed to marshal JSON response", "error", err) 457 return 458 } 459 460 if _, err := w.Write(b); err != nil { 461 s.logger.Error("failed to write response", "error", err) 462 } 463} 464 465func (s *Server) writeJSONLResponse(w http.ResponseWriter, statusCode int, data any) { 466 w.Header().Set("Content-Type", "application/jsonlines") 467 w.WriteHeader(statusCode) 468 469 rv := reflect.ValueOf(data) 470 if rv.Kind() != reflect.Slice { 471 s.logger.Error("JSONL response expects slice data") 472 return 473 } 474 475 for i := range rv.Len() { 476 item := rv.Index(i).Interface() 477 if err := s.writeJSONLine(w, item); err != nil { 478 s.logger.Error("failed to write item as JSON line", "error", err) 479 return 480 } 481 } 482} 483 484func (s *Server) writeJSONLine(w http.ResponseWriter, data any) error { 485 b, err := json.Marshal(data) 486 if err != nil { 487 return err 488 } 489 490 if _, err := w.Write(b); err != nil { 491 return err 492 } 493 if _, err := w.Write([]byte("\n")); err != nil { 494 return err 495 } 496 497 return nil 498}