aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/api/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests/api/activitypub')
-rw-r--r--server/tests/api/activitypub/cleaner.ts342
-rw-r--r--server/tests/api/activitypub/client.ts136
-rw-r--r--server/tests/api/activitypub/fetch.ts82
-rw-r--r--server/tests/api/activitypub/helpers.ts167
-rw-r--r--server/tests/api/activitypub/index.ts6
-rw-r--r--server/tests/api/activitypub/refresher.ts157
-rw-r--r--server/tests/api/activitypub/security.ts321
7 files changed, 0 insertions, 1211 deletions
diff --git a/server/tests/api/activitypub/cleaner.ts b/server/tests/api/activitypub/cleaner.ts
deleted file mode 100644
index d67175e20..000000000
--- a/server/tests/api/activitypub/cleaner.ts
+++ /dev/null
@@ -1,342 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@server/tests/shared'
5import { wait } from '@shared/core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@shared/server-commands'
14
15describe('Test AP cleaner', function () {
16 let servers: PeerTubeServer[] = []
17 const sqlCommands: SQLCommand[] = []
18
19 let videoUUID1: string
20 let videoUUID2: string
21 let videoUUID3: string
22
23 let videoUUIDs: string[]
24
25 before(async function () {
26 this.timeout(120000)
27
28 const config = {
29 federation: {
30 videos: { cleanup_remote_interactions: true }
31 }
32 }
33 servers = await createMultipleServers(3, config)
34
35 // Get the access tokens
36 await setAccessTokensToServers(servers)
37
38 await Promise.all([
39 doubleFollow(servers[0], servers[1]),
40 doubleFollow(servers[1], servers[2]),
41 doubleFollow(servers[0], servers[2])
42 ])
43
44 // Update 1 local share, check 6 shares
45
46 // Create 1 comment per video
47 // Update 1 remote URL and 1 local URL on
48
49 videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid
50 videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
51 videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid
52
53 videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ]
54
55 await waitJobs(servers)
56
57 for (const server of servers) {
58 for (const uuid of videoUUIDs) {
59 await server.videos.rate({ id: uuid, rating: 'like' })
60 await server.comments.createThread({ videoId: uuid, text: 'comment' })
61 }
62
63 sqlCommands.push(new SQLCommand(server))
64 }
65
66 await waitJobs(servers)
67 })
68
69 it('Should have the correct likes', async function () {
70 for (const server of servers) {
71 for (const uuid of videoUUIDs) {
72 const video = await server.videos.get({ id: uuid })
73
74 expect(video.likes).to.equal(3)
75 expect(video.dislikes).to.equal(0)
76 }
77 }
78 })
79
80 it('Should destroy server 3 internal likes and correctly clean them', async function () {
81 this.timeout(20000)
82
83 await sqlCommands[2].deleteAll('accountVideoRate')
84 for (const uuid of videoUUIDs) {
85 await sqlCommands[2].setVideoField(uuid, 'likes', '0')
86 }
87
88 await wait(5000)
89 await waitJobs(servers)
90
91 // Updated rates of my video
92 {
93 const video = await servers[0].videos.get({ id: videoUUID1 })
94 expect(video.likes).to.equal(2)
95 expect(video.dislikes).to.equal(0)
96 }
97
98 // Did not update rates of a remote video
99 {
100 const video = await servers[0].videos.get({ id: videoUUID2 })
101 expect(video.likes).to.equal(3)
102 expect(video.dislikes).to.equal(0)
103 }
104 })
105
106 it('Should update rates to dislikes', async function () {
107 this.timeout(20000)
108
109 for (const server of servers) {
110 for (const uuid of videoUUIDs) {
111 await server.videos.rate({ id: uuid, rating: 'dislike' })
112 }
113 }
114
115 await waitJobs(servers)
116
117 for (const server of servers) {
118 for (const uuid of videoUUIDs) {
119 const video = await server.videos.get({ id: uuid })
120 expect(video.likes).to.equal(0)
121 expect(video.dislikes).to.equal(3)
122 }
123 }
124 })
125
126 it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
127 this.timeout(20000)
128
129 await sqlCommands[2].deleteAll('accountVideoRate')
130
131 for (const uuid of videoUUIDs) {
132 await sqlCommands[2].setVideoField(uuid, 'dislikes', '0')
133 }
134
135 await wait(5000)
136 await waitJobs(servers)
137
138 // Updated rates of my video
139 {
140 const video = await servers[0].videos.get({ id: videoUUID1 })
141 expect(video.likes).to.equal(0)
142 expect(video.dislikes).to.equal(2)
143 }
144
145 // Did not update rates of a remote video
146 {
147 const video = await servers[0].videos.get({ id: videoUUID2 })
148 expect(video.likes).to.equal(0)
149 expect(video.dislikes).to.equal(3)
150 }
151 })
152
153 it('Should destroy server 3 internal shares and correctly clean them', async function () {
154 this.timeout(20000)
155
156 const preCount = await sqlCommands[0].getVideoShareCount()
157 expect(preCount).to.equal(6)
158
159 await sqlCommands[2].deleteAll('videoShare')
160 await wait(5000)
161 await waitJobs(servers)
162
163 // Still 6 because we don't have remote shares on local videos
164 const postCount = await sqlCommands[0].getVideoShareCount()
165 expect(postCount).to.equal(6)
166 })
167
168 it('Should destroy server 3 internal comments and correctly clean them', async function () {
169 this.timeout(20000)
170
171 {
172 const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
173 expect(total).to.equal(3)
174 }
175
176 await sqlCommands[2].deleteAll('videoComment')
177
178 await wait(5000)
179 await waitJobs(servers)
180
181 {
182 const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
183 expect(total).to.equal(2)
184 }
185 })
186
187 it('Should correctly update rate URLs', async function () {
188 this.timeout(30000)
189
190 async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
191 const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
192 `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
193 const res = await sqlCommands[0].selectQuery<{ url: string }>(query)
194
195 for (const rate of res) {
196 const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
197 expect(rate.url).to.match(matcher)
198 }
199 }
200
201 async function checkLocal () {
202 const startsWith = 'http://' + servers[0].host + '%'
203 // On local videos
204 await check(startsWith, servers[0].url, '', 'false')
205 // On remote videos
206 await check(startsWith, servers[0].url, '', 'true')
207 }
208
209 async function checkRemote (suffix: string) {
210 const startsWith = 'http://' + servers[1].host + '%'
211 // On local videos
212 await check(startsWith, servers[1].url, suffix, 'false')
213 // On remote videos, we should not update URLs so no suffix
214 await check(startsWith, servers[1].url, '', 'true')
215 }
216
217 await checkLocal()
218 await checkRemote('')
219
220 {
221 const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
222 await sqlCommands[1].updateQuery(query)
223
224 await wait(5000)
225 await waitJobs(servers)
226 }
227
228 await checkLocal()
229 await checkRemote('stan')
230 })
231
232 it('Should correctly update comment URLs', async function () {
233 this.timeout(30000)
234
235 async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
236 const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
237 `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`
238
239 const res = await sqlCommands[0].selectQuery<{ url: string, videoUUID: string }>(query)
240
241 for (const comment of res) {
242 const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
243 expect(comment.url).to.match(matcher)
244 }
245 }
246
247 async function checkLocal () {
248 const startsWith = 'http://' + servers[0].host + '%'
249 // On local videos
250 await check(startsWith, servers[0].url, '', 'false')
251 // On remote videos
252 await check(startsWith, servers[0].url, '', 'true')
253 }
254
255 async function checkRemote (suffix: string) {
256 const startsWith = 'http://' + servers[1].host + '%'
257 // On local videos
258 await check(startsWith, servers[1].url, suffix, 'false')
259 // On remote videos, we should not update URLs so no suffix
260 await check(startsWith, servers[1].url, '', 'true')
261 }
262
263 {
264 const query = `UPDATE "videoComment" SET url = url || 'kyle'`
265 await sqlCommands[1].updateQuery(query)
266
267 await wait(5000)
268 await waitJobs(servers)
269 }
270
271 await checkLocal()
272 await checkRemote('kyle')
273 })
274
275 it('Should remove unavailable remote resources', async function () {
276 this.timeout(240000)
277
278 async function expectNotDeleted () {
279 {
280 const video = await servers[0].videos.get({ id: uuid })
281
282 expect(video.likes).to.equal(3)
283 expect(video.dislikes).to.equal(0)
284 }
285
286 {
287 const { total } = await servers[0].comments.listThreads({ videoId: uuid })
288 expect(total).to.equal(3)
289 }
290 }
291
292 async function expectDeleted () {
293 {
294 const video = await servers[0].videos.get({ id: uuid })
295
296 expect(video.likes).to.equal(2)
297 expect(video.dislikes).to.equal(0)
298 }
299
300 {
301 const { total } = await servers[0].comments.listThreads({ videoId: uuid })
302 expect(total).to.equal(2)
303 }
304 }
305
306 const uuid = (await servers[0].videos.quickUpload({ name: 'server 1 video 2' })).uuid
307
308 await waitJobs(servers)
309
310 for (const server of servers) {
311 await server.videos.rate({ id: uuid, rating: 'like' })
312 await server.comments.createThread({ videoId: uuid, text: 'comment' })
313 }
314
315 await waitJobs(servers)
316
317 await expectNotDeleted()
318
319 await servers[1].kill()
320
321 await wait(5000)
322 await expectNotDeleted()
323
324 let continueWhile = true
325
326 do {
327 try {
328 await expectDeleted()
329 continueWhile = false
330 } catch {
331 }
332 } while (continueWhile)
333 })
334
335 after(async function () {
336 for (const sql of sqlCommands) {
337 await sql.cleanup()
338 }
339
340 await cleanupTests(servers)
341 })
342})
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
deleted file mode 100644
index 572a358a0..000000000
--- a/server/tests/api/activitypub/client.ts
+++ /dev/null
@@ -1,136 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { processViewersStats } from '@server/tests/shared'
5import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@shared/models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 makeActivityPubGetRequest,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel
14} from '@shared/server-commands'
15
16describe('Test activitypub', function () {
17 let servers: PeerTubeServer[] = []
18 let video: { id: number, uuid: string, shortUUID: string }
19 let playlist: { id: number, uuid: string, shortUUID: string }
20
21 async function testAccount (path: string) {
22 const res = await makeActivityPubGetRequest(servers[0].url, path)
23 const object = res.body
24
25 expect(object.type).to.equal('Person')
26 expect(object.id).to.equal(servers[0].url + '/accounts/root')
27 expect(object.name).to.equal('root')
28 expect(object.preferredUsername).to.equal('root')
29 }
30
31 async function testChannel (path: string) {
32 const res = await makeActivityPubGetRequest(servers[0].url, path)
33 const object = res.body
34
35 expect(object.type).to.equal('Group')
36 expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel')
37 expect(object.name).to.equal('Main root channel')
38 expect(object.preferredUsername).to.equal('root_channel')
39 }
40
41 async function testVideo (path: string) {
42 const res = await makeActivityPubGetRequest(servers[0].url, path)
43 const object = res.body
44
45 expect(object.type).to.equal('Video')
46 expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
47 expect(object.name).to.equal('video')
48 }
49
50 async function testPlaylist (path: string) {
51 const res = await makeActivityPubGetRequest(servers[0].url, path)
52 const object = res.body
53
54 expect(object.type).to.equal('Playlist')
55 expect(object.id).to.equal(servers[0].url + '/video-playlists/' + playlist.uuid)
56 expect(object.name).to.equal('playlist')
57 }
58
59 before(async function () {
60 this.timeout(30000)
61
62 servers = await createMultipleServers(2)
63
64 await setAccessTokensToServers(servers)
65 await setDefaultVideoChannel(servers)
66
67 {
68 video = await servers[0].videos.quickUpload({ name: 'video' })
69 }
70
71 {
72 const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id }
73 playlist = await servers[0].playlists.create({ attributes })
74 }
75
76 await doubleFollow(servers[0], servers[1])
77 })
78
79 it('Should return the account object', async function () {
80 await testAccount('/accounts/root')
81 await testAccount('/a/root')
82 })
83
84 it('Should return the channel object', async function () {
85 await testChannel('/video-channels/root_channel')
86 await testChannel('/c/root_channel')
87 })
88
89 it('Should return the video object', async function () {
90 await testVideo('/videos/watch/' + video.id)
91 await testVideo('/videos/watch/' + video.uuid)
92 await testVideo('/videos/watch/' + video.shortUUID)
93 await testVideo('/w/' + video.id)
94 await testVideo('/w/' + video.uuid)
95 await testVideo('/w/' + video.shortUUID)
96 })
97
98 it('Should return the playlist object', async function () {
99 await testPlaylist('/video-playlists/' + playlist.id)
100 await testPlaylist('/video-playlists/' + playlist.uuid)
101 await testPlaylist('/video-playlists/' + playlist.shortUUID)
102 await testPlaylist('/w/p/' + playlist.id)
103 await testPlaylist('/w/p/' + playlist.uuid)
104 await testPlaylist('/w/p/' + playlist.shortUUID)
105 await testPlaylist('/videos/watch/playlist/' + playlist.id)
106 await testPlaylist('/videos/watch/playlist/' + playlist.uuid)
107 await testPlaylist('/videos/watch/playlist/' + playlist.shortUUID)
108 })
109
110 it('Should redirect to the origin video object', async function () {
111 const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + video.uuid, HttpStatusCode.FOUND_302)
112
113 expect(res.header.location).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
114 })
115
116 it('Should return the watch action', async function () {
117 this.timeout(50000)
118
119 await servers[0].views.simulateViewer({ id: video.uuid, currentTimes: [ 0, 2 ] })
120 await processViewersStats(servers)
121
122 const res = await makeActivityPubGetRequest(servers[0].url, '/videos/local-viewer/1', HttpStatusCode.OK_200)
123
124 const object: WatchActionObject = res.body
125 expect(object.type).to.equal('WatchAction')
126 expect(object.duration).to.equal('PT2S')
127 expect(object.actionStatus).to.equal('CompletedActionStatus')
128 expect(object.watchSections).to.have.lengthOf(1)
129 expect(object.watchSections[0].startTimestamp).to.equal(0)
130 expect(object.watchSections[0].endTimestamp).to.equal(2)
131 })
132
133 after(async function () {
134 await cleanupTests(servers)
135 })
136})
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
deleted file mode 100644
index 3899a6a49..000000000
--- a/server/tests/api/activitypub/fetch.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@server/tests/shared'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@shared/server-commands'
13
14describe('Test ActivityPub fetcher', function () {
15 let servers: PeerTubeServer[]
16 let sqlCommandServer1: SQLCommand
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(60000)
22
23 servers = await createMultipleServers(3)
24
25 // Get the access tokens
26 await setAccessTokensToServers(servers)
27
28 const user = { username: 'user1', password: 'password' }
29 for (const server of servers) {
30 await server.users.create({ username: user.username, password: user.password })
31 }
32
33 const userAccessToken = await servers[0].login.getAccessToken(user)
34
35 await servers[0].videos.upload({ attributes: { name: 'video root' } })
36 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } })
37 await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } })
38
39 sqlCommandServer1 = new SQLCommand(servers[0])
40
41 {
42 const to = servers[0].url + '/accounts/user1'
43 const value = servers[1].url + '/accounts/user1'
44 await sqlCommandServer1.setActorField(to, 'url', value)
45 }
46
47 {
48 const value = servers[2].url + '/videos/watch/' + uuid
49 await sqlCommandServer1.setVideoField(uuid, 'url', value)
50 }
51 })
52
53 it('Should add only the video with a valid actor URL', async function () {
54 this.timeout(60000)
55
56 await doubleFollow(servers[0], servers[1])
57 await waitJobs(servers)
58
59 {
60 const { total, data } = await servers[0].videos.list({ sort: 'createdAt' })
61
62 expect(total).to.equal(3)
63 expect(data[0].name).to.equal('video root')
64 expect(data[1].name).to.equal('bad video root')
65 expect(data[2].name).to.equal('video user')
66 }
67
68 {
69 const { total, data } = await servers[1].videos.list({ sort: 'createdAt' })
70
71 expect(total).to.equal(1)
72 expect(data[0].name).to.equal('video root')
73 }
74 })
75
76 after(async function () {
77 this.timeout(20000)
78
79 await sqlCommandServer1.cleanup()
80 await cleanupTests(servers)
81 })
82})
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
deleted file mode 100644
index bad86ef47..000000000
--- a/server/tests/api/activitypub/helpers.ts
+++ /dev/null
@@ -1,167 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { cloneDeep } from 'lodash'
5import { signAndContextify } from '@server/lib/activitypub/send'
6import { buildRequestStub } from '@server/tests/shared'
7import { buildAbsoluteFixturePath } from '@shared/core-utils'
8import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
9
10describe('Test activity pub helpers', function () {
11
12 describe('When checking the Linked Signature', function () {
13
14 it('Should fail with an invalid Mastodon signature', async function () {
15 const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create-bad-signature.json'))
16 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
17 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
18
19 const result = await isJsonLDSignatureVerified(fromActor as any, body)
20
21 expect(result).to.be.false
22 })
23
24 it('Should fail with an invalid public key', async function () {
25 const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create.json'))
26 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
27 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
28
29 const result = await isJsonLDSignatureVerified(fromActor as any, body)
30
31 expect(result).to.be.false
32 })
33
34 it('Should succeed with a valid Mastodon signature', async function () {
35 const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create.json'))
36 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
37 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
38
39 const result = await isJsonLDSignatureVerified(fromActor as any, body)
40
41 expect(result).to.be.true
42 })
43
44 it('Should fail with an invalid PeerTube signature', async function () {
45 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
46 const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
47
48 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
49 const signedBody = await signAndContextify(actorSignature as any, body, 'Announce')
50
51 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
52 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
53
54 expect(result).to.be.false
55 })
56
57 it('Should succeed with a valid PeerTube signature', async function () {
58 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
59 const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
60
61 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
62 const signedBody = await signAndContextify(actorSignature as any, body, 'Announce')
63
64 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
65 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
66
67 expect(result).to.be.true
68 })
69 })
70
71 describe('When checking HTTP signature', function () {
72 it('Should fail with an invalid http signature', async function () {
73 const req = buildRequestStub()
74 req.method = 'POST'
75 req.url = '/accounts/ronan/inbox'
76
77 const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-http-signature.json')))
78 req.body = mastodonObject.body
79 req.headers = mastodonObject.headers
80
81 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
82 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
83
84 const actor = { publicKey }
85 const verified = isHTTPSignatureVerified(parsed, actor as any)
86
87 expect(verified).to.be.false
88 })
89
90 it('Should fail with an invalid public key', async function () {
91 const req = buildRequestStub()
92 req.method = 'POST'
93 req.url = '/accounts/ronan/inbox'
94
95 const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json')))
96 req.body = mastodonObject.body
97 req.headers = mastodonObject.headers
98
99 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
100 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
101
102 const actor = { publicKey }
103 const verified = isHTTPSignatureVerified(parsed, actor as any)
104
105 expect(verified).to.be.false
106 })
107
108 it('Should fail because of clock skew', async function () {
109 const req = buildRequestStub()
110 req.method = 'POST'
111 req.url = '/accounts/ronan/inbox'
112
113 const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json')))
114 req.body = mastodonObject.body
115 req.headers = mastodonObject.headers
116
117 let errored = false
118 try {
119 parseHTTPSignature(req)
120 } catch {
121 errored = true
122 }
123
124 expect(errored).to.be.true
125 })
126
127 it('Should with a scheme', async function () {
128 const req = buildRequestStub()
129 req.method = 'POST'
130 req.url = '/accounts/ronan/inbox'
131
132 const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json')))
133 req.body = mastodonObject.body
134 req.headers = mastodonObject.headers
135 req.headers = 'Signature ' + mastodonObject.headers
136
137 let errored = false
138 try {
139 parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
140 } catch {
141 errored = true
142 }
143
144 expect(errored).to.be.true
145 })
146
147 it('Should succeed with a valid signature', async function () {
148 const req = buildRequestStub()
149 req.method = 'POST'
150 req.url = '/accounts/ronan/inbox'
151
152 const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json')))
153 req.body = mastodonObject.body
154 req.headers = mastodonObject.headers
155
156 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
157 const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
158
159 const actor = { publicKey }
160 const verified = isHTTPSignatureVerified(parsed, actor as any)
161
162 expect(verified).to.be.true
163 })
164
165 })
166
167})
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
deleted file mode 100644
index 324b444e4..000000000
--- a/server/tests/api/activitypub/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
1import './cleaner'
2import './client'
3import './fetch'
4import './refresher'
5import './helpers'
6import './security'
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts
deleted file mode 100644
index 4ea7929ec..000000000
--- a/server/tests/api/activitypub/refresher.ts
+++ /dev/null
@@ -1,157 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { SQLCommand } from '@server/tests/shared'
4import { wait } from '@shared/core-utils'
5import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@shared/server-commands'
16
17describe('Test AP refresher', function () {
18 let servers: PeerTubeServer[] = []
19 let sqlCommandServer2: SQLCommand
20 let videoUUID1: string
21 let videoUUID2: string
22 let videoUUID3: string
23 let playlistUUID1: string
24 let playlistUUID2: string
25
26 before(async function () {
27 this.timeout(60000)
28
29 servers = await createMultipleServers(2)
30
31 // Get the access tokens
32 await setAccessTokensToServers(servers)
33 await setDefaultVideoChannel(servers)
34
35 for (const server of servers) {
36 await server.config.disableTranscoding()
37 }
38
39 {
40 videoUUID1 = (await servers[1].videos.quickUpload({ name: 'video1' })).uuid
41 videoUUID2 = (await servers[1].videos.quickUpload({ name: 'video2' })).uuid
42 videoUUID3 = (await servers[1].videos.quickUpload({ name: 'video3' })).uuid
43 }
44
45 {
46 const token1 = await servers[1].users.generateUserAndToken('user1')
47 await servers[1].videos.upload({ token: token1, attributes: { name: 'video4' } })
48
49 const token2 = await servers[1].users.generateUserAndToken('user2')
50 await servers[1].videos.upload({ token: token2, attributes: { name: 'video5' } })
51 }
52
53 {
54 const attributes = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
55 const created = await servers[1].playlists.create({ attributes })
56 playlistUUID1 = created.uuid
57 }
58
59 {
60 const attributes = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
61 const created = await servers[1].playlists.create({ attributes })
62 playlistUUID2 = created.uuid
63 }
64
65 await doubleFollow(servers[0], servers[1])
66
67 sqlCommandServer2 = new SQLCommand(servers[1])
68 })
69
70 describe('Videos refresher', function () {
71
72 it('Should remove a deleted remote video', async function () {
73 this.timeout(60000)
74
75 await wait(10000)
76
77 // Change UUID so the remote server returns a 404
78 await sqlCommandServer2.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
79
80 await servers[0].videos.get({ id: videoUUID1 })
81 await servers[0].videos.get({ id: videoUUID2 })
82
83 await waitJobs(servers)
84
85 await servers[0].videos.get({ id: videoUUID1, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
86 await servers[0].videos.get({ id: videoUUID2 })
87 })
88
89 it('Should not update a remote video if the remote instance is down', async function () {
90 this.timeout(70000)
91
92 await killallServers([ servers[1] ])
93
94 await sqlCommandServer2.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
95
96 // Video will need a refresh
97 await wait(10000)
98
99 await servers[0].videos.get({ id: videoUUID3 })
100 // The refresh should fail
101 await waitJobs([ servers[0] ])
102
103 await servers[1].run()
104
105 await servers[0].videos.get({ id: videoUUID3 })
106 })
107 })
108
109 describe('Actors refresher', function () {
110
111 it('Should remove a deleted actor', async function () {
112 this.timeout(60000)
113
114 const command = servers[0].accounts
115
116 await wait(10000)
117
118 // Change actor name so the remote server returns a 404
119 const to = servers[1].url + '/accounts/user2'
120 await sqlCommandServer2.setActorField(to, 'preferredUsername', 'toto')
121
122 await command.get({ accountName: 'user1@' + servers[1].host })
123 await command.get({ accountName: 'user2@' + servers[1].host })
124
125 await waitJobs(servers)
126
127 await command.get({ accountName: 'user1@' + servers[1].host, expectedStatus: HttpStatusCode.OK_200 })
128 await command.get({ accountName: 'user2@' + servers[1].host, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
129 })
130 })
131
132 describe('Playlist refresher', function () {
133
134 it('Should remove a deleted playlist', async function () {
135 this.timeout(60000)
136
137 await wait(10000)
138
139 // Change UUID so the remote server returns a 404
140 await sqlCommandServer2.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
141
142 await servers[0].playlists.get({ playlistId: playlistUUID1 })
143 await servers[0].playlists.get({ playlistId: playlistUUID2 })
144
145 await waitJobs(servers)
146
147 await servers[0].playlists.get({ playlistId: playlistUUID1, expectedStatus: HttpStatusCode.OK_200 })
148 await servers[0].playlists.get({ playlistId: playlistUUID2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
149 })
150 })
151
152 after(async function () {
153 await sqlCommandServer2.cleanup()
154
155 await cleanupTests(servers)
156 })
157})
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
deleted file mode 100644
index 8e87361a9..000000000
--- a/server/tests/api/activitypub/security.ts
+++ /dev/null
@@ -1,321 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { buildDigest } from '@server/helpers/peertube-crypto'
5import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
6import { activityPubContextify } from '@server/lib/activitypub/context'
7import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send'
8import { makePOSTAPRequest, SQLCommand } from '@server/tests/shared'
9import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
10import { HttpStatusCode } from '@shared/models'
11import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
12
13function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
14 const url = ofServerUrl + '/accounts/peertube'
15
16 return Promise.all([
17 onServer.setActorField(url, 'publicKey', publicKey),
18 onServer.setActorField(url, 'privateKey', privateKey)
19 ])
20}
21
22function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
23 const url = ofServerUrl + '/accounts/peertube'
24
25 return Promise.all([
26 onServer.setActorField(url, 'createdAt', updatedAt),
27 onServer.setActorField(url, 'updatedAt', updatedAt)
28 ])
29}
30
31function getAnnounceWithoutContext (server: PeerTubeServer) {
32 const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
33 const result: typeof json = {}
34
35 for (const key of Object.keys(json)) {
36 if (Array.isArray(json[key])) {
37 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
38 } else {
39 result[key] = json[key].replace(':9002', `:${server.port}`)
40 }
41 }
42
43 return result
44}
45
46async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
47 const follow = {
48 type: 'Follow',
49 id: by.url + '/' + new Date().getTime(),
50 actor: by.url,
51 object: to.url
52 }
53
54 const body = await activityPubContextify(follow, 'Follow')
55
56 const httpSignature = {
57 algorithm: HTTP_SIGNATURE.ALGORITHM,
58 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
59 keyId: by.url,
60 key: by.privateKey,
61 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
62 }
63 const headers = {
64 'digest': buildDigest(body),
65 'content-type': 'application/activity+json',
66 'accept': ACTIVITY_PUB.ACCEPT_HEADER
67 }
68
69 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
70}
71
72describe('Test ActivityPub security', function () {
73 let servers: PeerTubeServer[]
74 let sqlCommands: SQLCommand[] = []
75
76 let url: string
77
78 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
79 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
80 const baseHttpSignature = () => ({
81 algorithm: HTTP_SIGNATURE.ALGORITHM,
82 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
83 keyId: 'acct:peertube@' + servers[1].host,
84 key: keys.privateKey,
85 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
86 })
87
88 // ---------------------------------------------------------------
89
90 before(async function () {
91 this.timeout(60000)
92
93 servers = await createMultipleServers(3)
94
95 sqlCommands = servers.map(s => new SQLCommand(s))
96
97 url = servers[0].url + '/inbox'
98
99 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
100 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
101
102 const to = { url: servers[0].url + '/accounts/peertube' }
103 const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
104 await makeFollowRequest(to, by)
105 })
106
107 describe('When checking HTTP signature', function () {
108
109 it('Should fail with an invalid digest', async function () {
110 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
111 const headers = {
112 Digest: buildDigest({ hello: 'coucou' })
113 }
114
115 try {
116 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
117 expect(true, 'Did not throw').to.be.false
118 } catch (err) {
119 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
120 }
121 })
122
123 it('Should fail with an invalid date', async function () {
124 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
125 const headers = buildGlobalHeaders(body)
126 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
127
128 try {
129 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
130 expect(true, 'Did not throw').to.be.false
131 } catch (err) {
132 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
133 }
134 })
135
136 it('Should fail with bad keys', async function () {
137 await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
138 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
139
140 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
141 const headers = buildGlobalHeaders(body)
142
143 try {
144 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
145 expect(true, 'Did not throw').to.be.false
146 } catch (err) {
147 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
148 }
149 })
150
151 it('Should reject requests without appropriate signed headers', async function () {
152 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
153 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
154
155 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
156 const headers = buildGlobalHeaders(body)
157
158 const signatureOptions = baseHttpSignature()
159 const badHeadersMatrix = [
160 [ '(request-target)', 'date', 'digest' ],
161 [ 'host', 'date', 'digest' ],
162 [ '(request-target)', 'host', 'digest' ]
163 ]
164
165 for (const badHeaders of badHeadersMatrix) {
166 signatureOptions.headers = badHeaders
167
168 try {
169 await makePOSTAPRequest(url, body, signatureOptions, headers)
170 expect(true, 'Did not throw').to.be.false
171 } catch (err) {
172 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
173 }
174 }
175 })
176
177 it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
178 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
179 const headers = buildGlobalHeaders(body)
180
181 const signatureOptions = baseHttpSignature()
182 signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
183
184 const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
185 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
186 })
187
188 it('Should succeed with a valid HTTP signature', async function () {
189 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
190 const headers = buildGlobalHeaders(body)
191
192 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
193 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
194 })
195
196 it('Should refresh the actor keys', async function () {
197 this.timeout(20000)
198
199 // Update keys of server 2 to invalid keys
200 // Server 1 should refresh the actor and fail
201 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
202 await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00')
203
204 // Invalid peertube actor cache
205 await killallServers([ servers[1] ])
206 await servers[1].run()
207
208 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
209 const headers = buildGlobalHeaders(body)
210
211 try {
212 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
213 expect(true, 'Did not throw').to.be.false
214 } catch (err) {
215 console.error(err)
216 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
217 }
218 })
219 })
220
221 describe('When checking Linked Data Signature', function () {
222 before(async function () {
223 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
224 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
225 await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey)
226
227 const to = { url: servers[0].url + '/accounts/peertube' }
228 const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
229 await makeFollowRequest(to, by)
230 })
231
232 it('Should fail with bad keys', async function () {
233 await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
234 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
235
236 const body = getAnnounceWithoutContext(servers[1])
237 body.actor = servers[2].url + '/accounts/peertube'
238
239 const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
240 const signedBody = await signAndContextify(signer, body, 'Announce')
241
242 const headers = buildGlobalHeaders(signedBody)
243
244 try {
245 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
246 expect(true, 'Did not throw').to.be.false
247 } catch (err) {
248 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
249 }
250 })
251
252 it('Should fail with an altered body', async function () {
253 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
254 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
255
256 const body = getAnnounceWithoutContext(servers[1])
257 body.actor = servers[2].url + '/accounts/peertube'
258
259 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
260 const signedBody = await signAndContextify(signer, body, 'Announce')
261
262 signedBody.actor = servers[2].url + '/account/peertube'
263
264 const headers = buildGlobalHeaders(signedBody)
265
266 try {
267 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
268 expect(true, 'Did not throw').to.be.false
269 } catch (err) {
270 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
271 }
272 })
273
274 it('Should succeed with a valid signature', async function () {
275 const body = getAnnounceWithoutContext(servers[1])
276 body.actor = servers[2].url + '/accounts/peertube'
277
278 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
279 const signedBody = await signAndContextify(signer, body, 'Announce')
280
281 const headers = buildGlobalHeaders(signedBody)
282
283 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
284 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
285 })
286
287 it('Should refresh the actor keys', async function () {
288 this.timeout(20000)
289
290 // Wait refresh invalidation
291 await wait(10000)
292
293 // Update keys of server 3 to invalid keys
294 // Server 1 should refresh the actor and fail
295 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
296
297 const body = getAnnounceWithoutContext(servers[1])
298 body.actor = servers[2].url + '/accounts/peertube'
299
300 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
301 const signedBody = await signAndContextify(signer, body, 'Announce')
302
303 const headers = buildGlobalHeaders(signedBody)
304
305 try {
306 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
307 expect(true, 'Did not throw').to.be.false
308 } catch (err) {
309 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
310 }
311 })
312 })
313
314 after(async function () {
315 for (const sql of sqlCommands) {
316 await sql.cleanup()
317 }
318
319 await cleanupTests(servers)
320 })
321})