diff options
-rw-r--r-- | http/router.go | 28 | ||||
-rw-r--r-- | main.go | 8 | ||||
-rw-r--r-- | registry/registry.go | 408 |
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) }) @@ -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 ®, 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 -} |