aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/activitypub.ts45
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts34
-rw-r--r--server/helpers/custom-validators/activitypub/index.ts1
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts12
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts223
-rw-r--r--server/helpers/custom-validators/index.ts1
-rw-r--r--server/helpers/custom-validators/video-authors.ts45
-rw-r--r--server/helpers/custom-validators/videos.ts16
-rw-r--r--server/helpers/requests.ts11
9 files changed, 190 insertions, 198 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index ecb509b66..75de2278c 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -2,10 +2,48 @@ import * as url from 'url'
2 2
3import { database as db } from '../initializers' 3import { database as db } from '../initializers'
4import { logger } from './logger' 4import { logger } from './logger'
5import { doRequest } from './requests' 5import { doRequest, doRequestAndSaveToFile } from './requests'
6import { isRemoteAccountValid } from './custom-validators' 6import { isRemoteAccountValid } from './custom-validators'
7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' 7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
8import { ResultList } from '../../shared/models/result-list.model' 8import { ResultList } from '../../shared/models/result-list.model'
9import { CONFIG } from '../initializers/constants'
10import { VideoInstance } from '../models/video/video-interface'
11import { ActivityIconObject } from '../../shared/index'
12import { join } from 'path'
13
14function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
15 const thumbnailName = video.getThumbnailName()
16 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
17
18 const options = {
19 method: 'GET',
20 uri: icon.url
21 }
22 return doRequestAndSaveToFile(options, thumbnailPath)
23}
24
25function getActivityPubUrl (type: 'video' | 'videoChannel', uuid: string) {
26 if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + uuid
27 else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + uuid
28
29 return ''
30}
31
32async function getOrCreateAccount (accountUrl: string) {
33 let account = await db.Account.loadByUrl(accountUrl)
34
35 // We don't have this account in our database, fetch it on remote
36 if (!account) {
37 const { account } = await fetchRemoteAccountAndCreatePod(accountUrl)
38
39 if (!account) throw new Error('Cannot fetch remote account.')
40
41 // Save our new account in database
42 await account.save()
43 }
44
45 return account
46}
9 47
10async function fetchRemoteAccountAndCreatePod (accountUrl: string) { 48async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
11 const options = { 49 const options = {
@@ -100,7 +138,10 @@ function activityPubCollectionPagination (url: string, page: number, result: Res
100export { 138export {
101 fetchRemoteAccountAndCreatePod, 139 fetchRemoteAccountAndCreatePod,
102 activityPubContextify, 140 activityPubContextify,
103 activityPubCollectionPagination 141 activityPubCollectionPagination,
142 getActivityPubUrl,
143 generateThumbnailFromUrl,
144 getOrCreateAccount
104} 145}
105 146
106// --------------------------------------------------------------------------- 147// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
new file mode 100644
index 000000000..dd671c4cf
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -0,0 +1,34 @@
1import * as validator from 'validator'
2import {
3 isVideoChannelCreateActivityValid,
4 isVideoTorrentAddActivityValid,
5 isVideoTorrentUpdateActivityValid,
6 isVideoChannelUpdateActivityValid
7} from './videos'
8
9function isRootActivityValid (activity: any) {
10 return Array.isArray(activity['@context']) &&
11 (
12 (activity.type === 'Collection' || activity.type === 'OrderedCollection') &&
13 validator.isInt(activity.totalItems, { min: 0 }) &&
14 Array.isArray(activity.items)
15 ) ||
16 (
17 validator.isURL(activity.id) &&
18 validator.isURL(activity.actor)
19 )
20}
21
22function isActivityValid (activity: any) {
23 return isVideoTorrentAddActivityValid(activity) ||
24 isVideoChannelCreateActivityValid(activity) ||
25 isVideoTorrentUpdateActivityValid(activity) ||
26 isVideoChannelUpdateActivityValid(activity)
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 isRootActivityValid,
33 isActivityValid
34}
diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts
index 800f0ddf3..0eba06a7b 100644
--- a/server/helpers/custom-validators/activitypub/index.ts
+++ b/server/helpers/custom-validators/activitypub/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './activity'
2export * from './signature' 3export * from './signature'
3export * from './misc' 4export * from './misc'
4export * from './videos' 5export * from './videos'
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 806d33483..f049f5a8c 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -12,6 +12,16 @@ function isActivityPubUrlValid (url: string) {
12 return exists(url) && validator.isURL(url, isURLOptions) 12 return exists(url) && validator.isURL(url, isURLOptions)
13} 13}
14 14
15function isBaseActivityValid (activity: any, type: string) {
16 return Array.isArray(activity['@context']) &&
17 activity.type === type &&
18 validator.isURL(activity.id) &&
19 validator.isURL(activity.actor) &&
20 Array.isArray(activity.to) &&
21 activity.to.every(t => validator.isURL(t))
22}
23
15export { 24export {
16 isActivityPubUrlValid 25 isActivityPubUrlValid,
26 isBaseActivityValid
17} 27}
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index e0ffba679..9233a1359 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,184 +1,117 @@
1import 'express-validator' 1import * as validator from 'validator'
2import { has, values } from 'lodash'
3 2
4import { 3import {
5 REQUEST_ENDPOINTS, 4 ACTIVITY_PUB
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_VIDEO_EVENT_TYPES
8} from '../../../initializers' 5} from '../../../initializers'
9import { isArray, isDateValid, isUUIDValid } from '../misc' 6import { isDateValid, isUUIDValid } from '../misc'
10import { 7import {
11 isVideoThumbnailDataValid,
12 isVideoAbuseReasonValid,
13 isVideoAbuseReporterUsernameValid,
14 isVideoViewsValid, 8 isVideoViewsValid,
15 isVideoLikesValid,
16 isVideoDislikesValid,
17 isVideoEventCountValid,
18 isRemoteVideoCategoryValid,
19 isRemoteVideoLicenceValid,
20 isRemoteVideoLanguageValid,
21 isVideoNSFWValid, 9 isVideoNSFWValid,
22 isVideoTruncatedDescriptionValid, 10 isVideoTruncatedDescriptionValid,
23 isVideoDurationValid, 11 isVideoDurationValid,
24 isVideoFileInfoHashValid,
25 isVideoNameValid, 12 isVideoNameValid,
26 isVideoTagsValid, 13 isVideoTagValid
27 isVideoFileExtnameValid,
28 isVideoFileResolutionValid
29} from '../videos' 14} from '../videos'
30import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' 15import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
31import { isVideoAuthorNameValid } from '../video-authors' 16import { isBaseActivityValid } from './misc'
32 17
33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 18function isVideoTorrentAddActivityValid (activity: any) {
34 19 return isBaseActivityValid(activity, 'Add') &&
35const checkers: { [ id: string ]: (obj: any) => boolean } = {} 20 isVideoTorrentObjectValid(activity.object)
36checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo 21}
37checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo 22
38checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo 23function isVideoTorrentUpdateActivityValid (activity: any) {
39checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo 24 return isBaseActivityValid(activity, 'Update') &&
40checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel 25 isVideoTorrentObjectValid(activity.object)
41checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel
42checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel
43checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor
44checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor
45
46function removeBadRequestVideos (requests: any[]) {
47 for (let i = requests.length - 1; i >= 0 ; i--) {
48 const request = requests[i]
49 const video = request.data
50
51 if (
52 !video ||
53 checkers[request.type] === undefined ||
54 checkers[request.type](video) === false
55 ) {
56 requests.splice(i, 1)
57 }
58 }
59} 26}
60 27
61function removeBadRequestVideosQadu (requests: any[]) { 28function isVideoTorrentObjectValid (video: any) {
62 for (let i = requests.length - 1; i >= 0 ; i--) { 29 return video.type === 'Video' &&
63 const request = requests[i] 30 isVideoNameValid(video.name) &&
64 const video = request.data 31 isVideoDurationValid(video.duration) &&
65 32 isUUIDValid(video.uuid) &&
66 if ( 33 setValidRemoteTags(video) &&
67 !video || 34 isRemoteIdentifierValid(video.category) &&
68 ( 35 isRemoteIdentifierValid(video.licence) &&
69 isUUIDValid(video.uuid) && 36 isRemoteIdentifierValid(video.language) &&
70 (has(video, 'views') === false || isVideoViewsValid(video.views)) && 37 isVideoViewsValid(video.video) &&
71 (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && 38 isVideoNSFWValid(video.nsfw) &&
72 (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) 39 isDateValid(video.published) &&
73 ) === false 40 isDateValid(video.updated) &&
74 ) { 41 isRemoteVideoContentValid(video.mediaType, video.content) &&
75 requests.splice(i, 1) 42 isRemoteVideoIconValid(video.icon) &&
76 } 43 setValidRemoteVideoUrls(video.url)
77 }
78} 44}
79 45
80function removeBadRequestVideosEvents (requests: any[]) { 46function isVideoChannelCreateActivityValid (activity: any) {
81 for (let i = requests.length - 1; i >= 0 ; i--) { 47 return isBaseActivityValid(activity, 'Create') &&
82 const request = requests[i] 48 isVideoChannelObjectValid(activity.object)
83 const eventData = request.data 49}
84 50
85 if ( 51function isVideoChannelUpdateActivityValid (activity: any) {
86 !eventData || 52 return isBaseActivityValid(activity, 'Update') &&
87 ( 53 isVideoChannelObjectValid(activity.object)
88 isUUIDValid(eventData.uuid) && 54}
89 values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && 55
90 isVideoEventCountValid(eventData.count) 56function isVideoChannelObjectValid (videoChannel: any) {
91 ) === false 57 return videoChannel.type === 'VideoChannel' &&
92 ) { 58 isVideoChannelNameValid(videoChannel.name) &&
93 requests.splice(i, 1) 59 isVideoChannelDescriptionValid(videoChannel.description) &&
94 } 60 isUUIDValid(videoChannel.uuid)
95 }
96} 61}
97 62
98// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
99 64
100export { 65export {
101 removeBadRequestVideos, 66 isVideoTorrentAddActivityValid,
102 removeBadRequestVideosQadu, 67 isVideoChannelCreateActivityValid,
103 removeBadRequestVideosEvents 68 isVideoTorrentUpdateActivityValid,
69 isVideoChannelUpdateActivityValid
104} 70}
105 71
106// --------------------------------------------------------------------------- 72// ---------------------------------------------------------------------------
107 73
108function isCommonVideoAttributesValid (video: any) { 74function setValidRemoteTags (video: any) {
109 return isDateValid(video.createdAt) && 75 if (Array.isArray(video.tag) === false) return false
110 isDateValid(video.updatedAt) &&
111 isRemoteVideoCategoryValid(video.category) &&
112 isRemoteVideoLicenceValid(video.licence) &&
113 isRemoteVideoLanguageValid(video.language) &&
114 isVideoNSFWValid(video.nsfw) &&
115 isVideoTruncatedDescriptionValid(video.truncatedDescription) &&
116 isVideoDurationValid(video.duration) &&
117 isVideoNameValid(video.name) &&
118 isVideoTagsValid(video.tags) &&
119 isUUIDValid(video.uuid) &&
120 isVideoViewsValid(video.views) &&
121 isVideoLikesValid(video.likes) &&
122 isVideoDislikesValid(video.dislikes) &&
123 isArray(video.files) &&
124 video.files.every(videoFile => {
125 if (!videoFile) return false
126
127 return (
128 isVideoFileInfoHashValid(videoFile.infoHash) &&
129 isVideoFileExtnameValid(videoFile.extname) &&
130 isVideoFileResolutionValid(videoFile.resolution)
131 )
132 })
133}
134 76
135function checkAddVideo (video: any) { 77 const newTag = video.tag.filter(t => {
136 return isCommonVideoAttributesValid(video) && 78 return t.type === 'Hashtag' &&
137 isUUIDValid(video.channelUUID) && 79 isVideoTagValid(t.name)
138 isVideoThumbnailDataValid(video.thumbnailData) 80 })
139}
140 81
141function checkUpdateVideo (video: any) { 82 video.tag = newTag
142 return isCommonVideoAttributesValid(video) 83 return true
143} 84}
144 85
145function checkRemoveVideo (video: any) { 86function isRemoteIdentifierValid (data: any) {
146 return isUUIDValid(video.uuid) 87 return validator.isInt(data.identifier, { min: 0 })
147} 88}
148 89
149function checkReportVideo (abuse: any) { 90function isRemoteVideoContentValid (mediaType: string, content: string) {
150 return isUUIDValid(abuse.videoUUID) && 91 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
151 isVideoAbuseReasonValid(abuse.reportReason) &&
152 isVideoAbuseReporterUsernameValid(abuse.reporterUsername)
153} 92}
154 93
155function checkAddVideoChannel (videoChannel: any) { 94function isRemoteVideoIconValid (icon: any) {
156 return isUUIDValid(videoChannel.uuid) && 95 return icon.type === 'Image' &&
157 isVideoChannelNameValid(videoChannel.name) && 96 validator.isURL(icon.url) &&
158 isVideoChannelDescriptionValid(videoChannel.description) && 97 icon.mediaType === 'image/jpeg' &&
159 isDateValid(videoChannel.createdAt) && 98 validator.isInt(icon.width, { min: 0 }) &&
160 isDateValid(videoChannel.updatedAt) && 99 validator.isInt(icon.height, { min: 0 })
161 isUUIDValid(videoChannel.ownerUUID)
162} 100}
163 101
164function checkUpdateVideoChannel (videoChannel: any) { 102function setValidRemoteVideoUrls (video: any) {
165 return isUUIDValid(videoChannel.uuid) && 103 if (Array.isArray(video.url) === false) return false
166 isVideoChannelNameValid(videoChannel.name) &&
167 isVideoChannelDescriptionValid(videoChannel.description) &&
168 isDateValid(videoChannel.createdAt) &&
169 isDateValid(videoChannel.updatedAt) &&
170 isUUIDValid(videoChannel.ownerUUID)
171}
172 104
173function checkRemoveVideoChannel (videoChannel: any) { 105 const newUrl = video.url.filter(u => isRemoteVideoUrlValid(u))
174 return isUUIDValid(videoChannel.uuid) 106 video.url = newUrl
175}
176 107
177function checkAddAuthor (author: any) { 108 return true
178 return isUUIDValid(author.uuid) &&
179 isVideoAuthorNameValid(author.name)
180} 109}
181 110
182function checkRemoveAuthor (author: any) { 111function isRemoteVideoUrlValid (url: any) {
183 return isUUIDValid(author.uuid) 112 return url.type === 'Link' &&
113 ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 &&
114 validator.isURL(url.url) &&
115 validator.isInt(url.width, { min: 0 }) &&
116 validator.isInt(url.size, { min: 0 })
184} 117}
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index 869b08870..58a40249b 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -3,6 +3,5 @@ export * from './misc'
3export * from './pods' 3export * from './pods'
4export * from './pods' 4export * from './pods'
5export * from './users' 5export * from './users'
6export * from './video-authors'
7export * from './video-channels' 6export * from './video-channels'
8export * from './videos' 7export * from './videos'
diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts
deleted file mode 100644
index 48ca9b200..000000000
--- a/server/helpers/custom-validators/video-authors.ts
+++ /dev/null
@@ -1,45 +0,0 @@
1import * as Promise from 'bluebird'
2import * as validator from 'validator'
3import * as express from 'express'
4import 'express-validator'
5
6import { database as db } from '../../initializers'
7import { AuthorInstance } from '../../models'
8import { logger } from '../logger'
9
10import { isUserUsernameValid } from './users'
11
12function isVideoAuthorNameValid (value: string) {
13 return isUserUsernameValid(value)
14}
15
16function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) {
17 let promise: Promise<AuthorInstance>
18 if (validator.isInt(id)) {
19 promise = db.Author.load(+id)
20 } else { // UUID
21 promise = db.Author.loadByUUID(id)
22 }
23
24 promise.then(author => {
25 if (!author) {
26 return res.status(404)
27 .json({ error: 'Video author not found' })
28 .end()
29 }
30
31 res.locals.author = author
32 callback()
33 })
34 .catch(err => {
35 logger.error('Error in video author request validator.', err)
36 return res.sendStatus(500)
37 })
38}
39
40// ---------------------------------------------------------------------------
41
42export {
43 checkVideoAuthorExists,
44 isVideoAuthorNameValid
45}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index f3fdcaf2d..83407f17b 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -73,19 +73,26 @@ function isVideoDescriptionValid (value: string) {
73} 73}
74 74
75function isVideoDurationValid (value: string) { 75function isVideoDurationValid (value: string) {
76 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) 76 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
77 return exists(value) &&
78 typeof value === 'string' &&
79 value.startsWith('PT') &&
80 value.endsWith('S') &&
81 validator.isInt(value.replace(/[^0-9]+/, ''), VIDEOS_CONSTRAINTS_FIELDS.DURATION)
77} 82}
78 83
79function isVideoNameValid (value: string) { 84function isVideoNameValid (value: string) {
80 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) 85 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
81} 86}
82 87
88function isVideoTagValid (tag: string) {
89 return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
90}
91
83function isVideoTagsValid (tags: string[]) { 92function isVideoTagsValid (tags: string[]) {
84 return isArray(tags) && 93 return isArray(tags) &&
85 validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) && 94 validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
86 tags.every(tag => { 95 tags.every(tag => isVideoTagValid(tag))
87 return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
88 })
89} 96}
90 97
91function isVideoThumbnailValid (value: string) { 98function isVideoThumbnailValid (value: string) {
@@ -209,6 +216,7 @@ export {
209 isRemoteVideoPrivacyValid, 216 isRemoteVideoPrivacyValid,
210 isVideoFileResolutionValid, 217 isVideoFileResolutionValid,
211 checkVideoExists, 218 checkVideoExists,
219 isVideoTagValid,
212 isRemoteVideoCategoryValid, 220 isRemoteVideoCategoryValid,
213 isRemoteVideoLicenceValid, 221 isRemoteVideoLicenceValid,
214 isRemoteVideoLanguageValid 222 isRemoteVideoLanguageValid
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 8c4c983f7..31cedd768 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -10,6 +10,7 @@ import {
10import { PodInstance } from '../models' 10import { PodInstance } from '../models'
11import { PodSignature } from '../../shared' 11import { PodSignature } from '../../shared'
12import { signObject } from './peertube-crypto' 12import { signObject } from './peertube-crypto'
13import { createWriteStream } from 'fs'
13 14
14function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { 15function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
15 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { 16 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
@@ -17,6 +18,15 @@ function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
17 }) 18 })
18} 19}
19 20
21function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) {
22 return new Promise<request.RequestResponse>((res, rej) => {
23 request(requestOptions)
24 .on('response', response => res(response as request.RequestResponse))
25 .on('error', err => rej(err))
26 .pipe(createWriteStream(destPath))
27 })
28}
29
20type MakeRetryRequestParams = { 30type MakeRetryRequestParams = {
21 url: string, 31 url: string,
22 method: 'GET' | 'POST', 32 method: 'GET' | 'POST',
@@ -88,6 +98,7 @@ function makeSecureRequest (params: MakeSecureRequestParams) {
88 98
89export { 99export {
90 doRequest, 100 doRequest,
101 doRequestAndSaveToFile,
91 makeRetryRequest, 102 makeRetryRequest,
92 makeSecureRequest 103 makeSecureRequest
93} 104}