@crowdin/app-project-module 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import { Response } from 'express';
2
2
  import { CrowdinClientRequest, FileStore } from '../../../types';
3
3
  import { CustomFileFormatLogic } from '../types';
4
- export default function handle({ baseUrl, folder, config, fileStore, }: {
4
+ export default function handle({ baseUrl, folder, config, fileStore, clientSecret, }: {
5
5
  baseUrl: string;
6
6
  folder: string;
7
7
  config: CustomFileFormatLogic;
8
8
  fileStore?: FileStore;
9
+ clientSecret: string;
9
10
  }): (req: import("express").Request | CrowdinClientRequest, res: Response, next: Function) => void;
@@ -17,8 +17,9 @@ const fs_1 = __importDefault(require("fs"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const util_1 = require("../../../util");
19
19
  const files_1 = require("../util/files");
20
+ const signed_url_1 = require("../util/signed-url");
20
21
  const types_1 = require("../types");
21
- function handle({ baseUrl, folder, config, fileStore, }) {
22
+ function handle({ baseUrl, folder, config, fileStore, clientSecret, }) {
22
23
  if (!fileStore) {
23
24
  if (!fs_1.default.existsSync(path_1.default.join(folder, 'custom-file-format'))) {
24
25
  fs_1.default.mkdirSync(path_1.default.join(folder, 'custom-file-format'), { recursive: true });
@@ -56,6 +57,7 @@ function handle({ baseUrl, folder, config, fileStore, }) {
56
57
  projectId: req.crowdinContext.jwtPayload.context.project_id,
57
58
  file,
58
59
  fileStore,
60
+ clientSecret,
59
61
  });
60
62
  response = buildFileResult.response;
61
63
  error = buildFileResult.error;
@@ -71,6 +73,7 @@ function handle({ baseUrl, folder, config, fileStore, }) {
71
73
  projectId: req.crowdinContext.jwtPayload.context.project_id,
72
74
  file,
73
75
  fileStore,
76
+ clientSecret,
74
77
  });
75
78
  response = parseFileResult.response;
76
79
  error = parseFileResult.error;
@@ -82,7 +85,7 @@ function handle({ baseUrl, folder, config, fileStore, }) {
82
85
  }));
83
86
  }
84
87
  function handleBuildFile(_a) {
85
- return __awaiter(this, arguments, void 0, function* ({ baseUrl, dataFolder, config, req, client, context, projectId, file, fileStore, }) {
88
+ return __awaiter(this, arguments, void 0, function* ({ baseUrl, dataFolder, config, req, client, context, projectId, file, fileStore, clientSecret, }) {
86
89
  const response = {};
87
90
  if (!req.strings && !req.stringsUrl) {
88
91
  throw new Error('Bad payload received: No strings found');
@@ -112,7 +115,7 @@ function handleBuildFile(_a) {
112
115
  let url;
113
116
  if (fileStore) {
114
117
  const fileRef = yield fileStore.storeFile(res.contentFile);
115
- url = `${baseUrl}?file=${fileRef}`;
118
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, fileRef, clientSecret);
116
119
  }
117
120
  else {
118
121
  if (config.storeFile) {
@@ -120,7 +123,7 @@ function handleBuildFile(_a) {
120
123
  }
121
124
  else {
122
125
  const storedFile = yield (0, files_1.storeFile)(res.contentFile, path_1.default.join(dataFolder, 'custom-file-format'));
123
- url = `${baseUrl}?file=${storedFile}`;
126
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, storedFile, clientSecret);
124
127
  }
125
128
  }
126
129
  response.contentUrl = url;
@@ -129,7 +132,7 @@ function handleBuildFile(_a) {
129
132
  });
130
133
  }
131
134
  function handleParseFile(_a) {
132
- return __awaiter(this, arguments, void 0, function* ({ baseUrl, dataFolder, config, req, client, context, projectId, file, fileStore, }) {
135
+ return __awaiter(this, arguments, void 0, function* ({ baseUrl, dataFolder, config, req, client, context, projectId, file, fileStore, clientSecret, }) {
133
136
  const response = {};
134
137
  if (!config.parseFile || !file) {
135
138
  return { response };
@@ -147,7 +150,7 @@ function handleParseFile(_a) {
147
150
  let url;
148
151
  if (fileStore) {
149
152
  const fileRef = yield fileStore.storeFile(res.previewFile);
150
- url = `${baseUrl}?file=${fileRef}`;
153
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, fileRef, clientSecret);
151
154
  }
152
155
  else {
153
156
  if (config.storeFile) {
@@ -155,7 +158,7 @@ function handleParseFile(_a) {
155
158
  }
156
159
  else {
157
160
  const storedFile = yield (0, files_1.storeFile)(res.previewFile, path_1.default.join(dataFolder, 'custom-file-format'));
158
- url = `${baseUrl}?file=${storedFile}`;
161
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, storedFile, clientSecret);
159
162
  }
160
163
  }
161
164
  response.previewUrl = url;
@@ -181,7 +184,7 @@ function handleParseFile(_a) {
181
184
  let url;
182
185
  if (fileStore) {
183
186
  const fileRef = yield fileStore.storeFile(bufferData);
184
- url = `${baseUrl}?file=${fileRef}`;
187
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, fileRef, clientSecret);
185
188
  }
186
189
  else {
187
190
  if (config.storeFile) {
@@ -189,7 +192,7 @@ function handleParseFile(_a) {
189
192
  }
190
193
  else {
191
194
  const storedFile = yield (0, files_1.storeFile)(bufferData, path_1.default.join(dataFolder, 'custom-file-format'));
192
- url = `${baseUrl}?file=${storedFile}`;
195
+ url = (0, signed_url_1.signDownloadUrl)(baseUrl, storedFile, clientSecret);
193
196
  }
194
197
  }
195
198
  response.stringsUrl = url;
@@ -18,15 +18,23 @@ const path_1 = __importDefault(require("path"));
18
18
  const util_1 = require("../../../util");
19
19
  const logger_1 = require("../../../util/logger");
20
20
  const folder_1 = require("../util/folder");
21
+ const signed_url_1 = require("../util/signed-url");
21
22
  function handle(config, processingConfig, folderName) {
22
23
  const folder = (0, folder_1.resolveModuleFilesFolder)(config.dbFolder, processingConfig.filesFolder);
23
24
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
25
+ const fileRef = req.query.file;
26
+ const expires = req.query.expires;
27
+ const sig = req.query.sig;
28
+ if (!fileRef || !expires || !sig || !(0, signed_url_1.verifySignedDownloadUrl)(fileRef, expires, sig, config.clientSecret)) {
29
+ res.sendStatus(403);
30
+ return;
31
+ }
24
32
  if (config.fileStore) {
25
- const fileContent = yield config.fileStore.getFile(req.query.file);
33
+ const fileContent = yield config.fileStore.getFile(fileRef);
26
34
  res.send(fileContent);
27
35
  return;
28
36
  }
29
- const filePath = path_1.default.join(folder, folderName, req.query.file);
37
+ const filePath = path_1.default.join(folder, folderName, path_1.default.basename(fileRef));
30
38
  (0, logger_1.log)(`Downloading file ${filePath}`);
31
39
  res.download(filePath, function (err) {
32
40
  if (err) {
@@ -19,6 +19,7 @@ const util_1 = require("../../../util");
19
19
  const types_1 = require("../types");
20
20
  const folder_1 = require("../util/folder");
21
21
  const files_1 = require("../util/files");
22
+ const signed_url_1 = require("../util/signed-url");
22
23
  function handle(baseConfig, config, folderName) {
23
24
  const folderPath = (0, folder_1.resolveModuleFilesFolder)(baseConfig.dbFolder, config.filesFolder);
24
25
  if (!baseConfig.fileStore) {
@@ -84,7 +85,7 @@ function handle(baseConfig, config, folderName) {
84
85
  let url;
85
86
  if (baseConfig.fileStore) {
86
87
  const fileRef = yield baseConfig.fileStore.storeFile(contentFile);
87
- url = `${baseFilesUrl}?file=${fileRef}`;
88
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, fileRef, baseConfig.clientSecret);
88
89
  }
89
90
  else {
90
91
  if (config.storeFile) {
@@ -92,7 +93,7 @@ function handle(baseConfig, config, folderName) {
92
93
  }
93
94
  else {
94
95
  const storedFile = yield (0, files_1.storeFile)(contentFile, path_1.default.join(folderPath, folderName));
95
- url = `${baseFilesUrl}?file=${storedFile}`;
96
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, storedFile, baseConfig.clientSecret);
96
97
  }
97
98
  }
98
99
  response.contentUrl = url;
@@ -121,7 +122,7 @@ function handle(baseConfig, config, folderName) {
121
122
  let url;
122
123
  if (baseConfig.fileStore) {
123
124
  const fileRef = yield baseConfig.fileStore.storeFile(previewFile);
124
- url = `${baseFilesUrl}?file=${fileRef}`;
125
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, fileRef, baseConfig.clientSecret);
125
126
  }
126
127
  else {
127
128
  if (config.storeFile) {
@@ -129,7 +130,7 @@ function handle(baseConfig, config, folderName) {
129
130
  }
130
131
  else {
131
132
  const storedFile = yield (0, files_1.storeFile)(previewFile, path_1.default.join(folderPath, folderName));
132
- url = `${baseFilesUrl}?file=${storedFile}`;
133
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, storedFile, baseConfig.clientSecret);
133
134
  }
134
135
  }
135
136
  response.previewUrl = url;
@@ -145,7 +146,7 @@ function handle(baseConfig, config, folderName) {
145
146
  let url;
146
147
  if (baseConfig.fileStore) {
147
148
  const fileRef = yield baseConfig.fileStore.storeFile(bufferData);
148
- url = `${baseFilesUrl}?file=${fileRef}`;
149
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, fileRef, baseConfig.clientSecret);
149
150
  }
150
151
  else {
151
152
  if (config.storeFile) {
@@ -153,7 +154,7 @@ function handle(baseConfig, config, folderName) {
153
154
  }
154
155
  else {
155
156
  const storedFile = yield (0, files_1.storeFile)(bufferData, path_1.default.join(folderPath, folderName));
156
- url = `${baseFilesUrl}?file=${storedFile}`;
157
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, storedFile, baseConfig.clientSecret);
157
158
  }
158
159
  }
159
160
  response.stringsUrl = url;
@@ -18,6 +18,7 @@ const path_1 = __importDefault(require("path"));
18
18
  const util_1 = require("../../../util");
19
19
  const folder_1 = require("../util/folder");
20
20
  const files_1 = require("../util/files");
21
+ const signed_url_1 = require("../util/signed-url");
21
22
  function handle(baseConfig, config, folderName) {
22
23
  const folderPath = (0, folder_1.resolveModuleFilesFolder)(baseConfig.dbFolder, config.filesFolder);
23
24
  if (!fs_1.default.existsSync(path_1.default.join(folderPath, folderName))) {
@@ -65,12 +66,16 @@ function handle(baseConfig, config, folderName) {
65
66
  }
66
67
  else {
67
68
  let url;
68
- if (config.storeFile) {
69
+ if (baseConfig.fileStore) {
70
+ const fileRef = yield baseConfig.fileStore.storeFile(bufferData);
71
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, fileRef, baseConfig.clientSecret);
72
+ }
73
+ else if (config.storeFile) {
69
74
  url = yield config.storeFile(bufferData);
70
75
  }
71
76
  else {
72
77
  const storedFile = yield (0, files_1.storeFile)(bufferData, path_1.default.join(folderPath, folderName));
73
- url = `${baseFilesUrl}?file=${storedFile}`;
78
+ url = (0, signed_url_1.signDownloadUrl)(baseFilesUrl, storedFile, baseConfig.clientSecret);
74
79
  }
75
80
  response.translationsUrl = url;
76
81
  }
@@ -39,6 +39,7 @@ function registerCustomFileFormat({ config, app }) {
39
39
  folder: (0, folder_1.resolveModuleFilesFolder)(config.dbFolder, ff.filesFolder),
40
40
  config: ff,
41
41
  fileStore: config.fileStore,
42
+ clientSecret: config.clientSecret,
42
43
  }));
43
44
  app.get(isSingle ? '/file/download' : `/file/${key}/download`, (0, file_download_1.default)(config, ff, 'custom-file-format'));
44
45
  }
@@ -54,6 +55,7 @@ function registerCustomFileFormat({ config, app }) {
54
55
  folder: (0, folder_1.resolveModuleFilesFolder)(config.dbFolder, ff.filesFolder),
55
56
  config: ff,
56
57
  fileStore: config.fileStore,
58
+ clientSecret: config.clientSecret,
57
59
  }));
58
60
  // END TEMPORARY CODE
59
61
  }
@@ -17,11 +17,12 @@ exports.storeFile = storeFile;
17
17
  exports.getFileContent = getFileContent;
18
18
  exports.getFileStrings = getFileStrings;
19
19
  const axios_1 = __importDefault(require("axios"));
20
+ const crypto_1 = require("crypto");
20
21
  const fs_1 = __importDefault(require("fs"));
21
22
  const path_1 = __importDefault(require("path"));
22
23
  exports.MAX_BODY_SIZE = 4.9 * 1024 * 1024; //4.9mb
23
24
  function storeFile(fileContent, folder) {
24
- const fileName = `file${Date.now()}`;
25
+ const fileName = (0, crypto_1.randomBytes)(16).toString('hex');
25
26
  return new Promise((res, rej) => fs_1.default.writeFile(path_1.default.join(folder, fileName), fileContent, (err) => {
26
27
  if (err) {
27
28
  rej(err);
@@ -0,0 +1,2 @@
1
+ export declare function signDownloadUrl(baseUrl: string, fileRef: string, secret: string, expiresInMs?: number): string;
2
+ export declare function verifySignedDownloadUrl(fileRef: string, expires: string, sig: string, secret: string): boolean;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signDownloadUrl = signDownloadUrl;
4
+ exports.verifySignedDownloadUrl = verifySignedDownloadUrl;
5
+ const crypto_1 = require("crypto");
6
+ const DEFAULT_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
7
+ function signDownloadUrl(baseUrl, fileRef, secret, expiresInMs = DEFAULT_EXPIRY_MS) {
8
+ const expires = Date.now() + expiresInMs;
9
+ const sig = computeHmac(fileRef, expires, secret);
10
+ return `${baseUrl}?file=${encodeURIComponent(fileRef)}&expires=${expires}&sig=${sig}`;
11
+ }
12
+ function verifySignedDownloadUrl(fileRef, expires, sig, secret) {
13
+ const expiresMs = parseInt(expires, 10);
14
+ if (isNaN(expiresMs) || Date.now() > expiresMs) {
15
+ return false;
16
+ }
17
+ const expected = computeHmac(fileRef, expiresMs, secret);
18
+ const expectedBuf = Buffer.from(expected, 'hex');
19
+ const sigBuf = Buffer.from(sig, 'hex');
20
+ if (expectedBuf.length !== sigBuf.length) {
21
+ return false;
22
+ }
23
+ return (0, crypto_1.timingSafeEqual)(expectedBuf, sigBuf);
24
+ }
25
+ function computeHmac(fileRef, expires, secret) {
26
+ return (0, crypto_1.createHmac)('sha256', secret).update(`${fileRef}:${expires}`).digest('hex');
27
+ }