owl-blogs/app/activity_pub_service.go

216 lines
5.9 KiB
Go
Raw Normal View History

2024-05-13 17:03:21 +00:00
package app
import (
2024-05-14 19:23:37 +00:00
"crypto/rand"
2024-05-13 17:03:21 +00:00
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io"
"log/slog"
"net/http"
"net/url"
"owl-blogs/app/repository"
"owl-blogs/config"
"owl-blogs/domain/model"
"owl-blogs/render"
2024-05-14 19:23:37 +00:00
"reflect"
2024-05-13 17:03:21 +00:00
"time"
vocab "github.com/go-ap/activitypub"
"github.com/go-fed/httpsig"
)
type ActivityPubConfig struct {
PreferredUsername string
PublicKeyPem string
PrivateKeyPem string
}
// Form implements app.AppConfig.
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
return f
}
// ParseFormData implements app.AppConfig.
func (cfg *ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
cfg.PreferredUsername = data.FormValue("PreferredUsername")
cfg.PublicKeyPem = data.FormValue("PublicKeyPem")
cfg.PrivateKeyPem = data.FormValue("PrivateKeyPem")
return nil
}
func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey {
block, _ := pem.Decode([]byte(cfg.PrivateKeyPem))
privKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
return privKey
}
type ActivityPubService struct {
followersRepo repository.FollowerRepository
configRepo repository.ConfigRepository
}
func NewActivityPubService(followersRepo repository.FollowerRepository, configRepo repository.ConfigRepository) *ActivityPubService {
return &ActivityPubService{
followersRepo: followersRepo,
configRepo: configRepo,
}
}
2024-05-14 19:23:37 +00:00
func (svc *ActivityPubService) defaultConfig() ActivityPubConfig {
privKey, _ := rsa.GenerateKey(rand.Reader, 2048)
pubKey := privKey.Public().(*rsa.PublicKey)
pubKeyPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(pubKey),
},
)
privKeyPrm := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
},
)
return ActivityPubConfig{
PreferredUsername: "blog",
PublicKeyPem: string(pubKeyPem),
PrivateKeyPem: string(privKeyPrm),
}
}
func (svc *ActivityPubService) GetApConfig() (ActivityPubConfig, error) {
apConfig := ActivityPubConfig{}
err := svc.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
if err != nil {
println("ERROR IN ACTIVITY PUB CONFIG")
return ActivityPubConfig{}, err
}
if reflect.ValueOf(apConfig).IsZero() {
cfg := svc.defaultConfig()
svc.configRepo.Update(config.ACT_PUB_CONF_NAME, cfg)
return cfg, nil
}
return apConfig, nil
}
2024-05-13 17:03:21 +00:00
func (s *ActivityPubService) AddFollower(follower string) error {
return s.followersRepo.Add(follower)
}
func (s *ActivityPubService) RemoveFollower(follower string) error {
return s.followersRepo.Remove(follower)
}
func (s *ActivityPubService) AllFollowers() ([]string, error) {
return s.followersRepo.All()
}
func (s *ActivityPubService) sign(privateKey *rsa.PrivateKey, pubKeyId string, body []byte, r *http.Request) error {
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
digestAlgorithm := httpsig.DigestSha256
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
headersToSign := []string{httpsig.RequestTarget, "host", "date"}
if body != nil {
headersToSign = append(headersToSign, "digest")
}
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0)
if err != nil {
return err
}
// To sign the digest, we need to give the signer a copy of the body...
// ...but it is optional, no digest will be signed if given "nil"
// If r were a http.ResponseWriter, call SignResponse instead.
err = signer.SignRequest(privateKey, pubKeyId, r, body)
slog.Info("Signed Request", "req", r.Header)
return err
}
func (s *ActivityPubService) GetActor(reqUrl string, fromGame string) (vocab.Actor, error) {
c := http.Client{}
parsedUrl, err := url.Parse(reqUrl)
if err != nil {
slog.Error("parse error", "err", err)
return vocab.Actor{}, err
}
req, _ := http.NewRequest("GET", reqUrl, nil)
req.Header.Set("Accept", "application/ld+json")
req.Header.Set("Date", time.Now().Format(http.TimeFormat))
req.Header.Set("Host", parsedUrl.Host)
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
2024-05-16 18:27:06 +00:00
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/activitypub/actor#main-key", nil, req)
2024-05-13 17:03:21 +00:00
if err != nil {
slog.Error("Signing error", "err", err)
return vocab.Actor{}, err
}
resp, err := c.Do(req)
if err != nil {
2024-05-16 18:27:06 +00:00
slog.Error("failed to retrieve sender actor", "err", err, "url", reqUrl)
2024-05-13 17:03:21 +00:00
return vocab.Actor{}, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return vocab.Actor{}, err
}
item, err := vocab.UnmarshalJSON(data)
if err != nil {
return vocab.Actor{}, err
}
var actor vocab.Actor
err = vocab.OnActor(item, func(o *vocab.Actor) error {
actor = *o
return nil
})
return actor, err
}
func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
actor, err := s.GetActor(sender, siteConfig.FullUrl+"/activitypub/actor")
// actor does not have a pub key -> don't verify
if actor.PublicKey.PublicKeyPem == "" {
return nil
}
if err != nil {
2024-05-16 18:27:06 +00:00
slog.Error("unable to retrieve actor for sig verification", "sender", sender)
2024-05-13 17:03:21 +00:00
return err
}
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
2024-05-16 18:27:06 +00:00
slog.Error("unable to decode pub key pem", "pubKeyPem", actor.PublicKey.PublicKeyPem)
2024-05-13 17:03:21 +00:00
return err
}
slog.Info("retrieved pub key of sender", "actor", actor, "pubKey", pubKey)
verifier, err := httpsig.NewVerifier(r)
if err != nil {
2024-05-16 18:27:06 +00:00
slog.Error("invalid signature", "err", err)
2024-05-13 17:03:21 +00:00
return err
}
return verifier.Verify(pubKey, httpsig.RSA_SHA256)
}