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 }