diff --git a/internal/apimodule/account/test/accountcreate_test.go b/internal/apimodule/account/accountcreate_test.go similarity index 99% rename from internal/apimodule/account/test/accountcreate_test.go rename to internal/apimodule/account/accountcreate_test.go index 81eab467a..da2b22510 100644 --- a/internal/apimodule/account/test/accountcreate_test.go +++ b/internal/apimodule/account/accountcreate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account +package account_test import ( "bytes" diff --git a/internal/apimodule/account/test/accountupdate_test.go b/internal/apimodule/account/accountupdate_test.go similarity index 99% rename from internal/apimodule/account/test/accountupdate_test.go rename to internal/apimodule/account/accountupdate_test.go index 1c6f528a1..78664f19c 100644 --- a/internal/apimodule/account/test/accountupdate_test.go +++ b/internal/apimodule/account/accountupdate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account +package account_test import ( "bytes" diff --git a/internal/apimodule/account/test/accountverify_test.go b/internal/apimodule/account/accountverify_test.go similarity index 97% rename from internal/apimodule/account/test/accountverify_test.go rename to internal/apimodule/account/accountverify_test.go index 223a0c145..85b0dce50 100644 --- a/internal/apimodule/account/test/accountverify_test.go +++ b/internal/apimodule/account/accountverify_test.go @@ -16,4 +16,4 @@ along with this program. If not, see . */ -package account +package account_test diff --git a/internal/apimodule/app/test/app_test.go b/internal/apimodule/app/app_test.go similarity index 97% rename from internal/apimodule/app/test/app_test.go rename to internal/apimodule/app/app_test.go index d45b04e74..42760a2db 100644 --- a/internal/apimodule/app/test/app_test.go +++ b/internal/apimodule/app/app_test.go @@ -16,6 +16,6 @@ along with this program. If not, see . */ -package app +package app_test // TODO: write tests diff --git a/internal/apimodule/auth/test/auth_test.go b/internal/apimodule/auth/auth_test.go similarity index 99% rename from internal/apimodule/auth/test/auth_test.go rename to internal/apimodule/auth/auth_test.go index 2c272e985..95ae0e751 100644 --- a/internal/apimodule/auth/test/auth_test.go +++ b/internal/apimodule/auth/auth_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package auth +package auth_test import ( "context" diff --git a/internal/apimodule/fileserver/test/servefile_test.go b/internal/apimodule/fileserver/servefile_test.go similarity index 99% rename from internal/apimodule/fileserver/test/servefile_test.go rename to internal/apimodule/fileserver/servefile_test.go index 516e3528c..4c0844010 100644 --- a/internal/apimodule/fileserver/test/servefile_test.go +++ b/internal/apimodule/fileserver/servefile_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package test +package fileserver_test import ( "context" diff --git a/internal/apimodule/media/test/mediacreate_test.go b/internal/apimodule/media/mediacreate_test.go similarity index 99% rename from internal/apimodule/media/test/mediacreate_test.go rename to internal/apimodule/media/mediacreate_test.go index 30bbb117a..0c13ab8ab 100644 --- a/internal/apimodule/media/test/mediacreate_test.go +++ b/internal/apimodule/media/mediacreate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package test +package media_test import ( "bytes" diff --git a/internal/apimodule/status/test/statuscreate_test.go b/internal/apimodule/status/statuscreate_test.go similarity index 99% rename from internal/apimodule/status/test/statuscreate_test.go rename to internal/apimodule/status/statuscreate_test.go index d143ac9a7..8c2212b26 100644 --- a/internal/apimodule/status/test/statuscreate_test.go +++ b/internal/apimodule/status/statuscreate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package status_test import ( "encoding/json" diff --git a/internal/apimodule/status/test/statusfave_test.go b/internal/apimodule/status/statusfave_test.go similarity index 99% rename from internal/apimodule/status/test/statusfave_test.go rename to internal/apimodule/status/statusfave_test.go index 9ccf58948..1c8a508ad 100644 --- a/internal/apimodule/status/test/statusfave_test.go +++ b/internal/apimodule/status/statusfave_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package status_test import ( "encoding/json" diff --git a/internal/apimodule/status/test/statusfavedby_test.go b/internal/apimodule/status/statusfavedby_test.go similarity index 99% rename from internal/apimodule/status/test/statusfavedby_test.go rename to internal/apimodule/status/statusfavedby_test.go index 169543a81..4156023f0 100644 --- a/internal/apimodule/status/test/statusfavedby_test.go +++ b/internal/apimodule/status/statusfavedby_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package status_test import ( "encoding/json" diff --git a/internal/apimodule/status/test/statusget_test.go b/internal/apimodule/status/statusget_test.go similarity index 99% rename from internal/apimodule/status/test/statusget_test.go rename to internal/apimodule/status/statusget_test.go index ce817d247..bef51ee29 100644 --- a/internal/apimodule/status/test/statusget_test.go +++ b/internal/apimodule/status/statusget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package status_test import ( "testing" diff --git a/internal/apimodule/status/test/statusunfave_test.go b/internal/apimodule/status/statusunfave_test.go similarity index 99% rename from internal/apimodule/status/test/statusunfave_test.go rename to internal/apimodule/status/statusunfave_test.go index 5f5277921..3838ad9c5 100644 --- a/internal/apimodule/status/test/statusunfave_test.go +++ b/internal/apimodule/status/statusunfave_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package status_test import ( "encoding/json" diff --git a/internal/federation/actor.go b/internal/federation/actor.go new file mode 100644 index 000000000..c5cd37c18 --- /dev/null +++ b/internal/federation/actor.go @@ -0,0 +1,44 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +// Package federation provides ActivityPub/federation functionality for GoToSocial +package federation + +import ( + "github.com/go-fed/activity/pub" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" +) + +// New returns a go-fed compatible federating actor +func New(db db.DB, config *config.Config, log *logrus.Logger) pub.FederatingActor { + + c := &Commoner{ + db: db, + log: log, + config: config, + } + + f := &Federator{ + db: db, + log: log, + config: config, + } + return pub.NewFederatingActor(c, f, db.Federation(), &Clock{}) +} diff --git a/internal/federation/clock.go b/internal/federation/clock.go new file mode 100644 index 000000000..1d85df097 --- /dev/null +++ b/internal/federation/clock.go @@ -0,0 +1,34 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package federation + +import "time" + +/* + GOFED CLOCK INTERFACE + Determines the time. +*/ + +// Clock implements the Clock interface of go-fed +type Clock struct{} + +// Now just returns the time now +func (c *Clock) Now() time.Time { + return time.Now() +} diff --git a/internal/federation/common.go b/internal/federation/common.go new file mode 100644 index 000000000..e752654e6 --- /dev/null +++ b/internal/federation/common.go @@ -0,0 +1,162 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package federation + +import ( + "context" + "net/http" + "net/url" + + "github.com/go-fed/activity/pub" + "github.com/go-fed/activity/streams/vocab" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" +) + +// Commoner implements the go-fed common behavior interface +type Commoner struct { + db db.DB + log *logrus.Logger + config *config.Config +} + +/* + GOFED COMMON BEHAVIOR INTERFACE + Contains functions required for both the Social API and Federating Protocol. + It is passed to the library as a dependency injection from the client + application. +*/ + +// AuthenticateGetInbox delegates the authentication of a GET to an +// inbox. +// +// Always called, regardless whether the Federated Protocol or Social +// API is enabled. +// +// If an error is returned, it is passed back to the caller of +// GetInbox. In this case, the implementation must not write a +// response to the ResponseWriter as is expected that the client will +// do so when handling the error. The 'authenticated' is ignored. +// +// If no error is returned, but authentication or authorization fails, +// then authenticated must be false and error nil. It is expected that +// the implementation handles writing to the ResponseWriter in this +// case. +// +// Finally, if the authentication and authorization succeeds, then +// authenticated must be true and error nil. The request will continue +// to be processed. +func (c *Commoner) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { + // TODO + // use context.WithValue() and context.Value() to set and get values through here + return nil, false, nil +} + +// AuthenticateGetOutbox delegates the authentication of a GET to an +// outbox. +// +// Always called, regardless whether the Federated Protocol or Social +// API is enabled. +// +// If an error is returned, it is passed back to the caller of +// GetOutbox. In this case, the implementation must not write a +// response to the ResponseWriter as is expected that the client will +// do so when handling the error. The 'authenticated' is ignored. +// +// If no error is returned, but authentication or authorization fails, +// then authenticated must be false and error nil. It is expected that +// the implementation handles writing to the ResponseWriter in this +// case. +// +// Finally, if the authentication and authorization succeeds, then +// authenticated must be true and error nil. The request will continue +// to be processed. +func (c *Commoner) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { + // TODO + return nil, false, nil +} + +// GetOutbox returns the OrderedCollection inbox of the actor for this +// context. It is up to the implementation to provide the correct +// collection for the kind of authorization given in the request. +// +// AuthenticateGetOutbox will be called prior to this. +// +// Always called, regardless whether the Federated Protocol or Social +// API is enabled. +func (c *Commoner) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { + // TODO + return nil, nil +} + +// NewTransport returns a new Transport on behalf of a specific actor. +// +// The actorBoxIRI will be either the inbox or outbox of an actor who is +// attempting to do the dereferencing or delivery. Any authentication +// scheme applied on the request must be based on this actor. The +// request must contain some sort of credential of the user, such as a +// HTTP Signature. +// +// The gofedAgent passed in should be used by the Transport +// implementation in the User-Agent, as well as the application-specific +// user agent string. The gofedAgent will indicate this library's use as +// well as the library's version number. +// +// Any server-wide rate-limiting that needs to occur should happen in a +// Transport implementation. This factory function allows this to be +// created, so peer servers are not DOS'd. +// +// Any retry logic should also be handled by the Transport +// implementation. +// +// Note that the library will not maintain a long-lived pointer to the +// returned Transport so that any private credentials are able to be +// garbage collected. +func (c *Commoner) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) { + // TODO + // prefs := []httpsig.Algorithm{httpsig.RSA_SHA256} + // digestPref := httpsig.DigestSha256 + // getHeadersToSign := []string{httpsig.RequestTarget, "Date"} + // postHeadersToSign := []string{httpsig.RequestTarget, "Date", "Digest"} + // // Using github.com/go-fed/httpsig for HTTP Signatures: + // getSigner, _, err := httpsig.NewSigner(prefs, digestPref, getHeadersToSign, httpsig.Signature) + // if err != nil { + // return nil, err + // } + // postSigner, _, err := httpsig.NewSigner(prefs, digestPref, postHeadersToSign, httpsig.Signature) + // if err != nil { + // return nil, err + // } + // pubKeyId, privKey, err := s.getKeysForActorBoxIRI(actorBoxIRI) + // client := &http.Client{ + // Timeout: time.Second * 30, + // } + // t := pub.NewHttpSigTransport( + // client, + // f.config.Host, + // &Clock{}, + // getSigner, + // postSigner, + // pubKeyId, + // privKey) + + return nil, nil + +} diff --git a/internal/federation/federation.go b/internal/federation/protocol.go similarity index 60% rename from internal/federation/federation.go rename to internal/federation/protocol.go index a2aba3fcf..ec50dd4c6 100644 --- a/internal/federation/federation.go +++ b/internal/federation/protocol.go @@ -16,32 +16,37 @@ along with this program. If not, see . */ -// Package federation provides ActivityPub/federation functionality for GoToSocial package federation import ( "context" + "errors" + "fmt" "net/http" "net/url" - "time" "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/util" ) -// New returns a go-fed compatible federating actor -func New(db db.DB, log *logrus.Logger) pub.FederatingActor { - f := &Federator{ - db: db, - } - return pub.NewFederatingActor(f, f, db.Federation(), f) +// Federator implements the go-fed federating protocol interface +type Federator struct { + db db.DB + log *logrus.Logger + config *config.Config } -// Federator implements several go-fed interfaces in one convenient location -type Federator struct { - db db.DB +// NewFederator returns the gotosocial implementation of the go-fed FederatingProtocol interface +func NewFederator(db db.DB, log *logrus.Logger, config *config.Config) pub.FederatingProtocol { + return &Federator{ + db: db, + log: log, + config: config, + } } /* @@ -71,8 +76,35 @@ type Federator struct { // write a response to the ResponseWriter as is expected that the caller // to PostInbox will do so when handling the error. func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) { - // TODO - return nil, nil + if activity == nil { + return nil, errors.New("nil activity in PostInboxRequestBodyHook") + } + + l := f.log.WithFields(logrus.Fields{ + "func": "PostInboxRequestBodyHook", + "useragent": r.UserAgent(), + "url": r.URL.String(), + "aptype": activity.GetTypeName(), + }) + l.Debugf("received inbox post request %+v", activity) + + if !util.IsInboxPath(r.URL) { + err := fmt.Errorf("url %s did not corresponding to inbox path", r.URL.String()) + l.Debug(err) + return nil, err + } + + username, err := util.ParseInboxPath(r.URL) + if err != nil { + err := fmt.Errorf("could not parse username from url: %s", r.URL.String()) + l.Debug(err) + return nil, err + } + l.Tracef("parsed username %s from %s", username, r.URL.String()) + + ctxWithUsername := context.WithValue(ctx, util.APUsernameKey, username) + ctxWithActivity := context.WithValue(ctxWithUsername, util.APActivityKey, activity) + return ctxWithActivity, nil } // AuthenticatePostInbox delegates the authentication of a POST to an @@ -147,7 +179,11 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrap // type and extension, so the unhandled ones are passed to // DefaultCallback. func (f *Federator) DefaultCallback(ctx context.Context, activity pub.Activity) error { - // TODO + l := f.log.WithFields(logrus.Fields{ + "func": "DefaultCallback", + "aptype": activity.GetTypeName(), + }) + l.Debugf("received unhandle-able activity type so ignoring it") return nil } @@ -194,110 +230,3 @@ func (f *Federator) GetInbox(ctx context.Context, r *http.Request) (vocab.Activi // TODO return nil, nil } - -/* - GOFED COMMON BEHAVIOR INTERFACE - Contains functions required for both the Social API and Federating Protocol. - It is passed to the library as a dependency injection from the client - application. -*/ - -// AuthenticateGetInbox delegates the authentication of a GET to an -// inbox. -// -// Always called, regardless whether the Federated Protocol or Social -// API is enabled. -// -// If an error is returned, it is passed back to the caller of -// GetInbox. In this case, the implementation must not write a -// response to the ResponseWriter as is expected that the client will -// do so when handling the error. The 'authenticated' is ignored. -// -// If no error is returned, but authentication or authorization fails, -// then authenticated must be false and error nil. It is expected that -// the implementation handles writing to the ResponseWriter in this -// case. -// -// Finally, if the authentication and authorization succeeds, then -// authenticated must be true and error nil. The request will continue -// to be processed. -func (f *Federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // TODO - // use context.WithValue() and context.Value() to set and get values through here - return nil, false, nil -} - -// AuthenticateGetOutbox delegates the authentication of a GET to an -// outbox. -// -// Always called, regardless whether the Federated Protocol or Social -// API is enabled. -// -// If an error is returned, it is passed back to the caller of -// GetOutbox. In this case, the implementation must not write a -// response to the ResponseWriter as is expected that the client will -// do so when handling the error. The 'authenticated' is ignored. -// -// If no error is returned, but authentication or authorization fails, -// then authenticated must be false and error nil. It is expected that -// the implementation handles writing to the ResponseWriter in this -// case. -// -// Finally, if the authentication and authorization succeeds, then -// authenticated must be true and error nil. The request will continue -// to be processed. -func (f *Federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // TODO - return nil, false, nil -} - -// GetOutbox returns the OrderedCollection inbox of the actor for this -// context. It is up to the implementation to provide the correct -// collection for the kind of authorization given in the request. -// -// AuthenticateGetOutbox will be called prior to this. -// -// Always called, regardless whether the Federated Protocol or Social -// API is enabled. -func (f *Federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // TODO - return nil, nil -} - -// NewTransport returns a new Transport on behalf of a specific actor. -// -// The actorBoxIRI will be either the inbox or outbox of an actor who is -// attempting to do the dereferencing or delivery. Any authentication -// scheme applied on the request must be based on this actor. The -// request must contain some sort of credential of the user, such as a -// HTTP Signature. -// -// The gofedAgent passed in should be used by the Transport -// implementation in the User-Agent, as well as the application-specific -// user agent string. The gofedAgent will indicate this library's use as -// well as the library's version number. -// -// Any server-wide rate-limiting that needs to occur should happen in a -// Transport implementation. This factory function allows this to be -// created, so peer servers are not DOS'd. -// -// Any retry logic should also be handled by the Transport -// implementation. -// -// Note that the library will not maintain a long-lived pointer to the -// returned Transport so that any private credentials are able to be -// garbage collected. -func (f *Federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) { - // TODO - return nil, nil -} - -/* - GOFED CLOCK INTERFACE - Determines the time. -*/ - -// Now returns the current time. -func (f *Federator) Now() time.Time { - return time.Now() -} diff --git a/internal/federation/protocol_test.go b/internal/federation/protocol_test.go new file mode 100644 index 000000000..6a57c8d55 --- /dev/null +++ b/internal/federation/protocol_test.go @@ -0,0 +1,85 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package federation_test + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/suite" + + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type ProtocolTestSuite struct { + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + federator *federation.Federator +} + +// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout +func (suite *ProtocolTestSuite) SetupSuite() { + // setup standard items + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + + // setup module being tested + suite.federator = federation.NewFederator(suite.db, suite.log, suite.config).(*federation.Federator) +} + +func (suite *ProtocolTestSuite) SetupTest() { + testrig.StandardDBSetup(suite.db) +} + +// TearDownTest drops tables to make sure there's no data in the db +func (suite *ProtocolTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} + +func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { + + // setup + // recorder := httptest.NewRecorder() + // ctx := context.Background() + // request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting + + // activity := + + // _, err := suite.federator.PostInboxRequestBodyHook(ctx, request, nil) + // assert.NoError(suite.T(), err) + + // check response + // suite.EqualValues(http.StatusOK, recorder.Code) + + // result := recorder.Result() + // defer result.Body.Close() + // b, err := ioutil.ReadAll(result.Body) + // assert.NoError(suite.T(), err) + +} + +func TestProtocolTestSuite(t *testing.T) { + suite.Run(t, new(ProtocolTestSuite)) +} diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 2f90858b4..195cbb68d 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -113,7 +113,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr return fmt.Errorf("error creating instance account: %s", err) } - gts, err := New(dbService, &cache.MockCache{}, router, federation.New(dbService, log), c) + gts, err := New(dbService, &cache.MockCache{}, router, federation.New(dbService, c, log), c) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) } diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index 4e678891a..5241cf412 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -30,7 +30,8 @@ type clientStore struct { db db.DB } -func newClientStore(db db.DB) oauth2.ClientStore { +// NewClientStore returns an implementation of the oauth2 ClientStore interface, using the given db as a storage backend. +func NewClientStore(db db.DB) oauth2.ClientStore { pts := &clientStore{ db: db, } diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index a7028228d..39d64ccab 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package oauth +package oauth_test import ( "context" @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/oauth2/v4/models" ) @@ -69,7 +70,7 @@ func (suite *PgClientStoreTestSuite) SetupTest() { suite.db = db models := []interface{}{ - &Client{}, + &oauth.Client{}, } for _, m := range models { @@ -82,7 +83,7 @@ func (suite *PgClientStoreTestSuite) SetupTest() { // TearDownTest drops the oauth_clients table and closes the pg connection after each test func (suite *PgClientStoreTestSuite) TearDownTest() { models := []interface{}{ - &Client{}, + &oauth.Client{}, } for _, m := range models { if err := suite.db.DropTable(m); err != nil { @@ -97,7 +98,7 @@ func (suite *PgClientStoreTestSuite) TearDownTest() { func (suite *PgClientStoreTestSuite) TestClientStoreSetAndGet() { // set a new client in the store - cs := newClientStore(suite.db) + cs := oauth.NewClientStore(suite.db) if err := cs.Set(context.Background(), suite.testClientID, models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID)); err != nil { suite.FailNow(err.Error()) } @@ -115,7 +116,7 @@ func (suite *PgClientStoreTestSuite) TestClientStoreSetAndGet() { func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() { // set a new client in the store - cs := newClientStore(suite.db) + cs := oauth.NewClientStore(suite.db) if err := cs.Set(context.Background(), suite.testClientID, models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID)); err != nil { suite.FailNow(err.Error()) } diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go index 594b9b5a9..1b8449619 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/oauth/oauth_test.go @@ -16,6 +16,6 @@ along with this program. If not, see . */ -package oauth +package oauth_test // TODO: write tests diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 1ddf18b03..7bd4f8e7c 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -215,7 +215,7 @@ func (s *s) GenerateUserAccessToken(ti oauth2.TokenInfo, clientSecret string, us // New returns a new oauth server that implements the Server interface func New(database db.DB, log *logrus.Logger) Server { ts := newTokenStore(context.Background(), database, log) - cs := newClientStore(database) + cs := NewClientStore(database) manager := manage.NewDefaultManager() manager.MapTokenStorage(ts) diff --git a/internal/oauth/tokenstore_test.go b/internal/oauth/tokenstore_test.go index 594b9b5a9..1b8449619 100644 --- a/internal/oauth/tokenstore_test.go +++ b/internal/oauth/tokenstore_test.go @@ -16,6 +16,6 @@ along with this program. If not, see . */ -package oauth +package oauth_test // TODO: write tests diff --git a/internal/util/regexes.go b/internal/util/regexes.go index db8124cdb..015d8d829 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -60,6 +60,10 @@ var ( // userPathRegex parses a path that validates and captures the username part from eg /users/example_username userPathRegex = regexp.MustCompile(userPathRegexString) + inboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, InboxPath) + // inboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/inbox + inboxPathRegex = regexp.MustCompile(inboxPathRegexString) + actorPathRegexString = fmt.Sprintf(`^?/%s/(%s)$`, ActorsPath, usernameRegexString) // actorPathRegex parses a path that validates and captures the username part from eg /actors/example_username actorPathRegex = regexp.MustCompile(actorPathRegexString) diff --git a/internal/util/uri.go b/internal/util/uri.go index a4c7b8a48..9d16fee32 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -47,6 +47,16 @@ const ( FeaturedPath = "featured" ) +// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains +type APContextKey string + +const ( + // APActivityKey can be used to set and retrieve the actual go-fed pub.Activity within a context. + APActivityKey APContextKey = "activity" + // APUsernameKey can be used to set and retrieve the username of the user being interacted with. + APUsernameKey APContextKey = "username" +) + // UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc. type UserURIs struct { // The web URL of the instance host, eg https://example.org @@ -111,6 +121,11 @@ func IsUserPath(id *url.URL) bool { return userPathRegex.MatchString(strings.ToLower(id.Path)) } +// IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox +func IsInboxPath(id *url.URL) bool { + return inboxPathRegex.MatchString(strings.ToLower(id.Path)) +} + // IsInstanceActorPath returns true if the given URL path corresponds to eg /actors/example_username func IsInstanceActorPath(id *url.URL) bool { return actorPathRegex.MatchString(strings.ToLower(id.Path)) @@ -128,7 +143,7 @@ func IsFollowingPath(id *url.URL) bool { // IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked func IsLikedPath(id *url.URL) bool { - return followingPathRegex.MatchString(strings.ToLower(id.Path)) + return likedPathRegex.MatchString(strings.ToLower(id.Path)) } // IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS @@ -158,3 +173,14 @@ func ParseUserPath(id *url.URL) (username string, err error) { username = matches[1] return } + +// ParseInboxPath returns the username from a path such as /users/example_username/inbox +func ParseInboxPath(id *url.URL) (username string, err error) { + matches := inboxPathRegex.FindStringSubmatch(id.Path) + if len(matches) != 2 { + err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches)) + return + } + username = matches[1] + return +} diff --git a/internal/util/validation_test.go b/internal/util/validation_test.go index dbac5e248..73f5cb977 100644 --- a/internal/util/validation_test.go +++ b/internal/util/validation_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package util +package util_test import ( "errors" @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/util" ) type ValidationTestSuite struct { @@ -42,42 +43,42 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() { strongPassword := "3dX5@Zc%mV*W2MBNEy$@" var err error - err = ValidateNewPassword(empty) + err = util.ValidateNewPassword(empty) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("no password provided"), err) } - err = ValidateNewPassword(terriblePassword) + err = util.ValidateNewPassword(terriblePassword) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"), err) } - err = ValidateNewPassword(weakPassword) + err = util.ValidateNewPassword(weakPassword) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("insecure password, try including more special characters, using numbers or using a longer password"), err) } - err = ValidateNewPassword(shortPassword) + err = util.ValidateNewPassword(shortPassword) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err) } - err = ValidateNewPassword(specialPassword) + err = util.ValidateNewPassword(specialPassword) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("insecure password, try including more special characters or using a longer password"), err) } - err = ValidateNewPassword(longPassword) + err = util.ValidateNewPassword(longPassword) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateNewPassword(tooLong) + err = util.ValidateNewPassword(tooLong) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("password should be no more than 64 chars"), err) } - err = ValidateNewPassword(strongPassword) + err = util.ValidateNewPassword(strongPassword) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } @@ -94,42 +95,42 @@ func (suite *ValidationTestSuite) TestValidateUsername() { goodUsername := "this_is_a_good_username" var err error - err = ValidateUsername(empty) + err = util.ValidateUsername(empty) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("no username provided"), err) } - err = ValidateUsername(tooLong) + err = util.ValidateUsername(tooLong) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("username should be no more than 64 chars but '%s' was 66", tooLong), err) } - err = ValidateUsername(withSpaces) + err = util.ValidateUsername(withSpaces) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", withSpaces), err) } - err = ValidateUsername(weirdChars) + err = util.ValidateUsername(weirdChars) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", weirdChars), err) } - err = ValidateUsername(leadingSpace) + err = util.ValidateUsername(leadingSpace) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", leadingSpace), err) } - err = ValidateUsername(trailingSpace) + err = util.ValidateUsername(trailingSpace) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", trailingSpace), err) } - err = ValidateUsername(newlines) + err = util.ValidateUsername(newlines) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", newlines), err) } - err = ValidateUsername(goodUsername) + err = util.ValidateUsername(goodUsername) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } @@ -144,32 +145,32 @@ func (suite *ValidationTestSuite) TestValidateEmail() { emailAddress := "thisis.actually@anemail.address" var err error - err = ValidateEmail(empty) + err = util.ValidateEmail(empty) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("no email provided"), err) } - err = ValidateEmail(notAnEmailAddress) + err = util.ValidateEmail(notAnEmailAddress) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) } - err = ValidateEmail(almostAnEmailAddress) + err = util.ValidateEmail(almostAnEmailAddress) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("mail: no angle-addr"), err) } - err = ValidateEmail(aWebsite) + err = util.ValidateEmail(aWebsite) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("mail: missing '@' or angle-addr"), err) } - err = ValidateEmail(tooLong) + err = util.ValidateEmail(tooLong) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), fmt.Errorf("email address should be no more than 256 chars but '%s' was 286", tooLong), err) } - err = ValidateEmail(emailAddress) + err = util.ValidateEmail(emailAddress) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } @@ -187,47 +188,47 @@ func (suite *ValidationTestSuite) TestValidateLanguage() { german := "de" var err error - err = ValidateLanguage(empty) + err = util.ValidateLanguage(empty) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("no language provided"), err) } - err = ValidateLanguage(notALanguage) + err = util.ValidateLanguage(notALanguage) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) } - err = ValidateLanguage(english) + err = util.ValidateLanguage(english) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateLanguage(capitalEnglish) + err = util.ValidateLanguage(capitalEnglish) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateLanguage(arabic3Letters) + err = util.ValidateLanguage(arabic3Letters) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateLanguage(mixedCapsEnglish) + err = util.ValidateLanguage(mixedCapsEnglish) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateLanguage(englishUS) + err = util.ValidateLanguage(englishUS) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("language: tag is not well-formed"), err) } - err = ValidateLanguage(dutch) + err = util.ValidateLanguage(dutch) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateLanguage(german) + err = util.ValidateLanguage(german) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } @@ -241,43 +242,43 @@ func (suite *ValidationTestSuite) TestValidateReason() { var err error // check with no reason required - err = ValidateSignUpReason(empty, false) + err = util.ValidateSignUpReason(empty, false) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateSignUpReason(badReason, false) + err = util.ValidateSignUpReason(badReason, false) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateSignUpReason(tooLong, false) + err = util.ValidateSignUpReason(tooLong, false) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } - err = ValidateSignUpReason(goodReason, false) + err = util.ValidateSignUpReason(goodReason, false) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } // check with reason required - err = ValidateSignUpReason(empty, true) + err = util.ValidateSignUpReason(empty, true) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("no reason provided"), err) } - err = ValidateSignUpReason(badReason, true) + err = util.ValidateSignUpReason(badReason, true) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("reason should be at least 40 chars but 'because' was 7"), err) } - err = ValidateSignUpReason(tooLong, true) + err = util.ValidateSignUpReason(tooLong, true) if assert.Error(suite.T(), err) { assert.Equal(suite.T(), errors.New("reason should be no more than 500 chars but given reason was 600"), err) } - err = ValidateSignUpReason(goodReason, true) + err = util.ValidateSignUpReason(goodReason, true) if assert.NoError(suite.T(), err) { assert.Equal(suite.T(), nil, err) } diff --git a/testrig/actions.go b/testrig/actions.go index 1caa18581..4387f4269 100644 --- a/testrig/actions.go +++ b/testrig/actions.go @@ -97,7 +97,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr // return fmt.Errorf("error creating instance account: %s", err) // } - gts, err := gotosocial.New(dbService, &cache.MockCache{}, router, federation.New(dbService, log), c) + gts, err := gotosocial.New(dbService, &cache.MockCache{}, router, federation.New(dbService, c, log), c) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) }