forked from go-fed/activity
-
Notifications
You must be signed in to change notification settings - Fork 1
/
base_actor.go
494 lines (476 loc) · 16.8 KB
/
base_actor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
package pub
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
)
// baseActor must satisfy the Actor interface.
var _ Actor = &baseActor{}
// baseActor is an application-independent ActivityPub implementation. It does
// not implement the entire protocol, and relies on a delegate to do so. It
// only implements the part of the protocol that is side-effect-free, allowing
// an existing application to write a DelegateActor that glues their application
// into the ActivityPub world.
//
// It is preferred to use a DelegateActor provided by this library, so that the
// application does not need to worry about the ActivityPub implementation.
type baseActor struct {
// delegate contains application-specific delegation logic.
delegate DelegateActor
// enableSocialProtocol enables or disables the Social API, the client to
// server part of ActivityPub. Useful if permitting remote clients to
// act on behalf of the users of the client application.
enableSocialProtocol bool
// enableFederatedProtocol enables or disables the Federated Protocol, or the
// server to server part of ActivityPub. Useful to permit integrating
// with the rest of the federative web.
enableFederatedProtocol bool
// clock simply tracks the current time.
clock Clock
}
// baseActorFederating must satisfy the FederatingActor interface.
var _ FederatingActor = &baseActorFederating{}
// baseActorFederating is a baseActor that also satisfies the FederatingActor
// interface.
//
// The baseActor is preserved as an Actor which will not successfully cast to a
// FederatingActor.
type baseActorFederating struct {
baseActor
}
// NewSocialActor builds a new Actor concept that handles only the Social
// Protocol part of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
//
// Do not try to use NewSocialActor and NewFederatingActor together to cover
// both the Social and Federating parts of the protocol. Instead, use NewActor.
func NewSocialActor(c CommonBehavior,
c2s SocialProtocol,
db Database,
clock Clock) Actor {
return &baseActor{
// Use SideEffectActor without s2s.
delegate: &SideEffectActor{
common: c,
c2s: c2s,
db: db,
clock: clock,
},
enableSocialProtocol: true,
clock: clock,
}
}
// NewFederatingActor builds a new Actor concept that handles only the Federating
// Protocol part of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
//
// Do not try to use NewSocialActor and NewFederatingActor together to cover
// both the Social and Federating parts of the protocol. Instead, use NewActor.
func NewFederatingActor(c CommonBehavior,
s2s FederatingProtocol,
db Database,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
// Use SideEffectActor without c2s.
delegate: &SideEffectActor{
common: c,
s2s: s2s,
db: db,
clock: clock,
},
enableFederatedProtocol: true,
clock: clock,
},
}
}
// NewActor builds a new Actor concept that handles both the Social and
// Federating Protocol parts of ActivityPub.
//
// This Actor can be created once in an application and reused to handle
// multiple requests concurrently and for different endpoints.
//
// It leverages as much of go-fed as possible to ensure the implementation is
// compliant with the ActivityPub specification, while providing enough freedom
// to be productive without shooting one's self in the foot.
func NewActor(c CommonBehavior,
c2s SocialProtocol,
s2s FederatingProtocol,
db Database,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
delegate: NewSideEffectActor(c, s2s, c2s, db, clock),
enableSocialProtocol: true,
enableFederatedProtocol: true,
clock: clock,
},
}
}
// NewCustomActor allows clients to create a custom ActivityPub implementation
// for the Social Protocol, Federating Protocol, or both.
//
// It still uses the library as a high-level scaffold, which has the benefit of
// allowing applications to grow into a custom ActivityPub solution without
// having to refactor the code that passes HTTP requests into the Actor.
//
// It is possible to create a DelegateActor that is not ActivityPub compliant.
// Use with due care.
//
// If you find yourself passing a SideEffectActor in as the DelegateActor,
// consider using NewActor, NewFederatingActor, or NewSocialActor instead.
func NewCustomActor(delegate DelegateActor,
enableSocialProtocol, enableFederatedProtocol bool,
clock Clock) FederatingActor {
return &baseActorFederating{
baseActor{
delegate: delegate,
enableSocialProtocol: enableSocialProtocol,
enableFederatedProtocol: enableFederatedProtocol,
clock: clock,
},
}
}
// PostInbox implements the generic algorithm for handling a POST request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Only supports serving data with identifiers having the HTTPS scheme.
func (b *baseActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return b.PostInboxScheme(c, w, r, "https")
}
// PostInbox implements the generic algorithm for handling a POST request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Specifying the "scheme" allows for retrieving ActivityStreams content with
// identifiers such as HTTP, HTTPS, or other protocol schemes.
func (b *baseActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
// Do nothing if it is not an ActivityPub POST request.
if !isActivityPubPost(r) {
return false, nil
}
// If the Federated Protocol is not enabled, then this endpoint is not
// enabled.
if !b.enableFederatedProtocol {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
// Check the peer request is authentic.
c, authenticated, err := b.delegate.AuthenticatePostInbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Begin processing the request, but have not yet applied
// authorization (ex: blocks). Obtain the activity reject unknown
// activities.
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(raw, &m); err != nil {
return true, err
}
asValue, err := streams.ToType(c, m)
if err != nil && !streams.IsUnmatchedErr(err) {
return true, err
} else if streams.IsUnmatchedErr(err) {
// Respond with bad request -- we do not understand the type.
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
activity, ok := asValue.(Activity)
if !ok {
return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue)
}
if activity.GetJSONLDId() == nil {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
// Allow server implementations to set context data with a hook.
c, err = b.delegate.PostInboxRequestBodyHook(c, r, activity)
if err != nil {
return true, err
}
// Check authorization of the activity.
authorized, err := b.delegate.AuthorizePostInbox(c, w, activity)
if err != nil {
return true, err
} else if !authorized {
return true, nil
}
// Post the activity to the actor's inbox and trigger side effects for
// that particular Activity type. It is up to the delegate to resolve
// the given map.
inboxId := requestId(r, scheme)
err = b.delegate.PostInbox(c, inboxId, activity)
if err != nil {
// Special case: We know it is a bad request if the object or
// target properties needed to be populated, but weren't.
//
// Send the rejection to the peer.
if err == ErrObjectRequired || err == ErrTargetRequired {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
return true, err
}
// Our side effects are complete, now delegate determining whether to
// do inbox forwarding, as well as the action to do it.
if err := b.delegate.InboxForwarding(c, inboxId, activity); err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Simply respond with an OK status to the peer.
w.WriteHeader(http.StatusOK)
return true, nil
}
// GetInbox implements the generic algorithm for handling a GET request to an
// actor's inbox independent on an application. It relies on a delegate to
// implement application specific functionality.
func (b *baseActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
// Do nothing if it is not an ActivityPub GET request.
if !isActivityPubGet(r) {
return false, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticateGetInbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
oc, err := b.delegate.GetInbox(c, r)
if err != nil {
return true, err
}
// Deduplicate the 'orderedItems' property by ID.
err = dedupeOrderedItems(oc)
if err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Serialize the OrderedCollection.
m, err := streams.Serialize(oc)
if err != nil {
return true, err
}
raw, err := json.Marshal(m)
if err != nil {
return true, err
}
// Write the response.
addResponseHeaders(w.Header(), b.clock, raw)
w.WriteHeader(http.StatusOK)
n, err := w.Write(raw)
if err != nil {
return true, err
} else if n != len(raw) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
}
return true, nil
}
// PostOutbox implements the generic algorithm for handling a POST request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Only supports serving data with identifiers having the HTTPS scheme.
func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return b.PostOutboxScheme(c, w, r, "https")
}
// PostOutbox implements the generic algorithm for handling a POST request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
//
// Specifying the "scheme" allows for retrieving ActivityStreams content with
// identifiers such as HTTP, HTTPS, or other protocol schemes.
func (b *baseActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
// Do nothing if it is not an ActivityPub POST request.
if !isActivityPubPost(r) {
return false, nil
}
// If the Social API is not enabled, then this endpoint is not enabled.
if !b.enableSocialProtocol {
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticatePostOutbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
raw, err := ioutil.ReadAll(r.Body)
if err != nil {
return true, err
}
var m map[string]interface{}
if err = json.Unmarshal(raw, &m); err != nil {
return true, err
}
// Note that converting to a Type will NOT successfully convert types
// not known to go-fed. This prevents accidentally wrapping an Activity
// type unknown to go-fed in a Create below. Instead,
// streams.ErrUnhandledType will be returned here.
asValue, err := streams.ToType(c, m)
if err != nil && !streams.IsUnmatchedErr(err) {
return true, err
} else if streams.IsUnmatchedErr(err) {
// Respond with bad request -- we do not understand the type.
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
// Allow server implementations to set context data with a hook.
c, err = b.delegate.PostOutboxRequestBodyHook(c, r, asValue)
if err != nil {
return true, err
}
// The HTTP request steps are complete, complete the rest of the outbox
// and delivery process.
outboxId := requestId(r, scheme)
activity, err := b.deliver(c, outboxId, asValue, m)
// Special case: We know it is a bad request if the object or
// target properties needed to be populated, but weren't.
//
// Send the rejection to the client.
if err == ErrObjectRequired || err == ErrTargetRequired {
w.WriteHeader(http.StatusBadRequest)
return true, nil
} else if err != nil {
return true, err
}
// Respond to the request with the new Activity's IRI location.
w.Header().Set(locationHeader, activity.GetJSONLDId().Get().String())
w.WriteHeader(http.StatusCreated)
return true, nil
}
// GetOutbox implements the generic algorithm for handling a Get request to an
// actor's outbox independent on an application. It relies on a delegate to
// implement application specific functionality.
func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
// Do nothing if it is not an ActivityPub GET request.
if !isActivityPubGet(r) {
return false, nil
}
// Delegate authenticating and authorizing the request.
c, authenticated, err := b.delegate.AuthenticateGetOutbox(c, w, r)
if err != nil {
return true, err
} else if !authenticated {
return true, nil
}
// Everything is good to begin processing the request.
oc, err := b.delegate.GetOutbox(c, r)
if err != nil {
return true, err
}
// Request has been processed. Begin responding to the request.
//
// Serialize the OrderedCollection.
m, err := streams.Serialize(oc)
if err != nil {
return true, err
}
raw, err := json.Marshal(m)
if err != nil {
return true, err
}
// Write the response.
addResponseHeaders(w.Header(), b.clock, raw)
w.WriteHeader(http.StatusOK)
n, err := w.Write(raw)
if err != nil {
return true, err
} else if n != len(raw) {
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
}
return true, nil
}
// deliver delegates all outbox handling steps and optionally will federate the
// activity if the federated protocol is enabled.
//
// This function is not exported so an Actor that only supports C2S cannot be
// type casted to a FederatingActor. It doesn't exactly fit the Send method
// signature anyways.
//
// Note: 'm' is nilable.
func (b *baseActor) deliver(c context.Context, outbox *url.URL, asValue vocab.Type, m map[string]interface{}) (activity Activity, err error) {
// If the value is not an Activity or type extending from Activity, then
// we need to wrap it in a Create Activity.
if !streams.IsOrExtendsActivityStreamsActivity(asValue) {
asValue, err = b.delegate.WrapInCreate(c, asValue, outbox)
if err != nil {
return
}
}
// At this point, this should be a safe conversion. If this error is
// triggered, then there is either a bug in the delegation of
// WrapInCreate, behavior is not lining up in the generated ExtendedBy
// code, or something else is incorrect with the type system.
var ok bool
activity, ok = asValue.(Activity)
if !ok {
err = fmt.Errorf("activity streams value is not an Activity: %T", asValue)
return
}
// Delegate generating new IDs for the activity and all new objects.
if err = b.delegate.AddNewIDs(c, activity); err != nil {
return
}
// Post the activity to the actor's outbox and trigger side effects for
// that particular Activity type.
//
// Since 'm' is nil-able and side effects may need access to literal nil
// values, such as for Update activities, ensure 'm' is non-nil.
if m == nil {
m, err = asValue.Serialize()
if err != nil {
return
}
}
deliverable, err := b.delegate.PostOutbox(c, activity, outbox, m)
if err != nil {
return
}
// Request has been processed and all side effects internal to this
// application server have finished. Begin side effects affecting other
// servers and/or the client who sent this request.
//
// If we are federating and the type is a deliverable one, then deliver
// the activity to federating peers.
if b.enableFederatedProtocol && deliverable {
if err = b.delegate.Deliver(c, outbox, activity); err != nil {
return
}
}
return
}
// Send is programmatically accessible if the federated protocol is enabled.
func (b *baseActorFederating) Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) {
return b.deliver(c, outbox, t, nil)
}