Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
8.1 kB · 232 lines
Go
at spaces
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233package main
import ( "encoding/json" "fmt" "net/http" "slices" "strconv" "strings" "time"
"github.com/bluesky-social/indigo/atproto/auth" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/labeling" "github.com/bluesky-social/indigo/atproto/syntax")
func (srv *Server) RunAPI(bind string) {
svcAuth := auth.ServiceAuthValidator{ Audience: srv.ServiceDID.String(), // TODO: full audience with fragment? Dir: identity.DefaultDirectory(), TimestampLeeway: time.Second * 5, }
srv.httpServer = &http.Server{ Addr: bind, ReadTimeout: 20 * time.Second, WriteTimeout: 20 * time.Second, }
http.HandleFunc("GET /", HomeEndpoint) if srv.AdminPassword != "" { srv.Logger.Info("configuring admin auth") http.HandleFunc("POST /xrpc/at.nsid.cobalt.label.createLabel", auth.AdminAuthMiddleware(srv.CreateLabelEndpoint, []string{srv.AdminPassword})) http.HandleFunc("POST /xrpc/at.nsid.cobalt.label.negateLabel", auth.AdminAuthMiddleware(srv.NegateLabelEndpoint, []string{srv.AdminPassword})) } else { srv.Logger.Info("configuring service auth", "authorized", srv.AuthorizedDIDs) http.HandleFunc("POST /xrpc/at.nsid.cobalt.label.createLabel", svcAuth.Middleware(srv.CreateLabelEndpoint, true)) http.HandleFunc("POST /xrpc/at.nsid.cobalt.label.negateLabel", svcAuth.Middleware(srv.NegateLabelEndpoint, true)) } http.HandleFunc("GET /xrpc/com.atproto.label.queryLabels", srv.QueryLabelsEndpoint) http.HandleFunc("GET /xrpc/com.atproto.label.subscribeLabels", srv.SubscribeLabelsEndpoint)
if err := srv.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { srv.Logger.Error("http server bad shutdown", "err", err) } return}
type LabelRequestBody struct { SourceDID *string `json:"src,omitempty"` URI string `json:"uri"` CID *string `json:"cid,omitempty"` Val string `json:"val"` ExpiresAt *string `json:"exp,omitempty"`}
func (obj *LabelRequestBody) Validate() error { if obj.SourceDID != nil { _, err := syntax.ParseDID(*obj.SourceDID) if err != nil { return fmt.Errorf("invalid source DID: %s", *obj.SourceDID) } } if obj.CID != nil { _, err := syntax.ParseCID(*obj.CID) if err != nil { return fmt.Errorf("invalid subject CID: %s", *obj.CID) } } if obj.ExpiresAt != nil { _, err := syntax.ParseDatetime(*obj.ExpiresAt) if err != nil { return fmt.Errorf("invalid exp datetime: %s", *obj.ExpiresAt) } } if strings.HasPrefix(obj.URI, "did:") { _, err := syntax.ParseDID(obj.URI) if err != nil { return fmt.Errorf("invalid subjbect uri DID: %s", obj.URI) } } else if strings.HasPrefix(obj.URI, "at:") { _, err := syntax.ParseATURI(obj.URI) if err != nil { return fmt.Errorf("invalid subject uri AT-URI: %s", obj.URI) } } else { // TODO: could allow generic URLs? or just HTTP/S? return fmt.Errorf("unsupported label URI scheme: %s", obj.URI) } // TODO: more label syntax validation? if obj.Val == "" || len([]byte(obj.Val)) > 128 { return fmt.Errorf("label val empty or too long: '%s'", obj.Val) } return nil}
func (srv *Server) CreateLabelEndpoint(w http.ResponseWriter, r *http.Request) { srv.mutateLabelEndpoint(w, r, false)}
func (srv *Server) NegateLabelEndpoint(w http.ResponseWriter, r *http.Request) { srv.mutateLabelEndpoint(w, r, true)}
func (srv *Server) mutateLabelEndpoint(w http.ResponseWriter, r *http.Request, negate bool) { w.Header().Set("Content-Type", "application/json")
// verify authenticated DID from auth middleware if did := r.Context().Value("did"); did != nil { did := did.(syntax.DID) if !slices.Contains(srv.AuthorizedDIDs, did) { http.Error(w, fmt.Sprintf(`{"error": "Forbidden", "message": "Not an authorized account."}`), http.StatusForbidden) return } }
// parse request body JSON var reqBody LabelRequestBody if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { http.Error(w, fmt.Sprintf(`{"error": "BadRequest", "message": "%s"}`, err.Error()), http.StatusBadRequest) return } if err := reqBody.Validate(); err != nil { http.Error(w, fmt.Sprintf(`{"error": "BadRequest", "message": "%s"}`, err.Error()), http.StatusBadRequest) return }
src := srv.ServiceDID.String() if reqBody.SourceDID != nil && *reqBody.SourceDID != src { http.Error(w, fmt.Sprintf(`{"error": "BadRequest", "message": "unsupported src DID: %s"}`, *reqBody.SourceDID), http.StatusBadRequest) return }
lbl := labeling.Label{ CID: reqBody.CID, CreatedAt: syntax.DatetimeNow().String(), ExpiresAt: reqBody.ExpiresAt, SourceDID: src, URI: reqBody.URI, Val: reqBody.Val, Version: labeling.ATPROTO_LABEL_VERSION, } if negate { lbl.Negated = &negate }
if err := lbl.Sign(srv.SigningKey); err != nil { http.Error(w, fmt.Sprintf(`{"error": "InternalServerError", "message": "%s"}`, err.Error()), http.StatusInternalServerError) return }
if err := srv.CreateLabels(r.Context(), []labeling.Label{lbl}); err != nil { http.Error(w, fmt.Sprintf(`{"error": "InternalServerError", "message": "%s"}`, err.Error()), http.StatusInternalServerError) return }
if err := json.NewEncoder(w).Encode(lbl); err != nil { http.Error(w, fmt.Sprintf(`{"error": "InternalServerError", "message": "%s"}`, err.Error()), http.StatusInternalServerError) return }}
type QueryLabelsResponse struct { Cursor string `json:"cursor,omitempty"` Labels []labeling.Label `json:"labels"`}
func (srv *Server) QueryLabelsEndpoint(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")
// parse query params params := r.URL.Query() uriPatterns := params["uriPatterns"] var sources []syntax.DID for _, raw := range params["sources"] { did, err := syntax.ParseDID(raw) if err != nil { http.Error(w, fmt.Sprintf(`{"error": "BadRequest", "message": "invalid source DID: %s"}`, raw), http.StatusBadRequest) return } sources = append(sources, did) } limit := 50 rawLimit := params.Get("limit") if rawLimit != "" { limit, err := strconv.Atoi(rawLimit) if err != nil || limit < 1 || limit > 250 { http.Error(w, fmt.Sprintf(`{"error": "BadRequest", "message": "invalid limit"}`), http.StatusBadRequest) return } } cursor := params.Get("limit")
labels, nextCursor, err := srv.Store.QueryLabels(r.Context(), uriPatterns, sources, cursor, limit) if err != nil { http.Error(w, fmt.Sprintf(`{"error": "InternalServerError", "message": "%s"}`, err.Error()), http.StatusInternalServerError) return } respBody := QueryLabelsResponse{ Labels: labels, Cursor: nextCursor, }
if err := json.NewEncoder(w).Encode(respBody); err != nil { http.Error(w, fmt.Sprintf(`{"error": "InternalServerError", "message": "%s"}`, err.Error()), http.StatusInternalServerError) return }}
func HomeEndpoint(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte(` /$$ /$$ /$$ /$$ | $$ | $$ | $$ | $$ | $$ /$$$$$$ | $$$$$$$ /$$$$$$ | $$ /$$$$$$/$$$$ /$$$$$$ | $$ /$$ /$$$$$$ /$$$$$$ | $$ |____ $$| $$__ $$ /$$__ $$| $$| $$_ $$_ $$ |____ $$| $$ /$$/ /$$__ $$ /$$__ $$| $$ /$$$$$$$| $$ \ $$| $$$$$$$$| $$| $$ \ $$ \ $$ /$$$$$$$| $$$$$$/ | $$$$$$$$| $$ \__/| $$ /$$__ $$| $$ | $$| $$_____/| $$| $$ | $$ | $$ /$$__ $$| $$_ $$ | $$_____/| $$ | $$| $$$$$$$| $$$$$$$/| $$$$$$$| $$| $$ | $$ | $$| $$$$$$$| $$ \ $$| $$$$$$$| $$ |__/ \_______/|_______/ \_______/|__/|__/ |__/ |__/ \_______/|__/ \__/ \_______/|__/ This is an AT Protocol labeling service.
Most API routes are under /xrpc/ Code: https://tangled.org/bnewbold.net/cobalt/cmd/labelmaker Protocol: https://atproto.com`))}