aboutsummaryrefslogtreecommitdiff
path: root/registry
diff options
context:
space:
mode:
authorJulio Capote <jcapote@gmail.com>2023-01-08 13:59:00 +0000
committerJulio Capote <jcapote@gmail.com>2023-01-08 13:59:00 +0000
commit555a2b6ba33afbb43bf84836747e5810eeee365d (patch)
treed8b5665f2f0dc91dbc78cad2118833b2cf79efc6 /registry
parenta3ae6eaa65744014c225e82d142b217b8ce78488 (diff)
downloadcommunique-555a2b6ba33afbb43bf84836747e5810eeee365d.tar.gz
rename registry to controller
Diffstat (limited to 'registry')
-rw-r--r--registry/registry.go408
1 files changed, 0 insertions, 408 deletions
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
-}