diff options
Diffstat (limited to 'server/helpers')
-rw-r--r-- | server/helpers/activitypub.ts | 123 | ||||
-rw-r--r-- | server/helpers/core-utils.ts | 26 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/account.ts | 123 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/index.ts | 4 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/misc.ts | 17 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/signature.ts | 22 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/videos.ts (renamed from server/helpers/custom-validators/remote/videos.ts) | 0 | ||||
-rw-r--r-- | server/helpers/custom-validators/index.ts | 2 | ||||
-rw-r--r-- | server/helpers/custom-validators/remote/index.ts | 1 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 1 | ||||
-rw-r--r-- | server/helpers/index.ts | 2 | ||||
-rw-r--r-- | server/helpers/peertube-crypto.ts | 158 | ||||
-rw-r--r-- | server/helpers/requests.ts | 78 | ||||
-rw-r--r-- | server/helpers/webfinger.ts | 44 |
14 files changed, 443 insertions, 158 deletions
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 @@ | |||
1 | import * as url from 'url' | ||
2 | |||
3 | import { database as db } from '../initializers' | ||
4 | import { logger } from './logger' | ||
5 | import { doRequest } from './requests' | ||
6 | import { isRemoteAccountValid } from './custom-validators' | ||
7 | import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' | ||
8 | import { ResultList } from '../../shared/models/result-list.model' | ||
9 | |||
10 | async function fetchRemoteAccountAndCreatePod (accountUrl: string) { | ||
11 | const options = { | ||
12 | uri: accountUrl, | ||
13 | method: 'GET' | ||
14 | } | ||
15 | |||
16 | let requestResult | ||
17 | try { | ||
18 | requestResult = await doRequest(options) | ||
19 | } catch (err) { | ||
20 | logger.warning('Cannot fetch remote account %s.', accountUrl, err) | ||
21 | return undefined | ||
22 | } | ||
23 | |||
24 | const accountJSON: ActivityPubActor = requestResult.body | ||
25 | if (isRemoteAccountValid(accountJSON) === false) return undefined | ||
26 | |||
27 | const followersCount = await fetchAccountCount(accountJSON.followers) | ||
28 | const followingCount = await fetchAccountCount(accountJSON.following) | ||
29 | |||
30 | const account = db.Account.build({ | ||
31 | uuid: accountJSON.uuid, | ||
32 | name: accountJSON.preferredUsername, | ||
33 | url: accountJSON.url, | ||
34 | publicKey: accountJSON.publicKey.publicKeyPem, | ||
35 | privateKey: null, | ||
36 | followersCount: followersCount, | ||
37 | followingCount: followingCount, | ||
38 | inboxUrl: accountJSON.inbox, | ||
39 | outboxUrl: accountJSON.outbox, | ||
40 | sharedInboxUrl: accountJSON.endpoints.sharedInbox, | ||
41 | followersUrl: accountJSON.followers, | ||
42 | followingUrl: accountJSON.following | ||
43 | }) | ||
44 | |||
45 | const accountHost = url.parse(account.url).host | ||
46 | const podOptions = { | ||
47 | where: { | ||
48 | host: accountHost | ||
49 | }, | ||
50 | defaults: { | ||
51 | host: accountHost | ||
52 | } | ||
53 | } | ||
54 | const pod = await db.Pod.findOrCreate(podOptions) | ||
55 | |||
56 | return { account, pod } | ||
57 | } | ||
58 | |||
59 | function activityPubContextify (data: object) { | ||
60 | return Object.assign(data,{ | ||
61 | '@context': [ | ||
62 | 'https://www.w3.org/ns/activitystreams', | ||
63 | 'https://w3id.org/security/v1', | ||
64 | { | ||
65 | 'Hashtag': 'as:Hashtag', | ||
66 | 'uuid': 'http://schema.org/identifier', | ||
67 | 'category': 'http://schema.org/category', | ||
68 | 'licence': 'http://schema.org/license', | ||
69 | 'nsfw': 'as:sensitive', | ||
70 | 'language': 'http://schema.org/inLanguage', | ||
71 | 'views': 'http://schema.org/Number', | ||
72 | 'size': 'http://schema.org/Number' | ||
73 | } | ||
74 | ] | ||
75 | }) | ||
76 | } | ||
77 | |||
78 | function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) { | ||
79 | const baseUrl = url.split('?').shift | ||
80 | |||
81 | const obj = { | ||
82 | id: baseUrl, | ||
83 | type: 'Collection', | ||
84 | totalItems: result.total, | ||
85 | first: { | ||
86 | id: baseUrl + '?page=' + page, | ||
87 | type: 'CollectionPage', | ||
88 | totalItems: result.total, | ||
89 | next: baseUrl + '?page=' + (page + 1), | ||
90 | partOf: baseUrl, | ||
91 | items: result.data | ||
92 | } | ||
93 | } | ||
94 | |||
95 | return activityPubContextify(obj) | ||
96 | } | ||
97 | |||
98 | // --------------------------------------------------------------------------- | ||
99 | |||
100 | export { | ||
101 | fetchRemoteAccountAndCreatePod, | ||
102 | activityPubContextify, | ||
103 | activityPubCollectionPagination | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | async function fetchAccountCount (url: string) { | ||
109 | const options = { | ||
110 | uri: url, | ||
111 | method: 'GET' | ||
112 | } | ||
113 | |||
114 | let requestResult | ||
115 | try { | ||
116 | requestResult = await doRequest(options) | ||
117 | } catch (err) { | ||
118 | logger.warning('Cannot fetch remote account count %s.', url, err) | ||
119 | return undefined | ||
120 | } | ||
121 | |||
122 | return requestResult.totalItems ? requestResult.totalItems : 0 | ||
123 | } | ||
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' | |||
19 | import * as bcrypt from 'bcrypt' | 19 | import * as bcrypt from 'bcrypt' |
20 | import * as createTorrent from 'create-torrent' | 20 | import * as createTorrent from 'create-torrent' |
21 | import * as rimraf from 'rimraf' | 21 | import * as rimraf from 'rimraf' |
22 | import * as openssl from 'openssl-wrapper' | 22 | import * as pem from 'pem' |
23 | import * as Promise from 'bluebird' | 23 | import * as jsonld from 'jsonld' |
24 | import * as jsig from 'jsonld-signatures' | ||
25 | jsig.use('jsonld', jsonld) | ||
24 | 26 | ||
25 | function isTestInstance () { | 27 | function isTestInstance () { |
26 | return process.env.NODE_ENV === 'test' | 28 | return process.env.NODE_ENV === 'test' |
@@ -54,6 +56,12 @@ function escapeHTML (stringParam) { | |||
54 | return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s]) | 56 | return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s]) |
55 | } | 57 | } |
56 | 58 | ||
59 | function pageToStartAndCount (page: number, itemsPerPage: number) { | ||
60 | const start = (page - 1) * itemsPerPage | ||
61 | |||
62 | return { start, count: itemsPerPage } | ||
63 | } | ||
64 | |||
57 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 65 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
58 | return function promisified (): Promise<A> { | 66 | return function promisified (): Promise<A> { |
59 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | 67 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { |
@@ -104,13 +112,16 @@ const readdirPromise = promisify1<string, string[]>(readdir) | |||
104 | const mkdirpPromise = promisify1<string, string>(mkdirp) | 112 | const mkdirpPromise = promisify1<string, string>(mkdirp) |
105 | const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes) | 113 | const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes) |
106 | const accessPromise = promisify1WithVoid<string | Buffer>(access) | 114 | const accessPromise = promisify1WithVoid<string | Buffer>(access) |
107 | const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec) | 115 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) |
116 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | ||
108 | const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) | 117 | const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) |
109 | const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) | 118 | const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) |
110 | const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash) | 119 | const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash) |
111 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) | 120 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) |
112 | const rimrafPromise = promisify1WithVoid<string>(rimraf) | 121 | const rimrafPromise = promisify1WithVoid<string>(rimraf) |
113 | const statPromise = promisify1<string, Stats>(stat) | 122 | const statPromise = promisify1<string, Stats>(stat) |
123 | const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign) | ||
124 | const jsonldVerifyPromise = promisify2<object, object, object>(jsig.verify) | ||
114 | 125 | ||
115 | // --------------------------------------------------------------------------- | 126 | // --------------------------------------------------------------------------- |
116 | 127 | ||
@@ -118,9 +129,11 @@ export { | |||
118 | isTestInstance, | 129 | isTestInstance, |
119 | root, | 130 | root, |
120 | escapeHTML, | 131 | escapeHTML, |
132 | pageToStartAndCount, | ||
121 | 133 | ||
122 | promisify0, | 134 | promisify0, |
123 | promisify1, | 135 | promisify1, |
136 | |||
124 | readdirPromise, | 137 | readdirPromise, |
125 | readFilePromise, | 138 | readFilePromise, |
126 | readFileBufferPromise, | 139 | readFileBufferPromise, |
@@ -130,11 +143,14 @@ export { | |||
130 | mkdirpPromise, | 143 | mkdirpPromise, |
131 | pseudoRandomBytesPromise, | 144 | pseudoRandomBytesPromise, |
132 | accessPromise, | 145 | accessPromise, |
133 | opensslExecPromise, | 146 | createPrivateKey, |
147 | getPublicKey, | ||
134 | bcryptComparePromise, | 148 | bcryptComparePromise, |
135 | bcryptGenSaltPromise, | 149 | bcryptGenSaltPromise, |
136 | bcryptHashPromise, | 150 | bcryptHashPromise, |
137 | createTorrentPromise, | 151 | createTorrentPromise, |
138 | rimrafPromise, | 152 | rimrafPromise, |
139 | statPromise | 153 | statPromise, |
154 | jsonldSignPromise, | ||
155 | jsonldVerifyPromise | ||
140 | } | 156 | } |
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 @@ | |||
1 | import * as validator from 'validator' | ||
2 | |||
3 | import { exists, isUUIDValid } from '../misc' | ||
4 | import { isActivityPubUrlValid } from './misc' | ||
5 | import { isUserUsernameValid } from '../users' | ||
6 | |||
7 | function isAccountEndpointsObjectValid (endpointObject: any) { | ||
8 | return isAccountSharedInboxValid(endpointObject.sharedInbox) | ||
9 | } | ||
10 | |||
11 | function isAccountSharedInboxValid (sharedInbox: string) { | ||
12 | return isActivityPubUrlValid(sharedInbox) | ||
13 | } | ||
14 | |||
15 | function isAccountPublicKeyObjectValid (publicKeyObject: any) { | ||
16 | return isAccountPublicKeyIdValid(publicKeyObject.id) && | ||
17 | isAccountPublicKeyOwnerValid(publicKeyObject.owner) && | ||
18 | isAccountPublicKeyValid(publicKeyObject.publicKeyPem) | ||
19 | } | ||
20 | |||
21 | function isAccountPublicKeyIdValid (id: string) { | ||
22 | return isActivityPubUrlValid(id) | ||
23 | } | ||
24 | |||
25 | function isAccountTypeValid (type: string) { | ||
26 | return type === 'Person' || type === 'Application' | ||
27 | } | ||
28 | |||
29 | function isAccountPublicKeyOwnerValid (owner: string) { | ||
30 | return isActivityPubUrlValid(owner) | ||
31 | } | ||
32 | |||
33 | function isAccountPublicKeyValid (publicKey: string) { | ||
34 | return exists(publicKey) && | ||
35 | typeof publicKey === 'string' && | ||
36 | publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && | ||
37 | publicKey.endsWith('-----END PUBLIC KEY-----') | ||
38 | } | ||
39 | |||
40 | function isAccountIdValid (id: string) { | ||
41 | return isActivityPubUrlValid(id) | ||
42 | } | ||
43 | |||
44 | function isAccountFollowingValid (id: string) { | ||
45 | return isActivityPubUrlValid(id) | ||
46 | } | ||
47 | |||
48 | function isAccountFollowersValid (id: string) { | ||
49 | return isActivityPubUrlValid(id) | ||
50 | } | ||
51 | |||
52 | function isAccountInboxValid (inbox: string) { | ||
53 | return isActivityPubUrlValid(inbox) | ||
54 | } | ||
55 | |||
56 | function isAccountOutboxValid (outbox: string) { | ||
57 | return isActivityPubUrlValid(outbox) | ||
58 | } | ||
59 | |||
60 | function isAccountNameValid (name: string) { | ||
61 | return isUserUsernameValid(name) | ||
62 | } | ||
63 | |||
64 | function isAccountPreferredUsernameValid (preferredUsername: string) { | ||
65 | return isAccountNameValid(preferredUsername) | ||
66 | } | ||
67 | |||
68 | function isAccountUrlValid (url: string) { | ||
69 | return isActivityPubUrlValid(url) | ||
70 | } | ||
71 | |||
72 | function isAccountPrivateKeyValid (privateKey: string) { | ||
73 | return exists(privateKey) && | ||
74 | typeof privateKey === 'string' && | ||
75 | privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && | ||
76 | privateKey.endsWith('-----END RSA PRIVATE KEY-----') | ||
77 | } | ||
78 | |||
79 | function isRemoteAccountValid (remoteAccount: any) { | ||
80 | return isAccountIdValid(remoteAccount.id) && | ||
81 | isUUIDValid(remoteAccount.uuid) && | ||
82 | isAccountTypeValid(remoteAccount.type) && | ||
83 | isAccountFollowingValid(remoteAccount.following) && | ||
84 | isAccountFollowersValid(remoteAccount.followers) && | ||
85 | isAccountInboxValid(remoteAccount.inbox) && | ||
86 | isAccountOutboxValid(remoteAccount.outbox) && | ||
87 | isAccountPreferredUsernameValid(remoteAccount.preferredUsername) && | ||
88 | isAccountUrlValid(remoteAccount.url) && | ||
89 | isAccountPublicKeyObjectValid(remoteAccount.publicKey) && | ||
90 | isAccountEndpointsObjectValid(remoteAccount.endpoint) | ||
91 | } | ||
92 | |||
93 | function isAccountFollowingCountValid (value: string) { | ||
94 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
95 | } | ||
96 | |||
97 | function isAccountFollowersCountValid (value: string) { | ||
98 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | export { | ||
104 | isAccountEndpointsObjectValid, | ||
105 | isAccountSharedInboxValid, | ||
106 | isAccountPublicKeyObjectValid, | ||
107 | isAccountPublicKeyIdValid, | ||
108 | isAccountTypeValid, | ||
109 | isAccountPublicKeyOwnerValid, | ||
110 | isAccountPublicKeyValid, | ||
111 | isAccountIdValid, | ||
112 | isAccountFollowingValid, | ||
113 | isAccountFollowersValid, | ||
114 | isAccountInboxValid, | ||
115 | isAccountOutboxValid, | ||
116 | isAccountPreferredUsernameValid, | ||
117 | isAccountUrlValid, | ||
118 | isAccountPrivateKeyValid, | ||
119 | isRemoteAccountValid, | ||
120 | isAccountFollowingCountValid, | ||
121 | isAccountFollowersCountValid, | ||
122 | isAccountNameValid | ||
123 | } | ||
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 @@ | |||
1 | export * from './account' | ||
2 | export * from './signature' | ||
3 | export * from './misc' | ||
4 | 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 @@ | |||
1 | import { exists } from '../misc' | ||
2 | |||
3 | function isActivityPubUrlValid (url: string) { | ||
4 | const isURLOptions = { | ||
5 | require_host: true, | ||
6 | require_tld: true, | ||
7 | require_protocol: true, | ||
8 | require_valid_protocol: true, | ||
9 | protocols: [ 'http', 'https' ] | ||
10 | } | ||
11 | |||
12 | return exists(url) && validator.isURL(url, isURLOptions) | ||
13 | } | ||
14 | |||
15 | export { | ||
16 | isActivityPubUrlValid | ||
17 | } | ||
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 @@ | |||
1 | import { exists } from '../misc' | ||
2 | import { isActivityPubUrlValid } from './misc' | ||
3 | |||
4 | function isSignatureTypeValid (signatureType: string) { | ||
5 | return exists(signatureType) && signatureType === 'GraphSignature2012' | ||
6 | } | ||
7 | |||
8 | function isSignatureCreatorValid (signatureCreator: string) { | ||
9 | return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator) | ||
10 | } | ||
11 | |||
12 | function isSignatureValueValid (signatureValue: string) { | ||
13 | return exists(signatureValue) && signatureValue.length > 0 | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | isSignatureTypeValid, | ||
20 | isSignatureCreatorValid, | ||
21 | isSignatureValueValid | ||
22 | } | ||
diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index e0ffba679..e0ffba679 100644 --- a/server/helpers/custom-validators/remote/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
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 @@ | |||
1 | export * from './remote' | 1 | export * from './activitypub' |
2 | export * from './misc' | 2 | export * from './misc' |
3 | export * from './pods' | 3 | export * from './pods' |
4 | export * from './pods' | 4 | 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 @@ | |||
1 | export * from './videos' | ||
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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
3 | 2 | ||
4 | import { CONFIG } from '../initializers' | 3 | 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 @@ | |||
1 | export * from './activitypub' | ||
1 | export * from './core-utils' | 2 | export * from './core-utils' |
2 | export * from './logger' | 3 | export * from './logger' |
3 | export * from './custom-validators' | 4 | export * from './custom-validators' |
@@ -6,3 +7,4 @@ export * from './database-utils' | |||
6 | export * from './peertube-crypto' | 7 | export * from './peertube-crypto' |
7 | export * from './requests' | 8 | export * from './requests' |
8 | export * from './utils' | 9 | export * from './utils' |
10 | 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 @@ | |||
1 | import * as crypto from 'crypto' | 1 | import * as jsig from 'jsonld-signatures' |
2 | import { join } from 'path' | ||
3 | 2 | ||
4 | import { | 3 | import { |
5 | SIGNATURE_ALGORITHM, | 4 | PRIVATE_RSA_KEY_SIZE, |
6 | SIGNATURE_ENCODING, | 5 | BCRYPT_SALT_SIZE |
7 | PRIVATE_CERT_NAME, | ||
8 | CONFIG, | ||
9 | BCRYPT_SALT_SIZE, | ||
10 | PUBLIC_CERT_NAME | ||
11 | } from '../initializers' | 6 | } from '../initializers' |
12 | import { | 7 | import { |
13 | readFilePromise, | ||
14 | bcryptComparePromise, | 8 | bcryptComparePromise, |
15 | bcryptGenSaltPromise, | 9 | bcryptGenSaltPromise, |
16 | bcryptHashPromise, | 10 | bcryptHashPromise, |
17 | accessPromise, | 11 | createPrivateKey, |
18 | opensslExecPromise | 12 | getPublicKey, |
13 | jsonldSignPromise, | ||
14 | jsonldVerifyPromise | ||
19 | } from './core-utils' | 15 | } from './core-utils' |
20 | import { logger } from './logger' | 16 | import { logger } from './logger' |
17 | import { AccountInstance } from '../models/account/account-interface' | ||
21 | 18 | ||
22 | function checkSignature (publicKey: string, data: string, hexSignature: string) { | 19 | async function createPrivateAndPublicKeys () { |
23 | const verify = crypto.createVerify(SIGNATURE_ALGORITHM) | 20 | logger.info('Generating a RSA key...') |
24 | |||
25 | let dataString | ||
26 | if (typeof data === 'string') { | ||
27 | dataString = data | ||
28 | } else { | ||
29 | try { | ||
30 | dataString = JSON.stringify(data) | ||
31 | } catch (err) { | ||
32 | logger.error('Cannot check signature.', err) | ||
33 | return false | ||
34 | } | ||
35 | } | ||
36 | 21 | ||
37 | verify.update(dataString, 'utf8') | 22 | const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE) |
23 | const { publicKey } = await getPublicKey(key) | ||
38 | 24 | ||
39 | const isValid = verify.verify(publicKey, hexSignature, SIGNATURE_ENCODING) | 25 | return { privateKey: key, publicKey } |
40 | return isValid | ||
41 | } | 26 | } |
42 | 27 | ||
43 | async function sign (data: string | Object) { | 28 | function isSignatureVerified (fromAccount: AccountInstance, signedDocument: object) { |
44 | const sign = crypto.createSign(SIGNATURE_ALGORITHM) | 29 | const publicKeyObject = { |
45 | 30 | '@context': jsig.SECURITY_CONTEXT_URL, | |
46 | let dataString: string | 31 | '@id': fromAccount.url, |
47 | if (typeof data === 'string') { | 32 | '@type': 'CryptographicKey', |
48 | dataString = data | 33 | owner: fromAccount.url, |
49 | } else { | 34 | publicKeyPem: fromAccount.publicKey |
50 | try { | ||
51 | dataString = JSON.stringify(data) | ||
52 | } catch (err) { | ||
53 | logger.error('Cannot sign data.', err) | ||
54 | return '' | ||
55 | } | ||
56 | } | 35 | } |
57 | 36 | ||
58 | sign.update(dataString, 'utf8') | 37 | const publicKeyOwnerObject = { |
38 | '@context': jsig.SECURITY_CONTEXT_URL, | ||
39 | '@id': fromAccount.url, | ||
40 | publicKey: [ publicKeyObject ] | ||
41 | } | ||
59 | 42 | ||
60 | const myKey = await getMyPrivateCert() | 43 | const options = { |
61 | return sign.sign(myKey, SIGNATURE_ENCODING) | 44 | publicKey: publicKeyObject, |
62 | } | 45 | publicKeyOwner: publicKeyOwnerObject |
46 | } | ||
63 | 47 | ||
64 | function comparePassword (plainPassword: string, hashPassword: string) { | 48 | return jsonldVerifyPromise(signedDocument, options) |
65 | return bcryptComparePromise(plainPassword, hashPassword) | 49 | .catch(err => { |
50 | logger.error('Cannot check signature.', err) | ||
51 | return false | ||
52 | }) | ||
66 | } | 53 | } |
67 | 54 | ||
68 | async function createCertsIfNotExist () { | 55 | function signObject (byAccount: AccountInstance, data: any) { |
69 | const exist = await certsExist() | 56 | const options = { |
70 | if (exist === true) { | 57 | privateKeyPem: byAccount.privateKey, |
71 | return | 58 | creator: byAccount.url |
72 | } | 59 | } |
73 | 60 | ||
74 | return createCerts() | 61 | return jsonldSignPromise(data, options) |
62 | } | ||
63 | |||
64 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
65 | return bcryptComparePromise(plainPassword, hashPassword) | ||
75 | } | 66 | } |
76 | 67 | ||
77 | async function cryptPassword (password: string) { | 68 | async function cryptPassword (password: string) { |
@@ -80,69 +71,12 @@ async function cryptPassword (password: string) { | |||
80 | return bcryptHashPromise(password, salt) | 71 | return bcryptHashPromise(password, salt) |
81 | } | 72 | } |
82 | 73 | ||
83 | function getMyPrivateCert () { | ||
84 | const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) | ||
85 | return readFilePromise(certPath, 'utf8') | ||
86 | } | ||
87 | |||
88 | function getMyPublicCert () { | ||
89 | const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME) | ||
90 | return readFilePromise(certPath, 'utf8') | ||
91 | } | ||
92 | |||
93 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
94 | 75 | ||
95 | export { | 76 | export { |
96 | checkSignature, | 77 | isSignatureVerified, |
97 | comparePassword, | 78 | comparePassword, |
98 | createCertsIfNotExist, | 79 | createPrivateAndPublicKeys, |
99 | cryptPassword, | 80 | cryptPassword, |
100 | getMyPrivateCert, | 81 | signObject |
101 | getMyPublicCert, | ||
102 | sign | ||
103 | } | ||
104 | |||
105 | // --------------------------------------------------------------------------- | ||
106 | |||
107 | async function certsExist () { | ||
108 | const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) | ||
109 | |||
110 | // If there is an error the certificates do not exist | ||
111 | try { | ||
112 | await accessPromise(certPath) | ||
113 | |||
114 | return true | ||
115 | } catch { | ||
116 | return false | ||
117 | } | ||
118 | } | ||
119 | |||
120 | async function createCerts () { | ||
121 | const exist = await certsExist() | ||
122 | if (exist === true) { | ||
123 | const errorMessage = 'Certs already exist.' | ||
124 | logger.warning(errorMessage) | ||
125 | throw new Error(errorMessage) | ||
126 | } | ||
127 | |||
128 | logger.info('Generating a RSA key...') | ||
129 | |||
130 | const privateCertPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) | ||
131 | const genRsaOptions = { | ||
132 | 'out': privateCertPath, | ||
133 | '2048': false | ||
134 | } | ||
135 | |||
136 | await opensslExecPromise('genrsa', genRsaOptions) | ||
137 | logger.info('RSA key generated.') | ||
138 | logger.info('Managing public key...') | ||
139 | |||
140 | const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub') | ||
141 | const rsaOptions = { | ||
142 | 'in': privateCertPath, | ||
143 | 'pubout': true, | ||
144 | 'out': publicCertPath | ||
145 | } | ||
146 | |||
147 | await opensslExecPromise('rsa', rsaOptions) | ||
148 | } | 82 | } |
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 { | |||
9 | } from '../initializers' | 9 | } from '../initializers' |
10 | import { PodInstance } from '../models' | 10 | import { PodInstance } from '../models' |
11 | import { PodSignature } from '../../shared' | 11 | import { PodSignature } from '../../shared' |
12 | import { sign } from './peertube-crypto' | 12 | import { signObject } from './peertube-crypto' |
13 | |||
14 | function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { | ||
15 | return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { | ||
16 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) | ||
17 | }) | ||
18 | } | ||
13 | 19 | ||
14 | type MakeRetryRequestParams = { | 20 | type MakeRetryRequestParams = { |
15 | url: string, | 21 | url: string, |
@@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) { | |||
31 | } | 37 | } |
32 | 38 | ||
33 | type MakeSecureRequestParams = { | 39 | type MakeSecureRequestParams = { |
34 | method: 'GET' | 'POST' | ||
35 | toPod: PodInstance | 40 | toPod: PodInstance |
36 | path: string | 41 | path: string |
37 | data?: Object | 42 | data?: Object |
38 | } | 43 | } |
39 | function makeSecureRequest (params: MakeSecureRequestParams) { | 44 | function makeSecureRequest (params: MakeSecureRequestParams) { |
40 | return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { | 45 | const requestParams: { |
41 | const requestParams: { | 46 | method: 'POST', |
42 | url: string, | 47 | uri: string, |
43 | json: { | 48 | json: { |
44 | signature: PodSignature, | 49 | signature: PodSignature, |
45 | data: any | 50 | data: any |
46 | } | ||
47 | } = { | ||
48 | url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, | ||
49 | json: { | ||
50 | signature: null, | ||
51 | data: null | ||
52 | } | ||
53 | } | 51 | } |
54 | 52 | } = { | |
55 | if (params.method !== 'POST') { | 53 | method: 'POST', |
56 | return rej(new Error('Cannot make a secure request with a non POST method.')) | 54 | uri: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, |
55 | json: { | ||
56 | signature: null, | ||
57 | data: null | ||
57 | } | 58 | } |
59 | } | ||
58 | 60 | ||
59 | const host = CONFIG.WEBSERVER.HOST | 61 | const host = CONFIG.WEBSERVER.HOST |
60 | 62 | ||
61 | let dataToSign | 63 | let dataToSign |
62 | if (params.data) { | 64 | if (params.data) { |
63 | dataToSign = params.data | 65 | dataToSign = params.data |
64 | } else { | 66 | } else { |
65 | // We do not have data to sign so we just take our host | 67 | // We do not have data to sign so we just take our host |
66 | // It is not ideal but the connection should be in HTTPS | 68 | // It is not ideal but the connection should be in HTTPS |
67 | dataToSign = host | 69 | dataToSign = host |
68 | } | 70 | } |
69 | 71 | ||
70 | sign(dataToSign).then(signature => { | 72 | sign(dataToSign).then(signature => { |
71 | requestParams.json.signature = { | 73 | requestParams.json.signature = { |
72 | host, // Which host we pretend to be | 74 | host, // Which host we pretend to be |
73 | signature | 75 | signature |
74 | } | 76 | } |
75 | 77 | ||
76 | // If there are data information | 78 | // If there are data information |
77 | if (params.data) { | 79 | if (params.data) { |
78 | requestParams.json.data = params.data | 80 | requestParams.json.data = params.data |
79 | } | 81 | } |
80 | 82 | ||
81 | request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body })) | 83 | return doRequest(requestParams) |
82 | }) | ||
83 | }) | 84 | }) |
84 | } | 85 | } |
85 | 86 | ||
86 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
87 | 88 | ||
88 | export { | 89 | export { |
90 | doRequest, | ||
89 | makeRetryRequest, | 91 | makeRetryRequest, |
90 | makeSecureRequest | 92 | makeSecureRequest |
91 | } | 93 | } |
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 @@ | |||
1 | import * as WebFinger from 'webfinger.js' | ||
2 | |||
3 | import { isTestInstance } from './core-utils' | ||
4 | import { isActivityPubUrlValid } from './custom-validators' | ||
5 | import { WebFingerData } from '../../shared' | ||
6 | import { fetchRemoteAccountAndCreatePod } from './activitypub' | ||
7 | |||
8 | const webfinger = new WebFinger({ | ||
9 | webfist_fallback: false, | ||
10 | tls_only: isTestInstance(), | ||
11 | uri_fallback: false, | ||
12 | request_timeout: 3000 | ||
13 | }) | ||
14 | |||
15 | async function getAccountFromWebfinger (url: string) { | ||
16 | const webfingerData: WebFingerData = await webfingerLookup(url) | ||
17 | |||
18 | if (Array.isArray(webfingerData.links) === false) return undefined | ||
19 | |||
20 | const selfLink = webfingerData.links.find(l => l.rel === 'self') | ||
21 | if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined | ||
22 | |||
23 | const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href) | ||
24 | |||
25 | return account | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | export { | ||
31 | getAccountFromWebfinger | ||
32 | } | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | function webfingerLookup (url: string) { | ||
37 | return new Promise<WebFingerData>((res, rej) => { | ||
38 | webfinger.lookup('nick@silverbucket.net', (err, p) => { | ||
39 | if (err) return rej(err) | ||
40 | |||
41 | return p | ||
42 | }) | ||
43 | }) | ||
44 | } | ||