173 lines
5.8 KiB
Go
173 lines
5.8 KiB
Go
package api
|
|
|
|
// Signed claims (meta-core v2.0.0, docs/implementation/metacore-claims-model.md).
|
|
//
|
|
// POST /api/claims the only metadata write path
|
|
// GET /api/meta/{cid}/claims every live claim of the cid's group
|
|
// GET /api/metadata/{rid}/claims the same, addressed by rid
|
|
// POST /api/merge stateless merge of claims the caller holds
|
|
//
|
|
// meta-core never signs a claim here: it verifies each one (MMUR2, low-S) and
|
|
// the signature is the write authentication.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/metazla/meta-core/internal/claim"
|
|
"github.com/metazla/meta-core/internal/storage"
|
|
)
|
|
|
|
// GoneResponse is the body of every removed unsigned write route.
|
|
type GoneResponse struct {
|
|
Error string `json:"error"`
|
|
Use string `json:"use"`
|
|
}
|
|
|
|
// handleGone answers the unsigned write routes removed in v2.0.0.
|
|
func (s *Server) handleGone(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusGone, GoneResponse{Error: "gone", Use: "POST /api/claims"})
|
|
}
|
|
|
|
// decodeClaims accepts a JSON array of claims or {"claims": [...]}.
|
|
func decodeClaims(r *http.Request) ([]claim.Claim, error) {
|
|
var raw json.RawMessage
|
|
if err := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 64<<20)).Decode(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
var claims []claim.Claim
|
|
if len(raw) > 0 && raw[0] == '{' {
|
|
var env struct {
|
|
Claims []claim.Claim `json:"claims"`
|
|
}
|
|
if err := json.Unmarshal(raw, &env); err != nil {
|
|
return nil, err
|
|
}
|
|
return env.Claims, nil
|
|
}
|
|
if err := json.Unmarshal(raw, &claims); err != nil {
|
|
return nil, err
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// handlePostClaims handles POST /api/claims: one status per claim, in input
|
|
// order. 200 even when some claims are refused — the statuses say which.
|
|
func (s *Server) handlePostClaims(w http.ResponseWriter, r *http.Request) {
|
|
if !s.storage.IsConnected() {
|
|
writeError(w, http.StatusServiceUnavailable, "storage not connected")
|
|
return
|
|
}
|
|
claims, err := decodeClaims(r)
|
|
if err != nil {
|
|
writeErrorSlug(w, http.StatusBadRequest, "invalid_json", "body must be a JSON array of claims: "+err.Error(), false)
|
|
return
|
|
}
|
|
if len(claims) > storage.MaxClaimsPerBatch {
|
|
writeErrorSlug(w, http.StatusRequestEntityTooLarge, "too_many_claims", "at most 10000 claims per batch", false)
|
|
return
|
|
}
|
|
results, err := s.storage.WriteClaims(r.Context(), claims)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, results)
|
|
}
|
|
|
|
// ClaimsResponse is the body of the claims read routes: the record's rid, the
|
|
// cids of its group, and every live signed claim on them.
|
|
type ClaimsResponse struct {
|
|
RID string `json:"rid"`
|
|
CIDs []string `json:"cids"`
|
|
Claims []claim.Claim `json:"claims"`
|
|
}
|
|
|
|
// handleClaimsByCID handles GET /api/meta/{cid}/claims[?key=…].
|
|
func (s *Server) handleClaimsByCID(w http.ResponseWriter, r *http.Request) {
|
|
cid := mux.Vars(r)["cid"]
|
|
if !s.storage.IsConnected() {
|
|
writeError(w, http.StatusServiceUnavailable, "storage not connected")
|
|
return
|
|
}
|
|
rid, err := s.storage.GetByCID(cid)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if rid == "" {
|
|
writeErrorSlug(w, http.StatusNotFound, ErrUnknownCID, "no record for this CID", false)
|
|
return
|
|
}
|
|
s.writeClaims(w, r, rid)
|
|
}
|
|
|
|
// handleClaimsByRID handles GET /api/metadata/{rid}/claims[?key=…]. The rid
|
|
// may also be a cid (resolved like every /api/metadata read).
|
|
func (s *Server) handleClaimsByRID(w http.ResponseWriter, r *http.Request) {
|
|
if !s.storage.IsConnected() {
|
|
writeError(w, http.StatusServiceUnavailable, "storage not connected")
|
|
return
|
|
}
|
|
rid := s.storage.ResolveRoot(mux.Vars(r)["hashId"])
|
|
s.writeClaims(w, r, rid)
|
|
}
|
|
|
|
// writeClaims answers both claims routes: ?key= keeps one exact key, ?uid=
|
|
// one signer; 404 when the record does not exist.
|
|
func (s *Server) writeClaims(w http.ResponseWriter, r *http.Request, rid string) {
|
|
cids, claims, err := s.storage.RecordClaims(rid, r.URL.Query().Get("key"))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if len(cids) == 0 {
|
|
writeErrorSlug(w, http.StatusNotFound, ErrUnknownCID, "no record for this rid or cid", false)
|
|
return
|
|
}
|
|
out := make([]claim.Claim, 0, len(claims))
|
|
uid := r.URL.Query().Get("uid")
|
|
for _, cl := range claims {
|
|
if uid == "" || cl.UID == uid {
|
|
out = append(out, cl)
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, ClaimsResponse{RID: rid, CIDs: cids, Claims: out})
|
|
}
|
|
|
|
// MergeRequest is the body of POST /api/merge.
|
|
type MergeRequest struct {
|
|
Claims []claim.Claim `json:"claims"`
|
|
As string `json:"as,omitempty"`
|
|
}
|
|
|
|
// MergeResponse is the merged field map, the number of agreeing signers per
|
|
// key, and how many claims failed verification (dropped, never an error).
|
|
type MergeResponse struct {
|
|
Fields map[string]string `json:"fields"`
|
|
Nsrc map[string]int `json:"nsrc"`
|
|
Rejected int `json:"rejected"`
|
|
}
|
|
|
|
// handleMerge handles POST /api/merge: verify, merge per key with the same
|
|
// rule the cache uses, overlay `as`'s own votes held by this core. No write.
|
|
func (s *Server) handleMerge(w http.ResponseWriter, r *http.Request) {
|
|
var req MergeRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 64<<20)).Decode(&req); err != nil {
|
|
writeErrorSlug(w, http.StatusBadRequest, "invalid_json", err.Error(), false)
|
|
return
|
|
}
|
|
if len(req.Claims) > storage.MaxClaimsPerBatch {
|
|
writeErrorSlug(w, http.StatusRequestEntityTooLarge, "too_many_claims", "at most 10000 claims per merge", false)
|
|
return
|
|
}
|
|
fields, nsrc, rejected, err := s.storage.MergeClaims(req.Claims, req.As)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, MergeResponse{Fields: fields, Nsrc: nsrc, Rejected: rejected})
|
|
}
|