Add an option to sign federated fetches for mastodon compatibility (#5898)

* Fix player error modal

Not hidden when we change the video

* Correctly dispose player components

* Sign cross-server fetch requests for mastodon AUTHORIZED_FETCH compatibilty

* Add a remote fetch sign configuration knob

* Federated fetches refactoring

---------

Co-authored-by: Chocobozzz <me@florianbigard.com>
Co-authored-by: ira <ira@foxgirl.space>
This commit is contained in:
mira.bat 2023-07-27 17:01:15 +02:00 committed by GitHub
parent 787d822cd4
commit f862be2749
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 110 additions and 76 deletions

View File

@ -374,6 +374,9 @@ plugins:
url: 'https://packages.joinpeertube.org' url: 'https://packages.joinpeertube.org'
federation: federation:
# Some federated software such as Mastodon may require an HTTP signature to access content
sign_federated_fetches: true
videos: videos:
federate_unlisted: false federate_unlisted: false

View File

@ -372,6 +372,9 @@ plugins:
url: 'https://packages.joinpeertube.org' url: 'https://packages.joinpeertube.org'
federation: federation:
# Some federated software such as Mastodon may require an HTTP signature to access content
sign_federated_fetches: true
videos: videos:
federate_unlisted: false federate_unlisted: false

View File

@ -1,9 +1,10 @@
import express from 'express' import express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils' import { sanitizeUrl } from '@server/helpers/core-utils'
import { pickSearchChannelQuery } from '@server/helpers/query' import { pickSearchChannelQuery } from '@server/helpers/query'
import { doJSONRequest, findLatestRedirection } from '@server/helpers/requests' import { doJSONRequest } from '@server/helpers/requests'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants' import { WEBSERVER } from '@server/initializers/constants'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
@ -126,7 +127,7 @@ async function searchVideoChannelURI (search: string, res: express.Response) {
if (isUserAbleToSearchRemoteURI(res)) { if (isUserAbleToSearchRemoteURI(res)) {
try { try {
const latestUri = await findLatestRedirection(uri, { activityPub: true }) const latestUri = await findLatestAPRedirection(uri)
const actor = await getOrCreateAPActor(latestUri, 'all', true, true) const actor = await getOrCreateAPActor(latestUri, 'all', true, true)
videoChannel = actor.VideoChannel videoChannel = actor.VideoChannel

View File

@ -3,10 +3,11 @@ import { sanitizeUrl } from '@server/helpers/core-utils'
import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils' import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { pickSearchPlaylistQuery } from '@server/helpers/query' import { pickSearchPlaylistQuery } from '@server/helpers/query'
import { doJSONRequest, findLatestRedirection } from '@server/helpers/requests' import { doJSONRequest } from '@server/helpers/requests'
import { getFormattedObjects } from '@server/helpers/utils' import { getFormattedObjects } from '@server/helpers/utils'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants' import { WEBSERVER } from '@server/initializers/constants'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity'
import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get' import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
@ -105,7 +106,7 @@ async function searchVideoPlaylistsURI (search: string, res: express.Response) {
if (isUserAbleToSearchRemoteURI(res)) { if (isUserAbleToSearchRemoteURI(res)) {
try { try {
const url = await findLatestRedirection(search, { activityPub: true }) const url = await findLatestAPRedirection(search)
videoPlaylist = await getOrCreateAPVideoPlaylist(url) videoPlaylist = await getOrCreateAPVideoPlaylist(url)
} catch (err) { } catch (err) {

View File

@ -1,9 +1,10 @@
import express from 'express' import express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils' import { sanitizeUrl } from '@server/helpers/core-utils'
import { pickSearchVideoQuery } from '@server/helpers/query' import { pickSearchVideoQuery } from '@server/helpers/query'
import { doJSONRequest, findLatestRedirection } from '@server/helpers/requests' import { doJSONRequest } from '@server/helpers/requests'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants' import { WEBSERVER } from '@server/initializers/constants'
import { findLatestAPRedirection } from '@server/lib/activitypub/activity'
import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
@ -141,7 +142,7 @@ async function searchVideoURI (url: string, res: express.Response) {
} }
const result = await getOrCreateAPVideo({ const result = await getOrCreateAPVideo({
videoObject: await findLatestRedirection(url, { activityPub: true }), videoObject: await findLatestAPRedirection(url),
syncParam syncParam
}) })
video = result ? result.video : undefined video = result ? result.video : undefined

View File

@ -21,6 +21,7 @@ type PeerTubeRequestOptions = {
timeout?: number timeout?: number
activityPub?: boolean activityPub?: boolean
bodyKBLimit?: number // 1MB bodyKBLimit?: number // 1MB
httpSignature?: { httpSignature?: {
algorithm: string algorithm: string
authorizationHeaderName: string authorizationHeaderName: string
@ -28,7 +29,10 @@ type PeerTubeRequestOptions = {
key: string key: string
headers: string[] headers: string[]
} }
jsonResponse?: boolean jsonResponse?: boolean
followRedirect?: boolean
} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
const peertubeGot = got.extend({ const peertubeGot = got.extend({
@ -180,16 +184,6 @@ function isBinaryResponse (result: Response<any>) {
return BINARY_CONTENT_TYPES.has(result.headers['content-type']) return BINARY_CONTENT_TYPES.has(result.headers['content-type'])
} }
async function findLatestRedirection (url: string, options: PeerTubeRequestOptions, iteration = 1) {
if (iteration > 10) throw new Error('Too much iterations to find final URL ' + url)
const { headers } = await peertubeGot(url, { followRedirect: false, ...buildGotOptions(options) })
if (headers.location) return findLatestRedirection(headers.location, options, iteration + 1)
return url
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -200,7 +194,6 @@ export {
doRequestAndSaveToFile, doRequestAndSaveToFile,
isBinaryResponse, isBinaryResponse,
getAgent, getAgent,
findLatestRedirection,
peertubeGot peertubeGot
} }
@ -227,6 +220,7 @@ function buildGotOptions (options: PeerTubeRequestOptions) {
timeout: options.timeout ?? REQUEST_TIMEOUTS.DEFAULT, timeout: options.timeout ?? REQUEST_TIMEOUTS.DEFAULT,
json: options.json, json: options.json,
searchParams: options.searchParams, searchParams: options.searchParams,
followRedirect: options.followRedirect,
retry: 2, retry: 2,
headers, headers,
context context

View File

@ -309,7 +309,8 @@ const CONFIG = {
VIDEOS: { VIDEOS: {
FEDERATE_UNLISTED: config.get<boolean>('federation.videos.federate_unlisted'), FEDERATE_UNLISTED: config.get<boolean>('federation.videos.federate_unlisted'),
CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
} },
SIGN_FEDERATED_FETCHES: config.get<boolean>('federation.sign_federated_fetches')
}, },
PEERTUBE: { PEERTUBE: {
CHECK_LATEST_VERSION: { CHECK_LATEST_VERSION: {

View File

@ -712,7 +712,8 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
const HTTP_SIGNATURE = { const HTTP_SIGNATURE = {
HEADER_NAME: 'signature', HEADER_NAME: 'signature',
ALGORITHM: 'rsa-sha256', ALGORITHM: 'rsa-sha256',
HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ], HEADERS_TO_SIGN_WITH_PAYLOAD: [ '(request-target)', 'host', 'date', 'digest' ],
HEADERS_TO_SIGN_WITHOUT_PAYLOAD: [ '(request-target)', 'host', 'date' ],
CLOCK_SKEW_SECONDS: 1800 CLOCK_SKEW_SECONDS: 1800
} }

View File

@ -1,22 +1,26 @@
import { doJSONRequest } from '@server/helpers/requests' import { doJSONRequest, PeerTubeRequestOptions } from '@server/helpers/requests'
import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models' import { CONFIG } from '@server/initializers/config'
import { ActivityObject, ActivityPubActor, ActivityType, APObjectId } from '@shared/models'
import { buildSignedRequestOptions } from './send'
function getAPId (object: string | { id: string }) { export function getAPId (object: string | { id: string }) {
if (typeof object === 'string') return object if (typeof object === 'string') return object
return object.id return object.id
} }
function getActivityStreamDuration (duration: number) { export function getActivityStreamDuration (duration: number) {
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
return 'PT' + duration + 'S' return 'PT' + duration + 'S'
} }
function getDurationFromActivityStream (duration: string) { export function getDurationFromActivityStream (duration: string) {
return parseInt(duration.replace(/[^\d]+/, '')) return parseInt(duration.replace(/[^\d]+/, ''))
} }
function buildAvailableActivities (): ActivityType[] { // ---------------------------------------------------------------------------
export function buildAvailableActivities (): ActivityType[] {
return [ return [
'Create', 'Create',
'Update', 'Update',
@ -33,9 +37,25 @@ function buildAvailableActivities (): ActivityType[] {
] ]
} }
async function fetchAPObject <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) { // ---------------------------------------------------------------------------
export async function fetchAP <T> (url: string, moreOptions: PeerTubeRequestOptions = {}) {
const options = {
activityPub: true,
httpSignature: CONFIG.FEDERATION.SIGN_FEDERATED_FETCHES
? await buildSignedRequestOptions({ hasPayload: false })
: undefined,
...moreOptions
}
return doJSONRequest<T>(url, options)
}
export async function fetchAPObjectIfNeeded <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) {
if (typeof object === 'string') { if (typeof object === 'string') {
const { body } = await doJSONRequest<Exclude<T, string>>(object, { activityPub: true }) const { body } = await fetchAP<Exclude<T, string>>(object)
return body return body
} }
@ -43,10 +63,12 @@ async function fetchAPObject <T extends (ActivityObject | ActivityPubActor)> (ob
return object as Exclude<T, string> return object as Exclude<T, string>
} }
export { export async function findLatestAPRedirection (url: string, iteration = 1) {
getAPId, if (iteration > 10) throw new Error('Too much iterations to find final URL ' + url)
fetchAPObject,
getActivityStreamDuration, const { headers } = await fetchAP(url, { followRedirect: false })
buildAvailableActivities,
getDurationFromActivityStream if (headers.location) return findLatestAPRedirection(headers.location, iteration + 1)
return url
} }

View File

@ -5,7 +5,7 @@ import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders'
import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models'
import { arrayify } from '@shared/core-utils' import { arrayify } from '@shared/core-utils'
import { ActivityPubActor, APObjectId } from '@shared/models' import { ActivityPubActor, APObjectId } from '@shared/models'
import { fetchAPObject, getAPId } from '../activity' import { fetchAPObjectIfNeeded, getAPId } from '../activity'
import { checkUrlsSameHost } from '../url' import { checkUrlsSameHost } from '../url'
import { refreshActorIfNeeded } from './refresh' import { refreshActorIfNeeded } from './refresh'
import { APActorCreator, fetchRemoteActor } from './shared' import { APActorCreator, fetchRemoteActor } from './shared'
@ -87,7 +87,7 @@ async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: stri
async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') { async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') {
for (const actorToCheck of arrayify(attributedTo)) { for (const actorToCheck of arrayify(attributedTo)) {
const actorObject = await fetchAPObject<ActivityPubActor>(getAPId(actorToCheck)) const actorObject = await fetchAPObjectIfNeeded<ActivityPubActor>(getAPId(actorToCheck))
if (!actorObject) { if (!actorObject) {
logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl) logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl)

View File

@ -1,13 +1,13 @@
import { sanitizeAndCheckActorObject } from '@server/helpers/custom-validators/activitypub/actor' import { sanitizeAndCheckActorObject } from '@server/helpers/custom-validators/activitypub/actor'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { ActivityPubActor, ActivityPubOrderedCollection } from '@shared/models' import { ActivityPubActor, ActivityPubOrderedCollection } from '@shared/models'
import { fetchAP } from '../../activity'
import { checkUrlsSameHost } from '../../url' import { checkUrlsSameHost } from '../../url'
async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode: number, actorObject: ActivityPubActor }> { async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode: number, actorObject: ActivityPubActor }> {
logger.info('Fetching remote actor %s.', actorUrl) logger.info('Fetching remote actor %s.', actorUrl)
const { body, statusCode } = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true }) const { body, statusCode } = await fetchAP<ActivityPubActor>(actorUrl)
if (sanitizeAndCheckActorObject(body) === false) { if (sanitizeAndCheckActorObject(body) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: body }) logger.debug('Remote actor JSON is not valid.', { actorJSON: body })
@ -46,7 +46,7 @@ export {
async function fetchActorTotalItems (url: string) { async function fetchActorTotalItems (url: string) {
try { try {
const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true }) const { body } = await fetchAP<ActivityPubOrderedCollection<unknown>>(url)
return body.totalItems || 0 return body.totalItems || 0
} catch (err) { } catch (err) {

View File

@ -3,8 +3,8 @@ import { URL } from 'url'
import { retryTransactionWrapper } from '@server/helpers/database-utils' import { retryTransactionWrapper } from '@server/helpers/database-utils'
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { doJSONRequest } from '../../helpers/requests'
import { ACTIVITY_PUB, WEBSERVER } from '../../initializers/constants' import { ACTIVITY_PUB, WEBSERVER } from '../../initializers/constants'
import { fetchAP } from './activity'
type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
type CleanerFunction = (startedDate: Date) => Promise<any> type CleanerFunction = (startedDate: Date) => Promise<any>
@ -14,11 +14,9 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
logger.info('Crawling ActivityPub data on %s.', url) logger.info('Crawling ActivityPub data on %s.', url)
const options = { activityPub: true }
const startDate = new Date() const startDate = new Date()
const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) const response = await fetchAP<ActivityPubOrderedCollection<T>>(url)
const firstBody = response.body const firstBody = response.body
const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@ -34,7 +32,7 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
url = nextLink url = nextLink
const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) const res = await fetchAP<ActivityPubOrderedCollection<T>>(url)
body = res.body body = res.body
} else { } else {
// nextLink is already the object we want // nextLink is already the object we want

View File

@ -1,8 +1,8 @@
import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '@server/helpers/custom-validators/activitypub/playlist' import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '@server/helpers/custom-validators/activitypub/playlist'
import { isArray } from '@server/helpers/custom-validators/misc' import { isArray } from '@server/helpers/custom-validators/misc'
import { logger, loggerTagsFactory } from '@server/helpers/logger' import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { PlaylistElementObject, PlaylistObject } from '@shared/models' import { PlaylistElementObject, PlaylistObject } from '@shared/models'
import { fetchAP } from '../../activity'
import { checkUrlsSameHost } from '../../url' import { checkUrlsSameHost } from '../../url'
async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
@ -10,7 +10,7 @@ async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusC
logger.info('Fetching remote playlist %s.', playlistUrl, lTags()) logger.info('Fetching remote playlist %s.', playlistUrl, lTags())
const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) const { body, statusCode } = await fetchAP<any>(playlistUrl)
if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
logger.debug('Remote video playlist JSON is not valid.', { body, ...lTags() }) logger.debug('Remote video playlist JSON is not valid.', { body, ...lTags() })
@ -30,7 +30,7 @@ async function fetchRemotePlaylistElement (elementUrl: string): Promise<{ status
logger.debug('Fetching remote playlist element %s.', elementUrl, lTags()) logger.debug('Fetching remote playlist element %s.', elementUrl, lTags())
const { body, statusCode } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) const { body, statusCode } = await fetchAP<PlaylistElementObject>(elementUrl)
if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in fetch playlist element ${elementUrl}`) if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in fetch playlist element ${elementUrl}`)

View File

@ -18,7 +18,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models'
import { Notifier } from '../../notifier' import { Notifier } from '../../notifier'
import { fetchAPObject } from '../activity' import { fetchAPObjectIfNeeded } from '../activity'
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' import { createOrUpdateLocalVideoViewer } from '../local-video-viewer'
import { createOrUpdateVideoPlaylist } from '../playlists' import { createOrUpdateVideoPlaylist } from '../playlists'
@ -31,7 +31,7 @@ async function processCreateActivity (options: APProcessorOptions<ActivityCreate
// Only notify if it is not from a fetcher job // Only notify if it is not from a fetcher job
const notify = options.fromFetch !== true const notify = options.fromFetch !== true
const activityObject = await fetchAPObject<Exclude<ActivityObject, AbuseObject>>(activity.object) const activityObject = await fetchAPObjectIfNeeded<Exclude<ActivityObject, AbuseObject>>(activity.object)
const activityType = activityObject.type const activityType = activityObject.type
if (activityType === 'Video') { if (activityType === 'Video') {

View File

@ -19,7 +19,7 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { fetchAPObject } from '../activity' import { fetchAPObjectIfNeeded } from '../activity'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils' import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
@ -32,7 +32,7 @@ async function processUndoActivity (options: APProcessorOptions<ActivityUndo<Act
} }
if (activityToUndo.type === 'Create') { if (activityToUndo.type === 'Create') {
const objectToUndo = await fetchAPObject<CacheFileObject>(activityToUndo.object) const objectToUndo = await fetchAPObjectIfNeeded<CacheFileObject>(activityToUndo.object)
if (objectToUndo.type === 'CacheFile') { if (objectToUndo.type === 'CacheFile') {
return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo) return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo)

View File

@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { ActorModel } from '../../../models/actor/actor' import { ActorModel } from '../../../models/actor/actor'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorFull, MActorSignature } from '../../../types/models' import { MActorFull, MActorSignature } from '../../../types/models'
import { fetchAPObject } from '../activity' import { fetchAPObjectIfNeeded } from '../activity'
import { APActorUpdater } from '../actors/updater' import { APActorUpdater } from '../actors/updater'
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlists' import { createOrUpdateVideoPlaylist } from '../playlists'
@ -20,7 +20,7 @@ import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) { async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
const { activity, byActor } = options const { activity, byActor } = options
const object = await fetchAPObject(activity.object) const object = await fetchAPObjectIfNeeded(activity.object)
const objectType = object.type const objectType = object.type
if (objectType === 'Video') { if (objectType === 'Video') {

View File

@ -23,11 +23,14 @@ async function computeBody <T> (
return body return body
} }
async function buildSignedRequestOptions (payload: Payload<any>) { async function buildSignedRequestOptions (options: {
signatureActorId?: number
hasPayload: boolean
}) {
let actor: MActor | null let actor: MActor | null
if (payload.signatureActorId) { if (options.signatureActorId) {
actor = await ActorModel.load(payload.signatureActorId) actor = await ActorModel.load(options.signatureActorId)
if (!actor) throw new Error('Unknown signature actor id.') if (!actor) throw new Error('Unknown signature actor id.')
} else { } else {
// We need to sign the request, so use the server // We need to sign the request, so use the server
@ -40,7 +43,9 @@ async function buildSignedRequestOptions (payload: Payload<any>) {
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
keyId, keyId,
key: actor.privateKey, key: actor.privateKey,
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN headers: options.hasPayload
? HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITHOUT_PAYLOAD
} }
} }

View File

@ -2,11 +2,10 @@ import { map } from 'bluebird'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { logger, loggerTagsFactory } from '../../helpers/logger' import { logger, loggerTagsFactory } from '../../helpers/logger'
import { doJSONRequest } from '../../helpers/requests'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
import { getAPId } from './activity' import { fetchAP, getAPId } from './activity'
import { getOrCreateAPActor } from './actors' import { getOrCreateAPActor } from './actors'
import { sendUndoAnnounce, sendVideoAnnounce } from './send' import { sendUndoAnnounce, sendVideoAnnounce } from './send'
import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url' import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url'
@ -56,7 +55,7 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function addVideoShare (shareUrl: string, video: MVideoId) { async function addVideoShare (shareUrl: string, video: MVideoId) {
const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) const { body } = await fetchAP<any>(shareUrl)
if (!body?.actor) throw new Error('Body or body actor is invalid') if (!body?.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getAPId(body.actor) const actorUrl = getAPId(body.actor)

View File

@ -1,12 +1,13 @@
import { map } from 'bluebird' import { map } from 'bluebird'
import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { doJSONRequest } from '../../helpers/requests'
import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
import { VideoCommentModel } from '../../models/video/video-comment' import { VideoCommentModel } from '../../models/video/video-comment'
import { MComment, MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' import { MComment, MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
import { isRemoteVideoCommentAccepted } from '../moderation' import { isRemoteVideoCommentAccepted } from '../moderation'
import { Hooks } from '../plugins/hooks' import { Hooks } from '../plugins/hooks'
import { fetchAP } from './activity'
import { getOrCreateAPActor } from './actors' import { getOrCreateAPActor } from './actors'
import { checkUrlsSameHost } from './url' import { checkUrlsSameHost } from './url'
import { getOrCreateAPVideo } from './videos' import { getOrCreateAPVideo } from './videos'
@ -139,7 +140,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
throw new Error('Recursion limit reached when resolving a thread') throw new Error('Recursion limit reached when resolving a thread')
} }
const { body } = await doJSONRequest<any>(url, { activityPub: true }) const { body } = await fetchAP<any>(url)
if (sanitizeAndCheckVideoCommentObject(body) === false) { if (sanitizeAndCheckVideoCommentObject(body) === false) {
throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))

View File

@ -1,7 +1,7 @@
import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos' import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
import { logger, loggerTagsFactory } from '@server/helpers/logger' import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { VideoObject } from '@shared/models' import { VideoObject } from '@shared/models'
import { fetchAP } from '../../activity'
import { checkUrlsSameHost } from '../../url' import { checkUrlsSameHost } from '../../url'
const lTags = loggerTagsFactory('ap', 'video') const lTags = loggerTagsFactory('ap', 'video')
@ -9,7 +9,7 @@ const lTags = loggerTagsFactory('ap', 'video')
async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> { async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
logger.info('Fetching remote video %s.', videoUrl, lTags(videoUrl)) logger.info('Fetching remote video %s.', videoUrl, lTags(videoUrl))
const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true }) const { statusCode, body } = await fetchAP<any>(videoUrl)
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body, ...lTags(videoUrl) }) logger.debug('Remote video JSON is not valid.', { body, ...lTags(videoUrl) })

View File

@ -1,12 +1,12 @@
import { runInReadCommittedTransaction } from '@server/helpers/database-utils' import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
import { logger, loggerTagsFactory } from '@server/helpers/logger' import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { JobQueue } from '@server/lib/job-queue' import { JobQueue } from '@server/lib/job-queue'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { VideoCommentModel } from '@server/models/video/video-comment' import { VideoCommentModel } from '@server/models/video/video-comment'
import { VideoShareModel } from '@server/models/video/video-share' import { VideoShareModel } from '@server/models/video/video-share'
import { MVideo } from '@server/types/models' import { MVideo } from '@server/types/models'
import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models' import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
import { fetchAP } from '../../activity'
import { crawlCollectionPage } from '../../crawl' import { crawlCollectionPage } from '../../crawl'
import { addVideoShares } from '../../share' import { addVideoShares } from '../../share'
import { addVideoComments } from '../../video-comments' import { addVideoComments } from '../../video-comments'
@ -63,17 +63,15 @@ async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVi
: fetchedVideo.dislikes : fetchedVideo.dislikes
logger.info('Sync %s of video %s', type, video.url) logger.info('Sync %s of video %s', type, video.url)
const options = { activityPub: true }
const response = await doJSONRequest<ActivityPubOrderedCollection<any>>(uri, options) const { body } = await fetchAP<ActivityPubOrderedCollection<any>>(uri)
const totalItems = response.body.totalItems
if (isNaN(totalItems)) { if (isNaN(body.totalItems)) {
logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body: response.body }) logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body })
return return
} }
return totalItems return body.totalItems
} }
function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {

View File

@ -6,8 +6,9 @@ import {
isLikeActivityValid isLikeActivityValid
} from '@server/helpers/custom-validators/activitypub/activity' } from '@server/helpers/custom-validators/activitypub/activity'
import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests' import { PeerTubeRequestError } from '@server/helpers/requests'
import { AP_CLEANER } from '@server/initializers/constants' import { AP_CLEANER } from '@server/initializers/constants'
import { fetchAP } from '@server/lib/activitypub/activity'
import { checkUrlsSameHost } from '@server/lib/activitypub/url' import { checkUrlsSameHost } from '@server/lib/activitypub/url'
import { Redis } from '@server/lib/redis' import { Redis } from '@server/lib/redis'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
@ -85,7 +86,7 @@ async function updateObjectIfNeeded <T> (options: {
} }
try { try {
const { body } = await doJSONRequest<any>(url, { activityPub: true }) const { body } = await fetchAP<any>(url)
// If not same id, check same host and update // If not same id, check same host and update
if (!body?.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) if (!body?.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)

View File

@ -38,7 +38,7 @@ export {
async function buildRequestOptions (payload: ActivitypubHttpBroadcastPayload) { async function buildRequestOptions (payload: ActivitypubHttpBroadcastPayload) {
const body = await computeBody(payload) const body = await computeBody(payload)
const httpSignatureOptions = await buildSignedRequestOptions(payload) const httpSignatureOptions = await buildSignedRequestOptions({ signatureActorId: payload.signatureActorId, hasPayload: true })
return { return {
method: 'POST' as 'POST', method: 'POST' as 'POST',

View File

@ -12,7 +12,7 @@ async function processActivityPubHttpUnicast (job: Job) {
const uri = payload.uri const uri = payload.uri
const body = await computeBody(payload) const body = await computeBody(payload)
const httpSignatureOptions = await buildSignedRequestOptions(payload) const httpSignatureOptions = await buildSignedRequestOptions({ signatureActorId: payload.signatureActorId, hasPayload: true })
const options = { const options = {
method: 'POST' as 'POST', method: 'POST' as 'POST',

View File

@ -58,7 +58,7 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
keyId: by.url, keyId: by.url,
key: by.privateKey, key: by.privateKey,
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
} }
const headers = { const headers = {
'digest': buildDigest(body), 'digest': buildDigest(body),
@ -82,7 +82,7 @@ describe('Test ActivityPub security', function () {
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
keyId: 'acct:peertube@' + servers[1].host, keyId: 'acct:peertube@' + servers[1].host,
key: keys.privateKey, key: keys.privateKey,
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
}) })
// --------------------------------------------------------------- // ---------------------------------------------------------------

View File

@ -12,6 +12,11 @@ webserver:
__name: "PEERTUBE_WEBSERVER_HTTPS" __name: "PEERTUBE_WEBSERVER_HTTPS"
__format: "json" __format: "json"
federation:
sign_federated_fetches:
__name: "PEERTUBE_SIGN_FEDERATED_FETCHES"
__format: "json"
secrets: secrets:
peertube: "PEERTUBE_SECRET" peertube: "PEERTUBE_SECRET"