WIP copying code from fedi-games

This commit is contained in:
Niko Abeler 2024-05-13 19:03:21 +02:00
parent 10ca2bdcd9
commit 624f19a1d9
7 changed files with 302 additions and 52 deletions

169
app/activity_pub_service.go Normal file
View File

@ -0,0 +1,169 @@
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)
}

View File

@ -50,6 +50,7 @@ func App(db infra.Database) *web.WebApp {
authorRepo := infra.NewDefaultAuthorRepo(db)
configRepo := infra.NewConfigRepo(db)
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
followersRepo := infra.NewFollowerRepository(db)
// Create External Services
httpClient := &infra.OwlHttpClient{}
@ -65,6 +66,7 @@ func App(db infra.Database) *web.WebApp {
webmentionService := app.NewWebmentionService(
siteConfigService, interactionRepo, entryRepo, httpClient, eventBus,
)
apService := app.NewActivityPubService(followersRepo, configRepo)
// setup render functions
render.SiteConfigService = siteConfigService
@ -80,6 +82,7 @@ func App(db infra.Database) *web.WebApp {
entryService, entryRegister, binaryService,
authorService, configRepo, configRegister,
siteConfigService, webmentionService, interactionRepo,
apService,
)
}

View File

@ -4,6 +4,7 @@ import "os"
const (
SITE_CONFIG = "site_config"
ACT_PUB_CONF_NAME = "activity_pub"
)
type Config interface {

1
go.mod
View File

@ -26,6 +26,7 @@ require (
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect

8
go.sum
View File

@ -22,6 +22,8 @@ github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8R
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@ -84,15 +86,21 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@ -1,46 +1,28 @@
package web
import (
"errors"
"log/slog"
"net/http"
"net/url"
"owl-blogs/app"
"owl-blogs/app/repository"
"owl-blogs/config"
"owl-blogs/domain/model"
"owl-blogs/render"
vocab "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
)
const ACT_PUB_CONF_NAME = "activity_pub"
type ActivityPubServer struct {
configRepo repository.ConfigRepository
apService *app.ActivityPubService
entryService *app.EntryService
}
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
}
type WebfingerResponse struct {
Subject string `json:"subject"`
Aliases []string `json:"aliases"`
@ -53,17 +35,18 @@ type WebfingerLink struct {
Href string `json:"href"`
}
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService) *ActivityPubServer {
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
return &ActivityPubServer{
configRepo: configRepo,
entryService: entryService,
apService: apService,
}
}
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
domain, err := url.Parse(siteConfig.FullUrl)
@ -95,12 +78,14 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
func (s *ActivityPubServer) Router(router fiber.Router) {
router.Get("/actor", s.HandleActor)
router.Get("/outbox", s.HandleOutbox)
router.Get("/inbox", s.HandleInbox)
router.Get("/followers", s.HandleFollowers)
}
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
@ -126,8 +111,8 @@ func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
entries, err := s.entryService.FindAllByType(nil, true, false)
@ -157,5 +142,86 @@ func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
}
ctx.Set("Content-Type", "application/activity+json")
return ctx.Send(data)
}
func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity) error {
follower := act.Actor.GetID().String()
err := s.apService.VerifySignature(r, follower)
if err != nil {
return err
}
err = s.apService.AddFollower(follower)
if err != nil {
return err
}
// go acpub.Accept(gameName, act)
return nil
}
func (s *ActivityPubServer) processUndo(act *vocab.Activity) error {
return nil
}
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
body := ctx.Request().Body()
data, err := vocab.UnmarshalJSON(body)
if err != nil {
return err
}
err = vocab.OnActivity(data, func(act *vocab.Activity) error {
slog.Info("activity retrieved", "activity", act, "type", act.Type)
r, err := adaptor.ConvertRequest(ctx, true)
if err != nil {
return err
}
if act.Type == vocab.FollowType {
return s.processFollow(r, act)
}
if act.Type == vocab.UndoType {
slog.Info("processing undo")
return s.processUndo(act)
}
return errors.New("only follow and undo actions supported")
})
return err
}
func (s *ActivityPubServer) HandleFollowers(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := app.ActivityPubConfig{}
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
fs, err := s.apService.AllFollowers()
if err != nil {
return err
}
followers := vocab.Collection{}
for _, f := range fs {
followers.Append(vocab.IRI(f))
}
followers.TotalItems = uint(len(fs))
followers.ID = vocab.IRI(siteConfig.FullUrl + "/activitypub/followers")
data, err := jsonld.WithContext(
jsonld.IRI(vocab.ActivityBaseURI),
).Marshal(followers)
if err != nil {
return err
}
ctx.Set("Content-Type", "application/activity+json")
return ctx.Send(data)
}

View File

@ -7,6 +7,7 @@ import (
"net/url"
"owl-blogs/app"
"owl-blogs/app/repository"
"owl-blogs/config"
"owl-blogs/web/middleware"
"github.com/gofiber/fiber/v2"
@ -35,11 +36,12 @@ func NewWebApp(
siteConfigService *app.SiteConfigService,
webmentionService *app.WebmentionService,
interactionRepo repository.InteractionRepository,
apService *app.ActivityPubService,
) *WebApp {
app := fiber.New(fiber.Config{
fiberApp := fiber.New(fiber.Config{
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
})
app.Use(middleware.NewUserMiddleware(authorService).Handle)
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
indexHandler := NewIndexHandler(entryService, siteConfigService)
listHandler := NewListHandler(entryService, siteConfigService)
@ -51,15 +53,15 @@ func NewWebApp(
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
// Login
app.Get("/auth/login", loginHandler.HandleGet)
app.Post("/auth/login", loginHandler.HandlePost)
fiberApp.Get("/auth/login", loginHandler.HandleGet)
fiberApp.Post("/auth/login", loginHandler.HandlePost)
// admin
adminHandler := NewAdminHandler(configRepo, configRegister, typeRegistry)
draftHandler := NewDraftHandler(entryService, siteConfigService)
binaryManageHandler := NewBinaryManageHandler(configRepo, binService)
adminInteractionHandler := NewAdminInteractionHandler(configRepo, interactionRepo)
admin := app.Group("/admin")
admin := fiberApp.Group("/admin")
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
admin.Get("/", adminHandler.Handle)
admin.Get("/drafts/", draftHandler.Handle)
@ -75,7 +77,7 @@ func NewWebApp(
adminApi.Post("/binaries", binaryManageHandler.HandleUploadApi)
// Editor
editor := app.Group("/editor")
editor := fiberApp.Group("/editor")
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
@ -85,7 +87,7 @@ func NewWebApp(
editor.Post("/unpublish/:id/", editorHandler.HandlePostUnpublish)
// SiteConfig
siteConfig := app.Group("/site-config")
siteConfig := fiberApp.Group("/site-config")
siteConfig.Use(middleware.NewAuthMiddleware(authorService).Handle)
siteConfigHandler := NewSiteConfigHandler(siteConfigService)
@ -107,39 +109,39 @@ func NewWebApp(
siteConfig.Post("/menus/create/", siteConfigMenusHandler.HandleCreate)
siteConfig.Post("/menus/delete/", siteConfigMenusHandler.HandleDelete)
app.Use("/static", filesystem.New(filesystem.Config{
fiberApp.Use("/static", filesystem.New(filesystem.Config{
Root: http.FS(embedDirStatic),
PathPrefix: "static",
Browse: false,
}))
app.Get("/", indexHandler.Handle)
app.Get("/lists/:list/", listHandler.Handle)
fiberApp.Get("/", indexHandler.Handle)
fiberApp.Get("/lists/:list/", listHandler.Handle)
// Media
app.Get("/media/+", mediaHandler.Handle)
fiberApp.Get("/media/+", mediaHandler.Handle)
// RSS
app.Get("/index.xml", rssHandler.Handle)
fiberApp.Get("/index.xml", rssHandler.Handle)
// Posts
app.Get("/posts/:post/", entryHandler.Handle)
fiberApp.Get("/posts/:post/", entryHandler.Handle)
// Webmention
app.Post("/webmention/", webmentionHandler.Handle)
fiberApp.Post("/webmention/", webmentionHandler.Handle)
// robots.txt
app.Get("/robots.txt", func(c *fiber.Ctx) error {
fiberApp.Get("/robots.txt", func(c *fiber.Ctx) error {
siteConfig, _ := siteConfigService.GetSiteConfig()
sitemapUrl, _ := url.JoinPath(siteConfig.FullUrl, "/sitemap.xml")
c.Set("Content-Type", "text/plain")
return c.SendString(fmt.Sprintf("User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nAllow: /\n\nSitemap: %s\n", sitemapUrl))
})
// sitemap.xml
app.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
fiberApp.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
// ActivityPub
activityPubServer := NewActivityPubServer(configRepo, entryService)
configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{})
app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
app.Route("/activitypub", activityPubServer.Router)
activityPubServer := NewActivityPubServer(configRepo, entryService, apService)
configRegister.Register(config.ACT_PUB_CONF_NAME, &app.ActivityPubConfig{})
fiberApp.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
fiberApp.Route("/activitypub", activityPubServer.Router)
return &WebApp{
FiberApp: app,
FiberApp: fiberApp,
EntryService: entryService,
Registry: typeRegistry,
BinaryService: binService,