diff options
Diffstat (limited to 'registry/registry.go')
-rw-r--r-- | registry/registry.go | 408 |
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 ®, 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 -} |