From e4f97babf701481b55cc10fb3448feab5f97c867 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 9 Nov 2017 17:51:58 +0100 Subject: Begin activitypub --- server/helpers/activitypub.ts | 123 ++++++++++++++ server/helpers/core-utils.ts | 26 ++- .../custom-validators/activitypub/account.ts | 123 ++++++++++++++ .../helpers/custom-validators/activitypub/index.ts | 4 + .../helpers/custom-validators/activitypub/misc.ts | 17 ++ .../custom-validators/activitypub/signature.ts | 22 +++ .../custom-validators/activitypub/videos.ts | 184 +++++++++++++++++++++ server/helpers/custom-validators/index.ts | 2 +- server/helpers/custom-validators/remote/index.ts | 1 - server/helpers/custom-validators/remote/videos.ts | 184 --------------------- server/helpers/ffmpeg-utils.ts | 1 - server/helpers/index.ts | 2 + server/helpers/peertube-crypto.ts | 158 ++++++------------ server/helpers/requests.ts | 78 ++++----- server/helpers/webfinger.ts | 44 +++++ 15 files changed, 627 insertions(+), 342 deletions(-) create mode 100644 server/helpers/activitypub.ts create mode 100644 server/helpers/custom-validators/activitypub/account.ts create mode 100644 server/helpers/custom-validators/activitypub/index.ts create mode 100644 server/helpers/custom-validators/activitypub/misc.ts create mode 100644 server/helpers/custom-validators/activitypub/signature.ts create mode 100644 server/helpers/custom-validators/activitypub/videos.ts delete mode 100644 server/helpers/custom-validators/remote/index.ts delete mode 100644 server/helpers/custom-validators/remote/videos.ts create mode 100644 server/helpers/webfinger.ts (limited to 'server/helpers') diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts new file mode 100644 index 000000000..ecb509b66 --- /dev/null +++ b/server/helpers/activitypub.ts @@ -0,0 +1,123 @@ +import * as url from 'url' + +import { database as db } from '../initializers' +import { logger } from './logger' +import { doRequest } from './requests' +import { isRemoteAccountValid } from './custom-validators' +import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' +import { ResultList } from '../../shared/models/result-list.model' + +async function fetchRemoteAccountAndCreatePod (accountUrl: string) { + const options = { + uri: accountUrl, + method: 'GET' + } + + let requestResult + try { + requestResult = await doRequest(options) + } catch (err) { + logger.warning('Cannot fetch remote account %s.', accountUrl, err) + return undefined + } + + const accountJSON: ActivityPubActor = requestResult.body + if (isRemoteAccountValid(accountJSON) === false) return undefined + + const followersCount = await fetchAccountCount(accountJSON.followers) + const followingCount = await fetchAccountCount(accountJSON.following) + + const account = db.Account.build({ + uuid: accountJSON.uuid, + name: accountJSON.preferredUsername, + url: accountJSON.url, + publicKey: accountJSON.publicKey.publicKeyPem, + privateKey: null, + followersCount: followersCount, + followingCount: followingCount, + inboxUrl: accountJSON.inbox, + outboxUrl: accountJSON.outbox, + sharedInboxUrl: accountJSON.endpoints.sharedInbox, + followersUrl: accountJSON.followers, + followingUrl: accountJSON.following + }) + + const accountHost = url.parse(account.url).host + const podOptions = { + where: { + host: accountHost + }, + defaults: { + host: accountHost + } + } + const pod = await db.Pod.findOrCreate(podOptions) + + return { account, pod } +} + +function activityPubContextify (data: object) { + return Object.assign(data,{ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + 'Hashtag': 'as:Hashtag', + 'uuid': 'http://schema.org/identifier', + 'category': 'http://schema.org/category', + 'licence': 'http://schema.org/license', + 'nsfw': 'as:sensitive', + 'language': 'http://schema.org/inLanguage', + 'views': 'http://schema.org/Number', + 'size': 'http://schema.org/Number' + } + ] + }) +} + +function activityPubCollectionPagination (url: string, page: number, result: ResultList) { + const baseUrl = url.split('?').shift + + const obj = { + id: baseUrl, + type: 'Collection', + totalItems: result.total, + first: { + id: baseUrl + '?page=' + page, + type: 'CollectionPage', + totalItems: result.total, + next: baseUrl + '?page=' + (page + 1), + partOf: baseUrl, + items: result.data + } + } + + return activityPubContextify(obj) +} + +// --------------------------------------------------------------------------- + +export { + fetchRemoteAccountAndCreatePod, + activityPubContextify, + activityPubCollectionPagination +} + +// --------------------------------------------------------------------------- + +async function fetchAccountCount (url: string) { + const options = { + uri: url, + method: 'GET' + } + + let requestResult + try { + requestResult = await doRequest(options) + } catch (err) { + logger.warning('Cannot fetch remote account count %s.', url, err) + return undefined + } + + return requestResult.totalItems ? requestResult.totalItems : 0 +} diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 3dae78144..d8748e1d7 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -19,8 +19,10 @@ import * as mkdirp from 'mkdirp' import * as bcrypt from 'bcrypt' import * as createTorrent from 'create-torrent' import * as rimraf from 'rimraf' -import * as openssl from 'openssl-wrapper' -import * as Promise from 'bluebird' +import * as pem from 'pem' +import * as jsonld from 'jsonld' +import * as jsig from 'jsonld-signatures' +jsig.use('jsonld', jsonld) function isTestInstance () { return process.env.NODE_ENV === 'test' @@ -54,6 +56,12 @@ function escapeHTML (stringParam) { return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s]) } +function pageToStartAndCount (page: number, itemsPerPage: number) { + const start = (page - 1) * itemsPerPage + + return { start, count: itemsPerPage } +} + function promisify0 (func: (cb: (err: any, result: A) => void) => void): () => Promise { return function promisified (): Promise { return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { @@ -104,13 +112,16 @@ const readdirPromise = promisify1(readdir) const mkdirpPromise = promisify1(mkdirp) const pseudoRandomBytesPromise = promisify1(pseudoRandomBytes) const accessPromise = promisify1WithVoid(access) -const opensslExecPromise = promisify2WithVoid(openssl.exec) +const createPrivateKey = promisify1(pem.createPrivateKey) +const getPublicKey = promisify1(pem.getPublicKey) const bcryptComparePromise = promisify2(bcrypt.compare) const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) const bcryptHashPromise = promisify2(bcrypt.hash) const createTorrentPromise = promisify2(createTorrent) const rimrafPromise = promisify1WithVoid(rimraf) const statPromise = promisify1(stat) +const jsonldSignPromise = promisify2(jsig.sign) +const jsonldVerifyPromise = promisify2(jsig.verify) // --------------------------------------------------------------------------- @@ -118,9 +129,11 @@ export { isTestInstance, root, escapeHTML, + pageToStartAndCount, promisify0, promisify1, + readdirPromise, readFilePromise, readFileBufferPromise, @@ -130,11 +143,14 @@ export { mkdirpPromise, pseudoRandomBytesPromise, accessPromise, - opensslExecPromise, + createPrivateKey, + getPublicKey, bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createTorrentPromise, rimrafPromise, - statPromise + statPromise, + jsonldSignPromise, + jsonldVerifyPromise } diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts new file mode 100644 index 000000000..8a7d1b7fe --- /dev/null +++ b/server/helpers/custom-validators/activitypub/account.ts @@ -0,0 +1,123 @@ +import * as validator from 'validator' + +import { exists, isUUIDValid } from '../misc' +import { isActivityPubUrlValid } from './misc' +import { isUserUsernameValid } from '../users' + +function isAccountEndpointsObjectValid (endpointObject: any) { + return isAccountSharedInboxValid(endpointObject.sharedInbox) +} + +function isAccountSharedInboxValid (sharedInbox: string) { + return isActivityPubUrlValid(sharedInbox) +} + +function isAccountPublicKeyObjectValid (publicKeyObject: any) { + return isAccountPublicKeyIdValid(publicKeyObject.id) && + isAccountPublicKeyOwnerValid(publicKeyObject.owner) && + isAccountPublicKeyValid(publicKeyObject.publicKeyPem) +} + +function isAccountPublicKeyIdValid (id: string) { + return isActivityPubUrlValid(id) +} + +function isAccountTypeValid (type: string) { + return type === 'Person' || type === 'Application' +} + +function isAccountPublicKeyOwnerValid (owner: string) { + return isActivityPubUrlValid(owner) +} + +function isAccountPublicKeyValid (publicKey: string) { + return exists(publicKey) && + typeof publicKey === 'string' && + publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && + publicKey.endsWith('-----END PUBLIC KEY-----') +} + +function isAccountIdValid (id: string) { + return isActivityPubUrlValid(id) +} + +function isAccountFollowingValid (id: string) { + return isActivityPubUrlValid(id) +} + +function isAccountFollowersValid (id: string) { + return isActivityPubUrlValid(id) +} + +function isAccountInboxValid (inbox: string) { + return isActivityPubUrlValid(inbox) +} + +function isAccountOutboxValid (outbox: string) { + return isActivityPubUrlValid(outbox) +} + +function isAccountNameValid (name: string) { + return isUserUsernameValid(name) +} + +function isAccountPreferredUsernameValid (preferredUsername: string) { + return isAccountNameValid(preferredUsername) +} + +function isAccountUrlValid (url: string) { + return isActivityPubUrlValid(url) +} + +function isAccountPrivateKeyValid (privateKey: string) { + return exists(privateKey) && + typeof privateKey === 'string' && + privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && + privateKey.endsWith('-----END RSA PRIVATE KEY-----') +} + +function isRemoteAccountValid (remoteAccount: any) { + return isAccountIdValid(remoteAccount.id) && + isUUIDValid(remoteAccount.uuid) && + isAccountTypeValid(remoteAccount.type) && + isAccountFollowingValid(remoteAccount.following) && + isAccountFollowersValid(remoteAccount.followers) && + isAccountInboxValid(remoteAccount.inbox) && + isAccountOutboxValid(remoteAccount.outbox) && + isAccountPreferredUsernameValid(remoteAccount.preferredUsername) && + isAccountUrlValid(remoteAccount.url) && + isAccountPublicKeyObjectValid(remoteAccount.publicKey) && + isAccountEndpointsObjectValid(remoteAccount.endpoint) +} + +function isAccountFollowingCountValid (value: string) { + return exists(value) && validator.isInt('' + value, { min: 0 }) +} + +function isAccountFollowersCountValid (value: string) { + return exists(value) && validator.isInt('' + value, { min: 0 }) +} + +// --------------------------------------------------------------------------- + +export { + isAccountEndpointsObjectValid, + isAccountSharedInboxValid, + isAccountPublicKeyObjectValid, + isAccountPublicKeyIdValid, + isAccountTypeValid, + isAccountPublicKeyOwnerValid, + isAccountPublicKeyValid, + isAccountIdValid, + isAccountFollowingValid, + isAccountFollowersValid, + isAccountInboxValid, + isAccountOutboxValid, + isAccountPreferredUsernameValid, + isAccountUrlValid, + isAccountPrivateKeyValid, + isRemoteAccountValid, + isAccountFollowingCountValid, + isAccountFollowersCountValid, + isAccountNameValid +} diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts new file mode 100644 index 000000000..800f0ddf3 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/index.ts @@ -0,0 +1,4 @@ +export * from './account' +export * from './signature' +export * from './misc' +export * from './videos' diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts new file mode 100644 index 000000000..806d33483 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/misc.ts @@ -0,0 +1,17 @@ +import { exists } from '../misc' + +function isActivityPubUrlValid (url: string) { + const isURLOptions = { + require_host: true, + require_tld: true, + require_protocol: true, + require_valid_protocol: true, + protocols: [ 'http', 'https' ] + } + + return exists(url) && validator.isURL(url, isURLOptions) +} + +export { + isActivityPubUrlValid +} diff --git a/server/helpers/custom-validators/activitypub/signature.ts b/server/helpers/custom-validators/activitypub/signature.ts new file mode 100644 index 000000000..683ed2b1c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/signature.ts @@ -0,0 +1,22 @@ +import { exists } from '../misc' +import { isActivityPubUrlValid } from './misc' + +function isSignatureTypeValid (signatureType: string) { + return exists(signatureType) && signatureType === 'GraphSignature2012' +} + +function isSignatureCreatorValid (signatureCreator: string) { + return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator) +} + +function isSignatureValueValid (signatureValue: string) { + return exists(signatureValue) && signatureValue.length > 0 +} + +// --------------------------------------------------------------------------- + +export { + isSignatureTypeValid, + isSignatureCreatorValid, + isSignatureValueValid +} diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts new file mode 100644 index 000000000..e0ffba679 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -0,0 +1,184 @@ +import 'express-validator' +import { has, values } from 'lodash' + +import { + REQUEST_ENDPOINTS, + REQUEST_ENDPOINT_ACTIONS, + REQUEST_VIDEO_EVENT_TYPES +} from '../../../initializers' +import { isArray, isDateValid, isUUIDValid } from '../misc' +import { + isVideoThumbnailDataValid, + isVideoAbuseReasonValid, + isVideoAbuseReporterUsernameValid, + isVideoViewsValid, + isVideoLikesValid, + isVideoDislikesValid, + isVideoEventCountValid, + isRemoteVideoCategoryValid, + isRemoteVideoLicenceValid, + isRemoteVideoLanguageValid, + isVideoNSFWValid, + isVideoTruncatedDescriptionValid, + isVideoDurationValid, + isVideoFileInfoHashValid, + isVideoNameValid, + isVideoTagsValid, + isVideoFileExtnameValid, + isVideoFileResolutionValid +} from '../videos' +import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' +import { isVideoAuthorNameValid } from '../video-authors' + +const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] + +const checkers: { [ id: string ]: (obj: any) => boolean } = {} +checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo +checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo +checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo +checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo +checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel +checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel +checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel +checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor +checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor + +function removeBadRequestVideos (requests: any[]) { + for (let i = requests.length - 1; i >= 0 ; i--) { + const request = requests[i] + const video = request.data + + if ( + !video || + checkers[request.type] === undefined || + checkers[request.type](video) === false + ) { + requests.splice(i, 1) + } + } +} + +function removeBadRequestVideosQadu (requests: any[]) { + for (let i = requests.length - 1; i >= 0 ; i--) { + const request = requests[i] + const video = request.data + + if ( + !video || + ( + isUUIDValid(video.uuid) && + (has(video, 'views') === false || isVideoViewsValid(video.views)) && + (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && + (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) + ) === false + ) { + requests.splice(i, 1) + } + } +} + +function removeBadRequestVideosEvents (requests: any[]) { + for (let i = requests.length - 1; i >= 0 ; i--) { + const request = requests[i] + const eventData = request.data + + if ( + !eventData || + ( + isUUIDValid(eventData.uuid) && + values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && + isVideoEventCountValid(eventData.count) + ) === false + ) { + requests.splice(i, 1) + } + } +} + +// --------------------------------------------------------------------------- + +export { + removeBadRequestVideos, + removeBadRequestVideosQadu, + removeBadRequestVideosEvents +} + +// --------------------------------------------------------------------------- + +function isCommonVideoAttributesValid (video: any) { + return isDateValid(video.createdAt) && + isDateValid(video.updatedAt) && + isRemoteVideoCategoryValid(video.category) && + isRemoteVideoLicenceValid(video.licence) && + isRemoteVideoLanguageValid(video.language) && + isVideoNSFWValid(video.nsfw) && + isVideoTruncatedDescriptionValid(video.truncatedDescription) && + isVideoDurationValid(video.duration) && + isVideoNameValid(video.name) && + isVideoTagsValid(video.tags) && + isUUIDValid(video.uuid) && + isVideoViewsValid(video.views) && + isVideoLikesValid(video.likes) && + isVideoDislikesValid(video.dislikes) && + isArray(video.files) && + video.files.every(videoFile => { + if (!videoFile) return false + + return ( + isVideoFileInfoHashValid(videoFile.infoHash) && + isVideoFileExtnameValid(videoFile.extname) && + isVideoFileResolutionValid(videoFile.resolution) + ) + }) +} + +function checkAddVideo (video: any) { + return isCommonVideoAttributesValid(video) && + isUUIDValid(video.channelUUID) && + isVideoThumbnailDataValid(video.thumbnailData) +} + +function checkUpdateVideo (video: any) { + return isCommonVideoAttributesValid(video) +} + +function checkRemoveVideo (video: any) { + return isUUIDValid(video.uuid) +} + +function checkReportVideo (abuse: any) { + return isUUIDValid(abuse.videoUUID) && + isVideoAbuseReasonValid(abuse.reportReason) && + isVideoAbuseReporterUsernameValid(abuse.reporterUsername) +} + +function checkAddVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) && + isVideoChannelNameValid(videoChannel.name) && + isVideoChannelDescriptionValid(videoChannel.description) && + isDateValid(videoChannel.createdAt) && + isDateValid(videoChannel.updatedAt) && + isUUIDValid(videoChannel.ownerUUID) +} + +function checkUpdateVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) && + isVideoChannelNameValid(videoChannel.name) && + isVideoChannelDescriptionValid(videoChannel.description) && + isDateValid(videoChannel.createdAt) && + isDateValid(videoChannel.updatedAt) && + isUUIDValid(videoChannel.ownerUUID) +} + +function checkRemoveVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) +} + +function checkAddAuthor (author: any) { + return isUUIDValid(author.uuid) && + isVideoAuthorNameValid(author.name) +} + +function checkRemoveAuthor (author: any) { + return isUUIDValid(author.uuid) +} diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts index c79982660..869b08870 100644 --- a/server/helpers/custom-validators/index.ts +++ b/server/helpers/custom-validators/index.ts @@ -1,4 +1,4 @@ -export * from './remote' +export * from './activitypub' export * from './misc' export * from './pods' export * from './pods' diff --git a/server/helpers/custom-validators/remote/index.ts b/server/helpers/custom-validators/remote/index.ts deleted file mode 100644 index e29a9b767..000000000 --- a/server/helpers/custom-validators/remote/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './videos' diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/remote/videos.ts deleted file mode 100644 index e0ffba679..000000000 --- a/server/helpers/custom-validators/remote/videos.ts +++ /dev/null @@ -1,184 +0,0 @@ -import 'express-validator' -import { has, values } from 'lodash' - -import { - REQUEST_ENDPOINTS, - REQUEST_ENDPOINT_ACTIONS, - REQUEST_VIDEO_EVENT_TYPES -} from '../../../initializers' -import { isArray, isDateValid, isUUIDValid } from '../misc' -import { - isVideoThumbnailDataValid, - isVideoAbuseReasonValid, - isVideoAbuseReporterUsernameValid, - isVideoViewsValid, - isVideoLikesValid, - isVideoDislikesValid, - isVideoEventCountValid, - isRemoteVideoCategoryValid, - isRemoteVideoLicenceValid, - isRemoteVideoLanguageValid, - isVideoNSFWValid, - isVideoTruncatedDescriptionValid, - isVideoDurationValid, - isVideoFileInfoHashValid, - isVideoNameValid, - isVideoTagsValid, - isVideoFileExtnameValid, - isVideoFileResolutionValid -} from '../videos' -import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' -import { isVideoAuthorNameValid } from '../video-authors' - -const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] - -const checkers: { [ id: string ]: (obj: any) => boolean } = {} -checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo -checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo -checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo -checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo -checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel -checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel -checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel -checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor -checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor - -function removeBadRequestVideos (requests: any[]) { - for (let i = requests.length - 1; i >= 0 ; i--) { - const request = requests[i] - const video = request.data - - if ( - !video || - checkers[request.type] === undefined || - checkers[request.type](video) === false - ) { - requests.splice(i, 1) - } - } -} - -function removeBadRequestVideosQadu (requests: any[]) { - for (let i = requests.length - 1; i >= 0 ; i--) { - const request = requests[i] - const video = request.data - - if ( - !video || - ( - isUUIDValid(video.uuid) && - (has(video, 'views') === false || isVideoViewsValid(video.views)) && - (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && - (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) - ) === false - ) { - requests.splice(i, 1) - } - } -} - -function removeBadRequestVideosEvents (requests: any[]) { - for (let i = requests.length - 1; i >= 0 ; i--) { - const request = requests[i] - const eventData = request.data - - if ( - !eventData || - ( - isUUIDValid(eventData.uuid) && - values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && - isVideoEventCountValid(eventData.count) - ) === false - ) { - requests.splice(i, 1) - } - } -} - -// --------------------------------------------------------------------------- - -export { - removeBadRequestVideos, - removeBadRequestVideosQadu, - removeBadRequestVideosEvents -} - -// --------------------------------------------------------------------------- - -function isCommonVideoAttributesValid (video: any) { - return isDateValid(video.createdAt) && - isDateValid(video.updatedAt) && - isRemoteVideoCategoryValid(video.category) && - isRemoteVideoLicenceValid(video.licence) && - isRemoteVideoLanguageValid(video.language) && - isVideoNSFWValid(video.nsfw) && - isVideoTruncatedDescriptionValid(video.truncatedDescription) && - isVideoDurationValid(video.duration) && - isVideoNameValid(video.name) && - isVideoTagsValid(video.tags) && - isUUIDValid(video.uuid) && - isVideoViewsValid(video.views) && - isVideoLikesValid(video.likes) && - isVideoDislikesValid(video.dislikes) && - isArray(video.files) && - video.files.every(videoFile => { - if (!videoFile) return false - - return ( - isVideoFileInfoHashValid(videoFile.infoHash) && - isVideoFileExtnameValid(videoFile.extname) && - isVideoFileResolutionValid(videoFile.resolution) - ) - }) -} - -function checkAddVideo (video: any) { - return isCommonVideoAttributesValid(video) && - isUUIDValid(video.channelUUID) && - isVideoThumbnailDataValid(video.thumbnailData) -} - -function checkUpdateVideo (video: any) { - return isCommonVideoAttributesValid(video) -} - -function checkRemoveVideo (video: any) { - return isUUIDValid(video.uuid) -} - -function checkReportVideo (abuse: any) { - return isUUIDValid(abuse.videoUUID) && - isVideoAbuseReasonValid(abuse.reportReason) && - isVideoAbuseReporterUsernameValid(abuse.reporterUsername) -} - -function checkAddVideoChannel (videoChannel: any) { - return isUUIDValid(videoChannel.uuid) && - isVideoChannelNameValid(videoChannel.name) && - isVideoChannelDescriptionValid(videoChannel.description) && - isDateValid(videoChannel.createdAt) && - isDateValid(videoChannel.updatedAt) && - isUUIDValid(videoChannel.ownerUUID) -} - -function checkUpdateVideoChannel (videoChannel: any) { - return isUUIDValid(videoChannel.uuid) && - isVideoChannelNameValid(videoChannel.name) && - isVideoChannelDescriptionValid(videoChannel.description) && - isDateValid(videoChannel.createdAt) && - isDateValid(videoChannel.updatedAt) && - isUUIDValid(videoChannel.ownerUUID) -} - -function checkRemoveVideoChannel (videoChannel: any) { - return isUUIDValid(videoChannel.uuid) -} - -function checkAddAuthor (author: any) { - return isUUIDValid(author.uuid) && - isVideoAuthorNameValid(author.name) -} - -function checkRemoveAuthor (author: any) { - return isUUIDValid(author.uuid) -} diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f18b6bd9a..c07dddefe 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,4 +1,3 @@ -import * as Promise from 'bluebird' import * as ffmpeg from 'fluent-ffmpeg' import { CONFIG } from '../initializers' diff --git a/server/helpers/index.ts b/server/helpers/index.ts index 846bd796f..2c7ac3954 100644 --- a/server/helpers/index.ts +++ b/server/helpers/index.ts @@ -1,3 +1,4 @@ +export * from './activitypub' export * from './core-utils' export * from './logger' export * from './custom-validators' @@ -6,3 +7,4 @@ export * from './database-utils' export * from './peertube-crypto' export * from './requests' export * from './utils' +export * from './webfinger' diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 10a226af4..6d50e446f 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,77 +1,68 @@ -import * as crypto from 'crypto' -import { join } from 'path' +import * as jsig from 'jsonld-signatures' import { - SIGNATURE_ALGORITHM, - SIGNATURE_ENCODING, - PRIVATE_CERT_NAME, - CONFIG, - BCRYPT_SALT_SIZE, - PUBLIC_CERT_NAME + PRIVATE_RSA_KEY_SIZE, + BCRYPT_SALT_SIZE } from '../initializers' import { - readFilePromise, bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, - accessPromise, - opensslExecPromise + createPrivateKey, + getPublicKey, + jsonldSignPromise, + jsonldVerifyPromise } from './core-utils' import { logger } from './logger' +import { AccountInstance } from '../models/account/account-interface' -function checkSignature (publicKey: string, data: string, hexSignature: string) { - const verify = crypto.createVerify(SIGNATURE_ALGORITHM) - - let dataString - if (typeof data === 'string') { - dataString = data - } else { - try { - dataString = JSON.stringify(data) - } catch (err) { - logger.error('Cannot check signature.', err) - return false - } - } +async function createPrivateAndPublicKeys () { + logger.info('Generating a RSA key...') - verify.update(dataString, 'utf8') + const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE) + const { publicKey } = await getPublicKey(key) - const isValid = verify.verify(publicKey, hexSignature, SIGNATURE_ENCODING) - return isValid + return { privateKey: key, publicKey } } -async function sign (data: string | Object) { - const sign = crypto.createSign(SIGNATURE_ALGORITHM) - - let dataString: string - if (typeof data === 'string') { - dataString = data - } else { - try { - dataString = JSON.stringify(data) - } catch (err) { - logger.error('Cannot sign data.', err) - return '' - } +function isSignatureVerified (fromAccount: AccountInstance, signedDocument: object) { + const publicKeyObject = { + '@context': jsig.SECURITY_CONTEXT_URL, + '@id': fromAccount.url, + '@type': 'CryptographicKey', + owner: fromAccount.url, + publicKeyPem: fromAccount.publicKey } - sign.update(dataString, 'utf8') + const publicKeyOwnerObject = { + '@context': jsig.SECURITY_CONTEXT_URL, + '@id': fromAccount.url, + publicKey: [ publicKeyObject ] + } - const myKey = await getMyPrivateCert() - return sign.sign(myKey, SIGNATURE_ENCODING) -} + const options = { + publicKey: publicKeyObject, + publicKeyOwner: publicKeyOwnerObject + } -function comparePassword (plainPassword: string, hashPassword: string) { - return bcryptComparePromise(plainPassword, hashPassword) + return jsonldVerifyPromise(signedDocument, options) + .catch(err => { + logger.error('Cannot check signature.', err) + return false + }) } -async function createCertsIfNotExist () { - const exist = await certsExist() - if (exist === true) { - return +function signObject (byAccount: AccountInstance, data: any) { + const options = { + privateKeyPem: byAccount.privateKey, + creator: byAccount.url } - return createCerts() + return jsonldSignPromise(data, options) +} + +function comparePassword (plainPassword: string, hashPassword: string) { + return bcryptComparePromise(plainPassword, hashPassword) } async function cryptPassword (password: string) { @@ -80,69 +71,12 @@ async function cryptPassword (password: string) { return bcryptHashPromise(password, salt) } -function getMyPrivateCert () { - const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) - return readFilePromise(certPath, 'utf8') -} - -function getMyPublicCert () { - const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME) - return readFilePromise(certPath, 'utf8') -} - // --------------------------------------------------------------------------- export { - checkSignature, + isSignatureVerified, comparePassword, - createCertsIfNotExist, + createPrivateAndPublicKeys, cryptPassword, - getMyPrivateCert, - getMyPublicCert, - sign -} - -// --------------------------------------------------------------------------- - -async function certsExist () { - const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) - - // If there is an error the certificates do not exist - try { - await accessPromise(certPath) - - return true - } catch { - return false - } -} - -async function createCerts () { - const exist = await certsExist() - if (exist === true) { - const errorMessage = 'Certs already exist.' - logger.warning(errorMessage) - throw new Error(errorMessage) - } - - logger.info('Generating a RSA key...') - - const privateCertPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) - const genRsaOptions = { - 'out': privateCertPath, - '2048': false - } - - await opensslExecPromise('genrsa', genRsaOptions) - logger.info('RSA key generated.') - logger.info('Managing public key...') - - const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub') - const rsaOptions = { - 'in': privateCertPath, - 'pubout': true, - 'out': publicCertPath - } - - await opensslExecPromise('rsa', rsaOptions) + signObject } diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index af1f401de..8c4c983f7 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -9,7 +9,13 @@ import { } from '../initializers' import { PodInstance } from '../models' import { PodSignature } from '../../shared' -import { sign } from './peertube-crypto' +import { signObject } from './peertube-crypto' + +function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { + return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { + request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) + }) +} type MakeRetryRequestParams = { url: string, @@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) { } type MakeSecureRequestParams = { - method: 'GET' | 'POST' toPod: PodInstance path: string data?: Object } function makeSecureRequest (params: MakeSecureRequestParams) { - return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { - const requestParams: { - url: string, - json: { - signature: PodSignature, - data: any - } - } = { - url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, - json: { - signature: null, - data: null - } + const requestParams: { + method: 'POST', + uri: string, + json: { + signature: PodSignature, + data: any } - - if (params.method !== 'POST') { - return rej(new Error('Cannot make a secure request with a non POST method.')) + } = { + method: 'POST', + uri: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, + json: { + signature: null, + data: null } + } - const host = CONFIG.WEBSERVER.HOST + const host = CONFIG.WEBSERVER.HOST - let dataToSign - if (params.data) { - dataToSign = params.data - } else { - // We do not have data to sign so we just take our host - // It is not ideal but the connection should be in HTTPS - dataToSign = host - } + let dataToSign + if (params.data) { + dataToSign = params.data + } else { + // We do not have data to sign so we just take our host + // It is not ideal but the connection should be in HTTPS + dataToSign = host + } - sign(dataToSign).then(signature => { - requestParams.json.signature = { - host, // Which host we pretend to be - signature - } + sign(dataToSign).then(signature => { + requestParams.json.signature = { + host, // Which host we pretend to be + signature + } - // If there are data information - if (params.data) { - requestParams.json.data = params.data - } + // If there are data information + if (params.data) { + requestParams.json.data = params.data + } - request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body })) - }) + return doRequest(requestParams) }) } // --------------------------------------------------------------------------- export { + doRequest, makeRetryRequest, makeSecureRequest } diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts new file mode 100644 index 000000000..9586fa562 --- /dev/null +++ b/server/helpers/webfinger.ts @@ -0,0 +1,44 @@ +import * as WebFinger from 'webfinger.js' + +import { isTestInstance } from './core-utils' +import { isActivityPubUrlValid } from './custom-validators' +import { WebFingerData } from '../../shared' +import { fetchRemoteAccountAndCreatePod } from './activitypub' + +const webfinger = new WebFinger({ + webfist_fallback: false, + tls_only: isTestInstance(), + uri_fallback: false, + request_timeout: 3000 +}) + +async function getAccountFromWebfinger (url: string) { + const webfingerData: WebFingerData = await webfingerLookup(url) + + if (Array.isArray(webfingerData.links) === false) return undefined + + const selfLink = webfingerData.links.find(l => l.rel === 'self') + if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined + + const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href) + + return account +} + +// --------------------------------------------------------------------------- + +export { + getAccountFromWebfinger +} + +// --------------------------------------------------------------------------- + +function webfingerLookup (url: string) { + return new Promise((res, rej) => { + webfinger.lookup('nick@silverbucket.net', (err, p) => { + if (err) return rej(err) + + return p + }) + }) +} -- cgit v1.2.3