aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts36
-rw-r--r--server/controllers/activitypub/inbox.ts6
-rw-r--r--server/controllers/api/videos/index.ts6
-rw-r--r--server/controllers/api/videos/rate.ts17
-rw-r--r--server/helpers/activitypub.ts11
-rw-r--r--server/helpers/custom-jsonld-signature.ts4
-rw-r--r--server/helpers/ffmpeg-utils.ts1
-rw-r--r--server/helpers/peertube-crypto.ts71
-rw-r--r--server/helpers/requests.ts4
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/initializers/migrations/0290-account-video-rate-url.ts46
-rw-r--r--server/lib/activitypub/actor.ts13
-rw-r--r--server/lib/activitypub/crawl.ts5
-rw-r--r--server/lib/activitypub/process/index.ts8
-rw-r--r--server/lib/activitypub/process/process-create.ts19
-rw-r--r--server/lib/activitypub/process/process-like.ts4
-rw-r--r--server/lib/activitypub/process/process-undo.ts6
-rw-r--r--server/lib/activitypub/process/process.ts25
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/send/send-like.ts2
-rw-r--r--server/lib/activitypub/send/send-undo.ts2
-rw-r--r--server/lib/activitypub/share.ts15
-rw-r--r--server/lib/activitypub/url.ts12
-rw-r--r--server/lib/activitypub/video-comments.ts17
-rw-r--r--server/lib/activitypub/video-rates.ts36
-rw-r--r--server/lib/activitypub/videos.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts2
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts13
-rw-r--r--server/lib/job-queue/handlers/video-views.ts8
-rw-r--r--server/lib/job-queue/job-queue.ts4
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts5
-rw-r--r--server/middlewares/activitypub.ts5
-rw-r--r--server/middlewares/validators/videos/index.ts2
-rw-r--r--server/middlewares/validators/videos/video-rates.ts55
-rw-r--r--server/middlewares/validators/videos/video-shares.ts38
-rw-r--r--server/middlewares/validators/videos/videos.ts40
-rw-r--r--server/models/account/account-video-rate.ts60
-rw-r--r--server/models/oauth/oauth-token.ts2
-rw-r--r--server/models/redundancy/video-redundancy.ts5
-rw-r--r--server/models/video/video-share.ts2
-rw-r--r--server/tests/api/activitypub/client.ts (renamed from server/tests/activitypub.ts)1
-rw-r--r--server/tests/api/activitypub/fetch.ts86
-rw-r--r--server/tests/api/activitypub/helpers.ts182
-rw-r--r--server/tests/api/activitypub/index.ts4
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-public-key.json3
-rw-r--r--server/tests/api/activitypub/json/mastodon/create-bad-signature.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/create.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/public-key.json3
-rw-r--r--server/tests/api/activitypub/json/peertube/announce-without-context.json13
-rw-r--r--server/tests/api/activitypub/json/peertube/invalid-keys.json6
-rw-r--r--server/tests/api/activitypub/json/peertube/keys.json4
-rw-r--r--server/tests/api/activitypub/security.ts180
-rw-r--r--server/tests/api/index-4.ts1
-rw-r--r--server/tests/api/redundancy/redundancy.ts36
-rw-r--r--server/tests/api/server/handle-down.ts8
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/server/no-client.ts36
-rw-r--r--server/tests/cli/peertube.ts2
-rw-r--r--server/tests/index.ts1
-rw-r--r--server/tests/utils/miscs/sql.ts38
-rw-r--r--server/tests/utils/miscs/stubs.ts14
-rw-r--r--server/tests/utils/requests/activitypub.ts43
-rw-r--r--server/tools/peertube-auth.ts113
-rwxr-xr-xserver/tools/peertube.ts2
67 files changed, 1622 insertions, 227 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 433186179..ffbf1ba19 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -3,17 +3,22 @@ import * as express from 'express'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' 5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
6import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' 6import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send'
7import { audiencify, getAudience } from '../../lib/activitypub/audience' 7import { audiencify, getAudience } from '../../lib/activitypub/audience'
8import { buildCreateActivity } from '../../lib/activitypub/send/send-create' 8import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
9import { 9import {
10 asyncMiddleware, 10 asyncMiddleware,
11 videosShareValidator,
11 executeIfActivityPub, 12 executeIfActivityPub,
12 localAccountValidator, 13 localAccountValidator,
13 localVideoChannelValidator, 14 localVideoChannelValidator,
14 videosCustomGetValidator 15 videosCustomGetValidator
15} from '../../middlewares' 16} from '../../middlewares'
16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' 17import {
18 getAccountVideoRateValidator,
19 videoCommentGetValidator,
20 videosGetValidator
21} from '../../middlewares/validators'
17import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 23import { ActorModel } from '../../models/activitypub/actor'
19import { ActorFollowModel } from '../../models/activitypub/actor-follow' 24import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
25import { activityPubResponse } from './utils' 30import { activityPubResponse } from './utils'
26import { AccountVideoRateModel } from '../../models/account/account-video-rate' 31import { AccountVideoRateModel } from '../../models/account/account-video-rate'
27import { 32import {
33 getRateUrl,
28 getVideoCommentsActivityPubUrl, 34 getVideoCommentsActivityPubUrl,
29 getVideoDislikesActivityPubUrl, 35 getVideoDislikesActivityPubUrl,
30 getVideoLikesActivityPubUrl, 36 getVideoLikesActivityPubUrl,
@@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
48 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 54 executeIfActivityPub(asyncMiddleware(localAccountValidator)),
49 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 55 executeIfActivityPub(asyncMiddleware(accountFollowingController))
50) 56)
57activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
58 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
59 executeIfActivityPub(getAccountVideoRate('like'))
60)
61activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
62 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
63 executeIfActivityPub(getAccountVideoRate('dislike'))
64)
51 65
52activityPubClientRouter.get('/videos/watch/:id', 66activityPubClientRouter.get('/videos/watch/:id',
53 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), 67 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
@@ -62,7 +76,7 @@ activityPubClientRouter.get('/videos/watch/:id/announces',
62 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 76 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
63 executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) 77 executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
64) 78)
65activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', 79activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
66 executeIfActivityPub(asyncMiddleware(videosShareValidator)), 80 executeIfActivityPub(asyncMiddleware(videosShareValidator)),
67 executeIfActivityPub(asyncMiddleware(videoAnnounceController)) 81 executeIfActivityPub(asyncMiddleware(videoAnnounceController))
68) 82)
@@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
133 return activityPubResponse(activityPubContextify(activityPubResult), res) 147 return activityPubResponse(activityPubContextify(activityPubResult), res)
134} 148}
135 149
150function getAccountVideoRate (rateType: VideoRateType) {
151 return (req: express.Request, res: express.Response) => {
152 const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
153
154 const byActor = accountVideoRate.Account.Actor
155 const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
156 const APObject = rateType === 'like'
157 ? buildLikeActivity(url, byActor, accountVideoRate.Video)
158 : buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video))
159
160 return activityPubResponse(activityPubContextify(APObject), res)
161 }
162}
163
136async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 164async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
137 const video: VideoModel = res.locals.video 165 const video: VideoModel = res.locals.video
138 166
@@ -276,7 +304,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
276 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 304 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
277 return { 305 return {
278 total: result.count, 306 total: result.count,
279 data: result.rows.map(r => r.Account.Actor.url) 307 data: result.rows.map(r => r.url)
280 } 308 }
281 } 309 }
282 return activityPubCollectionPagination(url, handler, req.query.page) 310 return activityPubCollectionPagination(url, handler, req.query.page)
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index 738d155eb..f0e65015b 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -43,11 +43,13 @@ export {
43// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
44 44
45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { 45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
46 processActivities(task.activities, task.signatureActor, task.inboxActor) 46 const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
47
48 processActivities(task.activities, options)
47 .then(() => cb()) 49 .then(() => cb())
48}) 50})
49 51
50function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { 52function inboxController (req: express.Request, res: express.Response) {
51 const rootActivity: RootActivity = req.body 53 const rootActivity: RootActivity = req.body
52 let activities: Activity[] = [] 54 let activities: Activity[] = []
53 55
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 664154406..e654bdd09 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -405,7 +405,11 @@ async function viewVideo (req: express.Request, res: express.Response) {
405 405
406 const serverActor = await getServerActor() 406 const serverActor = await getServerActor()
407 407
408 await sendCreateView(serverActor, videoInstance, undefined) 408 // Send the event to the origin server
409 // If we own the video, we'll send an update event when we'll process the views (in our job queue)
410 if (videoInstance.isOwned() === false) {
411 await sendCreateView(serverActor, videoInstance, undefined)
412 }
409 413
410 return res.status(204).end() 414 return res.status(204).end()
411} 415}
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index dc322bb0c..53952a0a2 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -2,8 +2,8 @@ import * as express from 'express'
2import { UserVideoRateUpdate } from '../../../../shared' 2import { UserVideoRateUpdate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' 4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
5import { sendVideoRateChange } from '../../../lib/activitypub' 5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' 6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
7import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
12 12
13rateVideoRouter.put('/:id/rate', 13rateVideoRouter.put('/:id/rate',
14 authenticate, 14 authenticate,
15 asyncMiddleware(videoRateValidator), 15 asyncMiddleware(videoUpdateRateValidator),
16 asyncRetryTransactionMiddleware(rateVideo) 16 asyncRetryTransactionMiddleware(rateVideo)
17) 17)
18 18
@@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
28 const body: UserVideoRateUpdate = req.body 28 const body: UserVideoRateUpdate = req.body
29 const rateType = body.rating 29 const rateType = body.rating
30 const videoInstance: VideoModel = res.locals.video 30 const videoInstance: VideoModel = res.locals.video
31 const userAccount: AccountModel = res.locals.oauth.token.User.Account
31 32
32 await sequelizeTypescript.transaction(async t => { 33 await sequelizeTypescript.transaction(async t => {
33 const sequelizeOptions = { transaction: t } 34 const sequelizeOptions = { transaction: t }
34 35
35 const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 36 const accountInstance = await AccountModel.load(userAccount.id, t)
36 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) 37 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
37 38
38 let likesToIncrement = 0 39 let likesToIncrement = 0
@@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
44 // There was a previous rate, update it 45 // There was a previous rate, update it
45 if (previousRate) { 46 if (previousRate) {
46 // We will remove the previous rate, so we will need to update the video count attribute 47 // We will remove the previous rate, so we will need to update the video count attribute
47 if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- 48 if (previousRate.type === 'like') likesToIncrement--
48 else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- 49 else if (previousRate.type === 'dislike') dislikesToIncrement--
49 50
50 if (rateType === 'none') { // Destroy previous rate 51 if (rateType === 'none') { // Destroy previous rate
51 await previousRate.destroy(sequelizeOptions) 52 await previousRate.destroy(sequelizeOptions)
52 } else { // Update previous rate 53 } else { // Update previous rate
53 previousRate.type = rateType 54 previousRate.type = rateType
55 previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
54 await previousRate.save(sequelizeOptions) 56 await previousRate.save(sequelizeOptions)
55 } 57 }
56 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate 58 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
57 const query = { 59 const query = {
58 accountId: accountInstance.id, 60 accountId: accountInstance.id,
59 videoId: videoInstance.id, 61 videoId: videoInstance.id,
60 type: rateType 62 type: rateType,
63 url: getRateUrl(rateType, userAccount.Actor, videoInstance)
61 } 64 }
62 65
63 await AccountVideoRateModel.create(query, sequelizeOptions) 66 await AccountVideoRateModel.create(query, sequelizeOptions)
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 278010e78..4bf6e387d 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signJsonLDObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9import { parse } from 'url'
9 10
10function activityPubContextify <T> (data: T) { 11function activityPubContextify <T> (data: T) {
11 return Object.assign(data, { 12 return Object.assign(data, {
@@ -24,7 +25,7 @@ function activityPubContextify <T> (data: T) {
24 sensitive: 'as:sensitive', 25 sensitive: 'as:sensitive',
25 language: 'sc:inLanguage', 26 language: 'sc:inLanguage',
26 views: 'sc:Number', 27 views: 'sc:Number',
27 stats: 'sc:Number', 28 state: 'sc:Number',
28 size: 'sc:Number', 29 size: 'sc:Number',
29 fps: 'sc:Number', 30 fps: 'sc:Number',
30 commentsEnabled: 'sc:Boolean', 31 commentsEnabled: 'sc:Boolean',
@@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
111 return activityActor.id 112 return activityActor.id
112} 113}
113 114
115function checkUrlsSameHost (url1: string, url2: string) {
116 const idHost = parse(url1).host
117 const actorHost = parse(url2).host
118
119 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
120}
121
114// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
115 123
116export { 124export {
125 checkUrlsSameHost,
117 getActorUrl, 126 getActorUrl,
118 activityPubContextify, 127 activityPubContextify,
119 activityPubCollectionPagination, 128 activityPubCollectionPagination,
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index e4f28018e..27a187db1 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -1,5 +1,5 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld/' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures' 3import * as jsig from 'jsonld-signatures'
4 4
5const nodeDocumentLoader = jsonld.documentLoaders.node() 5const nodeDocumentLoader = jsonld.documentLoaders.node()
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
17 17
18jsig.use('jsonld', jsonld) 18jsig.use('jsonld', jsonld)
19 19
20export { jsig } 20export { jsig, jsonld }
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index a108d46a0..8b9045038 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -310,6 +310,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
310 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 310 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
311 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it 311 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it
312 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 312 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
313 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
313 .outputOption('-map_metadata -1') // strip all metadata 314 .outputOption('-map_metadata -1') // strip all metadata
314 .outputOption('-movflags faststart') 315 .outputOption('-movflags faststart')
315 316
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 8ef7b1359..ab9ec077e 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,9 +1,12 @@
1import { Request } from 'express' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
5import { jsig } from './custom-jsonld-signature' 5import { jsig, jsonld } from './custom-jsonld-signature'
6import { logger } from './logger' 6import { logger } from './logger'
7import { cloneDeep } from 'lodash'
8import { createVerify } from 'crypto'
9import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
7 10
8const httpSignature = require('http-signature') 11const httpSignature = require('http-signature')
9 12
@@ -30,21 +33,36 @@ async function cryptPassword (password: string) {
30 33
31// HTTP Signature 34// HTTP Signature
32 35
33function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) { 36function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
37 if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
38 return buildDigest(rawBody.toString()) === req.headers['digest']
39 }
40
41 return true
42}
43
44function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
34 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true 45 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
35} 46}
36 47
37function parseHTTPSignature (req: Request) { 48function parseHTTPSignature (req: Request, clockSkew?: number) {
38 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME }) 49 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
39} 50}
40 51
41// JSONLD 52// JSONLD
42 53
43function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) { 54async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
55 if (signedDocument.signature.type === 'RsaSignature2017') {
56 // Mastodon algorithm
57 const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
58 // Success? If no, try with our library
59 if (res === true) return true
60 }
61
44 const publicKeyObject = { 62 const publicKeyObject = {
45 '@context': jsig.SECURITY_CONTEXT_URL, 63 '@context': jsig.SECURITY_CONTEXT_URL,
46 id: fromActor.url, 64 id: fromActor.url,
47 type: 'CryptographicKey', 65 type: 'CryptographicKey',
48 owner: fromActor.url, 66 owner: fromActor.url,
49 publicKeyPem: fromActor.publicKey 67 publicKeyPem: fromActor.publicKey
50 } 68 }
@@ -69,6 +87,44 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any)
69 }) 87 })
70} 88}
71 89
90// Backward compatibility with "other" implementations
91async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
92 function hash (obj: any): Promise<any> {
93 return jsonld.promises
94 .normalize(obj, {
95 algorithm: 'URDNA2015',
96 format: 'application/n-quads'
97 })
98 .then(res => sha256(res))
99 }
100
101 const signatureCopy = cloneDeep(signedDocument.signature)
102 Object.assign(signatureCopy, {
103 '@context': [
104 'https://w3id.org/security/v1',
105 { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
106 ]
107 })
108 delete signatureCopy.type
109 delete signatureCopy.id
110 delete signatureCopy.signatureValue
111
112 const docWithoutSignature = cloneDeep(signedDocument)
113 delete docWithoutSignature.signature
114
115 const [ documentHash, optionsHash ] = await Promise.all([
116 hash(docWithoutSignature),
117 hash(signatureCopy)
118 ])
119
120 const toVerify = optionsHash + documentHash
121
122 const verify = createVerify('RSA-SHA256')
123 verify.update(toVerify, 'utf8')
124
125 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
126}
127
72function signJsonLDObject (byActor: ActorModel, data: any) { 128function signJsonLDObject (byActor: ActorModel, data: any) {
73 const options = { 129 const options = {
74 privateKeyPem: byActor.privateKey, 130 privateKeyPem: byActor.privateKey,
@@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) {
82// --------------------------------------------------------------------------- 138// ---------------------------------------------------------------------------
83 139
84export { 140export {
141 isHTTPSignatureDigestValid,
85 parseHTTPSignature, 142 parseHTTPSignature,
86 isHTTPSignatureVerified, 143 isHTTPSignatureVerified,
87 isJsonLDSignatureVerified, 144 isJsonLDSignatureVerified,
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index ee9e80404..51facc9e0 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers'
5 5
6function doRequest ( 6function doRequest <T> (
7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
8): Bluebird<{ response: request.RequestResponse, body: any }> { 8): Bluebird<{ response: request.RequestResponse, body: any }> {
9 if (requestOptions.activityPub === true) { 9 if (requestOptions.activityPub === true) {
@@ -11,7 +11,7 @@ function doRequest (
11 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 11 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
12 } 12 }
13 13
14 return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { 14 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
15 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 15 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
16 }) 16 })
17} 17}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 28d51068b..ae3d671bb 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -16,7 +16,7 @@ let config: IConfig = require('config')
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
18 18
19const LAST_MIGRATION_VERSION = 285 19const LAST_MIGRATION_VERSION = 290
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
@@ -336,6 +336,9 @@ const CONSTRAINTS_FIELDS = {
336 VIDEOS_REDUNDANCY: { 336 VIDEOS_REDUNDANCY: {
337 URL: { min: 3, max: 2000 } // Length 337 URL: { min: 3, max: 2000 } // Length
338 }, 338 },
339 VIDEO_RATES: {
340 URL: { min: 3, max: 2000 } // Length
341 },
339 VIDEOS: { 342 VIDEOS: {
340 NAME: { min: 3, max: 120 }, // Length 343 NAME: { min: 3, max: 120 }, // Length
341 LANGUAGE: { min: 1, max: 10 }, // Length 344 LANGUAGE: { min: 1, max: 10 }, // Length
@@ -535,7 +538,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
535const HTTP_SIGNATURE = { 538const HTTP_SIGNATURE = {
536 HEADER_NAME: 'signature', 539 HEADER_NAME: 'signature',
537 ALGORITHM: 'rsa-sha256', 540 ALGORITHM: 'rsa-sha256',
538 HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ] 541 HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ]
539} 542}
540 543
541// --------------------------------------------------------------------------- 544// ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0290-account-video-rate-url.ts b/server/initializers/migrations/0290-account-video-rate-url.ts
new file mode 100644
index 000000000..bdabf2929
--- /dev/null
+++ b/server/initializers/migrations/0290-account-video-rate-url.ts
@@ -0,0 +1,46 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.STRING(2000),
12 allowNull: true
13 }
14
15 await utils.queryInterface.addColumn('accountVideoRate', 'url', data)
16 }
17
18 {
19 const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` +
20 'FROM "accountVideoRate" ' +
21 'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
22 'INNER JOIN actor ON actor.id = account."actorId" ' +
23 'WHERE "base".id = "accountVideoRate".id'
24
25 const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL'
26 await utils.sequelize.query(query)
27 }
28
29 {
30 const data = {
31 type: Sequelize.STRING(2000),
32 allowNull: false,
33 defaultValue: null
34 }
35 await utils.queryInterface.changeColumn('accountVideoRate', 'url', data)
36 }
37}
38
39function down (options) {
40 throw new Error('Not implemented.')
41}
42
43export {
44 up,
45 down
46}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 45dd4443d..b16a00669 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -5,7 +5,7 @@ import * as url from 'url'
5import * as uuidv4 from 'uuid/v4' 5import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { getActorUrl } from '../../helpers/activitypub' 8import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' 9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
65 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') 65 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
66 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) 66 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
67 67
68 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
69 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
70 }
71
68 try { 72 try {
69 // Assert we don't recurse another time 73 // Don't recurse another time
70 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) 74 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
71 } catch (err) { 75 } catch (err) {
72 logger.error('Cannot get or create account attributed to video channel ' + actor.url) 76 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
@@ -297,12 +301,15 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
297 normalizeActor(requestResult.body) 301 normalizeActor(requestResult.body)
298 302
299 const actorJSON: ActivityPubActor = requestResult.body 303 const actorJSON: ActivityPubActor = requestResult.body
300
301 if (isActorObjectValid(actorJSON) === false) { 304 if (isActorObjectValid(actorJSON) === false) {
302 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) 305 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
303 return { result: undefined, statusCode: requestResult.response.statusCode } 306 return { result: undefined, statusCode: requestResult.response.statusCode }
304 } 307 }
305 308
309 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
310 throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
311 }
312
306 const followersCount = await fetchActorTotalItems(actorJSON.followers) 313 const followersCount = await fetchActorTotalItems(actorJSON.followers)
307 const followingCount = await fetchActorTotalItems(actorJSON.following) 314 const followingCount = await fetchActorTotalItems(actorJSON.following)
308 315
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index db9ce3293..1b9b14c2e 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
5 6
6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 7async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
7 logger.info('Crawling ActivityPub data on %s.', uri) 8 logger.info('Crawling ActivityPub data on %s.', uri)
@@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
14 timeout: JOB_REQUEST_TIMEOUT 15 timeout: JOB_REQUEST_TIMEOUT
15 } 16 }
16 17
17 const response = await doRequest(options) 18 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
18 const firstBody = response.body 19 const firstBody = response.body
19 20
20 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 21 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
23 while (nextLink && i < limit) { 24 while (nextLink && i < limit) {
24 options.uri = nextLink 25 options.uri = nextLink
25 26
26 const { body } = await doRequest(options) 27 const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
27 nextLink = body.next 28 nextLink = body.next
28 i++ 29 i++
29 30
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts
index db4980a72..5466739c1 100644
--- a/server/lib/activitypub/process/index.ts
+++ b/server/lib/activitypub/process/index.ts
@@ -1,9 +1 @@
1export * from './process' export * from './process'
2export * from './process-accept'
3export * from './process-announce'
4export * from './process-create'
5export * from './process-delete'
6export * from './process-follow'
7export * from './process-like'
8export * from './process-undo'
9export * from './process-update'
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index cefe89db0..214e14546 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -12,6 +12,9 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis' 13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { immutableAssign } from '../../../tests/utils'
16import { getVideoDislikeActivityPubUrl } from '../url'
17import { VideoModel } from '../../../models/video/video'
15 18
16async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 19async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
17 const activityObject = activity.object 20 const activityObject = activity.object
@@ -65,9 +68,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
65 videoId: video.id, 68 videoId: video.id,
66 accountId: byAccount.id 69 accountId: byAccount.id
67 } 70 }
71
68 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 72 const [ , created ] = await AccountVideoRateModel.findOrCreate({
69 where: rate, 73 where: rate,
70 defaults: rate, 74 defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
71 transaction: t 75 transaction: t
72 }) 76 })
73 if (created === true) await video.increment('dislikes', { transaction: t }) 77 if (created === true) await video.increment('dislikes', { transaction: t })
@@ -84,19 +88,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
84async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { 88async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
85 const view = activity.object as ViewObject 89 const view = activity.object as ViewObject
86 90
87 const options = { 91 const video = await VideoModel.loadByUrl(view.object)
88 videoObject: view.object, 92 if (!video || video.isOwned() === false) return
89 fetchType: 'only-video' as 'only-video'
90 }
91 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
92 93
93 await Redis.Instance.addVideoView(video.id) 94 await Redis.Instance.addVideoView(video.id)
94
95 if (video.isOwned()) {
96 // Don't resend the activity to the sender
97 const exceptions = [ byActor ]
98 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
99 }
100} 95}
101 96
102async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { 97async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) {
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index f7200db61..0dca17551 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { immutableAssign } from '../../../tests/utils'
9import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
8 10
9async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 11async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
10 return retryTransactionWrapper(processLikeVideo, byActor, activity) 12 return retryTransactionWrapper(processLikeVideo, byActor, activity)
@@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
34 } 36 }
35 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 37 const [ , created ] = await AccountVideoRateModel.findOrCreate({
36 where: rate, 38 where: rate,
37 defaults: rate, 39 defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
38 transaction: t 40 transaction: t
39 }) 41 })
40 if (created === true) await video.increment('likes', { transaction: t }) 42 if (created === true) await video.increment('likes', { transaction: t })
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index ff019cd8c..438a013b6 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -55,7 +55,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
55 return sequelizeTypescript.transaction(async t => { 55 return sequelizeTypescript.transaction(async t => {
56 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 56 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
57 57
58 const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) 58 let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t)
59 if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
59 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) 60 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
60 61
61 await rate.destroy({ transaction: t }) 62 await rate.destroy({ transaction: t })
@@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
78 return sequelizeTypescript.transaction(async t => { 79 return sequelizeTypescript.transaction(async t => {
79 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 80 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
80 81
81 const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) 82 let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t)
83 if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
82 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) 84 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
83 85
84 await rate.destroy({ transaction: t }) 86 await rate.destroy({ transaction: t })
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
index b263f1ea2..b9b255ddf 100644
--- a/server/lib/activitypub/process/process.ts
+++ b/server/lib/activitypub/process/process.ts
@@ -1,5 +1,5 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { getActorUrl } from '../../../helpers/activitypub' 2import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { ActorModel } from '../../../models/activitypub/actor'
5import { processAcceptActivity } from './process-accept' 5import { processAcceptActivity } from './process-accept'
@@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
25 Like: processLikeActivity 25 Like: processLikeActivity
26} 26}
27 27
28async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { 28async function processActivities (
29 activities: Activity[],
30 options: {
31 signatureActor?: ActorModel
32 inboxActor?: ActorModel
33 outboxUrl?: string
34 } = {}) {
29 const actorsCache: { [ url: string ]: ActorModel } = {} 35 const actorsCache: { [ url: string ]: ActorModel } = {}
30 36
31 for (const activity of activities) { 37 for (const activity of activities) {
32 if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { 38 if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
33 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) 39 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
34 continue 40 continue
35 } 41 }
@@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
37 const actorUrl = getActorUrl(activity.actor) 43 const actorUrl = getActorUrl(activity.actor)
38 44
39 // When we fetch remote data, we don't have signature 45 // When we fetch remote data, we don't have signature
40 if (signatureActor && actorUrl !== signatureActor.url) { 46 if (options.signatureActor && actorUrl !== options.signatureActor.url) {
41 logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) 47 logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
42 continue 48 continue
43 } 49 }
44 50
45 const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) 51 if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) {
52 logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl)
53 continue
54 }
55
56 const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
46 actorsCache[actorUrl] = byActor 57 actorsCache[actorUrl] = byActor
47 58
48 const activityProcessor = processActivity[activity.type] 59 const activityProcessor = processActivity[activity.type]
@@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
52 } 63 }
53 64
54 try { 65 try {
55 await activityProcessor(activity, byActor, inboxActor) 66 await activityProcessor(activity, byActor, options.inboxActor)
56 } catch (err) { 67 } catch (err) {
57 logger.warn('Cannot process activity %s.', activity.type, { err }) 68 logger.warn('Cannot process activity %s.', activity.type, { err })
58 } 69 }
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 285edba3b..e3fca0a17 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -95,7 +95,7 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
95 logger.info('Creating job to send view of %s.', video.url) 95 logger.info('Creating job to send view of %s.', video.url)
96 96
97 const url = getVideoViewActivityPubUrl(byActor, video) 97 const url = getVideoViewActivityPubUrl(byActor, video)
98 const viewActivity = buildViewActivity(byActor, video) 98 const viewActivity = buildViewActivity(url, byActor, video)
99 99
100 return sendVideoRelatedCreateActivity({ 100 return sendVideoRelatedCreateActivity({
101 // Use the server actor to send the view 101 // Use the server actor to send the view
@@ -111,7 +111,7 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
111 logger.info('Creating job to dislike %s.', video.url) 111 logger.info('Creating job to dislike %s.', video.url)
112 112
113 const url = getVideoDislikeActivityPubUrl(byActor, video) 113 const url = getVideoDislikeActivityPubUrl(byActor, video)
114 const dislikeActivity = buildDislikeActivity(byActor, video) 114 const dislikeActivity = buildDislikeActivity(url, byActor, video)
115 115
116 return sendVideoRelatedCreateActivity({ 116 return sendVideoRelatedCreateActivity({
117 byActor, 117 byActor,
@@ -136,16 +136,18 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
136 ) 136 )
137} 137}
138 138
139function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { 139function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
140 return { 140 return {
141 id: url,
141 type: 'Dislike', 142 type: 'Dislike',
142 actor: byActor.url, 143 actor: byActor.url,
143 object: video.url 144 object: video.url
144 } 145 }
145} 146}
146 147
147function buildViewActivity (byActor: ActorModel, video: VideoModel) { 148function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
148 return { 149 return {
150 id: url,
149 type: 'View', 151 type: 'View',
150 actor: byActor.url, 152 actor: byActor.url,
151 object: video.url 153 object: video.url
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index 89307acc6..35227887a 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
24 24
25 return audiencify( 25 return audiencify(
26 { 26 {
27 type: 'Like' as 'Like',
28 id: url, 27 id: url,
28 type: 'Like' as 'Like',
29 actor: byActor.url, 29 actor: byActor.url,
30 object: video.url 30 object: video.url
31 }, 31 },
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index 5236d2cb3..bf1b6e117 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -64,7 +64,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
64 logger.info('Creating job to undo a dislike of video %s.', video.url) 64 logger.info('Creating job to undo a dislike of video %s.', video.url)
65 65
66 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) 66 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
67 const dislikeActivity = buildDislikeActivity(byActor, video) 67 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
68 const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) 68 const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
69 69
70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) 70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t })
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 3ff60a97c..d2649e2d5 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { VideoShareModel } from '../../models/video/video-share' 5import { VideoShareModel } from '../../models/video/video-share'
6import { sendUndoAnnounce, sendVideoAnnounce } from './send' 6import { sendUndoAnnounce, sendVideoAnnounce } from './send'
7import { getAnnounceActivityPubUrl } from './url' 7import { getVideoAnnounceActivityPubUrl } from './url'
8import { VideoChannelModel } from '../../models/video/video-channel' 8import { VideoChannelModel } from '../../models/video/video-channel'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { doRequest } from '../../helpers/requests' 10import { doRequest } from '../../helpers/requests'
11import { getOrCreateActorAndServerAndModel } from './actor' 11import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
14import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
14 15
15async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
38 json: true, 39 json: true,
39 activityPub: true 40 activityPub: true
40 }) 41 })
41 if (!body || !body.actor) throw new Error('Body of body actor is invalid') 42 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
43
44 const actorUrl = getActorUrl(body.actor)
45 if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
46 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
47 }
42 48
43 const actorUrl = body.actor
44 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 49 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
45 50
46 const entry = { 51 const entry = {
@@ -72,7 +77,7 @@ export {
72async function shareByServer (video: VideoModel, t: Transaction) { 77async function shareByServer (video: VideoModel, t: Transaction) {
73 const serverActor = await getServerActor() 78 const serverActor = await getServerActor()
74 79
75 const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) 80 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
76 return VideoShareModel.findOrCreate({ 81 return VideoShareModel.findOrCreate({
77 defaults: { 82 defaults: {
78 actorId: serverActor.id, 83 actorId: serverActor.id,
@@ -91,7 +96,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
91} 96}
92 97
93async function shareByVideoChannel (video: VideoModel, t: Transaction) { 98async function shareByVideoChannel (video: VideoModel, t: Transaction) {
94 const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) 99 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
95 return VideoShareModel.findOrCreate({ 100 return VideoShareModel.findOrCreate({
96 defaults: { 101 defaults: {
97 actorId: video.VideoChannel.actorId, 102 actorId: video.VideoChannel.actorId,
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index e792be698..38f15448c 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
33} 33}
34 34
35function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { 35function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
36 return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() 36 return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
37} 37}
38 38
39function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { 39function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
40 return byActor.url + '/likes/' + video.id 40 return byActor.url + '/likes/' + video.id
41} 41}
42 42
43function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { 43function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
44 return byActor.url + '/dislikes/' + video.id 44 return byActor.url + '/dislikes/' + video.id
45} 45}
46 46
@@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
74 return follower.url + '/accepts/follows/' + me.id 74 return follower.url + '/accepts/follows/' + me.id
75} 75}
76 76
77function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { 77function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
78 return originalUrl + '/announces/' + byActor.id 78 return video.url + '/announces/' + byActor.id
79} 79}
80 80
81function getDeleteActivityPubUrl (originalUrl: string) { 81function getDeleteActivityPubUrl (originalUrl: string) {
@@ -97,7 +97,7 @@ export {
97 getVideoAbuseActivityPubUrl, 97 getVideoAbuseActivityPubUrl,
98 getActorFollowActivityPubUrl, 98 getActorFollowActivityPubUrl,
99 getActorFollowAcceptActivityPubUrl, 99 getActorFollowAcceptActivityPubUrl,
100 getAnnounceActivityPubUrl, 100 getVideoAnnounceActivityPubUrl,
101 getUpdateActivityPubUrl, 101 getUpdateActivityPubUrl,
102 getUndoActivityPubUrl, 102 getUndoActivityPubUrl,
103 getVideoViewActivityPubUrl, 103 getVideoViewActivityPubUrl,
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index c8c17f4c4..5868e7297 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
9import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
10import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
11import * as Bluebird from 'bluebird' 11import * as Bluebird from 'bluebird'
12import { checkUrlsSameHost } from '../../helpers/activitypub'
12 13
13async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { 14async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
14 let originCommentId: number = null 15 let originCommentId: number = null
@@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
61 const actorUrl = body.attributedTo 62 const actorUrl = body.attributedTo
62 if (!actorUrl) return { created: false } 63 if (!actorUrl) return { created: false }
63 64
65 if (checkUrlsSameHost(commentUrl, actorUrl) !== true) {
66 throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`)
67 }
68
69 if (checkUrlsSameHost(body.id, commentUrl) !== true) {
70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
71 }
72
64 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 73 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
65 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
66 if (!entry) return { created: false } 75 if (!entry) return { created: false }
@@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
134 const actorUrl = body.attributedTo 143 const actorUrl = body.attributedTo
135 if (!actorUrl) throw new Error('Miss attributed to in comment') 144 if (!actorUrl) throw new Error('Miss attributed to in comment')
136 145
146 if (checkUrlsSameHost(url, actorUrl) !== true) {
147 throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
148 }
149
150 if (checkUrlsSameHost(body.id, url) !== true) {
151 throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
152 }
153
137 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 154 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
138 const comment = new VideoCommentModel({ 155 const comment = new VideoCommentModel({
139 url: body.id, 156 url: body.id,
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 1619251c3..1854b44c4 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
11import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
13import { ActorModel } from '../../models/activitypub/actor'
14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
11 15
12async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { 16async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
13 let rateCounts = 0 17 let rateCounts = 0
14 18
15 await Bluebird.map(actorUrls, async actorUrl => { 19 await Bluebird.map(ratesUrl, async rateUrl => {
16 try { 20 try {
21 // Fetch url
22 const { body } = await doRequest({
23 uri: rateUrl,
24 json: true,
25 activityPub: true
26 })
27 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
28
29 const actorUrl = getActorUrl(body.actor)
30 if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
31 throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
32 }
33
34 if (checkUrlsSameHost(body.id, rateUrl) !== true) {
35 throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
36 }
37
17 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 38 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
39
18 const [ , created ] = await AccountVideoRateModel 40 const [ , created ] = await AccountVideoRateModel
19 .findOrCreate({ 41 .findOrCreate({
20 where: { 42 where: {
@@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
24 defaults: { 46 defaults: {
25 videoId: video.id, 47 videoId: video.id,
26 accountId: actor.Account.id, 48 accountId: actor.Account.id,
27 type: rate 49 type: rate,
50 url: body.id
28 } 51 }
29 }) 52 })
30 53
31 if (created) rateCounts += 1 54 if (created) rateCounts += 1
32 } catch (err) { 55 } catch (err) {
33 logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) 56 logger.warn('Cannot add rate %s.', rateUrl, { err })
34 } 57 }
35 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 58 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
36 59
@@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
62 if (dislikes > 0) await sendCreateDislike(actor, video, t) 85 if (dislikes > 0) await sendCreateDislike(actor, video, t)
63} 86}
64 87
88function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
89 return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
90}
91
65export { 92export {
93 getRateUrl,
66 createRates, 94 createRates,
67 sendVideoRateChange 95 sendVideoRateChange
68} 96}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 3da363c0a..5bd03c8c6 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -29,6 +29,7 @@ import { createRates } from './video-rates'
29import { addVideoShares, shareVideoByServerAndChannel } from './share' 29import { addVideoShares, shareVideoByServerAndChannel } from './share'
30import { AccountModel } from '../../models/account/account' 30import { AccountModel } from '../../models/account/account'
31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
32import { checkUrlsSameHost } from '../../helpers/activitypub'
32 33
33async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
34 // If the video is not private and published, we federate it 35 // If the video is not private and published, we federate it
@@ -63,7 +64,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
63 64
64 const { response, body } = await doRequest(options) 65 const { response, body } = await doRequest(options)
65 66
66 if (sanitizeAndCheckVideoTorrentObject(body) === false) { 67 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
67 logger.debug('Remote video JSON is not valid.', { body }) 68 logger.debug('Remote video JSON is not valid.', { body })
68 return { response, videoObject: undefined } 69 return { response, videoObject: undefined }
69 } 70 }
@@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
107 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 108 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
108 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 109 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
109 110
111 if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
112 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
113 }
114
110 return getOrCreateActorAndServerAndModel(channel.id, 'all') 115 return getOrCreateActorAndServerAndModel(channel.id, 'all')
111} 116}
112 117
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index 42217c27c..67ccfa995 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
23 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) 23 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
24 24
25 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { 25 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
26 'activity': items => processActivities(items), 26 'activity': items => processActivities(items, { outboxUrl: payload.uri }),
27 'video-likes': items => createRates(items, video, 'like'), 27 'video-likes': items => createRates(items, video, 'like'),
28 'video-dislikes': items => createRates(items, video, 'dislike'), 28 'video-dislikes': items => createRates(items, video, 'dislike'),
29 'video-shares': items => addVideoShares(items, video), 29 'video-shares': items => addVideoShares(items, video),
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index fd9c74341..4961d4502 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -38,15 +38,20 @@ async function buildSignedRequestOptions (payload: Payload) {
38 } 38 }
39} 39}
40 40
41function buildGlobalHeaders (body: object) { 41function buildGlobalHeaders (body: any) {
42 const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64')
43
44 return { 42 return {
45 'Digest': digest 43 'Digest': buildDigest(body)
46 } 44 }
47} 45}
48 46
47function buildDigest (body: any) {
48 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
49
50 return 'SHA-256=' + sha256(rawBody, 'base64')
51}
52
49export { 53export {
54 buildDigest,
50 buildGlobalHeaders, 55 buildGlobalHeaders,
51 computeBody, 56 computeBody,
52 buildSignedRequestOptions 57 buildSignedRequestOptions
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index cf180a11a..f44c3c727 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -3,8 +3,9 @@ import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video' 3import { VideoModel } from '../../../models/video/video'
4import { VideoViewModel } from '../../../models/video/video-views' 4import { VideoViewModel } from '../../../models/video/video-views'
5import { isTestInstance } from '../../../helpers/core-utils' 5import { isTestInstance } from '../../../helpers/core-utils'
6import { federateVideoIfNeeded } from '../../activitypub'
6 7
7async function processVideosViewsViews () { 8async function processVideosViews () {
8 const lastHour = new Date() 9 const lastHour = new Date()
9 10
10 // In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour 11 // In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour
@@ -36,6 +37,9 @@ async function processVideosViewsViews () {
36 views, 37 views,
37 videoId 38 videoId
38 }) 39 })
40
41 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
42 if (video.isOwned()) await federateVideoIfNeeded(video, false)
39 } catch (err) { 43 } catch (err) {
40 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) 44 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour)
41 } 45 }
@@ -51,5 +55,5 @@ async function processVideosViewsViews () {
51// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
52 56
53export { 57export {
54 processVideosViewsViews 58 processVideosViews
55} 59}
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 0696ba43c..4cfd4d253 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -10,7 +10,7 @@ import { EmailPayload, processEmail } from './handlers/email'
10import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' 10import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file'
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 12import { processVideoImport, VideoImportPayload } from './handlers/video-import'
13import { processVideosViewsViews } from './handlers/video-views' 13import { processVideosViews } from './handlers/video-views'
14 14
15type CreateJobArgument = 15type CreateJobArgument =
16 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 16 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -32,7 +32,7 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
32 'video-file': processVideoFile, 32 'video-file': processVideoFile,
33 'email': processEmail, 33 'email': processEmail,
34 'video-import': processVideoImport, 34 'video-import': processVideoImport,
35 'videos-views': processVideosViewsViews 35 'videos-views': processVideosViews
36} 36}
37 37
38const jobTypes: JobType[] = [ 38const jobTypes: JobType[] = [
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index c49a8c89a..8b7f33539 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -185,11 +185,12 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
185 } 185 }
186 186
187 private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { 187 private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) {
188 const maxSize = redundancy.size - this.getTotalFileSizes(filesToDuplicate) 188 const maxSize = redundancy.size
189 189
190 const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) 190 const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy)
191 const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate)
191 192
192 return totalDuplicated > maxSize 193 return totalWillDuplicate > maxSize
193 } 194 }
194 195
195 private buildNewExpiration (expiresAfterMs: number) { 196 private buildNewExpiration (expiresAfterMs: number) {
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index 1ec888477..01e5dd24e 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -53,7 +53,8 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
53 53
54export { 54export {
55 checkSignature, 55 checkSignature,
56 executeIfActivityPub 56 executeIfActivityPub,
57 checkHttpSignature
57} 58}
58 59
59// --------------------------------------------------------------------------- 60// ---------------------------------------------------------------------------
@@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) {
94async function checkJsonLDSignature (req: Request, res: Response) { 95async function checkJsonLDSignature (req: Request, res: Response) {
95 const signatureObject: ActivityPubSignature = req.body.signature 96 const signatureObject: ActivityPubSignature = req.body.signature
96 97
97 if (!signatureObject.creator) { 98 if (!signatureObject || !signatureObject.creator) {
98 res.sendStatus(403) 99 res.sendStatus(403)
99 return false 100 return false
100 } 101 }
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index 294783d85..a0d585b93 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -5,4 +5,6 @@ export * from './video-channels'
5export * from './video-comments' 5export * from './video-comments'
6export * from './video-imports' 6export * from './video-imports'
7export * from './video-watch' 7export * from './video-watch'
8export * from './video-rates'
9export * from './video-shares'
8export * from './videos' 10export * from './videos'
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
new file mode 100644
index 000000000..793354520
--- /dev/null
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -0,0 +1,55 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoRateType } from '../../../../shared/models/videos'
10import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
11
12const videoUpdateRateValidator = [
13 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
14 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
15
16 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking videoRate parameters', { parameters: req.body })
18
19 if (areValidationErrors(req, res)) return
20 if (!await isVideoExist(req.params.id, res)) return
21
22 return next()
23 }
24]
25
26const getAccountVideoRateValidator = function (rateType: VideoRateType) {
27 return [
28 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
29 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
30
31 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
33
34 if (areValidationErrors(req, res)) return
35
36 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
37 if (!rate) {
38 return res.status(404)
39 .json({ error: 'Video rate not found' })
40 .end()
41 }
42
43 res.locals.accountVideoRate = rate
44
45 return next()
46 }
47 ]
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 videoUpdateRateValidator,
54 getAccountVideoRateValidator
55}
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
new file mode 100644
index 000000000..646d7acb1
--- /dev/null
+++ b/server/middlewares/validators/videos/video-shares.ts
@@ -0,0 +1,38 @@
1import * as express from 'express'
2import 'express-validator'
3import { param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { VideoShareModel } from '../../../models/video/video-share'
8import { areValidationErrors } from '../utils'
9import { VideoModel } from '../../../models/video/video'
10
11const videosShareValidator = [
12 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
13 param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking videoShare parameters', { parameters: req.params })
17
18 if (areValidationErrors(req, res)) return
19 if (!await isVideoExist(req.params.id, res)) return
20
21 const video: VideoModel = res.locals.video
22
23 const share = await VideoShareModel.load(req.params.actorId, video.id)
24 if (!share) {
25 return res.status(404)
26 .end()
27 }
28
29 res.locals.videoShare = share
30 return next()
31 }
32]
33
34// ---------------------------------------------------------------------------
35
36export {
37 videosShareValidator
38}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 9dc52a134..656d161d8 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -26,14 +26,12 @@ import {
26 isVideoLicenceValid, 26 isVideoLicenceValid,
27 isVideoNameValid, 27 isVideoNameValid,
28 isVideoPrivacyValid, 28 isVideoPrivacyValid,
29 isVideoRatingTypeValid,
30 isVideoSupportValid, 29 isVideoSupportValid,
31 isVideoTagsValid 30 isVideoTagsValid
32} from '../../../helpers/custom-validators/videos' 31} from '../../../helpers/custom-validators/videos'
33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34import { logger } from '../../../helpers/logger' 33import { logger } from '../../../helpers/logger'
35import { CONSTRAINTS_FIELDS } from '../../../initializers' 34import { CONSTRAINTS_FIELDS } from '../../../initializers'
36import { VideoShareModel } from '../../../models/video/video-share'
37import { authenticate } from '../../oauth' 35import { authenticate } from '../../oauth'
38import { areValidationErrors } from '../utils' 36import { areValidationErrors } from '../utils'
39import { cleanUpReqFiles } from '../../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -188,41 +186,6 @@ const videosRemoveValidator = [
188 } 186 }
189] 187]
190 188
191const videoRateValidator = [
192 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
193 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
194
195 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 logger.debug('Checking videoRate parameters', { parameters: req.body })
197
198 if (areValidationErrors(req, res)) return
199 if (!await isVideoExist(req.params.id, res)) return
200
201 return next()
202 }
203]
204
205const videosShareValidator = [
206 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
207 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
208
209 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
210 logger.debug('Checking videoShare parameters', { parameters: req.params })
211
212 if (areValidationErrors(req, res)) return
213 if (!await isVideoExist(req.params.id, res)) return
214
215 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
216 if (!share) {
217 return res.status(404)
218 .end()
219 }
220
221 res.locals.videoShare = share
222 return next()
223 }
224]
225
226const videosChangeOwnershipValidator = [ 189const videosChangeOwnershipValidator = [
227 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 190 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
228 191
@@ -415,9 +378,6 @@ export {
415 videosGetValidator, 378 videosGetValidator,
416 videosCustomGetValidator, 379 videosCustomGetValidator,
417 videosRemoveValidator, 380 videosRemoveValidator,
418 videosShareValidator,
419
420 videoRateValidator,
421 381
422 videosChangeOwnershipValidator, 382 videosChangeOwnershipValidator,
423 videosTerminateChangeOwnershipValidator, 383 videosTerminateChangeOwnershipValidator,
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index c99e32012..18762f0c5 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,12 +1,14 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' 4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
5import { VideoRateType } from '../../../shared/models/videos' 5import { VideoRateType } from '../../../shared/models/videos'
6import { VIDEO_RATE_TYPES } from '../../initializers' 6import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
7import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
8import { AccountModel } from './account' 8import { AccountModel } from './account'
9import { ActorModel } from '../activitypub/actor' 9import { ActorModel } from '../activitypub/actor'
10import { throwIfNotValid } from '../utils'
11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10 12
11/* 13/*
12 Account rates per video. 14 Account rates per video.
@@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
26 }, 28 },
27 { 29 {
28 fields: [ 'videoId', 'type' ] 30 fields: [ 'videoId', 'type' ]
31 },
32 {
33 fields: [ 'url' ],
34 unique: true
29 } 35 }
30 ] 36 ]
31}) 37})
@@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
35 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) 41 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
36 type: VideoRateType 42 type: VideoRateType
37 43
44 @AllowNull(false)
45 @Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
46 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
47 url: string
48
38 @CreatedAt 49 @CreatedAt
39 createdAt: Date 50 createdAt: Date
40 51
@@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
65 }) 76 })
66 Account: AccountModel 77 Account: AccountModel
67 78
68 static load (accountId: number, videoId: number, transaction: Transaction) { 79 static load (accountId: number, videoId: number, transaction?: Transaction) {
69 const options: IFindOptions<AccountVideoRateModel> = { 80 const options: IFindOptions<AccountVideoRateModel> = {
70 where: { 81 where: {
71 accountId, 82 accountId,
@@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
77 return AccountVideoRateModel.findOne(options) 88 return AccountVideoRateModel.findOne(options)
78 } 89 }
79 90
91 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
92 const options: IFindOptions<AccountVideoRateModel> = {
93 where: {
94 videoId,
95 type: rateType
96 },
97 include: [
98 {
99 model: AccountModel.unscoped(),
100 required: true,
101 include: [
102 {
103 attributes: [ 'id', 'url', 'preferredUsername' ],
104 model: ActorModel.unscoped(),
105 required: true,
106 where: {
107 preferredUsername: accountName
108 }
109 }
110 ]
111 },
112 {
113 model: VideoModel.unscoped(),
114 required: true
115 }
116 ]
117 }
118 if (transaction) options.transaction = transaction
119
120 return AccountVideoRateModel.findOne(options)
121 }
122
123 static loadByUrl (url: string, transaction: Transaction) {
124 const options: IFindOptions<AccountVideoRateModel> = {
125 where: {
126 url
127 }
128 }
129 if (transaction) options.transaction = transaction
130
131 return AccountVideoRateModel.findOne(options)
132 }
133
80 static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { 134 static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
81 const query = { 135 const query = {
82 offset: start, 136 offset: start,
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index ef9592c04..ecf846821 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -47,7 +47,7 @@ enum ScopeNames {
47 required: true, 47 required: true,
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id' ], 50 attributes: [ 'id', 'url' ],
51 model: () => ActorModel.unscoped(), 51 model: () => ActorModel.unscoped(),
52 required: true 52 required: true
53 } 53 }
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index cbfc7f7fa..35e0cd3b1 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -293,6 +293,11 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
293 } 293 }
294 294
295 return VideoFileModel.sum('size', options as any) // FIXME: typings 295 return VideoFileModel.sum('size', options as any) // FIXME: typings
296 .then(v => {
297 if (!v || isNaN(v)) return 0
298
299 return v
300 })
296 } 301 }
297 302
298 static async listLocalExpired () { 303 static async listLocalExpired () {
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index fa9a70d50..c87f71277 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
88 }) 88 })
89 Video: VideoModel 89 Video: VideoModel
90 90
91 static load (actorId: number, videoId: number, t: Sequelize.Transaction) { 91 static load (actorId: number, videoId: number, t?: Sequelize.Transaction) {
92 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ 92 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
93 where: { 93 where: {
94 actorId, 94 actorId,
diff --git a/server/tests/activitypub.ts b/server/tests/api/activitypub/client.ts
index 0905c5dec..334cd4e5c 100644
--- a/server/tests/activitypub.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -11,6 +11,7 @@ import {
11 setAccessTokensToServers 11 setAccessTokensToServers
12} from '../../shared/utils' 12} from '../../shared/utils'
13 13
14
14const expect = chai.expect 15const expect = chai.expect
15 16
16describe('Test activitypub', function () { 17describe('Test activitypub', function () {
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
new file mode 100644
index 000000000..a42c606c6
--- /dev/null
+++ b/server/tests/api/activitypub/fetch.ts
@@ -0,0 +1,86 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 getVideosListSort,
11 killallServers,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo,
15 userLogin
16} from '../../utils'
17import * as chai from 'chai'
18import { setActorField, setVideoField } from '../../utils/miscs/sql'
19import { waitJobs } from '../../utils/server/jobs'
20import { Video } from '../../../../shared/models/videos'
21
22const expect = chai.expect
23
24describe('Test ActivityPub fetcher', function () {
25 let servers: ServerInfo[]
26
27 // ---------------------------------------------------------------
28
29 before(async function () {
30 this.timeout(60000)
31
32 servers = await flushAndRunMultipleServers(3)
33
34 // Get the access tokens
35 await setAccessTokensToServers(servers)
36
37 const user = { username: 'user1', password: 'password' }
38 for (const server of servers) {
39 await createUser(server.url, server.accessToken, user.username, user.password)
40 }
41
42 const userAccessToken = await userLogin(servers[0], user)
43
44 await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video root' })
45 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'bad video root' })
46 const badVideoUUID = res.body.video.uuid
47 await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' })
48
49 await setActorField(1, 'http://localhost:9001/accounts/user1', 'url', 'http://localhost:9002/accounts/user1')
50 await setVideoField(1, badVideoUUID, 'url', 'http://localhost:9003/videos/watch/' + badVideoUUID)
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 res = await getVideosListSort(servers[0].url, 'createdAt')
61 expect(res.body.total).to.equal(3)
62
63 const data: Video[] = res.body.data
64 expect(data[0].name).to.equal('video root')
65 expect(data[1].name).to.equal('bad video root')
66 expect(data[2].name).to.equal('video user')
67 }
68
69 {
70 const res = await getVideosListSort(servers[1].url, 'createdAt')
71 expect(res.body.total).to.equal(1)
72
73 const data: Video[] = res.body.data
74 expect(data[0].name).to.equal('video root')
75 }
76 })
77
78 after(async function () {
79 killallServers(servers)
80
81 // Keep the logs if the test failed
82 if (this['ok']) {
83 await flushTests()
84 }
85 })
86})
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
new file mode 100644
index 000000000..610846247
--- /dev/null
+++ b/server/tests/api/activitypub/helpers.ts
@@ -0,0 +1,182 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import { expect } from 'chai'
5import { buildRequestStub } from '../../utils'
6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
7import { cloneDeep } from 'lodash'
8import { buildSignedActivity } from '../../../helpers/activitypub'
9
10describe('Test activity pub helpers', function () {
11 describe('When checking the Linked Signature', function () {
12
13 it('Should fail with an invalid Mastodon signature', async function () {
14 const body = require('./json/mastodon/create-bad-signature.json')
15 const publicKey = require('./json/mastodon/public-key.json').publicKey
16 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
17
18 const result = await isJsonLDSignatureVerified(fromActor as any, body)
19
20 expect(result).to.be.false
21 })
22
23 it('Should fail with an invalid public key', async function () {
24 const body = require('./json/mastodon/create.json')
25 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
26 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
27
28 const result = await isJsonLDSignatureVerified(fromActor as any, body)
29
30 expect(result).to.be.false
31 })
32
33 it('Should succeed with a valid Mastodon signature', async function () {
34 const body = require('./json/mastodon/create.json')
35 const publicKey = require('./json/mastodon/public-key.json').publicKey
36 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
37
38 const result = await isJsonLDSignatureVerified(fromActor as any, body)
39
40 expect(result).to.be.true
41 })
42
43 it('Should fail with an invalid PeerTube signature', async function () {
44 const keys = require('./json/peertube/invalid-keys.json')
45 const body = require('./json/peertube/announce-without-context.json')
46
47 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
48 const signedBody = await buildSignedActivity(actorSignature as any, body)
49
50 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
51 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
52
53 expect(result).to.be.false
54 })
55
56 it('Should fail with an invalid PeerTube URL', async function () {
57 const keys = require('./json/peertube/keys.json')
58 const body = require('./json/peertube/announce-without-context.json')
59
60 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
61 const signedBody = await buildSignedActivity(actorSignature as any, body)
62
63 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
64 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
65
66 expect(result).to.be.false
67 })
68
69 it('Should succeed with a valid PeerTube signature', async function () {
70 const keys = require('./json/peertube/keys.json')
71 const body = require('./json/peertube/announce-without-context.json')
72
73 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
74 const signedBody = await buildSignedActivity(actorSignature as any, body)
75
76 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
77 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
78
79 expect(result).to.be.true
80 })
81 })
82
83 describe('When checking HTTP signature', function () {
84 it('Should fail with an invalid http signature', async function () {
85 const req = buildRequestStub()
86 req.method = 'POST'
87 req.url = '/accounts/ronan/inbox'
88
89 const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json'))
90 req.body = mastodonObject.body
91 req.headers = mastodonObject.headers
92 req.headers.signature = 'Signature ' + req.headers.signature
93
94 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
95 const publicKey = require('./json/mastodon/public-key.json').publicKey
96
97 const actor = { publicKey }
98 const verified = isHTTPSignatureVerified(parsed, actor as any)
99
100 expect(verified).to.be.false
101 })
102
103 it('Should fail with an invalid public key', async function () {
104 const req = buildRequestStub()
105 req.method = 'POST'
106 req.url = '/accounts/ronan/inbox'
107
108 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
109 req.body = mastodonObject.body
110 req.headers = mastodonObject.headers
111 req.headers.signature = 'Signature ' + req.headers.signature
112
113 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
114 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
115
116 const actor = { publicKey }
117 const verified = isHTTPSignatureVerified(parsed, actor as any)
118
119 expect(verified).to.be.false
120 })
121
122 it('Should fail because of clock skew', async function () {
123 const req = buildRequestStub()
124 req.method = 'POST'
125 req.url = '/accounts/ronan/inbox'
126
127 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
128 req.body = mastodonObject.body
129 req.headers = mastodonObject.headers
130 req.headers.signature = 'Signature ' + req.headers.signature
131
132 let errored = false
133 try {
134 parseHTTPSignature(req)
135 } catch {
136 errored = true
137 }
138
139 expect(errored).to.be.true
140 })
141
142 it('Should fail without scheme', async function () {
143 const req = buildRequestStub()
144 req.method = 'POST'
145 req.url = '/accounts/ronan/inbox'
146
147 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
148 req.body = mastodonObject.body
149 req.headers = mastodonObject.headers
150
151 let errored = false
152 try {
153 parseHTTPSignature(req, 3600 * 365 * 3)
154 } catch {
155 errored = true
156 }
157
158 expect(errored).to.be.true
159 })
160
161 it('Should succeed with a valid signature', async function () {
162 const req = buildRequestStub()
163 req.method = 'POST'
164 req.url = '/accounts/ronan/inbox'
165
166 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
167 req.body = mastodonObject.body
168 req.headers = mastodonObject.headers
169 req.headers.signature = 'Signature ' + req.headers.signature
170
171 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
172 const publicKey = require('./json/mastodon/public-key.json').publicKey
173
174 const actor = { publicKey }
175 const verified = isHTTPSignatureVerified(parsed, actor as any)
176
177 expect(verified).to.be.true
178 })
179
180 })
181
182})
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
new file mode 100644
index 000000000..e748f32e9
--- /dev/null
+++ b/server/tests/api/activitypub/index.ts
@@ -0,0 +1,4 @@
1import './client'
2import './fetch'
3import './helpers'
4import './security'
diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json
new file mode 100644
index 000000000..4e7bc3af5
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json
new file mode 100644
index 000000000..098597db0
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/api/activitypub/json/mastodon/bad-public-key.json
new file mode 100644
index 000000000..73d18b3ad
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/bad-public-key.json
@@ -0,0 +1,3 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
3}
diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json
new file mode 100644
index 000000000..2cd037241
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json
@@ -0,0 +1,81 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/api/activitypub/json/mastodon/create.json
new file mode 100644
index 000000000..0be271bb8
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/create.json
@@ -0,0 +1,81 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/api/activitypub/json/mastodon/http-signature.json
new file mode 100644
index 000000000..4e7bc3af5
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/http-signature.json
@@ -0,0 +1,93 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/api/activitypub/json/mastodon/public-key.json
new file mode 100644
index 000000000..b7b9b8308
--- /dev/null
+++ b/server/tests/api/activitypub/json/mastodon/public-key.json
@@ -0,0 +1,3 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
3}
diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/api/activitypub/json/peertube/announce-without-context.json
new file mode 100644
index 000000000..5f2af0cde
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/announce-without-context.json
@@ -0,0 +1,13 @@
1{
2 "type": "Announce",
3 "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1",
4 "actor": "http://localhost:9002/accounts/peertube",
5 "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05",
6 "to": [
7 "https://www.w3.org/ns/activitystreams#Public",
8 "http://localhost:9002/accounts/peertube/followers",
9 "http://localhost:9002/video-channels/root_channel/followers",
10 "http://localhost:9002/accounts/root/followers"
11 ],
12 "cc": []
13}
diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/api/activitypub/json/peertube/invalid-keys.json
new file mode 100644
index 000000000..0544e96b9
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/invalid-keys.json
@@ -0,0 +1,6 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
4}
5
6
diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/api/activitypub/json/peertube/keys.json
new file mode 100644
index 000000000..1a7700865
--- /dev/null
+++ b/server/tests/api/activitypub/json/peertube/keys.json
@@ -0,0 +1,4 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
4}
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
new file mode 100644
index 000000000..e7899bb14
--- /dev/null
+++ b/server/tests/api/activitypub/security.ts
@@ -0,0 +1,180 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
6import { HTTP_SIGNATURE } from '../../../initializers'
7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
8import * as chai from 'chai'
9import { setActorField } from '../../utils/miscs/sql'
10import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
11
12const expect = chai.expect
13
14function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) {
15 return Promise.all([
16 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey),
17 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey)
18 ])
19}
20
21function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) {
22 return Promise.all([
23 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey),
24 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey)
25 ])
26}
27
28describe('Test ActivityPub security', function () {
29 let servers: ServerInfo[]
30 let url: string
31
32 const keys = require('./json/peertube/keys.json')
33 const invalidKeys = require('./json/peertube/invalid-keys.json')
34 const baseHttpSignature = {
35 algorithm: HTTP_SIGNATURE.ALGORITHM,
36 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
37 keyId: 'acct:peertube@localhost:9002',
38 key: keys.privateKey,
39 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
40 }
41
42 // ---------------------------------------------------------------
43
44 before(async function () {
45 this.timeout(60000)
46
47 servers = await flushAndRunMultipleServers(3)
48
49 url = servers[0].url + '/inbox'
50
51 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
52
53 const to = { url: 'http://localhost:9001/accounts/peertube' }
54 const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
55 await makeFollowRequest(to, by)
56 })
57
58 describe('When checking HTTP signature', function () {
59
60 it('Should fail with an invalid digest', async function () {
61 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
62 const headers = {
63 Digest: buildDigest({ hello: 'coucou' })
64 }
65
66 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
67
68 expect(response.statusCode).to.equal(403)
69 })
70
71 it('Should fail with an invalid date', async function () {
72 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
73 const headers = buildGlobalHeaders(body)
74 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
75
76 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
77
78 expect(response.statusCode).to.equal(403)
79 })
80
81 it('Should fail with bad keys', async function () {
82 await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey)
83 await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey)
84
85 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
86 const headers = buildGlobalHeaders(body)
87
88 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
89
90 expect(response.statusCode).to.equal(403)
91 })
92
93 it('Should succeed with a valid HTTP signature', async function () {
94 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
95 await setKeysOfServer2(2, keys.publicKey, keys.privateKey)
96
97 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
98 const headers = buildGlobalHeaders(body)
99
100 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
101
102 expect(response.statusCode).to.equal(204)
103 })
104 })
105
106 describe('When checking Linked Data Signature', function () {
107 before(async () => {
108 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
109
110 const to = { url: 'http://localhost:9001/accounts/peertube' }
111 const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey }
112 await makeFollowRequest(to, by)
113 })
114
115 it('Should fail with bad keys', async function () {
116 this.timeout(10000)
117
118 await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey)
119 await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey)
120
121 const body = require('./json/peertube/announce-without-context.json')
122 body.actor = 'http://localhost:9003/accounts/peertube'
123
124 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
125 const signedBody = await buildSignedActivity(signer, body)
126
127 const headers = buildGlobalHeaders(signedBody)
128
129 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
130
131 expect(response.statusCode).to.equal(403)
132 })
133
134 it('Should fail with an altered body', async function () {
135 this.timeout(10000)
136
137 await setKeysOfServer3(1, keys.publicKey, keys.privateKey)
138 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
139
140 const body = require('./json/peertube/announce-without-context.json')
141 body.actor = 'http://localhost:9003/accounts/peertube'
142
143 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
144 const signedBody = await buildSignedActivity(signer, body)
145
146 signedBody.actor = 'http://localhost:9003/account/peertube'
147
148 const headers = buildGlobalHeaders(signedBody)
149
150 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
151
152 expect(response.statusCode).to.equal(403)
153 })
154
155 it('Should succeed with a valid signature', async function () {
156 this.timeout(10000)
157
158 const body = require('./json/peertube/announce-without-context.json')
159 body.actor = 'http://localhost:9003/accounts/peertube'
160
161 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
162 const signedBody = await buildSignedActivity(signer, body)
163
164 const headers = buildGlobalHeaders(signedBody)
165
166 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
167
168 expect(response.statusCode).to.equal(204)
169 })
170 })
171
172 after(async function () {
173 killallServers(servers)
174
175 // Keep the logs if the test failed
176 if (this['ok']) {
177 await flushTests()
178 }
179 })
180})
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts
index 8e69b95a6..7d8be2b3d 100644
--- a/server/tests/api/index-4.ts
+++ b/server/tests/api/index-4.ts
@@ -1 +1,2 @@
1import './redundancy' 1import './redundancy'
2import './activitypub'
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 0af52023c..663e31ead 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -54,7 +54,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams:
54 immutableAssign({ 54 immutableAssign({
55 min_lifetime: '1 hour', 55 min_lifetime: '1 hour',
56 strategy: strategy, 56 strategy: strategy,
57 size: '100KB' 57 size: '200KB'
58 }, additionalParams) 58 }, additionalParams)
59 ] 59 ]
60 } 60 }
@@ -111,8 +111,8 @@ async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
111 const stat = data.videosRedundancy[0] 111 const stat = data.videosRedundancy[0]
112 112
113 expect(stat.strategy).to.equal(strategy) 113 expect(stat.strategy).to.equal(strategy)
114 expect(stat.totalSize).to.equal(102400) 114 expect(stat.totalSize).to.equal(204800)
115 expect(stat.totalUsed).to.be.at.least(1).and.below(102401) 115 expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
116 expect(stat.totalVideoFiles).to.equal(4) 116 expect(stat.totalVideoFiles).to.equal(4)
117 expect(stat.totalVideos).to.equal(1) 117 expect(stat.totalVideos).to.equal(1)
118} 118}
@@ -125,7 +125,7 @@ async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
125 125
126 const stat = data.videosRedundancy[0] 126 const stat = data.videosRedundancy[0]
127 expect(stat.strategy).to.equal(strategy) 127 expect(stat.strategy).to.equal(strategy)
128 expect(stat.totalSize).to.equal(102400) 128 expect(stat.totalSize).to.equal(204800)
129 expect(stat.totalUsed).to.equal(0) 129 expect(stat.totalUsed).to.equal(0)
130 expect(stat.totalVideoFiles).to.equal(0) 130 expect(stat.totalVideoFiles).to.equal(0)
131 expect(stat.totalVideos).to.equal(0) 131 expect(stat.totalVideos).to.equal(0)
@@ -223,7 +223,7 @@ describe('Test videos redundancy', function () {
223 return enableRedundancyOnServer1() 223 return enableRedundancyOnServer1()
224 }) 224 })
225 225
226 it('Should have 2 webseed on the first video', async function () { 226 it('Should have 2 webseeds on the first video', async function () {
227 this.timeout(40000) 227 this.timeout(40000)
228 228
229 await waitJobs(servers) 229 await waitJobs(servers)
@@ -270,7 +270,7 @@ describe('Test videos redundancy', function () {
270 return enableRedundancyOnServer1() 270 return enableRedundancyOnServer1()
271 }) 271 })
272 272
273 it('Should have 2 webseed on the first video', async function () { 273 it('Should have 2 webseeds on the first video', async function () {
274 this.timeout(40000) 274 this.timeout(40000)
275 275
276 await waitJobs(servers) 276 await waitJobs(servers)
@@ -338,7 +338,7 @@ describe('Test videos redundancy', function () {
338 await waitJobs(servers) 338 await waitJobs(servers)
339 }) 339 })
340 340
341 it('Should have 2 webseed on the first video', async function () { 341 it('Should have 2 webseeds on the first video', async function () {
342 this.timeout(40000) 342 this.timeout(40000)
343 343
344 await waitJobs(servers) 344 await waitJobs(servers)
@@ -419,7 +419,7 @@ describe('Test videos redundancy', function () {
419 419
420 killallServers([ servers[0] ]) 420 killallServers([ servers[0] ])
421 421
422 await wait(10000) 422 await wait(15000)
423 423
424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') 424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425 }) 425 })
@@ -451,27 +451,23 @@ describe('Test videos redundancy', function () {
451 video2Server2UUID = res.body.video.uuid 451 video2Server2UUID = res.body.video.uuid
452 }) 452 })
453 453
454 it('Should cache video 2 webseed on the first video', async function () { 454 it('Should cache video 2 webseeds on the first video', async function () {
455 this.timeout(50000) 455 this.timeout(120000)
456 456
457 await waitJobs(servers) 457 await waitJobs(servers)
458 458
459 await wait(7000) 459 let checked = false
460 460
461 try { 461 while (checked === false) {
462 await check1WebSeed(strategy, video1Server2UUID) 462 await wait(1000)
463 await check2Webseeds(strategy, video2Server2UUID)
464 } catch {
465 await wait(3000)
466 463
467 try { 464 try {
468 await check1WebSeed(strategy, video1Server2UUID) 465 await check1WebSeed(strategy, video1Server2UUID)
469 await check2Webseeds(strategy, video2Server2UUID) 466 await check2Webseeds(strategy, video2Server2UUID)
470 } catch {
471 await wait(5000)
472 467
473 await check1WebSeed(strategy, video1Server2UUID) 468 checked = true
474 await check2Webseeds(strategy, video2Server2UUID) 469 } catch {
470 checked = false
475 } 471 }
476 } 472 }
477 }) 473 })
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index e6e0d6c7a..8e162b69e 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -5,6 +5,7 @@ import 'mocha'
5import { JobState, Video } from '../../../../shared/models' 5import { JobState, Video } from '../../../../shared/models'
6import { VideoPrivacy } from '../../../../shared/models/videos' 6import { VideoPrivacy } from '../../../../shared/models/videos'
7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
8
8import { 9import {
9 completeVideoCheck, 10 completeVideoCheck,
10 getVideo, 11 getVideo,
@@ -18,6 +19,7 @@ import {
18 ServerInfo, 19 ServerInfo,
19 setAccessTokensToServers, 20 setAccessTokensToServers,
20 uploadVideo, 21 uploadVideo,
22 updateVideo,
21 wait 23 wait
22} from '../../../../shared/utils' 24} from '../../../../shared/utils'
23import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows' 25import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
@@ -199,15 +201,15 @@ describe('Test handle downs', function () {
199 expect(res.body.data).to.have.lengthOf(2) 201 expect(res.body.data).to.have.lengthOf(2)
200 }) 202 })
201 203
202 it('Should send a view to server 3, and automatically fetch the video', async function () { 204 it('Should send an update to server 3, and automatically fetch the video', async function () {
203 this.timeout(15000) 205 this.timeout(15000)
204 206
205 const res1 = await getVideosList(servers[2].url) 207 const res1 = await getVideosList(servers[2].url)
206 expect(res1.body.data).to.be.an('array') 208 expect(res1.body.data).to.be.an('array')
207 expect(res1.body.data).to.have.lengthOf(11) 209 expect(res1.body.data).to.have.lengthOf(11)
208 210
209 await viewVideo(servers[0].url, missedVideo1.uuid) 211 await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { })
210 await viewVideo(servers[0].url, unlistedVideo.uuid) 212 await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { })
211 213
212 await waitJobs(servers) 214 await waitJobs(servers)
213 215
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index eeb8b7a28..78ab7e18b 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -6,3 +6,4 @@ import './jobs'
6import './reverse-proxy' 6import './reverse-proxy'
7import './stats' 7import './stats'
8import './tracker' 8import './tracker'
9import './no-client'
diff --git a/server/tests/api/server/no-client.ts b/server/tests/api/server/no-client.ts
new file mode 100644
index 000000000..6d6ce8532
--- /dev/null
+++ b/server/tests/api/server/no-client.ts
@@ -0,0 +1,36 @@
1import 'mocha'
2import * as request from 'supertest'
3import {
4 flushTests,
5 killallServers,
6 ServerInfo
7} from '../../utils/index'
8import { runServer } from '../../utils/server/servers'
9
10describe('Start and stop server without web client routes', function () {
11 let server: ServerInfo
12
13 before(async function () {
14 this.timeout(30000)
15
16 await flushTests()
17
18 server = await runServer(1, {}, ['--no-client'])
19 })
20
21 it('Should fail getting the client', function () {
22 const req = request(server.url)
23 .get('/')
24
25 return req.expect(404)
26 })
27
28 after(async function () {
29 killallServers([ server ])
30
31 // Keep the logs if the test failed
32 if (this['ok']) {
33 await flushTests()
34 }
35 })
36})
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index e5038838e..e2836d0c3 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -44,6 +44,8 @@ describe('Test CLI wrapper', function () {
44 }) 44 })
45 45
46 after(async function () { 46 after(async function () {
47 this.timeout(10000)
48
47 await execCLI(cmd + ` auth del ${server.url}`) 49 await execCLI(cmd + ` auth del ${server.url}`)
48 50
49 killallServers([ server ]) 51 killallServers([ server ])
diff --git a/server/tests/index.ts b/server/tests/index.ts
index e659fd3df..ed16d65dd 100644
--- a/server/tests/index.ts
+++ b/server/tests/index.ts
@@ -1,6 +1,5 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './client' 2import './client'
3import './activitypub'
4import './feeds/' 3import './feeds/'
5import './cli/' 4import './cli/'
6import './api/' 5import './api/'
diff --git a/server/tests/utils/miscs/sql.ts b/server/tests/utils/miscs/sql.ts
new file mode 100644
index 000000000..027f78131
--- /dev/null
+++ b/server/tests/utils/miscs/sql.ts
@@ -0,0 +1,38 @@
1import * as Sequelize from 'sequelize'
2
3function getSequelize (serverNumber: number) {
4 const dbname = 'peertube_test' + serverNumber
5 const username = 'peertube'
6 const password = 'peertube'
7 const host = 'localhost'
8 const port = 5432
9
10 return new Sequelize(dbname, username, password, {
11 dialect: 'postgres',
12 host,
13 port,
14 operatorsAliases: false,
15 logging: false
16 })
17}
18
19function setActorField (serverNumber: number, to: string, field: string, value: string) {
20 const seq = getSequelize(serverNumber)
21
22 const options = { type: Sequelize.QueryTypes.UPDATE }
23
24 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
25}
26
27function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
28 const seq = getSequelize(serverNumber)
29
30 const options = { type: Sequelize.QueryTypes.UPDATE }
31
32 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
33}
34
35export {
36 setVideoField,
37 setActorField
38}
diff --git a/server/tests/utils/miscs/stubs.ts b/server/tests/utils/miscs/stubs.ts
new file mode 100644
index 000000000..d1eb0e3b2
--- /dev/null
+++ b/server/tests/utils/miscs/stubs.ts
@@ -0,0 +1,14 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/server/tests/utils/requests/activitypub.ts b/server/tests/utils/requests/activitypub.ts
new file mode 100644
index 000000000..96fee60a8
--- /dev/null
+++ b/server/tests/utils/requests/activitypub.ts
@@ -0,0 +1,43 @@
1import { doRequest } from '../../../helpers/requests'
2import { HTTP_SIGNATURE } from '../../../initializers'
3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../helpers/activitypub'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makePOSTAPRequest,
42 makeFollowRequest
43}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
index 33438811e..a962944a4 100644
--- a/server/tools/peertube-auth.ts
+++ b/server/tools/peertube-auth.ts
@@ -5,34 +5,25 @@ import { getSettings, writeSettings, netrc } from './cli'
5import { isHostValid } from '../helpers/custom-validators/servers' 5import { isHostValid } from '../helpers/custom-validators/servers'
6import { isUserUsernameValid } from '../helpers/custom-validators/users' 6import { isUserUsernameValid } from '../helpers/custom-validators/users'
7 7
8function delInstance (url: string) { 8async function delInstance (url: string) {
9 return new Promise((res, rej): void => { 9 const settings = await getSettings()
10 getSettings() 10
11 .then(async (settings) => { 11 settings.remotes.splice(settings.remotes.indexOf(url))
12 settings.remotes.splice(settings.remotes.indexOf(url)) 12 await writeSettings(settings)
13 await writeSettings(settings) 13
14 delete netrc.machines[url] 14 delete netrc.machines[url]
15 netrc.save() 15 await netrc.save()
16 res()
17 })
18 .catch(err => rej(err))
19 })
20} 16}
21 17
22async function setInstance (url: string, username: string, password: string) { 18async function setInstance (url: string, username: string, password: string) {
23 return new Promise((res, rej): void => { 19 const settings = await getSettings()
24 getSettings() 20 if (settings.remotes.indexOf(url) === -1) {
25 .then(async settings => { 21 settings.remotes.push(url)
26 if (settings.remotes.indexOf(url) === -1) { 22 }
27 settings.remotes.push(url) 23 await writeSettings(settings)
28 } 24
29 await writeSettings(settings) 25 netrc.machines[url] = { login: username, password }
30 netrc.machines[url] = { login: username, password } 26 await netrc.save()
31 netrc.save()
32 res()
33 })
34 .catch(err => rej(err))
35 })
36} 27}
37 28
38function isURLaPeerTubeInstance (url: string) { 29function isURLaPeerTubeInstance (url: string) {
@@ -71,56 +62,60 @@ program
71 required: true 62 required: true
72 } 63 }
73 } 64 }
74 }, (_, result) => { 65 }, async (_, result) => {
75 setInstance(result.url, result.username, result.password) 66 await setInstance(result.url, result.username, result.password)
67
68 process.exit(0)
76 }) 69 })
77 }) 70 })
78 71
79program 72program
80 .command('del <url>') 73 .command('del <url>')
81 .description('unregisters a remote instance') 74 .description('unregisters a remote instance')
82 .action((url) => { 75 .action(async url => {
83 delInstance(url) 76 await delInstance(url)
77
78 process.exit(0)
84 }) 79 })
85 80
86program 81program
87 .command('list') 82 .command('list')
88 .description('lists registered remote instances') 83 .description('lists registered remote instances')
89 .action(() => { 84 .action(async () => {
90 getSettings() 85 const settings = await getSettings()
91 .then(settings => { 86 const table = new Table({
92 const table = new Table({ 87 head: ['instance', 'login'],
93 head: ['instance', 'login'], 88 colWidths: [30, 30]
94 colWidths: [30, 30] 89 })
95 }) 90 netrc.loadSync()
96 netrc.loadSync() 91 settings.remotes.forEach(element => {
97 settings.remotes.forEach(element => { 92 table.push([
98 table.push([ 93 element,
99 element, 94 netrc.machines[element].login
100 netrc.machines[element].login 95 ])
101 ]) 96 })
102 }) 97
103 98 console.log(table.toString())
104 console.log(table.toString()) 99
105 }) 100 process.exit(0)
106 }) 101 })
107 102
108program 103program
109 .command('set-default <url>') 104 .command('set-default <url>')
110 .description('set an existing entry as default') 105 .description('set an existing entry as default')
111 .action((url) => { 106 .action(async url => {
112 getSettings() 107 const settings = await getSettings()
113 .then(settings => { 108 const instanceExists = settings.remotes.indexOf(url) !== -1
114 const instanceExists = settings.remotes.indexOf(url) !== -1 109
115 110 if (instanceExists) {
116 if (instanceExists) { 111 settings.default = settings.remotes.indexOf(url)
117 settings.default = settings.remotes.indexOf(url) 112 await writeSettings(settings)
118 writeSettings(settings) 113
119 } else { 114 process.exit(0)
120 console.log('<url> is not a registered instance.') 115 } else {
121 process.exit(-1) 116 console.log('<url> is not a registered instance.')
122 } 117 process.exit(-1)
123 }) 118 }
124 }) 119 })
125 120
126program.on('--help', function () { 121program.on('--help', function () {
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
index c8b9fa744..5d3ab2815 100755
--- a/server/tools/peertube.ts
+++ b/server/tools/peertube.ts
@@ -58,7 +58,7 @@ if (!process.argv.slice(2).length) {
58 ,"\\/ 58 ,"\\/
59 _,.__/"\\/_ (the CLI for red chocobos) 59 _,.__/"\\/_ (the CLI for red chocobos)
60 / \\) "./, ". 60 / \\) "./, ".
61 --/---"---" "-) )---- by Chocobozzz et al.`) 61 --/---"---" "-) )---- by Chocobozzz et al.\n`)
62} 62}
63 63
64getSettings() 64getSettings()