diff options
Diffstat (limited to 'server/tests/api/activitypub')
-rw-r--r-- | server/tests/api/activitypub/cleaner.ts | 342 | ||||
-rw-r--r-- | server/tests/api/activitypub/client.ts | 136 | ||||
-rw-r--r-- | server/tests/api/activitypub/fetch.ts | 82 | ||||
-rw-r--r-- | server/tests/api/activitypub/helpers.ts | 167 | ||||
-rw-r--r-- | server/tests/api/activitypub/index.ts | 6 | ||||
-rw-r--r-- | server/tests/api/activitypub/refresher.ts | 157 | ||||
-rw-r--r-- | server/tests/api/activitypub/security.ts | 321 |
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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@server/tests/shared' | ||
5 | import { wait } from '@shared/core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@shared/server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { processViewersStats } from '@server/tests/shared' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@shared/models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | makeActivityPubGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel | ||
14 | } from '@shared/server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@server/tests/shared' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@shared/server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { cloneDeep } from 'lodash' | ||
5 | import { signAndContextify } from '@server/lib/activitypub/send' | ||
6 | import { buildRequestStub } from '@server/tests/shared' | ||
7 | import { buildAbsoluteFixturePath } from '@shared/core-utils' | ||
8 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' | ||
9 | |||
10 | describe('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 @@ | |||
1 | import './cleaner' | ||
2 | import './client' | ||
3 | import './fetch' | ||
4 | import './refresher' | ||
5 | import './helpers' | ||
6 | import './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 | |||
3 | import { SQLCommand } from '@server/tests/shared' | ||
4 | import { wait } from '@shared/core-utils' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@shared/server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { buildDigest } from '@server/helpers/peertube-crypto' | ||
5 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants' | ||
6 | import { activityPubContextify } from '@server/lib/activitypub/context' | ||
7 | import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send' | ||
8 | import { makePOSTAPRequest, SQLCommand } from '@server/tests/shared' | ||
9 | import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | ||
10 | import { HttpStatusCode } from '@shared/models' | ||
11 | import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands' | ||
12 | |||
13 | function 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 | |||
22 | function 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 | |||
31 | function 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 | |||
46 | async 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 | |||
72 | describe('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 | }) | ||