owl-blogs/app/activity_pub_service.go

170 lines
4.6 KiB
Go

package app
import (
"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"
"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,
}
}
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)
err = s.sign(apConfig.PrivateKey(), siteConfig.FullUrl+"/games/"+fromGame+"#main-key", nil, req)
if err != nil {
slog.Error("Signing error", "err", err)
return vocab.Actor{}, err
}
resp, err := c.Do(req)
if err != nil {
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 {
return err
}
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
slog.Info("retrieved pub key of sender", "actor", actor, "pubKey", pubKey)
verifier, err := httpsig.NewVerifier(r)
if err != nil {
return err
}
return verifier.Verify(pubKey, httpsig.RSA_SHA256)
}