diff --git a/pub/fed_test.go b/pub/fed_test.go index 0a2d259..38045a8 100644 --- a/pub/fed_test.go +++ b/pub/fed_test.go @@ -1896,6 +1896,7 @@ func TestPostInbox_RequiresObject(t *testing.T) { return nil } for _, test := range tests { + t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() req := ActivityPubRequest(httptest.NewRequest("POST", testInboxURI, bytes.NewBuffer(MustSerialize(test.input())))) handled, err := p.PostInbox(context.Background(), resp, req) @@ -1940,6 +1941,7 @@ func TestPostInbox_RequiresTarget(t *testing.T) { return nil } for _, test := range tests { + t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() req := ActivityPubRequest(httptest.NewRequest("POST", testInboxURI, bytes.NewBuffer(MustSerialize(test.input())))) handled, err := p.PostInbox(context.Background(), resp, req) @@ -2008,6 +2010,7 @@ func TestPostInbox_OriginMustMatch(t *testing.T) { }, } for _, test := range tests { + t.Logf("Running table test case %q", test.name) app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p := NewPubberTest(t) PreparePostInboxTest(t, app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p) resp := httptest.NewRecorder() @@ -2038,6 +2041,7 @@ func TestPostInbox_ActivityActorsMustCoverObjectActors(t *testing.T) { }, } for _, test := range tests { + t.Logf("Running table test case %q", test.name) app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p := NewPubberTest(t) PreparePostInboxTest(t, app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p) resp := httptest.NewRecorder() @@ -2291,6 +2295,7 @@ func TestPostInbox_Delete_SetsTombstone(t *testing.T) { return nil } for _, test := range tests { + t.Logf("Running table test case %q", test.name) app.get = func(c context.Context, id url.URL) (PubObject, error) { return test.input(), nil } @@ -4241,6 +4246,7 @@ func TestPostOutbox_RequiresObject(t *testing.T) { app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p := NewPubberTest(t) PreparePostOutboxTest(t, app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p) for _, test := range tests { + t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() req := Sign(ActivityPubRequest(httptest.NewRequest("POST", testOutboxURI, bytes.NewBuffer(MustSerialize(test.input()))))) handled, err := p.PostOutbox(context.Background(), resp, req) @@ -4283,6 +4289,7 @@ func TestPostOutbox_RequiresTarget(t *testing.T) { app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p := NewPubberTest(t) PreparePostOutboxTest(t, app, socialApp, fedApp, socialCb, fedCb, d, httpClient, p) for _, test := range tests { + t.Logf("Running table test case %q", test.name) resp := httptest.NewRecorder() req := Sign(ActivityPubRequest(httptest.NewRequest("POST", testOutboxURI, bytes.NewBuffer(MustSerialize(test.input()))))) handled, err := p.PostOutbox(context.Background(), resp, req) diff --git a/pub/handlers.go b/pub/handlers.go index bd03d82..4621c88 100644 --- a/pub/handlers.go +++ b/pub/handlers.go @@ -11,8 +11,8 @@ import ( ) // ServeActivityPubObject will serve the ActivityPub object with the given IRI -// in the request. Note that requests must be signed with HTTP signatures or -// else they will be denied access. To explicitly opt out of this protection, +// in the request. Note that requests may be signed with HTTP signatures or be +// permitted without any authentication scheme. To change this default behavior, // use ServeActivityPubObjectWithVerificationMethod instead. func ServeActivityPubObject(c context.Context, a Application, clock Clock, w http.ResponseWriter, r *http.Request) (handled bool, err error) { return serveActivityPubObject(c, a, clock, w, r, nil) diff --git a/pub/handlers_test.go b/pub/handlers_test.go new file mode 100644 index 0000000..53b55d2 --- /dev/null +++ b/pub/handlers_test.go @@ -0,0 +1,528 @@ +package pub + +import ( + "context" + "crypto" + "github.com/go-fed/activity/vocab" + "github.com/go-fed/httpsig" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestServeActivityPubObject(t *testing.T) { + tests := []struct { + name string + app *MockApplication + clock *MockClock + input *http.Request + expectedCode int + expectedObjFn func() vocab.Serializer + expectHandled bool + }{ + { + name: "unsigned request", + app: &MockApplication{ + t: t, + get: func(c context.Context, id url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "http signature request", + input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, url.URL, error) { + if publicKeyId != testPublicKeyId { + t.Fatalf("(%q) expected %s, got %s", testPublicKeyId, publicKeyId) + } + return testPrivateKey.Public(), httpsig.RSA_SHA256, *samIRI, nil + }, + getAsVerifiedUser: func(c context.Context, id, user url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } else if u := (&user).String(); u != samIRIString { + t.Fatalf("(%q) expected %s, got %s", samIRIString, u) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "not owned", + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return false + }, + }, + expectedCode: http.StatusNotFound, + expectHandled: true, + }, + { + name: "not activitypub get", + input: httptest.NewRequest("GET", noteURIString, nil), + expectHandled: false, + }, + { + name: "bad http signature", + input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, url.URL, error) { + if publicKeyId != testPublicKeyId { + t.Fatalf("(%q) expected %s, got %s", testPublicKeyId, publicKeyId) + } + return testPrivateKey.Public(), httpsig.RSA_SHA256, *samIRI, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + expectedCode: http.StatusForbidden, + expectHandled: true, + }, + } + for _, test := range tests { + t.Logf("Running table test case %q", test.name) + resp := httptest.NewRecorder() + handled, err := ServeActivityPubObject(context.Background(), test.app, test.clock, resp, test.input) + if err != nil { + t.Fatalf("(%q) %s", test.name, err) + } else if handled != test.expectHandled { + t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled) + } else if test.expectedCode != 0 { + if resp.Code != test.expectedCode { + t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code) + } + } else if test.expectedObjFn != nil { + if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil { + t.Fatalf("(%q) unexpected object: %s", test.name, err) + } + } + } +} + +func TestServeActivityPubObjectWithVerificationMethod(t *testing.T) { + tests := []struct { + name string + app *MockApplication + clock *MockClock + verifier *MockSocialAPIVerifier + input *http.Request + expectedCode int + expectedObjFn func() vocab.Serializer + expectHandled bool + }{ + { + name: "unsigned request", + app: &MockApplication{ + t: t, + get: func(c context.Context, id url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "http signature request", + input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, url.URL, error) { + if publicKeyId != testPublicKeyId { + t.Fatalf("(%q) expected %s, got %s", testPublicKeyId, publicKeyId) + } + return testPrivateKey.Public(), httpsig.RSA_SHA256, *samIRI, nil + }, + getAsVerifiedUser: func(c context.Context, id, user url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } else if u := (&user).String(); u != samIRIString { + t.Fatalf("(%q) expected %s, got %s", samIRIString, u) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "not owned", + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return false + }, + }, + expectedCode: http.StatusNotFound, + expectHandled: true, + }, + { + name: "not activitypub get", + input: httptest.NewRequest("GET", noteURIString, nil), + expectHandled: false, + }, + { + name: "bad http signature", + input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, url.URL, error) { + if publicKeyId != testPublicKeyId { + t.Fatalf("(%q) expected %s, got %s", testPublicKeyId, publicKeyId) + } + return testPrivateKey.Public(), httpsig.RSA_SHA256, *samIRI, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + expectedCode: http.StatusForbidden, + expectHandled: true, + }, + { + name: "unsigned request passes verifier", + app: &MockApplication{ + t: t, + getAsVerifiedUser: func(c context.Context, id, user url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } else if u := (&user).String(); u != samIRIString { + t.Fatalf("(%q) expected %s, got %s", samIRIString, u) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return samIRI, true, true, nil + }, + }, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "http signature request passes verifier", + input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getAsVerifiedUser: func(c context.Context, id, user url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } else if u := (&user).String(); u != samIRIString { + t.Fatalf("(%q) expected %s, got %s", samIRIString, u) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return samIRI, true, true, nil + }, + }, + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "verifier authed unauthz", + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return samIRI, true, false, nil + }, + }, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusForbidden, + expectHandled: true, + }, + { + name: "verifier unauthed unauthz", + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return nil, false, false, nil + }, + }, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusBadRequest, + expectHandled: true, + }, + { + name: "verifier unauthed authz unsigned fails", + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return nil, false, true, nil + }, + }, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusBadRequest, + expectHandled: true, + }, + { + name: "verifier unauthed authz signed success", + input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))), + app: &MockApplication{ + t: t, + getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, url.URL, error) { + if publicKeyId != testPublicKeyId { + t.Fatalf("(%q) expected %s, got %s", testPublicKeyId, publicKeyId) + } + return testPrivateKey.Public(), httpsig.RSA_SHA256, *samIRI, nil + }, + getAsVerifiedUser: func(c context.Context, id, user url.URL) (PubObject, error) { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } else if u := (&user).String(); u != samIRIString { + t.Fatalf("(%q) expected %s, got %s", samIRIString, u) + } + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote, nil + }, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return nil, false, true, nil + }, + }, + expectedCode: http.StatusOK, + expectedObjFn: func() vocab.Serializer { + testNote = &vocab.Note{} + testNote.SetId(*noteIRI) + testNote.AddNameString(noteName) + testNote.AddContentString("This is a simple note") + return testNote + }, + expectHandled: true, + }, + { + name: "verifier unauthed authz unsigned fails with bad impl returning user", + app: &MockApplication{ + t: t, + owns: func(c context.Context, id url.URL) bool { + if s := (&id).String(); s != noteURIString { + t.Fatalf("(%q) expected %s, got %s", noteURIString, s) + } + return true + }, + }, + clock: &MockClock{now}, + verifier: &MockSocialAPIVerifier{ + t: t, + verify: func(r *http.Request) (*url.URL, bool, bool, error) { + return samIRI, false, true, nil + }, + }, + input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)), + expectedCode: http.StatusBadRequest, + expectHandled: true, + }, + } + for _, test := range tests { + t.Logf("Running table test case %q", test.name) + resp := httptest.NewRecorder() + var handled bool + var err error + if test.verifier != nil { + handled, err = ServeActivityPubObjectWithVerificationMethod(context.Background(), test.app, test.clock, resp, test.input, test.verifier) + } else { + handled, err = ServeActivityPubObjectWithVerificationMethod(context.Background(), test.app, test.clock, resp, test.input, nil) + } + if err != nil { + t.Fatalf("(%q) %s", test.name, err) + } else if handled != test.expectHandled { + t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled) + } else if test.expectedCode != 0 { + if resp.Code != test.expectedCode { + t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code) + } + } else if test.expectedObjFn != nil { + if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil { + t.Fatalf("(%q) unexpected object: %s", test.name, err) + } + } + } +}