1package xrpc
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "net/http"
9 "strings"
10
11 "tangled.sh/tangled.sh/core/api/tangled"
12 "tangled.sh/tangled.sh/core/idresolver"
13 "tangled.sh/tangled.sh/core/jetstream"
14 "tangled.sh/tangled.sh/core/knotserver/config"
15 "tangled.sh/tangled.sh/core/knotserver/db"
16 "tangled.sh/tangled.sh/core/notifier"
17 "tangled.sh/tangled.sh/core/rbac"
18
19 "github.com/bluesky-social/indigo/atproto/auth"
20 "github.com/go-chi/chi/v5"
21)
22
23type Xrpc struct {
24 Config *config.Config
25 Db *db.DB
26 Ingester *jetstream.JetstreamClient
27 Enforcer *rbac.Enforcer
28 Logger *slog.Logger
29 Notifier *notifier.Notifier
30 Resolver *idresolver.Resolver
31}
32
33func (x *Xrpc) Router() http.Handler {
34 r := chi.NewRouter()
35
36 r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
37
38 return r
39}
40
41func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler {
42 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43 l := x.Logger.With("url", r.URL)
44
45 token := r.Header.Get("Authorization")
46 token = strings.TrimPrefix(token, "Bearer ")
47
48 s := auth.ServiceAuthValidator{
49 Audience: x.Config.Server.Did().String(),
50 Dir: x.Resolver.Directory(),
51 }
52
53 did, err := s.Validate(r.Context(), token, nil)
54 if err != nil {
55 l.Error("signature verification failed", "err", err)
56 writeError(w, AuthError(err), http.StatusForbidden)
57 return
58 }
59
60 r = r.WithContext(
61 context.WithValue(r.Context(), ActorDid, did),
62 )
63
64 next.ServeHTTP(w, r)
65 })
66}
67
68type XrpcError struct {
69 Tag string `json:"error"`
70 Message string `json:"message"`
71}
72
73func NewXrpcError(opts ...ErrOpt) XrpcError {
74 x := XrpcError{}
75 for _, o := range opts {
76 o(&x)
77 }
78
79 return x
80}
81
82type ErrOpt = func(xerr *XrpcError)
83
84func WithTag(tag string) ErrOpt {
85 return func(xerr *XrpcError) {
86 xerr.Tag = tag
87 }
88}
89
90func WithMessage[S ~string](s S) ErrOpt {
91 return func(xerr *XrpcError) {
92 xerr.Message = string(s)
93 }
94}
95
96func WithError(e error) ErrOpt {
97 return func(xerr *XrpcError) {
98 xerr.Message = e.Error()
99 }
100}
101
102var MissingActorDidError = NewXrpcError(
103 WithTag("MissingActorDid"),
104 WithMessage("actor DID not supplied"),
105)
106
107var AuthError = func(err error) XrpcError {
108 return NewXrpcError(
109 WithTag("Auth"),
110 WithError(fmt.Errorf("signature verification failed: %w", err)),
111 )
112}
113
114var InvalidRepoError = func(r string) XrpcError {
115 return NewXrpcError(
116 WithTag("InvalidRepo"),
117 WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),
118 )
119}
120
121var AccessControlError = func(d string) XrpcError {
122 return NewXrpcError(
123 WithTag("AccessControl"),
124 WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),
125 )
126}
127
128var GitError = func(e error) XrpcError {
129 return NewXrpcError(
130 WithTag("Git"),
131 WithError(fmt.Errorf("git error: %w", e)),
132 )
133}
134
135func GenericError(err error) XrpcError {
136 return NewXrpcError(
137 WithTag("InvalidRepo"),
138 WithError(err),
139 )
140}
141
142// this is slightly different from http_util::write_error to follow the spec:
143//
144// the json object returned must include an "error" and a "message"
145func writeError(w http.ResponseWriter, e XrpcError, status int) {
146 w.Header().Set("Content-Type", "application/json")
147 w.WriteHeader(status)
148 json.NewEncoder(w).Encode(e)
149}