@faasjs/http 0.0.2-beta.26 → 0.0.2-beta.260

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.
package/lib/index.es.js CHANGED
@@ -1,6 +1,8 @@
1
- import deepMerge from '@faasjs/deep_merge';
2
- import Logger from '@faasjs/logger';
1
+ import { usePlugin } from '@faasjs/func';
2
+ import { deepMerge } from '@faasjs/deep_merge';
3
+ import { Logger } from '@faasjs/logger';
3
4
  import { randomBytes, pbkdf2Sync, createCipheriv, createHmac, createDecipheriv } from 'crypto';
5
+ import { brotliCompressSync, gzipSync, deflateSync } from 'zlib';
4
6
 
5
7
  class Session {
6
8
  constructor(cookie, config) {
@@ -30,9 +32,8 @@ class Session {
30
32
  this.changed = false;
31
33
  }
32
34
  encode(text) {
33
- if (typeof text !== 'string') {
35
+ if (typeof text !== 'string')
34
36
  text = JSON.stringify(text);
35
- }
36
37
  const iv = randomBytes(16);
37
38
  const cipher = createCipheriv(this.config.cipherName, this.secret, iv);
38
39
  const encrypted = Buffer.concat([cipher.update(text), cipher.final()]).toString('base64');
@@ -48,9 +49,8 @@ class Session {
48
49
  const hmac = createHmac(this.config.digest, this.signedSecret);
49
50
  hmac.update(signedParts[0]);
50
51
  const digest = hmac.digest('hex');
51
- if (signedParts[1] !== digest) {
52
+ if (signedParts[1] !== digest)
52
53
  throw Error('Not valid');
53
- }
54
54
  const message = Buffer.from(signedParts[0], 'base64').toString();
55
55
  const parts = message.split('--').map(function (part) {
56
56
  return Buffer.from(part, 'base64');
@@ -65,19 +65,16 @@ class Session {
65
65
  return this.content[key];
66
66
  }
67
67
  write(key, value) {
68
- if (value === null || typeof value === 'undefined') {
68
+ if (value === null || typeof value === 'undefined')
69
69
  delete this.content[key];
70
- }
71
- else {
70
+ else
72
71
  this.content[key] = value;
73
- }
74
72
  this.changed = true;
75
73
  return this;
76
74
  }
77
75
  update() {
78
- if (this.changed) {
76
+ if (this.changed)
79
77
  this.cookie.write(this.config.key, this.encode(JSON.stringify(this.content)));
80
- }
81
78
  return this;
82
79
  }
83
80
  }
@@ -98,15 +95,13 @@ class Cookie {
98
95
  invoke(cookie) {
99
96
  this.content = Object.create(null);
100
97
  // 解析 cookie
101
- if (cookie) {
98
+ if (cookie)
102
99
  cookie.split(';').map((x) => {
103
100
  x = x.trim();
104
- const k = x.match(/([^=]+)/);
105
- if (k !== null) {
101
+ const k = /([^=]+)/.exec(x);
102
+ if (k !== null)
106
103
  this.content[k[0]] = decodeURIComponent(x.replace(`${k[0]}=`, '').replace(/;$/, ''));
107
- }
108
104
  });
109
- }
110
105
  this.setCookie = Object.create(null);
111
106
  // 预读取 session
112
107
  this.session.invoke(this.read(this.session.config.key));
@@ -127,78 +122,61 @@ class Cookie {
127
122
  cookie = `${key}=${encodeURIComponent(value)};`;
128
123
  this.content[key] = value;
129
124
  }
130
- if (typeof opts.expires === 'number') {
125
+ if (typeof opts.expires === 'number')
131
126
  cookie += `max-age=${opts.expires};`;
132
- }
133
- else if (typeof opts.expires === 'string') {
127
+ else if (typeof opts.expires === 'string')
134
128
  cookie += `expires=${opts.expires};`;
135
- }
136
129
  cookie += `path=${opts.path || '/'};`;
137
- if (opts.domain) {
130
+ if (opts.domain)
138
131
  cookie += `domain=${opts.domain};`;
139
- }
140
- if (opts.secure) {
132
+ if (opts.secure)
141
133
  cookie += 'Secure;';
142
- }
143
- if (opts.httpOnly) {
134
+ if (opts.httpOnly)
144
135
  cookie += 'HttpOnly;';
145
- }
146
- if (opts.sameSite) {
136
+ if (opts.sameSite)
147
137
  cookie += `SameSite=${opts.sameSite};`;
148
- }
149
138
  this.setCookie[key] = cookie;
150
139
  return this;
151
140
  }
152
141
  headers() {
153
- if (!Object.keys(this.setCookie).length) {
142
+ if (Object.keys(this.setCookie).length === 0)
154
143
  return {};
155
- }
156
- else {
157
- return {
158
- 'Set-Cookie': Object.values(this.setCookie)
159
- };
160
- }
144
+ else
145
+ return { 'Set-Cookie': Object.values(this.setCookie) };
161
146
  }
162
147
  }
163
148
 
164
- class ValidError extends Error {
165
- constructor({ statusCode, headers, message }) {
166
- super(message);
167
- this.statusCode = statusCode || 500;
168
- this.headers = headers || Object.create(null);
169
- }
170
- }
171
149
  class Validator {
172
150
  constructor(config) {
173
151
  this.paramsConfig = config.params;
174
152
  this.cookieConfig = config.cookie;
175
153
  this.sessionConfig = config.session;
154
+ this.before = config.before;
176
155
  this.logger = new Logger('Http.Validator');
177
156
  }
178
- valid({ params, cookie, session }) {
179
- this.request = {
180
- params,
181
- cookie,
182
- session
183
- };
157
+ async valid(request) {
184
158
  this.logger.debug('Begin');
185
- if (this.paramsConfig) {
159
+ if (this.before) {
160
+ const result = await this.before(request);
161
+ if (result)
162
+ throw new HttpError(result);
163
+ }
164
+ this.request = request;
165
+ if (this.paramsConfig && request.params) {
186
166
  this.logger.debug('Valid params');
187
- this.validContent('params', params, '', this.paramsConfig);
167
+ this.validContent('params', request.params, '', this.paramsConfig);
188
168
  }
189
- if (this.cookieConfig) {
169
+ if (this.cookieConfig && request.cookie) {
190
170
  this.logger.debug('Valid cookie');
191
- if (!cookie) {
171
+ if (request.cookie == null)
192
172
  throw Error('Not found Cookie');
193
- }
194
- this.validContent('cookie', cookie.content, '', this.cookieConfig);
173
+ this.validContent('cookie', request.cookie.content, '', this.cookieConfig);
195
174
  }
196
- if (this.sessionConfig) {
175
+ if (this.sessionConfig && request.session) {
197
176
  this.logger.debug('Valid Session');
198
- if (!session) {
177
+ if (request.session == null)
199
178
  throw Error('Not found Session');
200
- }
201
- this.validContent('session', session.content, '', this.sessionConfig);
179
+ this.validContent('session', request.session.content, '', this.sessionConfig);
202
180
  }
203
181
  }
204
182
  validContent(type, params, baseKey, config) {
@@ -206,57 +184,50 @@ class Validator {
206
184
  const paramsKeys = Object.keys(params);
207
185
  const rulesKeys = Object.keys(config.rules);
208
186
  const diff = paramsKeys.filter(k => !rulesKeys.includes(k));
209
- if (diff.length) {
187
+ if (diff.length > 0)
210
188
  if (config.whitelist === 'error') {
211
189
  const diffKeys = diff.map(k => `${baseKey}${k}`);
212
190
  const error = Error(`[${type}] Unpermitted keys: ${diffKeys.join(', ')}`);
213
191
  if (config.onError) {
214
192
  const res = config.onError(`${type}.whitelist`, baseKey, diffKeys);
215
- if (res) {
216
- throw new ValidError(res);
217
- }
193
+ if (res)
194
+ throw new HttpError(res);
218
195
  }
219
196
  throw error;
220
197
  }
221
- else if (config.whitelist === 'ignore') {
222
- for (const key of diff) {
198
+ else if (config.whitelist === 'ignore')
199
+ for (const key of diff)
223
200
  delete params[key];
224
- }
225
- }
226
- }
227
201
  }
228
202
  for (const key in config.rules) {
229
203
  const rule = config.rules[key];
204
+ if (!rule)
205
+ continue;
230
206
  let value = params[key];
231
207
  // default
232
- if (rule.default) {
233
- if (type === 'cookie' || type === 'session') {
208
+ if (rule.default)
209
+ if (type === 'cookie' || type === 'session')
234
210
  this.logger.warn('Cookie and Session not support default rule.');
235
- }
236
211
  else if (typeof value === 'undefined' && rule.default) {
237
212
  value = typeof rule.default === 'function' ? rule.default(this.request) : rule.default;
238
213
  params[key] = value;
239
214
  }
240
- }
241
215
  // required
242
- if (rule.required === true) {
216
+ if (rule.required)
243
217
  if (typeof value === 'undefined' || value === null) {
244
218
  const error = Error(`[${type}] ${baseKey}${key} is required.`);
245
219
  if (config.onError) {
246
220
  const res = config.onError(`${type}.rule.required`, `${baseKey}${key}`, value);
247
- if (res) {
248
- throw new ValidError(res);
249
- }
221
+ if (res)
222
+ throw new HttpError(res);
250
223
  }
251
224
  throw error;
252
225
  }
253
- }
254
226
  if (typeof value !== 'undefined' && value !== null) {
255
227
  // type
256
- if (rule.type) {
257
- if (type === 'cookie') {
228
+ if (rule.type)
229
+ if (type === 'cookie')
258
230
  this.logger.warn('Cookie not support type rule');
259
- }
260
231
  else {
261
232
  let typed = true;
262
233
  switch (rule.type) {
@@ -274,43 +245,33 @@ class Validator {
274
245
  const error = Error(`[${type}] ${baseKey}${key} must be a ${rule.type}.`);
275
246
  if (config.onError) {
276
247
  const res = config.onError(`${type}.rule.type`, `${baseKey}${key}`, value);
277
- if (res) {
278
- throw new ValidError(res);
279
- }
248
+ if (res)
249
+ throw new HttpError(res);
280
250
  }
281
251
  throw error;
282
252
  }
283
253
  }
284
- }
285
254
  // in
286
- if (rule.in && !rule.in.includes(value)) {
255
+ if ((rule.in) && !rule.in.includes(value)) {
287
256
  const error = Error(`[${type}] ${baseKey}${key} must be in ${rule.in.join(', ')}.`);
288
257
  if (config.onError) {
289
258
  const res = config.onError(`${type}.rule.in`, `${baseKey}${key}`, value);
290
- if (res) {
291
- throw new ValidError(res);
292
- }
259
+ if (res)
260
+ throw new HttpError(res);
293
261
  }
294
262
  throw error;
295
263
  }
296
264
  // nest config
297
- if (rule.config) {
298
- if (type === 'cookie') {
265
+ if (rule.config)
266
+ if (type === 'cookie')
299
267
  this.logger.warn('Cookie not support nest rule.');
300
- }
301
- else {
302
- if (Array.isArray(value)) {
303
- // array
304
- for (const val of value) {
305
- this.validContent(type, val, (baseKey ? `${baseKey}.${key}.` : `${key}.`), rule.config);
306
- }
307
- }
308
- else if (typeof value === 'object') {
309
- // object
310
- this.validContent(type, value, (baseKey ? `${baseKey}.${key}.` : `${key}.`), rule.config);
311
- }
312
- }
313
- }
268
+ else if (Array.isArray(value))
269
+ // array
270
+ for (const val of value)
271
+ this.validContent(type, val, (baseKey ? `${baseKey}.${key}.` : `${key}.`), rule.config);
272
+ else if (typeof value === 'object')
273
+ // object
274
+ this.validContent(type, value, (baseKey ? `${baseKey}.${key}.` : `${key}.`), rule.config);
314
275
  }
315
276
  }
316
277
  }
@@ -326,12 +287,27 @@ const ContentType = {
326
287
  json: 'application/json',
327
288
  jsonp: 'application/javascript'
328
289
  };
290
+ class HttpError extends Error {
291
+ constructor({ statusCode, message }) {
292
+ super(message);
293
+ if (Error.captureStackTrace)
294
+ Error.captureStackTrace(this, HttpError);
295
+ this.statusCode = statusCode || 500;
296
+ this.message = message;
297
+ }
298
+ }
299
+ const Name = 'http';
300
+ const globals = {};
329
301
  class Http {
330
302
  /**
331
303
  * 创建 Http 插件实例
332
304
  * @param config {object} 配置项
333
305
  * @param config.name {string} 配置名
334
306
  * @param config.config {object} 网关配置
307
+ * @param config.config.method {string} 请求方法,默认为 POST
308
+ * @param config.config.path {string} 请求路径,默认为云函数文件路径
309
+ * @param config.config.ignorePathPrefix {string} 排除的路径前缀,当设置 path 时无效
310
+ * @param config.config.cookie {object} Cookie 配置
335
311
  * @param config.validator {object} 入参校验配置
336
312
  * @param config.validator.params {object} params 校验配置
337
313
  * @param config.validator.params.whitelist {string} 白名单配置
@@ -345,36 +321,50 @@ class Http {
345
321
  * @param config.validator.session.whitelist {string} 白名单配置
346
322
  * @param config.validator.session.onError {function} 自定义报错
347
323
  * @param config.validator.session.rules {object} 参数校验规则
324
+ * @param config.validator.before {function} 参数校验前自定义函数
348
325
  */
349
- constructor(config = Object.create(null)) {
350
- this.logger = new Logger('Http');
351
- this.type = 'http';
352
- this.name = config.name;
353
- this.config = config.config || Object.create(null);
354
- if (config.validator)
326
+ constructor(config) {
327
+ this.type = Name;
328
+ this.name = Name;
329
+ this.name = (config === null || config === void 0 ? void 0 : config.name) || this.type;
330
+ this.config = ((config === null || config === void 0 ? void 0 : config.config)) || Object.create(null);
331
+ if ((config === null || config === void 0 ? void 0 : config.validator))
355
332
  this.validatorOptions = config.validator;
333
+ this.logger = new Logger(this.name);
356
334
  this.headers = Object.create(null);
357
335
  this.cookie = new Cookie(this.config.cookie || {});
358
336
  this.session = this.cookie.session;
359
337
  }
360
338
  async onDeploy(data, next) {
339
+ var _a;
361
340
  await next();
362
- this.logger.debug('[Http] 组装网关配置');
341
+ this.logger.debug('组装网关配置');
363
342
  this.logger.debug('%o', data);
364
- const config = deepMerge(data.config.plugins[this.name || this.type], { config: this.config });
343
+ const config = data.config.plugins ?
344
+ deepMerge(data.config.plugins[this.name || this.type], { config: this.config }) :
345
+ { config: this.config };
365
346
  // 根据文件及文件夹名生成路径
366
- config.config.path = '=/' + data.name.replace(/_/g, '/').replace(/\/index$/, '');
367
- this.logger.debug('[Http] 组装完成 %o', config);
347
+ if (!config.config.path) {
348
+ config.config.path = '=/' + ((_a = data.name) === null || _a === void 0 ? void 0 : _a.replace(/_/g, '/').replace(/\/index$/, ''));
349
+ if (config.config.path === '=/index')
350
+ config.config.path = '=/';
351
+ if (config.config.ignorePathPrefix) {
352
+ config.config.path = config.config.path.replace(new RegExp('^=' + config.config.ignorePathPrefix), '=');
353
+ if (config.config.path === '=')
354
+ config.config.path = '=/';
355
+ }
356
+ }
357
+ this.logger.debug('组装完成 %o', config);
368
358
  // 引用服务商部署插件
369
- // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
370
- const Provider = require(config.provider.type);
359
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
360
+ const Provider = require(config.provider.type).Provider;
371
361
  const provider = new Provider(config.provider.config);
372
362
  // 部署网关
373
363
  await provider.deploy(this.type, data, config);
374
364
  }
375
365
  async onMount(data, next) {
376
366
  this.logger.debug('[onMount] merge config');
377
- if (data.config.plugins[this.name || this.type])
367
+ if (data.config.plugins && data.config.plugins[this.name || this.type])
378
368
  this.config = deepMerge(this.config, data.config.plugins[this.name || this.type].config);
379
369
  this.logger.debug('[onMount] prepare cookie & session');
380
370
  this.cookie = new Cookie(this.config.cookie || {});
@@ -383,11 +373,11 @@ class Http {
383
373
  this.logger.debug('[onMount] prepare validator');
384
374
  this.validator = new Validator(this.validatorOptions);
385
375
  }
376
+ globals[this.name] = this;
386
377
  await next();
387
378
  }
388
379
  async onInvoke(data, next) {
389
- this.logger.debug('[onInvoke] Parse & valid');
390
- this.logger.time('http');
380
+ var _a, _b;
391
381
  this.headers = data.event.headers || Object.create(null);
392
382
  this.params = Object.create(null);
393
383
  this.response = { headers: Object.create(null) };
@@ -404,14 +394,16 @@ class Http {
404
394
  this.logger.debug('[onInvoke] Parse params from queryString');
405
395
  this.params = data.event.queryString;
406
396
  }
397
+ this.logger.debug('[onInvoke] Params: %j', this.params);
407
398
  this.logger.debug('[onInvoke] Parse cookie');
408
- this.cookie.invoke(this.headers['cookie']);
409
- this.logger.debug('[onInvoke] Cookie: %o', this.cookie.content);
410
- this.logger.debug('[onInvoke] Session: %o', this.session.content);
411
- if (this.validator) {
399
+ this.cookie.invoke(this.headers.cookie);
400
+ this.logger.debug('[onInvoke] Cookie: %j', this.cookie.content);
401
+ this.logger.debug('[onInvoke] Session: %j', this.session.content);
402
+ if (this.validator && data.event.httpMethod) {
412
403
  this.logger.debug('[onInvoke] Valid request');
413
404
  try {
414
- this.validator.valid({
405
+ await this.validator.valid({
406
+ headers: this.headers,
415
407
  params: this.params,
416
408
  cookie: this.cookie,
417
409
  session: this.session
@@ -421,52 +413,88 @@ class Http {
421
413
  this.logger.error(error);
422
414
  data.response = {
423
415
  statusCode: error.statusCode || 500,
424
- headers: Object.assign({
416
+ headers: {
425
417
  'Content-Type': 'application/json; charset=utf-8',
426
- 'X-Request-Id': data.context.request_id
427
- }, error.headers || {}),
418
+ 'X-SCF-RequestId': data.context.request_id
419
+ },
428
420
  body: JSON.stringify({ error: { message: error.message } })
429
421
  };
430
422
  return;
431
423
  }
432
424
  }
433
- this.logger.timeEnd('http', '[onInvoke] Parse & valid done');
434
425
  try {
435
426
  await next();
436
427
  }
437
428
  catch (error) {
438
429
  data.response = error;
439
430
  }
440
- this.logger.debug('[onInvoke] Generate response');
441
- this.logger.time('http');
442
431
  // update seesion
443
432
  this.session.update();
444
433
  // 处理 body
445
434
  if (data.response)
446
- if (data.response instanceof Error || (data.response.constructor && data.response.constructor.name === 'Error')) {
435
+ if (data.response instanceof Error || ((_a = data.response.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'Error') {
447
436
  // 当结果是错误类型时
448
437
  this.logger.error(data.response);
449
438
  this.response.body = JSON.stringify({ error: { message: data.response.message } });
450
- this.response.statusCode = 500;
439
+ try {
440
+ this.response.statusCode = data.response.statusCode || 500;
441
+ }
442
+ catch (error) {
443
+ this.response.statusCode = 500;
444
+ }
451
445
  }
452
- else if (Object.prototype.toString.call(data.response) === '[object Object]' && data.response.statusCode && data.response.headers) {
446
+ else if (Object.prototype.toString.call(data.response) === '[object Object]' && data.response.statusCode && data.response.headers)
453
447
  // 当返回结果是响应结构体时
454
448
  this.response = data.response;
455
- }
456
- else {
449
+ else
457
450
  this.response.body = JSON.stringify({ data: data.response });
458
- }
459
451
  // 处理 statusCode
460
452
  if (!this.response.statusCode)
461
453
  this.response.statusCode = this.response.body ? 200 : 201;
462
454
  // 处理 headers
463
455
  this.response.headers = Object.assign({
464
456
  'Content-Type': 'application/json; charset=utf-8',
465
- 'X-Request-Id': new Date().getTime().toString()
457
+ 'X-SCF-RequestId': data.context.request_id
466
458
  }, this.cookie.headers(), this.response.headers);
467
- /* eslint-disable-next-line require-atomic-updates */
468
459
  data.response = this.response;
469
- this.logger.timeEnd('http', '[onInvoke] done');
460
+ if (process.env.FaasMode === 'local') {
461
+ this.logger.debug('[onInvoke] Response: %j', data.response);
462
+ return;
463
+ }
464
+ // 判断是否需要压缩
465
+ if (data.response.isBase64Encoded ||
466
+ typeof data.response.body !== 'string' ||
467
+ !((_b = data.response.headers['Content-Type']) === null || _b === void 0 ? void 0 : _b.includes('json')) ||
468
+ data.response.body.length < 100)
469
+ return;
470
+ const acceptEncoding = this.headers['accept-encoding'] || this.headers['Accept-Encoding'];
471
+ if (!acceptEncoding || !/(br|gzip|deflate)/.test(acceptEncoding))
472
+ return;
473
+ const originBody = data.response.body;
474
+ try {
475
+ if (acceptEncoding.includes('br')) {
476
+ data.response.headers['Content-Encoding'] = 'br';
477
+ data.response.body = brotliCompressSync(originBody).toString('base64');
478
+ }
479
+ else if (acceptEncoding.includes('gzip')) {
480
+ data.response.headers['Content-Encoding'] = 'gzip';
481
+ data.response.body = gzipSync(originBody).toString('base64');
482
+ }
483
+ else if (acceptEncoding.includes('deflate')) {
484
+ data.response.headers['Content-Encoding'] = 'deflate';
485
+ data.response.body = deflateSync(originBody).toString('base64');
486
+ }
487
+ else
488
+ throw Error('No matched compression.');
489
+ data.response.isBase64Encoded = true;
490
+ data.response.originBody = originBody;
491
+ }
492
+ catch (error) {
493
+ console.error(error);
494
+ // 若压缩失败还原
495
+ data.response.body = originBody;
496
+ delete data.response.headers['Content-Encoding'];
497
+ }
470
498
  }
471
499
  /**
472
500
  * 设置 header
@@ -505,6 +533,12 @@ class Http {
505
533
  this.response.body = body;
506
534
  return this;
507
535
  }
536
+ }
537
+ function useHttp(config) {
538
+ const name = (config === null || config === void 0 ? void 0 : config.name) || Name;
539
+ if (process.env.FaasEnv !== 'testing' && globals[name])
540
+ return usePlugin(globals[name]);
541
+ return usePlugin(new Http(config));
508
542
  }
509
543
 
510
- export { ContentType, Cookie, Http, Session, Validator };
544
+ export { ContentType, Cookie, Http, HttpError, Session, Validator, useHttp };