forked from tangled.org/core
this repo has no description

xrpc/{errors,serviceauth}: split shared xrpc code to top-level package

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

Changed files
+168
xrpc
errors
serviceauth
+103
xrpc/errors/errors.go
···
+
package errors
+
+
import (
+
"encoding/json"
+
"fmt"
+
)
+
+
type XrpcError struct {
+
Tag string `json:"error"`
+
Message string `json:"message"`
+
}
+
+
func (x XrpcError) Error() string {
+
if x.Message != "" {
+
return fmt.Sprintf("%s: %s", x.Tag, x.Message)
+
}
+
return x.Tag
+
}
+
+
func NewXrpcError(opts ...ErrOpt) XrpcError {
+
x := XrpcError{}
+
for _, o := range opts {
+
o(&x)
+
}
+
+
return x
+
}
+
+
type ErrOpt = func(xerr *XrpcError)
+
+
func WithTag(tag string) ErrOpt {
+
return func(xerr *XrpcError) {
+
xerr.Tag = tag
+
}
+
}
+
+
func WithMessage[S ~string](s S) ErrOpt {
+
return func(xerr *XrpcError) {
+
xerr.Message = string(s)
+
}
+
}
+
+
func WithError(e error) ErrOpt {
+
return func(xerr *XrpcError) {
+
xerr.Message = e.Error()
+
}
+
}
+
+
var MissingActorDidError = NewXrpcError(
+
WithTag("MissingActorDid"),
+
WithMessage("actor DID not supplied"),
+
)
+
+
var AuthError = func(err error) XrpcError {
+
return NewXrpcError(
+
WithTag("Auth"),
+
WithError(fmt.Errorf("signature verification failed: %w", err)),
+
)
+
}
+
+
var InvalidRepoError = func(r string) XrpcError {
+
return NewXrpcError(
+
WithTag("InvalidRepo"),
+
WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),
+
)
+
}
+
+
var GitError = func(e error) XrpcError {
+
return NewXrpcError(
+
WithTag("Git"),
+
WithError(fmt.Errorf("git error: %w", e)),
+
)
+
}
+
+
var AccessControlError = func(d string) XrpcError {
+
return NewXrpcError(
+
WithTag("AccessControl"),
+
WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),
+
)
+
}
+
+
var RepoExistsError = func(r string) XrpcError {
+
return NewXrpcError(
+
WithTag("RepoExists"),
+
WithError(fmt.Errorf("repo already exists: %s", r)),
+
)
+
}
+
+
func GenericError(err error) XrpcError {
+
return NewXrpcError(
+
WithTag("Generic"),
+
WithError(err),
+
)
+
}
+
+
func Unmarshal(errStr string) (XrpcError, error) {
+
var xerr XrpcError
+
err := json.Unmarshal([]byte(errStr), &xerr)
+
if err != nil {
+
return XrpcError{}, fmt.Errorf("failed to unmarshal XrpcError: %w", err)
+
}
+
return xerr, nil
+
}
+65
xrpc/serviceauth/service_auth.go
···
+
package serviceauth
+
+
import (
+
"context"
+
"encoding/json"
+
"log/slog"
+
"net/http"
+
"strings"
+
+
"github.com/bluesky-social/indigo/atproto/auth"
+
"tangled.sh/tangled.sh/core/idresolver"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
+
)
+
+
const ActorDid string = "ActorDid"
+
+
type ServiceAuth struct {
+
logger *slog.Logger
+
resolver *idresolver.Resolver
+
audienceDid string
+
}
+
+
func NewServiceAuth(logger *slog.Logger, resolver *idresolver.Resolver, audienceDid string) *ServiceAuth {
+
return &ServiceAuth{
+
logger: logger,
+
resolver: resolver,
+
audienceDid: audienceDid,
+
}
+
}
+
+
func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
l := sa.logger.With("url", r.URL)
+
+
token := r.Header.Get("Authorization")
+
token = strings.TrimPrefix(token, "Bearer ")
+
+
s := auth.ServiceAuthValidator{
+
Audience: sa.audienceDid,
+
Dir: sa.resolver.Directory(),
+
}
+
+
did, err := s.Validate(r.Context(), token, nil)
+
if err != nil {
+
l.Error("signature verification failed", "err", err)
+
writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
+
return
+
}
+
+
r = r.WithContext(
+
context.WithValue(r.Context(), ActorDid, did),
+
)
+
+
next.ServeHTTP(w, r)
+
})
+
}
+
+
// this is slightly different from http_util::write_error to follow the spec:
+
//
+
// the json object returned must include an "error" and a "message"
+
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
+
w.Header().Set("Content-Type", "application/json")
+
w.WriteHeader(status)
+
json.NewEncoder(w).Encode(e)
+
}