Compact json-ld AP objects

This commit is contained in:
Chocobozzz 2024-04-24 09:16:28 +02:00
parent f2895d29e1
commit 871384533b
No known key found for this signature in database
GPG key ID: 583A612D890159BE
21 changed files with 385 additions and 94 deletions

View file

@ -116,6 +116,8 @@ export interface ActivityView extends BaseActivity {
// If sending a "viewer" event
expires?: string
expiration?: string // FIXME: remove in 7.0, here for compat with < 6.1
result?: {
type: 'InteractionCounter'
interactionType: 'WatchAction'

View file

@ -9,7 +9,9 @@ export interface PlaylistObject {
content: string
mediaType: 'text/markdown'
uuid: string
// FIXME: remove identifier in the future, introduced for federation compatibility in 6.1
uuid?: string
identifier?: string
totalItems: number
attributedTo: ActivityPubAttributedTo[]

View file

@ -13,13 +13,16 @@ export interface VideoObject {
id: string
name: string
duration: string
uuid: string
tag: ActivityTagObject[]
category: ActivityIdentifierObject
licence: ActivityIdentifierObject
language: ActivityIdentifierObject
subtitleLanguage: ActivityIdentifierObject[]
// FIXME: remove identifier in the future, introduced for federation compatibility in 6.1
uuid?: string
identifier?: string
views: number
sensitive: boolean

View file

@ -10,7 +10,15 @@ export interface WatchActionObject {
addressRegion: string
}
uuid: string
uuid?: string
// FIXME: remove following fields in the future, introduced for federation compatibility in 6.1
identifier?: string
_actionStatus?: 'CompletedActionStatus'
_watchSections?: {
startTimestamp: number
endTimestamp: number
}[]
object: string
actionStatus: 'CompletedActionStatus'

View file

@ -0,0 +1,113 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"Update": "as:Create"
}
],
"id": "https://example.com/users/bob/statuses/107928807471117876/activity",
"type": "Update",
"actor": "https://example.com/users/bob",
"published": "2022-03-09T21:55:07Z",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": "https://example.com/users/bob/followers",
"object": {
"@context": {
"id": {
"@id": "as:attributedTo",
"@type": "@id"
},
"Person": "as:Note",
"following": {
"@id": "as:cc",
"@type": "@id"
},
"followers": {
"@id": "as:cc",
"@type": "@id"
},
"inbox": {
"@id": "as:cc",
"@type": "@id"
},
"sharedInbox": {
"@id": "as:cc",
"@type": "@id"
},
"outbox": {
"@id": "as:cc",
"@type": "@id"
},
"preferredUsername": "@type",
"bob": "as:Note",
"name": "@type",
"BEING TAKEN OVER": "as:Note",
"summary": "@type",
"THIS ACCOUNT IS BEING TAKEN OVER BY AN ATTACKER": "as:Note",
"url": {
"@id": "as:attributedTo",
"@type": "@id"
},
"publicKey": {
"@id": "as:replies",
"@type": "@id"
},
"ostatus": "http://ostatus.org#"
},
"id": "https://example.com/users/bob",
"type": "Person",
"following": "https://example.com/users/bob/followers",
"followers": "https://example.com/users/bob/followers",
"inbox": "https://example.com/users/bob/followers",
"sharedInbox": "https://example.com/users/bob/followers",
"outbox": "https://example.com/users/bob/followers",
"preferredUsername": "bob",
"name": "BEING TAKEN OVER",
"summary": "THIS ACCOUNT IS BEING TAKEN OVER BY AN ATTACKER",
"url": "https://example.com/users/bob",
"published": "2022-03-09T21:55:07Z",
"publicKey": {
"@context": {
"id": "@id",
"owner": {
"@reverse": "as:replies",
"@type": "@id"
},
"publicKeyPem": "@type",
"-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL2hdo3culcPqz6y7AT0rlE5hgiNceL4\n28VkreQP2rSecXgeMZnjeW42GExS73F71pGMkx7b9svVK4IfPTlMN2ECAwEAAQ==\n-----END PUBLIC KEY-----\n": "as:Collection"
},
"id": "https://example.com/users/bob/statuses/107928807471117876/replies",
"owner": "https://example.com/users/bob/statuses/107928807471117876",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL2hdo3culcPqz6y7AT0rlE5hgiNceL4\n28VkreQP2rSecXgeMZnjeW42GExS73F71pGMkx7b9svVK4IfPTlMN2ECAwEAAQ==\n-----END PUBLIC KEY-----\n",
"as:first": {
"type": "CollectionPage",
"items": [],
"next": "https://example.com/users/bob/statuses/107928807471117876/replies?only_other_accounts=true&page=true",
"partOf": "https://example.com/users/bob/statuses/107928807471117876/replies"
}
},
"@id": "https://example.com/users/bob/statuses/107928807471117876",
"ostatus:atomUri": "https://example.com/users/bob/statuses/107928807471117876",
"ostatus:conversation": "tag:example.com,2022-03-09:objectId=15:objectType=Conversation",
"as:content": [
"<p>hello world</p>",
{
"@value": "<p>hello world</p>",
"@language": "en"
}
],
"as:sensitive": false,
"as:to": {
"@id": "https://www.w3.org/ns/activitystreams#Public"
},
"as:url": {
"@id": "https://example.com/@bob/107928807471117876"
}
},
"signature": {
"type": "RsaSignature2017",
"creator": "https://example.com/users/bob#main-key",
"created": "2022-03-09T21:57:25Z",
"signatureValue": "WculK0LelTQ0MvGwU9TPoq5pFzFfGYRDCJqjZ232/Udj4CHqDTGOSw5UTDLShqBOyycCkbZGrQwXG+dpyDpQLSe1UVPZ5TPQtc/9XtI57WlS2nMNpdvRuxGnnb2btPdesXZ7n3pCxo0zjaXrJMe0mqQh5QJO22mahb4bDwwmfTHgbD3nmkD+fBfGi+UV2qWwqr+jlV4L4JqNkh0gWljF5KTePLRRZCuWiQ/FAt7c67636cdIPf7fR+usjuZltTQyLZKEGuK8VUn2Gkfsx5qns7Vcjvlz1JqlAjyO8HPBbzTTHzUG2nUOIgC3PojCSWv6mNTmRGoLZzOscCAYQA6cKw=="
}
}

View file

@ -0,0 +1,6 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuuYyoyfsRkYnXRotMsId\nW3euBDDfiv9oVqOxUVC7bhel8KednIMrMCRWFAkgJhbrlzbIkjVr68o1MP9qLcn7\nCmH/BXHp7yhuFTr4byjdJKpwB+/i2jNEsvDH5jR8WTAeTCe0x/QHg21V3F7dSI5m\nCCZ/1dSIyOXLRTWVlfDlm3rE4ntlCo+US3/7oSWbg/4/4qEnt1HC32kvklgScxua\n4LR5ATdoXa5bFoopPWhul7MJ6NyWCyQyScUuGdlj8EN4kmKQJvphKHrI9fvhgOuG\nTvhTR1S5InA4azSSchY0tXEEw/VNxraeX0KPjbgr6DPcwhPd/m0nhVDq0zVyVBBD\nMwIDAQAB\n-----END PUBLIC KEY-----\n",
"privateKey": "-----BEGIN PRIVATE KEY-----\nMIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAvaF2jdy6Vw+rPrLs\nBPSuUTmGCI1x4vjbxWSt5A/atJ5xeB4xmeN5bjYYTFLvcXvWkYyTHtv2y9Urgh89\nOUw3YQIDAQABAkAWF4BrSILA78dgd5G9hg/k0JHH30qcSae42GDVx+8PyY5LTW/k\n2luohqd2aFbVl/64eV8wU4FaTqhuPRAXJKYRAiEA45eyOxOpnCMlO4OuTEItmDE0\n8i5pdasWI+YbFxAJVI0CIQDVTMP43JcgjtGxU7s6eTYGTH1T1LHi8MxZj0q33/C7\nJQIgFJYcIQveQ6lKLN/0XCGATkvlJiLclzAqiIS/3o4syeECIQCHrtpmvyPfkRow\n3BuYmaxVG2kJ3538x8KmIfGcv/ZphQIgXOULuGECR0nnOFwcZ9arqIWp7BE825da\nskc6vNULKBA=\n-----END PRIVATE KEY-----"
}

View file

@ -3,7 +3,7 @@
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { signAndContextify } from '@peertube/peertube-server/core/helpers/activity-pub-utils.js'
import { isHTTPSignatureVerified, parseHTTPSignature } from '@peertube/peertube-server/core/helpers/peertube-crypto.js'
import { isJsonLDSignatureVerified, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { compactJSONLDAndCheckSignature, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { expect } from 'chai'
import { readJsonSync } from 'fs-extra/esm'
import cloneDeep from 'lodash-es/cloneDeep.js'
@ -24,6 +24,14 @@ function fakeFilter () {
return (data: any) => Promise.resolve(data)
}
function fakeExpressReq (body: any) {
return { body, locals: { bodyBeforeJSONLDCompaction: {} as any } }
}
function fakeExpressRes () {
return { locals: { bodyBeforeJSONLDCompaction: {} as any } }
}
describe('Test activity pub helpers', function () {
describe('When checking the Linked Signature', function () {
@ -33,7 +41,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body), fakeExpressRes())
expect(result).to.be.false
})
@ -43,7 +51,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body), fakeExpressRes())
expect(result).to.be.false
})
@ -53,7 +61,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body), fakeExpressRes())
expect(result).to.be.true
})
@ -72,11 +80,27 @@ describe('Test activity pub helpers', function () {
})
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody), fakeExpressRes())
expect(result).to.be.false
})
it('Should compact JSONLD input when checking JSONLD signature', async function () {
const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys-updated.json'))
const signedBody = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-updated.json'))
const fromActor = { publicKey: keys.publicKey }
const req = { body: signedBody }
const res = { locals: { bodyBeforeJSONLDCompaction: null } }
const result = await compactJSONLDAndCheckSignature(fromActor as any, req, res)
expect(res.locals.bodyBeforeJSONLDCompaction).to.equal(signedBody)
expect(res.locals.bodyBeforeJSONLDCompaction).to.deep.equal(signedBody)
expect(req.body.type).to.equal('Create')
expect(result).to.be.true
})
it('Should succeed with a valid PeerTube signature', async function () {
const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
@ -91,7 +115,7 @@ describe('Test activity pub helpers', function () {
})
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody), fakeExpressRes())
expect(result).to.be.true
})

View file

@ -1,9 +1,9 @@
import { ContextType } from '@peertube/peertube-models'
import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
import { isArray } from './custom-validators/misc.js'
import { buildDigest } from './peertube-crypto.js'
import type { signJsonLDObject } from './peertube-jsonld.js'
import { doJSONRequest } from './requests.js'
import { isArray } from './custom-validators/misc.js'
export type ContextFilter = <T> (arg: T) => Promise<T>
@ -233,13 +233,63 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
Rate: buildContext(),
Chapters: buildContext({
name: 'sc:name',
hasPart: 'sc:hasPart',
endOffset: 'sc:endOffset',
startOffset: 'sc:startOffset'
})
}
let allContext: (string | ContextValue)[]
export function getAllContext () {
if (allContext) return allContext
const processed = new Set<string>()
allContext = []
for (const v of Object.values(contextStore)) {
for (const item of v) {
if (typeof item === 'string') {
if (!processed.has(item)) {
allContext.push(item)
}
processed.add(item)
} else {
for (const subKey of Object.keys(item)) {
if (!processed.has(subKey)) {
allContext.push({ [subKey]: item[subKey] })
}
processed.add(subKey)
}
}
}
}
// FIXME: replace uuid context with this value in global context
allContext = allContext.concat([
{
uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
}
},
{
endTimestamp: {
'@type': 'sc:Number',
'@id': 'pt:endTimestamp'
},
actionStatus: 'sc:actionStatus',
watchSections: {
'@type': '@id',
'@id': 'pt:watchSections'
}
}
])
return allContext
}
async function getContextData (type: ContextType, contextFilter: ContextFilter) {
const contextData = contextFilter
? await contextFilter(contextStore[type])

View file

@ -1,20 +1,15 @@
import { CacheFileObject } from '@peertube/peertube-models'
import { exists, isDateValid } from '../misc.js'
import { MIMETYPES } from '@server/initializers/constants.js'
import validator from 'validator'
import { isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
import { isRemoteVideoUrlValid } from './videos.js'
function isCacheFileObjectValid (object: CacheFileObject) {
return exists(object) &&
object.type === 'CacheFile' &&
(object.expires === null || isDateValid(object.expires)) &&
export function isCacheFileObjectValid (object: CacheFileObject) {
if (!object || object.type !== 'CacheFile') return false
return (object.expires === null || isDateValid(object.expires)) &&
isActivityPubUrlValid(object.object) &&
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
export {
isCacheFileObjectValid
(isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
@ -24,3 +19,15 @@ function isPlaylistRedundancyUrlValid (url: any) {
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href)
}
// TODO: compat with < 6.1, use isRemoteVideoUrlValid instead in 7.0
function isRedundancyUrlVideoValid (url: any) {
const size = url.size || url['_:size']
const fps = url.fps || url['_fps']
return MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 }) &&
validator.default.isInt(size + '', { min: 0 }) &&
(!fps || validator.default.isInt(fps + '', { min: -1 }))
}

View file

@ -1,29 +1,26 @@
import validator from 'validator'
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
import validator from 'validator'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isVideoPlaylistNameValid } from '../video-playlists.js'
import { isActivityPubUrlValid } from './misc.js'
function isPlaylistObjectValid (object: PlaylistObject) {
return exists(object) &&
object.type === 'Playlist' &&
validator.default.isInt(object.totalItems + '') &&
export function isPlaylistObjectValid (object: PlaylistObject) {
if (!object || object.type !== 'Playlist') return false
if (isUUIDValid(object.identifier) && !isUUIDValid(object.uuid)) {
object.uuid = object.identifier
}
return validator.default.isInt(object.totalItems + '') &&
isVideoPlaylistNameValid(object.name) &&
isUUIDValid(object.uuid) &&
isDateValid(object.published) &&
isDateValid(object.updated)
}
function isPlaylistElementObjectValid (object: PlaylistElementObject) {
export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
return exists(object) &&
object.type === 'PlaylistElement' &&
validator.default.isInt(object.position + '') &&
isActivityPubUrlValid(object.url)
}
// ---------------------------------------------------------------------------
export {
isPlaylistObjectValid,
isPlaylistElementObjectValid
}

View file

@ -27,7 +27,7 @@ function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
sanitizeAndCheckVideoTorrentObject(activity.object)
}
function sanitizeAndCheckVideoTorrentObject (video: any) {
function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
if (!video || video.type !== 'Video') return false
if (!setValidRemoteTags(video)) {
@ -59,6 +59,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
return false
}
if (isUUIDValid(video.identifier) && !isUUIDValid(video.uuid)) {
video.uuid = video.identifier
}
// Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false

View file

@ -1,19 +1,27 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { WatchActionObject } from '@peertube/peertube-models'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isDateValid, isUUIDValid } from '../misc.js'
import { isVideoTimeValid } from '../video-view.js'
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
function isWatchActionObjectValid (action: WatchActionObject) {
return exists(action) &&
action.type === 'WatchAction' &&
isObjectValid(action.id) &&
if (!action || action.type !== 'WatchAction') return false
if (isUUIDValid(action.identifier) && !isUUIDValid(action.uuid)) {
action.uuid = action.identifier
}
if (action['_:actionStatus'] && !action.actionStatus) action.actionStatus = action['_:actionStatus']
if (action['_:watchSections'] && !action.watchSections) action.watchSections = arrayify(action['_:watchSections'])
return isObjectValid(action.id) &&
isActivityPubVideoDurationValid(action.duration) &&
isDateValid(action.startTime) &&
isDateValid(action.endTime) &&
isLocationValid(action.location) &&
isUUIDValid(action.uuid) &&
isObjectValid(action.object) &&
isWatchSectionsValid(action.watchSections)
areWatchSectionsValid(action.watchSections)
}
// ---------------------------------------------------------------------------
@ -34,8 +42,10 @@ function isLocationValid (location: any) {
return true
}
function isWatchSectionsValid (sections: WatchActionObject['watchSections']) {
function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
return Array.isArray(sections) && sections.every(s => {
if (s['_:endTimestamp'] && !s.endTimestamp) s.endTimestamp = s['_:endTimestamp']
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
})
}

View file

@ -70,7 +70,7 @@ export function areVideoTagsValid (tags: string[]) {
)
}
export function isVideoViewsValid (value: string) {
export function isVideoViewsValid (value: string | number) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
}

View file

@ -1,26 +1,54 @@
import { omit } from '@peertube/peertube-core-utils'
import { sha256 } from '@peertube/peertube-node-utils'
import { createSign, createVerify } from 'crypto'
import cloneDeep from 'lodash-es/cloneDeep.js'
import { MActor } from '../types/models/index.js'
import { getAllContext } from './activity-pub-utils.js'
import { jsonld } from './custom-jsonld-signature.js'
import { isArray } from './custom-validators/misc.js'
import { logger } from './logger.js'
import { assertIsInWorkerThread } from './threads.js'
import { jsonld } from './custom-jsonld-signature.js'
export function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
return isJsonLDRSA2017Verified(fromActor, signedDocument)
type ExpressRequest = { body: any }
type ExpressResponse = { locals: { bodyBeforeJSONLDCompaction?: any } }
export function compactJSONLDAndCheckSignature (fromActor: MActor, req: ExpressRequest, res: ExpressResponse): Promise<boolean> {
if (req.body.signature.type === 'RsaSignature2017') {
return compactJSONLDAndCheckRSA2017Signature(fromActor, req, res)
}
logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
logger.warn('Unknown JSON LD signature %s.', req.body.signature.type, req.body)
return Promise.resolve(false)
}
// Backward compatibility with "other" implementations
export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor, req: ExpressRequest, res: ExpressResponse) {
const compacted = await jsonldCompact(omit(req.body, [ 'signature' ]))
fixCompacted(req.body, compacted)
res.locals.bodyBeforeJSONLDCompaction = req.body
req.body = { ...compacted, signature: req.body.signature }
if (compacted['@include']) {
logger.warn('JSON-LD @include is not supported')
return false
}
// TODO: compat with < 6.1, remove in 7.0
let safe = true
if (
(compacted.type === 'Create' && (compacted?.object?.type === 'WatchAction' || compacted?.object?.type === 'CacheFile')) ||
(compacted.type === 'Create' && (compacted?.object?.type === 'WatchAction' || compacted?.object?.type === 'CacheFile')) ||
(compacted.type === 'Undo' && (compacted?.object?.type === 'Create' || compacted?.object?.object.type === 'CacheFile'))
) {
safe = false
}
const [ documentHash, optionsHash ] = await Promise.all([
createDocWithoutSignatureHash(signedDocument),
createSignatureHash(signedDocument.signature)
hashObject(compacted, safe),
createSignatureHash(req.body.signature, safe)
])
const toVerify = optionsHash + documentHash
@ -28,7 +56,39 @@ export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument
const verify = createVerify('RSA-SHA256')
verify.update(toVerify, 'utf8')
return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
return verify.verify(fromActor.publicKey, req.body.signature.signatureValue, 'base64')
}
function fixCompacted (original: any, compacted: any) {
if (!original || !compacted) return
for (const [ k, v ] of Object.entries(original)) {
if (k === '@context' || k === 'signature') continue
if (v === undefined || v === null) continue
const cv = compacted[k]
if (cv === undefined || cv === null) continue
if (typeof v === 'string') {
if (v === 'https://www.w3.org/ns/activitystreams#Public' && cv === 'as:Public') {
compacted[k] = v
}
}
if (isArray(v) && !isArray(cv)) {
compacted[k] = [ cv ]
for (let i = 0; i < v.length; i++) {
if (v[i] === 'https://www.w3.org/ns/activitystreams#Public' && cv[i] === 'as:Public') {
compacted[k][i] = v[i]
}
}
}
if (typeof v === 'object') {
fixCompacted(original[k], compacted[k])
}
}
}
export async function signJsonLDObject <T> (options: {
@ -48,7 +108,7 @@ export async function signJsonLDObject <T> (options: {
const [ documentHash, optionsHash ] = await Promise.all([
createDocWithoutSignatureHash(data),
createSignatureHash(signature)
createSignatureHash(signature, false)
])
const toSign = optionsHash + documentHash
@ -66,35 +126,40 @@ export async function signJsonLDObject <T> (options: {
// Private
// ---------------------------------------------------------------------------
async function hashObject (obj: any): Promise<any> {
const res = await (jsonld as any).promises.normalize(obj, {
safe: false,
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
async function hashObject (obj: any, safe: boolean): Promise<any> {
const res = await jsonldNormalize(obj, safe)
return sha256(res)
}
function createSignatureHash (signature: any) {
const signatureCopy = cloneDeep(signature)
Object.assign(signatureCopy, {
function jsonldCompact (obj: any) {
return (jsonld as any).promises.compact(obj, getAllContext())
}
function jsonldNormalize (obj: any, safe: boolean) {
return (jsonld as any).promises.normalize(obj, {
safe,
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
}
// ---------------------------------------------------------------------------
function createSignatureHash (signature: any, safe: boolean) {
return hashObject({
'@context': [
'https://w3id.org/security/v1',
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
]
})
],
delete signatureCopy.type
delete signatureCopy.id
delete signatureCopy.signatureValue
return hashObject(signatureCopy)
...omit(signature, [ 'type', 'id', 'signatureValue' ])
}, safe)
}
function createDocWithoutSignatureHash (doc: any) {
const docWithoutSignature = cloneDeep(doc)
delete docWithoutSignature.signature
return hashObject(docWithoutSignature)
return hashObject(docWithoutSignature, false)
}

View file

@ -2,6 +2,7 @@ import { Transaction } from 'sequelize'
import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models/index.js'
import { CacheFileObject, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js'
import { exists } from '@server/helpers/custom-validators/misc.js'
async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
@ -65,11 +66,15 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
}
const url = cacheFileObject.url
const urlFPS = exists(url.fps) // TODO: compat with < 6.1, remove in 7.0
? url.fps
: url['_:fps']
const videoFile = video.VideoFiles.find(f => {
return f.resolution === url.height && f.fps === url.fps
return f.resolution === url.height && f.fps === urlFPS
})
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${urlFPS} of video ${video.url}`)
return {
expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,

View file

@ -6,7 +6,7 @@ import { Activity } from '@peertube/peertube-models'
import { StatsManager } from '../stat-manager.js'
import { processActivities } from './process/index.js'
class InboxManager {
export class InboxManager {
private static instance: InboxManager
private readonly inboxQueue: PQueue
@ -39,9 +39,3 @@ class InboxManager {
return this.instance || (this.instance = new this())
}
}
// ---------------------------------------------------------------------------
export {
InboxManager
}

View file

@ -32,8 +32,8 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
video,
viewerId: activity.id,
viewerExpires: activity.expires
? new Date(activity.expires)
viewerExpires: getExpires(activity)
? new Date(getExpires(activity))
: undefined,
viewerResultCounter: getViewerResultCounter(activity)
})
@ -49,10 +49,14 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
function getViewerResultCounter (activity: ActivityView) {
const result = activity.result
if (!activity.expires || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
if (!getExpires(activity) || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
const counter = parseInt(result.userInteractionCount + '')
if (isNaN(counter)) return undefined
return counter
}
function getExpires (activity: ActivityView) {
return activity.expires || activity.expiration
}

View file

@ -34,7 +34,7 @@ const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Act
View: processViewActivity
}
async function processActivities (
export async function processActivities (
activities: Activity[],
options: {
signatureActor?: MActorSignature
@ -86,7 +86,3 @@ async function processActivities (
}
}
}
export {
processActivities
}

View file

@ -258,7 +258,6 @@ function unicastTo (options: {
export {
broadcastToFollowers,
unicastTo,
forwardActivity,
broadcastToActors,
sendVideoActivityToOrigin,
forwardVideoRelatedActivity,

View file

@ -18,7 +18,7 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
// Forwarded activity
const bodyActor = req.body.actor
const bodyActorId = getAPId(bodyActor)
if (bodyActorId && bodyActorId !== actor.url) {
if (bodyActorId && bodyActorId !== actor.url || bodyActorId === actor.url) {
const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
if (jsonLDSignatureChecked !== true) return
}
@ -123,7 +123,7 @@ async function checkHttpSignature (req: Request, res: Response) {
async function checkJsonLDSignature (req: Request, res: Response) {
// Lazy load the module as it's quite big with json.ld dependency
const { isJsonLDSignatureVerified } = await import('../helpers/peertube-jsonld.js')
const { compactJSONLDAndCheckSignature } = await import('../helpers/peertube-jsonld.js')
return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => {
const signatureObject: ActivityPubSignature = req.body.signature
@ -141,7 +141,7 @@ async function checkJsonLDSignature (req: Request, res: Response) {
logger.debug('Checking JsonLD signature of actor %s...', creator)
const actor = await getOrCreateAPActor(creator)
const verified = await isJsonLDSignatureVerified(actor, req.body)
const verified = await compactJSONLDAndCheckSignature(actor, req, res)
if (verified !== true) {
logger.warn('Signature not verified.', req.body)

View file

@ -1,4 +1,4 @@
import { HttpMethodType, PeerTubeProblemDocumentData, ServerLogLevel, VideoCreate } from '@peertube/peertube-models'
import { Activity, HttpMethodType, PeerTubeProblemDocumentData, ServerLogLevel, VideoCreate } from '@peertube/peertube-models'
import { RegisterServerAuthExternalOptions } from '@server/types/index.js'
import {
MAbuseMessage,
@ -132,6 +132,8 @@ declare module 'express' {
ffprobe?: FfprobeData
bodyBeforeJSONLDCompaction: Activity
videoAPI?: MVideoFormattableDetails
videoAll?: MVideoFullLight
onlyImmutableVideo?: MVideoImmutable