aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--http/router.go28
-rw-r--r--main.go8
-rw-r--r--registry/registry.go408
3 files changed, 18 insertions, 426 deletions
diff --git a/http/router.go b/http/router.go
index 24694b1..5ef172a 100644
--- a/http/router.go
+++ b/http/router.go
@@ -6,18 +6,18 @@ import (
"io"
"net/http"
- "git.capotej.com/capotej/communique/registry"
+ "git.capotej.com/capotej/communique/controller"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type Router struct {
- registry *registry.Registry
- log *zap.SugaredLogger
+ controller *controller.Controller
+ log *zap.SugaredLogger
}
-func NewRouter(registry *registry.Registry, log *zap.SugaredLogger) *Router {
- return &Router{registry: registry, log: log}
+func NewRouter(controller *controller.Controller, log *zap.SugaredLogger) *Router {
+ return &Router{controller: controller, log: log}
}
func render(c *gin.Context, resource map[string]interface{}, err error) {
@@ -41,7 +41,7 @@ func (s *Router) Start(zapWriter io.Writer) {
// Webfinger
router.GET("/.well-known/webfinger", func(c *gin.Context) {
resourceParam := c.Query("resource")
- resource, _ := s.registry.Webfinger(resourceParam)
+ resource, _ := s.controller.Webfinger(resourceParam)
if resource != nil {
c.Writer.Header().Set("Content-Type", "application/jrd+json")
c.JSON(http.StatusOK, resource)
@@ -54,7 +54,7 @@ func (s *Router) Start(zapWriter io.Writer) {
// Actor
router.GET("/actors/:actor", func(c *gin.Context) {
actorParam := c.Param("actor")
- resource, err := s.registry.Actor(actorParam)
+ resource, err := s.controller.Actor(actorParam)
render(c, resource, err)
})
@@ -62,7 +62,7 @@ func (s *Router) Start(zapWriter io.Writer) {
// Actor avatar
router.GET("/actors/:actor/avatar", func(c *gin.Context) {
actorParam := c.Param("actor")
- avatarBytes, mediaType, err := s.registry.ActorAvatar(actorParam)
+ avatarBytes, mediaType, err := s.controller.ActorAvatar(actorParam)
if err != nil || avatarBytes == nil || mediaType == "" {
c.Data(404, "text/plain", []byte("404 page not found"))
}
@@ -73,14 +73,14 @@ func (s *Router) Start(zapWriter io.Writer) {
// Actor Followers
router.GET("/actors/:actor/followers", func(c *gin.Context) {
actorParam := c.Param("actor")
- resource, err := s.registry.Followers(actorParam)
+ resource, err := s.controller.Followers(actorParam)
render(c, resource, err)
})
// Actor Following
router.GET("/actors/:actor/following", func(c *gin.Context) {
actorParam := c.Param("actor")
- resource, err := s.registry.Following(actorParam)
+ resource, err := s.controller.Following(actorParam)
render(c, resource, err)
})
@@ -104,7 +104,7 @@ func (s *Router) Start(zapWriter io.Writer) {
payload,
).Debug("received inbox item")
actorParam := c.Param("actor")
- err := s.registry.Inbox(actorParam, c.Request, buf.Bytes())
+ err := s.controller.Inbox(actorParam, c.Request, buf.Bytes())
resource := map[string]interface{}{}
render(c, resource, err)
})
@@ -112,7 +112,7 @@ func (s *Router) Start(zapWriter io.Writer) {
// Actor Outbox
router.GET("/actors/:actor/outbox", func(c *gin.Context) {
actorParam := c.Param("actor")
- resource, err := s.registry.OutboxCollection(actorParam)
+ resource, err := s.controller.OutboxCollection(actorParam)
render(c, resource, err)
})
@@ -120,7 +120,7 @@ func (s *Router) Start(zapWriter io.Writer) {
router.GET("/actors/:actor/activity/:id", func(c *gin.Context) {
actorParam := c.Param("actor")
idParam := c.Param("id")
- resource, err := s.registry.ActivityOrNote("activity", actorParam, idParam)
+ resource, err := s.controller.ActivityOrNote("activity", actorParam, idParam)
render(c, resource, err)
})
@@ -128,7 +128,7 @@ func (s *Router) Start(zapWriter io.Writer) {
router.GET("/actors/:actor/activity/:id/note", func(c *gin.Context) {
actorParam := c.Param("actor")
idParam := c.Param("id")
- resource, err := s.registry.ActivityOrNote("note", actorParam, idParam)
+ resource, err := s.controller.ActivityOrNote("note", actorParam, idParam)
render(c, resource, err)
})
diff --git a/main.go b/main.go
index b4ea011..b5720b1 100644
--- a/main.go
+++ b/main.go
@@ -6,9 +6,9 @@ import (
"git.capotej.com/capotej/communique/cgi"
"git.capotej.com/capotej/communique/config"
+ "git.capotej.com/capotej/communique/controller"
"git.capotej.com/capotej/communique/http"
"git.capotej.com/capotej/communique/models"
- "git.capotej.com/capotej/communique/registry"
"github.com/BurntSushi/toml"
"github.com/dgraph-io/badger/v3"
"github.com/microcosm-cc/bluemonday"
@@ -58,8 +58,8 @@ func main() {
// Persister
persister := models.NewPersister(log, db)
- // Registry
- registry, err := registry.NewRegistry(cfg, persister, log)
+ // Controller
+ controller, err := controller.NewController(cfg, persister, log)
if err != nil {
log.Fatal(err)
}
@@ -76,7 +76,7 @@ func main() {
// // External Http Server
writer := &zapio.Writer{Log: logger, Level: zap.DebugLevel}
defer writer.Close()
- router := http.NewRouter(registry, log)
+ router := http.NewRouter(controller, log)
mainWg.Add(1)
go router.Start(writer)
diff --git a/registry/registry.go b/registry/registry.go
deleted file mode 100644
index d5f8bd6..0000000
--- a/registry/registry.go
+++ /dev/null
@@ -1,408 +0,0 @@
-package registry
-
-import (
- "bytes"
- "context"
- "crypto/x509"
- "encoding/gob"
- "encoding/json"
- "encoding/pem"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "net/url"
- "strings"
-
- "git.capotej.com/capotej/communique/config"
- "git.capotej.com/capotej/communique/delivery"
- "git.capotej.com/capotej/communique/models"
- "git.capotej.com/capotej/communique/tools"
- "git.capotej.com/capotej/communique/urls"
- "git.capotej.com/capotej/communique/views"
- "github.com/go-fed/activity/streams"
- "github.com/go-fed/activity/streams/vocab"
- "go.uber.org/zap"
-)
-
-type Handler struct {
- handlerCfg config.Handler
-}
-
-// TODO rename to controller and controller.go
-type Registry struct {
- cfg config.Config
- persister *models.Persister
- handlerMap map[string]Handler
- log *zap.SugaredLogger
- delivery *delivery.Signed
-}
-
-func NewRegistry(cfg config.Config, persister *models.Persister, log *zap.SugaredLogger) (*Registry, error) {
- reg := Registry{cfg: cfg, persister: persister, log: log}
- reg.handlerMap = make(map[string]Handler)
- var err error
- for _, v := range cfg.Handlers {
- reg.handlerMap[v.Name] = Handler{handlerCfg: v}
- err = generateKeypairIfNeeded(v, persister)
- if err != nil {
- return nil, err
- }
- err = persistAvatarIfFound(v, persister)
- if err != nil {
- return nil, err
- }
- }
- reg.delivery, err = delivery.NewSigned(persister)
- if err != nil {
- return nil, err
- }
- return &reg, nil
-}
-
-func generateKeypairIfNeeded(v config.Handler, p *models.Persister) error {
- kp, err := models.CreateKeypair(v)
- if err != nil {
- return err
- }
- err = p.Store(kp)
- if err != nil {
- return err
- }
- return nil
-}
-
-func persistAvatarIfFound(v config.Handler, p *models.Persister) error {
- if v.AvatarUrl == "" || v.AvatarContentType == "" {
- return nil
- }
- resp, err := http.Get(v.AvatarUrl)
- if resp.StatusCode != 200 {
- return fmt.Errorf("request to avatarUrl %s failed", v.AvatarUrl)
- }
- contentType := resp.Header.Get("content-type")
- if contentType != v.AvatarContentType {
- return fmt.Errorf("avatarUrl response content-type '%s' does match avatarContentType '%s'", contentType, v.AvatarContentType)
- }
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- if len(body) == 0 {
- return fmt.Errorf("avatarUrl response was empty")
- }
- kp, err := models.CreateAvatar(v, v.AvatarContentType, body)
- if err != nil {
- return err
- }
- err = p.Store(kp)
- if err != nil {
- return err
- }
- return nil
-}
-
-func (r *Registry) Actor(name string) (map[string]interface{}, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, nil
- }
- aso := models.NewKeypair(handler.handlerCfg)
- result, err := r.persister.Find(aso)
- if err != nil {
- return nil, err
- }
- buf := bytes.NewBuffer(result)
- dec := gob.NewDecoder(buf)
- var keypair models.Keypair
- err = dec.Decode(&keypair)
- if err != nil {
- return nil, err
- }
- privKey := &keypair.PrivateKey
- pemdata := pem.EncodeToMemory(
- &pem.Block{
- Type: "RSA PUBLIC KEY",
- Bytes: x509.MarshalPKCS1PublicKey(&privKey.PublicKey),
- },
- )
- return views.RenderActor(handler.handlerCfg.Name, r.cfg.Domain, string(pemdata), handler.handlerCfg.AvatarContentType, handler.handlerCfg.Summary)
-}
-
-func (r *Registry) ActorAvatar(name string) ([]byte, string, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, "", nil
- }
- aso := models.NewAvatar(handler.handlerCfg)
- result, err := r.persister.Find(aso)
- if err != nil {
- return nil, "", err
- }
- buf := bytes.NewBuffer(result)
- dec := gob.NewDecoder(buf)
- var avatar models.Avatar
- err = dec.Decode(&avatar)
- if err != nil {
- return nil, "", err
- }
- return avatar.Bytes, handler.handlerCfg.AvatarContentType, nil
-}
-
-func (r *Registry) OutboxCollection(name string) (map[string]interface{}, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, nil
- }
- aso := models.NewOutboxItem(handler.handlerCfg)
- page, err := r.persister.Collect(aso)
- if err != nil {
- return nil, err
- }
- var outboxItems []models.OutboxItem
- for _, v := range page { //TODO pagination
- buf := bytes.NewBuffer(v)
- dec := gob.NewDecoder(buf)
- var outboxItem models.OutboxItem
- err = dec.Decode(&outboxItem)
- if err != nil {
- return nil, err
- }
- outboxItems = append(outboxItems, outboxItem)
- }
- return views.RenderOutboxCollection(handler.handlerCfg.Name, r.cfg.Domain, outboxItems)
-}
-
-func (r *Registry) Following(name string) (map[string]interface{}, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, nil
- }
- profile, err := urls.UrlProfile(name, r.cfg.Domain)
- if err != nil {
- return nil, err
- }
- following, err := urls.UrlFollowing(name, r.cfg.Domain)
- if err != nil {
- return nil, err
- }
- result := make(map[string]interface{})
- result["@context"] = "https://www.w3.org/ns/activitystreams"
- result["attributedTo"] = profile.String()
- result["id"] = following.String()
- result["totalItems"] = 0
- result["orderedItems"] = []bool{}
- result["type"] = "OrderedCollection"
- return result, nil
-}
-
-func (r *Registry) Followers(name string) (map[string]interface{}, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, nil
- }
- profile, err := urls.UrlProfile(name, r.cfg.Domain)
- if err != nil {
- return nil, err
- }
- followers, err := urls.UrlFollowers(name, r.cfg.Domain)
- if err != nil {
- return nil, err
- }
- result := make(map[string]interface{})
- result["@context"] = "https://www.w3.org/ns/activitystreams"
- result["attributedTo"] = profile.String()
- result["id"] = followers.String()
- result["totalItems"] = 0
- result["orderedItems"] = []bool{}
- result["type"] = "OrderedCollection"
- return result, nil
-}
-
-func (r *Registry) Inbox(name string, req *http.Request, payload []byte) error {
- handler := r.findByName(name)
- if handler == nil {
- return nil
- }
-
- domainUrl, err := url.Parse(r.cfg.Domain)
- if err != nil {
- return err
- }
-
- logger := r.log.With("type", "inbox")
-
- if req.Host != domainUrl.Host {
- logger.Warnf("%s != %s, configured domain should match incoming Host: header otherwise things may not work right. You'll need to enable 'ProxyPreserveHost' (for apache2) or proxy_set_header Host $host; (for nginx)", req.Host, r.cfg.Domain)
- }
-
- ctx := context.Background()
-
- person, verifyError := tools.VerifyRequest(ctx, req, r.log)
- if verifyError != nil {
- return verifyError
- }
-
- var followData map[string]interface{}
- err = json.Unmarshal(payload, &followData)
- if err != nil {
- return err
- }
-
- actorUrl, err := urls.UrlProfile(handler.handlerCfg.Name, r.cfg.Domain)
- if err != nil {
- return err
- }
-
- resolver, err := streams.NewJSONResolver(func(c context.Context, follow vocab.ActivityStreamsFollow) error {
- // Follow
- idProp := person.GetJSONLDId()
- idPropUrl := idProp.Get()
- inboxProp := person.GetActivityStreamsInbox()
- inboxUrl := inboxProp.GetIRI()
- logger.With("actor", idPropUrl).With("inbox", inboxUrl).Debugf("follow")
-
- actorKeyUrl, err := urls.UrlProfileKey(handler.handlerCfg.Name, r.cfg.Domain)
- if err != nil {
- return err
- }
- err = r.deliverAcceptToInbox(inboxUrl, actorUrl, actorKeyUrl, follow, handler.handlerCfg)
- if err != nil {
- return err
- }
- return r.subscribeActorToHandler(handler.handlerCfg, inboxUrl.String())
- }, func(c context.Context, note vocab.ActivityStreamsUndo) error {
- // Unfollow
- idProp := person.GetJSONLDId()
- idPropUrl := idProp.Get()
- inboxProp := person.GetActivityStreamsInbox()
- inboxUrl := inboxProp.GetIRI()
- logger.With("actor", idPropUrl).With("inbox", inboxUrl).Debugf("unfollow/undo")
- return r.unsubscribeActorToHandler(handler.handlerCfg, inboxUrl.String())
- })
- err = resolver.Resolve(ctx, followData)
-
- return err
-}
-func (r *Registry) subscribeActorToHandler(handler config.Handler, inboxUrl string) error {
- aso, err := models.CreateSubscription(handler, inboxUrl)
- r.persister.Store(aso)
- return err
-}
-
-func (r *Registry) unsubscribeActorToHandler(handler config.Handler, inboxUrl string) error {
- aso, err := models.CreateSubscription(handler, inboxUrl)
- r.persister.Delete(aso)
- return err
-}
-
-// TODO should probably be in its own delivery package
-func (r *Registry) deliverAcceptToInbox(url, actorUrl, actorKeyUrl *url.URL, follow vocab.ActivityStreamsFollow, handler config.Handler) error {
- accept := streams.NewActivityStreamsAccept()
- actorProp := streams.NewActivityStreamsActorProperty()
- actorProp.AppendIRI(actorUrl)
- accept.SetActivityStreamsActor(actorProp)
- objProp := streams.NewActivityStreamsObjectProperty()
- objProp.AppendActivityStreamsFollow(follow)
- accept.SetActivityStreamsObject(objProp)
- payload, err := streams.Serialize(accept)
- if err != nil {
- return err
- }
- jsonData, err := json.Marshal(payload)
- if err != nil {
- return err
- }
-
- request, err := r.delivery.SignedRequest(handler, jsonData, url, actorKeyUrl)
- if err != nil {
- return err
- }
-
- r.log.With(
- "type",
- "delivery",
- ).With(
- "inbox",
- url.String(),
- ).With(
- "digest",
- request.Header.Get("digest"),
- ).With(
- "signature",
- request.Header.Get("signature"),
- ).With(
- "actor",
- actorUrl.String(),
- ).Debug("sending signed accept request")
-
- client := &http.Client{}
- response, err := client.Do(request)
- if err != nil {
- return fmt.Errorf("could not send accept request: %w", err)
- }
- responseBody, err := io.ReadAll(response.Body)
- defer response.Body.Close()
- r.log.With("type", "delivery").With("response", responseBody).With("status", response.Status).Debugf("remote inbox response received")
- return err
-}
-
-func (r *Registry) ActivityOrNote(activityOrNote, name, id string) (map[string]interface{}, error) {
- handler := r.findByName(name)
- if handler == nil {
- return nil, nil
- }
- lookup := models.NewOutboxItem(handler.handlerCfg)
- lookup.Id = []byte(id)
- result, err := r.persister.Find(lookup)
- if err != nil {
- return nil, err
- }
- if result == nil {
- return nil, nil
- }
- buf := bytes.NewBuffer(result)
- dec := gob.NewDecoder(buf)
- var outboxItem models.OutboxItem
- err = dec.Decode(&outboxItem)
- if err != nil {
- return nil, err
- }
- if activityOrNote == "activity" {
- return views.RenderActivity(handler.handlerCfg.Name, r.cfg.Domain, outboxItem)
- }
- return views.RenderNote(handler.handlerCfg.Name, r.cfg.Domain, outboxItem)
-}
-
-// This has to handle various lookup formats:
-// ?resource=acct:actor@domain
-// ?resource=actor@domain
-// ?resource=actor
-// ?resource=acct:actor
-func (r *Registry) Webfinger(fqn string) (*views.WebfingerResource, error) {
- // Strip away acct: prefix, if found
- fqn = strings.TrimPrefix(fqn, "acct:")
-
- // Strip away @domain suffix, if found
- domainUrl, err := url.Parse(r.cfg.Domain)
- if err != nil {
- return nil, err
- }
- hostname := "@" + domainUrl.Hostname()
- fqn = strings.TrimSuffix(fqn, hostname)
-
- // We should just have $actor left
- handler := r.findByName(fqn)
- if handler == nil {
- return nil, nil
- }
- return views.RenderWebfinger(handler.handlerCfg.Name, r.cfg.Domain, hostname)
-}
-
-func (r *Registry) findByName(name string) *Handler {
- handler, ok := r.handlerMap[name]
- if !ok {
- return nil
- }
- return &handler
-}