···2233import (44 "context"55+ "fmt"5677+ "github.com/bluesky-social/indigo/atproto/syntax"68 "github.com/sethvargo/go-envconfig"79)810···25232624 // This disables signature verification so use with caution.2725 Dev bool `env:"DEV, default=false"`2626+}2727+2828+func (s Server) Did() syntax.DID {2929+ return syntax.DID(fmt.Sprintf("did:web:%s", s.Hostname))2830}29313032type Config struct {
+37-18
knotserver/handler.go
···88 "runtime/debug"991010 "github.com/go-chi/chi/v5"1111+ "tangled.sh/tangled.sh/core/idresolver"1112 "tangled.sh/tangled.sh/core/jetstream"1213 "tangled.sh/tangled.sh/core/knotserver/config"1314 "tangled.sh/tangled.sh/core/knotserver/db"1515+ "tangled.sh/tangled.sh/core/knotserver/xrpc"1616+ tlog "tangled.sh/tangled.sh/core/log"1417 "tangled.sh/tangled.sh/core/notifier"1518 "tangled.sh/tangled.sh/core/rbac"1619)17201818-const (1919- ThisServer = "thisserver" // resource identifier for rbac enforcement2020-)2121-2221type Handle struct {2323- c *config.Config2424- db *db.DB2525- jc *jetstream.JetstreamClient2626- e *rbac.Enforcer2727- l *slog.Logger2828- n *notifier.Notifier2222+ c *config.Config2323+ db *db.DB2424+ jc *jetstream.JetstreamClient2525+ e *rbac.Enforcer2626+ l *slog.Logger2727+ n *notifier.Notifier2828+ resolver *idresolver.Resolver29293030 // init is a channel that is closed when the knot has been initailized3131 // i.e. when the first user (knot owner) has been added.···3737 r := chi.NewRouter()38383939 h := Handle{4040- c: c,4141- db: db,4242- e: e,4343- l: l,4444- jc: jc,4545- n: n,4646- init: make(chan struct{}),4040+ c: c,4141+ db: db,4242+ e: e,4343+ l: l,4444+ jc: jc,4545+ n: n,4646+ resolver: idresolver.DefaultResolver(),4747+ init: make(chan struct{}),4748 }48494949- err := e.AddKnot(ThisServer)5050+ err := e.AddKnot(rbac.ThisServer)5051 if err != nil {5152 return nil, fmt.Errorf("failed to setup enforcer: %w", err)5253 }···132131 })133132 })134133134134+ // xrpc apis135135+ r.Mount("/xrpc", h.XrpcRouter())136136+135137 // Create a new repository.136138 r.Route("/repo", func(r chi.Router) {137139 r.Use(h.VerifySignature)···165161 r.Get("/keys", h.Keys)166162167163 return r, nil164164+}165165+166166+func (h *Handle) XrpcRouter() http.Handler {167167+ logger := tlog.New("knots")168168+169169+ xrpc := &xrpc.Xrpc{170170+ Config: h.c,171171+ Db: h.db,172172+ Ingester: h.jc,173173+ Enforcer: h.e,174174+ Logger: logger,175175+ Notifier: h.n,176176+ Resolver: h.resolver,177177+ }178178+ return xrpc.Router()168179}169180170181// version is set during build time.
+149
knotserver/xrpc/router.go
···11+package xrpc22+33+import (44+ "context"55+ "encoding/json"66+ "fmt"77+ "log/slog"88+ "net/http"99+ "strings"1010+1111+ "tangled.sh/tangled.sh/core/api/tangled"1212+ "tangled.sh/tangled.sh/core/idresolver"1313+ "tangled.sh/tangled.sh/core/jetstream"1414+ "tangled.sh/tangled.sh/core/knotserver/config"1515+ "tangled.sh/tangled.sh/core/knotserver/db"1616+ "tangled.sh/tangled.sh/core/notifier"1717+ "tangled.sh/tangled.sh/core/rbac"1818+1919+ "github.com/bluesky-social/indigo/atproto/auth"2020+ "github.com/go-chi/chi/v5"2121+)2222+2323+type Xrpc struct {2424+ Config *config.Config2525+ Db *db.DB2626+ Ingester *jetstream.JetstreamClient2727+ Enforcer *rbac.Enforcer2828+ Logger *slog.Logger2929+ Notifier *notifier.Notifier3030+ Resolver *idresolver.Resolver3131+}3232+3333+func (x *Xrpc) Router() http.Handler {3434+ r := chi.NewRouter()3535+3636+ r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)3737+3838+ return r3939+}4040+4141+func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler {4242+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {4343+ l := x.Logger.With("url", r.URL)4444+4545+ token := r.Header.Get("Authorization")4646+ token = strings.TrimPrefix(token, "Bearer ")4747+4848+ s := auth.ServiceAuthValidator{4949+ Audience: x.Config.Server.Did().String(),5050+ Dir: x.Resolver.Directory(),5151+ }5252+5353+ did, err := s.Validate(r.Context(), token, nil)5454+ if err != nil {5555+ l.Error("signature verification failed", "err", err)5656+ writeError(w, AuthError(err), http.StatusForbidden)5757+ return5858+ }5959+6060+ r = r.WithContext(6161+ context.WithValue(r.Context(), ActorDid, did),6262+ )6363+6464+ next.ServeHTTP(w, r)6565+ })6666+}6767+6868+type XrpcError struct {6969+ Tag string `json:"error"`7070+ Message string `json:"message"`7171+}7272+7373+func NewXrpcError(opts ...ErrOpt) XrpcError {7474+ x := XrpcError{}7575+ for _, o := range opts {7676+ o(&x)7777+ }7878+7979+ return x8080+}8181+8282+type ErrOpt = func(xerr *XrpcError)8383+8484+func WithTag(tag string) ErrOpt {8585+ return func(xerr *XrpcError) {8686+ xerr.Tag = tag8787+ }8888+}8989+9090+func WithMessage[S ~string](s S) ErrOpt {9191+ return func(xerr *XrpcError) {9292+ xerr.Message = string(s)9393+ }9494+}9595+9696+func WithError(e error) ErrOpt {9797+ return func(xerr *XrpcError) {9898+ xerr.Message = e.Error()9999+ }100100+}101101+102102+var MissingActorDidError = NewXrpcError(103103+ WithTag("MissingActorDid"),104104+ WithMessage("actor DID not supplied"),105105+)106106+107107+var AuthError = func(err error) XrpcError {108108+ return NewXrpcError(109109+ WithTag("Auth"),110110+ WithError(fmt.Errorf("signature verification failed: %w", err)),111111+ )112112+}113113+114114+var InvalidRepoError = func(r string) XrpcError {115115+ return NewXrpcError(116116+ WithTag("InvalidRepo"),117117+ WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),118118+ )119119+}120120+121121+var AccessControlError = func(d string) XrpcError {122122+ return NewXrpcError(123123+ WithTag("AccessControl"),124124+ WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),125125+ )126126+}127127+128128+var GitError = func(e error) XrpcError {129129+ return NewXrpcError(130130+ WithTag("Git"),131131+ WithError(fmt.Errorf("git error: %w", e)),132132+ )133133+}134134+135135+func GenericError(err error) XrpcError {136136+ return NewXrpcError(137137+ WithTag("InvalidRepo"),138138+ WithError(err),139139+ )140140+}141141+142142+// this is slightly different from http_util::write_error to follow the spec:143143+//144144+// the json object returned must include an "error" and a "message"145145+func writeError(w http.ResponseWriter, e XrpcError, status int) {146146+ w.Header().Set("Content-Type", "application/json")147147+ w.WriteHeader(status)148148+ json.NewEncoder(w).Encode(e)149149+}
+87
knotserver/xrpc/set_default_branch.go
···11+package xrpc22+33+import (44+ "encoding/json"55+ "fmt"66+ "net/http"77+88+ comatproto "github.com/bluesky-social/indigo/api/atproto"99+ "github.com/bluesky-social/indigo/atproto/syntax"1010+ "github.com/bluesky-social/indigo/xrpc"1111+ securejoin "github.com/cyphar/filepath-securejoin"1212+ "tangled.sh/tangled.sh/core/api/tangled"1313+ "tangled.sh/tangled.sh/core/knotserver/git"1414+ "tangled.sh/tangled.sh/core/rbac"1515+)1616+1717+const ActorDid string = "ActorDid"1818+1919+func (x *Xrpc) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {2020+ l := x.Logger2121+ fail := func(e XrpcError) {2222+ l.Error("failed", "kind", e.Tag, "error", e.Message)2323+ writeError(w, e, http.StatusBadRequest)2424+ }2525+2626+ actorDid, ok := r.Context().Value(ActorDid).(*syntax.DID)2727+ if !ok {2828+ fail(MissingActorDidError)2929+ return3030+ }3131+3232+ var data tangled.RepoSetDefaultBranch_Input3333+ if err := json.NewDecoder(r.Body).Decode(&data); err != nil {3434+ fail(GenericError(err))3535+ return3636+ }3737+3838+ // unfortunately we have to resolve repo-at here3939+ repoAt, err := syntax.ParseATURI(data.Repo)4040+ if err != nil {4141+ fail(InvalidRepoError(data.Repo))4242+ return4343+ }4444+4545+ // resolve this aturi to extract the repo record4646+ ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())4747+ if err != nil || ident.Handle.IsInvalidHandle() {4848+ fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))4949+ return5050+ }5151+5252+ xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}5353+ resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())5454+ if err != nil {5555+ fail(GenericError(err))5656+ return5757+ }5858+5959+ repo := resp.Value.Val.(*tangled.Repo)6060+ didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name)6161+ if err != nil {6262+ fail(GenericError(err))6363+ return6464+ }6565+6666+ if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {6767+ l.Error("insufficent permissions", "did", actorDid.String())6868+ writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)6969+ return7070+ }7171+7272+ path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath)7373+ gr, err := git.PlainOpen(path)7474+ if err != nil {7575+ fail(InvalidRepoError(data.Repo))7676+ return7777+ }7878+7979+ err = gr.SetDefaultBranch(data.DefaultBranch)8080+ if err != nil {8181+ l.Error("setting default branch", "error", err.Error())8282+ writeError(w, GitError(err), http.StatusInternalServerError)8383+ return8484+ }8585+8686+ w.WriteHeader(http.StatusNoContent)8787+}