versia-go/internal/service/svc_impls/federation_service_impl.go

182 lines
5.2 KiB
Go
Raw Permalink Normal View History

2024-08-11 03:51:22 +02:00
package svc_impls
import (
2024-08-20 22:43:26 +02:00
"bytes"
2024-08-11 03:51:22 +02:00
"context"
2024-08-20 22:43:26 +02:00
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
2024-08-11 03:51:22 +02:00
"git.devminer.xyz/devminer/unitel"
"github.com/go-logr/logr"
2024-08-28 00:25:25 +02:00
"github.com/versia-pub/versia-go/internal/entity"
"github.com/versia-pub/versia-go/internal/service"
"github.com/versia-pub/versia-go/pkg/protoretry"
"github.com/versia-pub/versia-go/pkg/versia"
versiacrypto "github.com/versia-pub/versia-go/pkg/versia/crypto"
versiautils "github.com/versia-pub/versia-go/pkg/versia/utils"
"github.com/versia-pub/versia-go/pkg/webfinger"
2024-08-20 22:43:26 +02:00
"net/http"
"net/url"
2024-08-11 03:51:22 +02:00
)
2024-08-20 22:43:26 +02:00
var (
_ service.FederationService = (*FederationServiceImpl)(nil)
ErrSignatureValidationFailed = errors.New("signature validation failed")
)
2024-08-11 03:51:22 +02:00
type FederationServiceImpl struct {
2024-08-20 22:43:26 +02:00
httpC *protoretry.Client
2024-08-11 03:51:22 +02:00
telemetry *unitel.Telemetry
log logr.Logger
}
2024-08-24 01:26:56 +02:00
func NewFederationServiceImpl(httpClient *http.Client, telemetry *unitel.Telemetry, log logr.Logger) *FederationServiceImpl {
2024-08-11 03:51:22 +02:00
return &FederationServiceImpl{
2024-08-24 01:26:56 +02:00
httpC: protoretry.New(httpClient),
telemetry: telemetry,
log: log,
2024-08-11 03:51:22 +02:00
}
}
2024-08-22 23:03:38 +02:00
func (i *FederationServiceImpl) GetUser(ctx context.Context, uri *versiautils.URL) (*versia.User, error) {
2024-08-20 22:43:26 +02:00
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.GetUser").
AddAttribute("userURI", uri.String())
2024-08-11 03:51:22 +02:00
defer s.End()
ctx = s.Context()
2024-08-20 22:43:26 +02:00
body, resp, err := i.httpC.GET(ctx, uri.ToStd())
if err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
2024-08-22 23:03:38 +02:00
u := &versia.User{}
2024-08-20 22:43:26 +02:00
if err := json.Unmarshal(body, u); err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
2024-08-22 23:03:38 +02:00
fedHeaders, err := versiacrypto.ExtractFederationHeaders(resp.Header)
2024-08-11 03:51:22 +02:00
if err != nil {
2024-08-20 22:43:26 +02:00
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
2024-08-11 03:51:22 +02:00
}
2024-08-22 23:03:38 +02:00
v := versiacrypto.Verifier{PublicKey: u.PublicKey.Key.Key}
2024-08-20 22:43:26 +02:00
if !v.Verify("GET", uri.ToStd(), body, fedHeaders) {
s.SetSimpleStatus(unitel.Error, ErrSignatureValidationFailed.Error())
i.log.V(1).Error(ErrSignatureValidationFailed, "signature validation failed", "user", u.URI.String())
return nil, ErrSignatureValidationFailed
}
s.SetSimpleStatus(unitel.Ok, "")
i.log.V(2).Info("signature verification succeeded", "user", u.URI.String())
return u, nil
2024-08-11 03:51:22 +02:00
}
2024-08-20 22:43:26 +02:00
func (i *FederationServiceImpl) DiscoverUser(ctx context.Context, baseURL, username string) (*webfinger.User, error) {
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.DiscoverUser").
AddAttribute("baseURL", baseURL).
AddAttribute("username", username)
2024-08-11 03:51:22 +02:00
defer s.End()
ctx = s.Context()
2024-08-20 22:43:26 +02:00
wf, err := webfinger.Discover(i.httpC, ctx, baseURL, username)
2024-08-11 03:51:22 +02:00
if err != nil {
2024-08-20 22:43:26 +02:00
s.SetSimpleStatus(unitel.Error, err.Error())
2024-08-11 03:51:22 +02:00
return nil, err
}
2024-08-20 22:43:26 +02:00
s.SetSimpleStatus(unitel.Ok, "")
return wf, nil
}
2024-08-24 01:26:56 +02:00
type ResponseError struct {
StatusCode int
URL *url.URL
}
func (e *ResponseError) Error() string {
return fmt.Sprintf("error from %s: %d", e.URL, e.StatusCode)
}
2024-08-22 23:03:38 +02:00
func (i *FederationServiceImpl) DiscoverInstance(ctx context.Context, baseURL string) (*versia.InstanceMetadata, error) {
2024-08-20 22:43:26 +02:00
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.DiscoverInstance").
AddAttribute("baseURL", baseURL)
defer s.End()
ctx = s.Context()
body, resp, err := i.httpC.GET(ctx, &url.URL{Scheme: "https", Host: baseURL, Path: "/.well-known/versia"})
if err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
} else if resp.StatusCode >= http.StatusBadRequest {
s.SetSimpleStatus(unitel.Error, fmt.Sprintf("unexpected response code: %d", resp.StatusCode))
2024-08-24 01:26:56 +02:00
return nil, &ResponseError{StatusCode: resp.StatusCode, URL: resp.Request.URL}
2024-08-20 22:43:26 +02:00
}
2024-08-22 23:03:38 +02:00
var metadata versia.InstanceMetadata
2024-08-20 22:43:26 +02:00
if err := json.Unmarshal(body, &metadata); err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
s.SetSimpleStatus(unitel.Ok, "")
return &metadata, nil
}
func (i *FederationServiceImpl) SendToInbox(ctx context.Context, author *entity.User, user *entity.User, object any) ([]byte, error) {
s := i.telemetry.StartSpan(ctx, "function", "svc_impls/FederationServiceImpl.SendToInbox").
SetUser(uint64(author.ID.ID()), author.Username, "", "").
AddAttribute("author", author.ID).
AddAttribute("authorURI", author.URI).
AddAttribute("target", user.ID).
AddAttribute("targetURI", user.URI)
defer s.End()
ctx = s.Context()
uri := user.Inbox.ToStd()
body, err := json.Marshal(object)
if err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
nonce := make([]byte, 32)
if _, err := rand.Read(nonce); err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
2024-08-22 23:03:38 +02:00
sigData := versiacrypto.NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, versiacrypto.SHA256(body))
2024-08-20 22:43:26 +02:00
sig := author.Signer.Sign(*sigData)
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
if err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
return nil, err
}
sig.Inject(req.Header)
body, _, err = i.httpC.DoReq(req)
if err != nil {
s.SetSimpleStatus(unitel.Error, err.Error())
i.log.Error(err, "Failed to send to inbox", "author", author.URI, "target", user.URI)
return nil, err
}
s.SetSimpleStatus(unitel.Ok, "")
return body, nil
2024-08-11 03:51:22 +02:00
}