From 9484880bcad3d4ac8bd52f649bc275bba9494abe Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 8 Mar 2021 11:43:38 -0500 Subject: [PATCH 1/4] Sign actor fetch request This fixes federation with Mastodon instances that have Authorized Fetch turned on by signing the GET request to fetch the actor when a blog is first followed. Ref T820 --- activitypub.go | 38 ++++++++++++++++++++++++++++++++++++-- app.go | 8 +++++++- routes.go | 7 ++++++- webfinger.go | 6 ++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/activitypub.go b/activitypub.go index 0e69075..855f9a7 100644 --- a/activitypub.go +++ b/activitypub.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2020 A Bunch Tell LLC. + * Copyright © 2018-2021 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -17,10 +17,12 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/writeas/writefreely/config" "io/ioutil" "net/http" "net/http/httputil" "net/url" + "path/filepath" "strconv" "time" @@ -41,6 +43,17 @@ const ( apCacheTime = time.Minute ) +var instanceColl *Collection + +func initActivityPub(cfg *config.Config) { + ur, _ := url.Parse(cfg.App.Host) + instanceColl = &Collection{ + ID: 0, + Alias: ur.Host, + Title: ur.Host, + } +} + type RemoteUser struct { ID int64 ActorID string @@ -76,12 +89,17 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re vars := mux.Vars(r) alias := vars["alias"] + if alias == "" { + alias = filepath.Base(r.RequestURI) + } // TODO: enforce visibility // Get base Collection data var c *Collection var err error - if app.cfg.App.SingleUser { + if alias == r.Host { + c = instanceColl + } else if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) @@ -546,6 +564,22 @@ func resolveIRI(hostName, url string) ([]byte, error) { r.Header.Add("Accept", "application/activity+json") r.Header.Set("User-Agent", ServerUserAgent(hostName)) + p := instanceColl.PersonObject() + h := sha256.New() + h.Write([]byte{}) + r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil))) + + // Sign using the 'Signature' header + privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey()) + if err != nil { + return nil, err + } + signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"}) + err = signer.SignSigHeader(r) + if err != nil { + log.Error("Can't sign: %v", err) + } + if debugging { dump, err := httputil.DumpRequestOut(r, true) if err != nil { diff --git a/app.go b/app.go index 2aed437..a0e754e 100644 --- a/app.go +++ b/app.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019 A Bunch Tell LLC. + * Copyright © 2018-2021 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -384,6 +384,8 @@ func Initialize(apper Apper, debug bool) (*App, error) { apper.App().InitDecoder() + apper.App().InitActivityPub() + err = ConnectToDatabase(apper.App()) if err != nil { return nil, fmt.Errorf("connect to DB: %s", err) @@ -499,6 +501,10 @@ func (app *App) InitDecoder() { app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) } +func (app *App) InitActivityPub() { + initActivityPub(app.cfg) +} + // ConnectToDatabase validates and connects to the configured database, then // tests the connection. func ConnectToDatabase(app *App) error { diff --git a/routes.go b/routes.go index bb1785f..d3c56ca 100644 --- a/routes.go +++ b/routes.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019 A Bunch Tell LLC. + * Copyright © 2018-2021 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -12,6 +12,7 @@ package writefreely import ( "net/http" + "net/url" "path/filepath" "strings" @@ -125,9 +126,13 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/api/markdown", handler.All(handleRenderMarkdown)).Methods("POST") + instanceURL, _ := url.Parse(apper.App().Config().App.Host) + host := instanceURL.Host + // Handle collections write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST") apiColls := write.PathPrefix("/api/collections/").Subrouter() + apiColls.HandleFunc("/"+host, handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE") apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET") diff --git a/webfinger.go b/webfinger.go index 993272f..76a9b66 100644 --- a/webfinger.go +++ b/webfinger.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2020 A Bunch Tell LLC. + * Copyright © 2018-2021 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -32,7 +32,9 @@ var wfUserNotFoundErr = impart.HTTPError{http.StatusNotFound, "User not found."} func (wfr wfResolver) FindUser(username string, host, requestHost string, r []webfinger.Rel) (*webfinger.Resource, error) { var c *Collection var err error - if wfr.cfg.App.SingleUser { + if username == host { + c = instanceColl + } else if wfr.cfg.App.SingleUser { c, err = wfr.db.GetCollectionByID(1) } else { c, err = wfr.db.GetCollection(username) From 9aeeb52bdb500832cad4c801817a07ad6f90a2ce Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 8 Mar 2021 12:50:08 -0500 Subject: [PATCH 2/4] Fix nil pointer on instance-wide actor lookup Ref T820 --- activitypub.go | 12 +++++++----- app.go | 8 ++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/activitypub.go b/activitypub.go index 855f9a7..9a3a3f0 100644 --- a/activitypub.go +++ b/activitypub.go @@ -45,12 +45,14 @@ const ( var instanceColl *Collection -func initActivityPub(cfg *config.Config) { - ur, _ := url.Parse(cfg.App.Host) +func initActivityPub(app *App) { + ur, _ := url.Parse(app.cfg.App.Host) instanceColl = &Collection{ - ID: 0, - Alias: ur.Host, - Title: ur.Host, + ID: 0, + Alias: ur.Host, + Title: ur.Host, + db: app.db, + hostName: app.cfg.App.Host, } } diff --git a/app.go b/app.go index a0e754e..d34daf5 100644 --- a/app.go +++ b/app.go @@ -384,13 +384,13 @@ func Initialize(apper Apper, debug bool) (*App, error) { apper.App().InitDecoder() - apper.App().InitActivityPub() - err = ConnectToDatabase(apper.App()) if err != nil { return nil, fmt.Errorf("connect to DB: %s", err) } + initActivityPub(apper.App()) + // Handle local timeline, if enabled if apper.App().cfg.App.LocalTimeline { log.Info("Initializing local timeline...") @@ -501,10 +501,6 @@ func (app *App) InitDecoder() { app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) } -func (app *App) InitActivityPub() { - initActivityPub(app.cfg) -} - // ConnectToDatabase validates and connects to the configured database, then // tests the connection. func ConnectToDatabase(app *App) error { From 9b336dee8c6eed18db4ce0adc135f35fae59962b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 8 Mar 2021 12:54:50 -0500 Subject: [PATCH 3/4] Fix instance-wide actor lookup This skips the silenced-user check. Ref T820 --- activitypub.go | 20 +++++++++++--------- collections.go | 7 ++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/activitypub.go b/activitypub.go index 9a3a3f0..6080884 100644 --- a/activitypub.go +++ b/activitypub.go @@ -17,7 +17,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/writeas/writefreely/config" "io/ioutil" "net/http" "net/http/httputil" @@ -109,16 +108,19 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re if err != nil { return err } - silenced, err := app.db.IsUserSilenced(c.OwnerID) - if err != nil { - log.Error("fetch collection activities: %v", err) - return ErrInternalGeneral - } - if silenced { - return ErrCollectionNotFound - } c.hostName = app.cfg.App.Host + if !c.IsInstanceColl() { + silenced, err := app.db.IsUserSilenced(c.OwnerID) + if err != nil { + log.Error("fetch collection activities: %v", err) + return ErrInternalGeneral + } + if silenced { + return ErrCollectionNotFound + } + } + p := c.PersonObject() setCacheControl(w, apCacheTime) diff --git a/collections.go b/collections.go index e1ebe48..c7b84dc 100644 --- a/collections.go +++ b/collections.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2020 A Bunch Tell LLC. + * Copyright © 2018-2021 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -180,6 +180,11 @@ func (c *Collection) NewFormat() *CollectionFormat { return cf } +func (c *Collection) IsInstanceColl() bool { + ur, _ := url.Parse(c.hostName) + return c.Alias == ur.Host +} + func (c *Collection) IsUnlisted() bool { return c.Visibility == 0 } From 9f525876f45beeaac15a7e50785c3355f15d0e74 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 8 Mar 2021 13:02:59 -0500 Subject: [PATCH 4/4] Fix instance-wide actor webfinger lookup This skips the silenced-user check. Ref T820 --- webfinger.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/webfinger.go b/webfinger.go index 76a9b66..581d940 100644 --- a/webfinger.go +++ b/webfinger.go @@ -43,15 +43,18 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we log.Error("Unable to get blog: %v", err) return nil, err } - silenced, err := wfr.db.IsUserSilenced(c.OwnerID) - if err != nil { - log.Error("webfinger find user: check is silenced: %v", err) - return nil, err - } - if silenced { - return nil, wfUserNotFoundErr - } c.hostName = wfr.cfg.App.Host + + if !c.IsInstanceColl() { + silenced, err := wfr.db.IsUserSilenced(c.OwnerID) + if err != nil { + log.Error("webfinger find user: check is silenced: %v", err) + return nil, err + } + if silenced { + return nil, wfUserNotFoundErr + } + } if wfr.cfg.App.SingleUser { // Ensure handle matches user-chosen one on single-user blogs if username != c.Alias {