@smpx/koa-request 0.2.5 → 0.2.6

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/Request.js CHANGED
@@ -240,6 +240,12 @@ class Request {
240
240
  return paramValue;
241
241
  }
242
242
 
243
+ /**
244
+ * Get parameter as an integer (from query or body)
245
+ * @param {string} key
246
+ * @param {number} defaultValue
247
+ * @returns {number}
248
+ */
243
249
  paramInt(key, defaultValue = 0) {
244
250
  const value = this.param(key, null);
245
251
  if (value === null) return defaultValue;
@@ -252,6 +258,15 @@ class Request {
252
258
  return intVal;
253
259
  }
254
260
 
261
+ /**
262
+ * Get parameter as a boolean
263
+ * Only '0', 'false', 'no', 'off' are considered falsy
264
+ * empty string is truthy, so url?param would be truthy
265
+ * but url?param=0 or url?param=false would be falsy
266
+ * @param {string} key
267
+ * @param {boolean} defaultValue
268
+ * @returns {boolean}
269
+ */
255
270
  paramBool(key, defaultValue = false) {
256
271
  const value = this.param(key, null);
257
272
  if (value === null) return defaultValue;
@@ -267,6 +282,13 @@ class Request {
267
282
  return true;
268
283
  }
269
284
 
285
+ /**
286
+ * Get the parameter as an id.
287
+ * id any is a max 20 character long alphanumeric string
288
+ * @param {string} key
289
+ * @param {string} defaultValue
290
+ * @returns {string}
291
+ */
270
292
  paramId(key, defaultValue = '') {
271
293
  const value = this.param(key, null);
272
294
  if (value === null) return defaultValue;
@@ -295,10 +317,20 @@ class Request {
295
317
  });
296
318
  }
297
319
 
320
+ /**
321
+ * Get the file with this param name
322
+ * @param {string} key
323
+ * @returns
324
+ */
298
325
  file(key) {
299
326
  return this.files(key)[0] || null;
300
327
  }
301
328
 
329
+ /**
330
+ * Get all the files with this param name
331
+ * @param {string} key
332
+ * @returns {array}
333
+ */
302
334
  files(key) {
303
335
  if (this.ctx.request.files && this.ctx.request.files[key]) {
304
336
  const result = this.ctx.request.files[key];
@@ -309,6 +341,11 @@ class Request {
309
341
  return [];
310
342
  }
311
343
 
344
+ /**
345
+ * Get the bearer token of the request.
346
+ * Authorization: Bearer abc would give abc as bearer token
347
+ * @returns {string}
348
+ */
312
349
  bearerToken() {
313
350
  const authorization = this.ctx.headers.authorization;
314
351
  if (!authorization) return '';
@@ -320,12 +357,19 @@ class Request {
320
357
  return parts[1];
321
358
  }
322
359
 
360
+ /**
361
+ * Get the api token of the request.
362
+ * API token can be sent as a header x-api-token
363
+ * @returns {string}
364
+ */
323
365
  apiToken() {
324
366
  return this.header('x-api-token');
325
367
  }
326
368
 
327
- // initialize a request
328
- // set important cookies and all
369
+ /**
370
+ * initialize a request
371
+ * set important cookies and all
372
+ */
329
373
  async init() {
330
374
  // don't allow other domains to embed our iframe
331
375
  if (isProduction) {
@@ -346,6 +390,11 @@ class Request {
346
390
  }
347
391
  }
348
392
 
393
+ /**
394
+ * check whether the request is http or https
395
+ * isHttp returns false if the request is https, true otherwise
396
+ * @returns {boolean}
397
+ */
349
398
  isHttp() {
350
399
  if (this._isHttp === undefined) {
351
400
  const ctx = this.ctx;
@@ -366,6 +415,7 @@ class Request {
366
415
  */
367
416
 
368
417
  /**
418
+ * Get or set a cookie
369
419
  * @template {string | number | boolean | undefined} V
370
420
  * @param {string} name
371
421
  * @param {V} value
@@ -463,18 +513,35 @@ class Request {
463
513
  return key ? this._trackingHeader[key] : this._trackingHeader;
464
514
  }
465
515
 
516
+ /**
517
+ * Get the user agent of the request.
518
+ * @returns {string}
519
+ */
466
520
  userAgent() {
467
521
  return this.trackingHeader('user-agent') || this.header('user-agent');
468
522
  }
469
523
 
524
+ /**
525
+ * Get the referer of the request.
526
+ * @returns {string}
527
+ */
470
528
  referer() {
471
529
  return this.trackingHeader('referer') || this.header('referer');
472
530
  }
473
531
 
532
+ /**
533
+ * Get the referer name of the request.
534
+ * @returns {string}
535
+ */
474
536
  refererName() {
475
537
  return this._refererName;
476
538
  }
477
539
 
540
+ /**
541
+ * Parse the user agent with ua-parser-js and return the result
542
+ * Returns {ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} }
543
+ * @returns {object}
544
+ */
478
545
  parseUserAgent() {
479
546
  if (!this._ua) {
480
547
  this._ua = uaParser.setUA(this.userAgent()).getResult() || {};
@@ -482,19 +549,31 @@ class Request {
482
549
  return this._ua;
483
550
  }
484
551
 
552
+ /**
553
+ * Get the browser name
554
+ * @returns {string}
555
+ */
485
556
  browser() {
486
557
  const ua = this.parseUserAgent();
487
558
  const deviceType = (ua && ua.device && ua.device.type) || '';
488
559
  const browserName = (ua && ua.browser && ua.browser.name) || '';
489
560
 
490
561
  if (deviceType === 'mobile') {
491
- if (browserName === 'Chrome') return 'Chrome Mobile';
492
- if (browserName === 'Firefox') return 'Firefox Mobile';
562
+ switch (browserName) {
563
+ case 'Chrome': return 'Chrome Mobile';
564
+ case 'Firefox': return 'Firefox Mobile';
565
+ case 'Safari': return 'Safari Mobile';
566
+ case 'Mobile Safari': return 'Safari Mobile';
567
+ }
493
568
  }
494
569
 
495
570
  return browserName;
496
571
  }
497
572
 
573
+ /**
574
+ * Get the browser name + version (eg. Chrome 96.1.0.110)
575
+ * @returns {string}
576
+ */
498
577
  browserVersion() {
499
578
  const ua = this.parseUserAgent();
500
579
  let browerVersion = (ua.browser && ua.browser.version) || '';
@@ -502,17 +581,29 @@ class Request {
502
581
  return this.browser() + browerVersion;
503
582
  }
504
583
 
584
+ /**
585
+ * Get the browser version only (eg. 96.1.0.110)
586
+ * @returns {string}
587
+ */
505
588
  browserVersionRaw() {
506
589
  const ua = this.parseUserAgent();
507
590
  const version = (ua.browser && ua.browser.version) || '';
508
591
  return String(version);
509
592
  }
510
593
 
594
+ /**
595
+ * Get the os name (eg. Windows)
596
+ * @returns {string}
597
+ */
511
598
  os() {
512
599
  const ua = this.parseUserAgent();
513
600
  return (ua.os && ua.os.name) || '';
514
601
  }
515
602
 
603
+ /**
604
+ * Get the os name + version (eg. Windows 11)
605
+ * @returns {string}
606
+ */
516
607
  osVersion() {
517
608
  const ua = this.parseUserAgent();
518
609
  let osVersion = (ua.os && ua.os.version) || '';
@@ -520,6 +611,10 @@ class Request {
520
611
  return this.os() + osVersion;
521
612
  }
522
613
 
614
+ /**
615
+ * Get the header sm-user-agent
616
+ * @returns {string}
617
+ */
523
618
  smUserAgent() {
524
619
  return this.header('sm-user-agent');
525
620
  }
@@ -850,6 +945,7 @@ class Request {
850
945
  }
851
946
 
852
947
  /**
948
+ * Get the ip of the request
853
949
  * @returns {string}
854
950
  */
855
951
  ip() {
@@ -871,6 +967,7 @@ class Request {
871
967
  }
872
968
 
873
969
  /**
970
+ * Get the real ip of the request (after considering x-forwarded-for)
874
971
  * @returns {string}
875
972
  */
876
973
  realIP() {
@@ -903,14 +1000,16 @@ class Request {
903
1000
  }
904
1001
 
905
1002
  /**
906
- * Is being proxied through local proxy
1003
+ * Is the request being proxied through local proxy
1004
+ * @returns {boolean}
907
1005
  */
908
1006
  isProxied() {
909
1007
  return this.header('x-real-ip') && this.ctx.ip.endsWith('127.0.0.1');
910
1008
  }
911
1009
 
912
1010
  /**
913
- * Is request being proxied through an external proxy (like chrome data-saver)
1011
+ * Is the request being proxied through an external proxy (like chrome data-saver)
1012
+ * @returns {boolean}
914
1013
  */
915
1014
  isVia() {
916
1015
  return this.header('via') && this.realIP() === this.ip();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smpx/koa-request",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Handle basic tasks for koajs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -32,12 +32,12 @@
32
32
  "dependencies": {
33
33
  "ip": "^1.1.5",
34
34
  "koa-basic-auth": "^4.0.0",
35
- "koa-body": "^4.1.1",
36
- "koa-send": "^5.0.0",
37
- "koa2-ratelimit": "^0.9.0",
38
- "maxmind": "^4.1.1",
39
- "tar": "^6.0.2",
40
- "ua-parser-js": "^0.7.21"
35
+ "koa-body": "^4.2.0",
36
+ "koa-send": "^5.0.1",
37
+ "koa2-ratelimit": "^0.9.1",
38
+ "maxmind": "^4.3.3",
39
+ "tar": "^6.1.11",
40
+ "ua-parser-js": "^1.0.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "eslint": "^5.7.0",
@@ -1,18 +0,0 @@
1
- {
2
- "sourceFile": ".gitignore",
3
- "activeCommit": 0,
4
- "commits": [
5
- {
6
- "activePatchIndex": 0,
7
- "patches": [
8
- {
9
- "date": 1622045206905,
10
- "content": "Index: \n===================================================================\n--- \n+++ \n"
11
- }
12
- ],
13
- "date": 1622045206905,
14
- "name": "Commit-0",
15
- "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# lock files\npackage-lock.json\nyarn.lock\n\nprivate\ndist\n\n# geoip files\nGeoLite2-City.mmdb\n\n# local history (vs code extension - xyz.local-history)\n.history/*\n.lh\n"
16
- }
17
- ]
18
- }
package/.lh/.lhignore DELETED
@@ -1,6 +0,0 @@
1
- # list file to not track by the local-history extension. comment line starts with a '#' character
2
- # each line describe a regular expression pattern (search for "Javascript regex")
3
- # it will relate to the workspace directory root. for example:
4
- # ".*\.txt" ignores any file with "txt" extension
5
- # "/test/.*" ignores all the files under the "test" directory
6
- # ".*/test/.*" ignores all the files under any "test" directory (even under sub-folders)
@@ -1,18 +0,0 @@
1
- {
2
- "sourceFile": "IPRange.js",
3
- "activeCommit": 0,
4
- "commits": [
5
- {
6
- "activePatchIndex": 0,
7
- "patches": [
8
- {
9
- "date": 1622576413980,
10
- "content": "Index: \n===================================================================\n--- \n+++ \n"
11
- }
12
- ],
13
- "date": 1622576413980,
14
- "name": "Commit-0",
15
- "content": "const ipLib = require('ip');\n\nconst PRIVATE_IPS = [\n\t'0.0.0.0/8',\n\t'240.0.0.0/4',\n\t'10.0.0.0/8',\n\t'127.0.0.0/8',\n\t'169.254.0.0/16',\n\t'172.16.0.0/12',\n\t'192.168.0.0/16',\n];\n\n// Source: https://www.lifewire.com/what-is-the-ip-address-of-google-818153\nconst GOOGLE_IPS = [\n\t'64.233.160.0 - 64.233.191.255',\n\t'66.102.0.0 - 66.102.15.255',\n\t'66.249.64.0 - 66.249.95.255',\n\t'72.14.192.0 - 72.14.255.255',\n\t'74.125.0.0 - 74.125.255.255',\n\t'209.85.128.0 - 209.85.255.255',\n\t'216.239.32.0 - 216.239.63.255',\n\t//\n\t'64.68.90.1 - 64.68.90.255',\n\t'64.233.173.193 - 64.233.173.255',\n\t'66.249.64.1 - 66.249.79.255',\n\t'216.239.33.96 - 216.239.59.128',\n];\n\nconst SEARCH_IPS = [\n\t'178.154.128.0/17', // YANDAX\n\t'87.250.224.0/19',\t// YANDAX\n\t'157.55.0.0/16',\t// Microsoft\n\t'207.46.0.0/19',\t// Microsoft\n\t'17.0.0.0/8',\t\t// Apple\n];\n\nlet compiledPrivateIps;\nlet compiledGoogleIps;\nlet compiledSearchIps;\nlet publicIp;\nlet privateIp;\n\nfunction ipRangeCheckRegex(cidrs) {\n\tconst cidrJoin = cidrs.map((cidr) => {\n\t\tif (!cidr.endsWith('.')) cidr += '.';\n\t\treturn cidr.replace(/\\./g, '\\\\.');\n\t}).join('|');\n\n\treturn new RegExp(`^(?:${cidrJoin})`);\n}\n\nfunction compile(cidrs) {\n\tif (!Array.isArray(cidrs)) {\n\t\tcidrs = [cidrs];\n\t}\n\n\tconst ranges = [];\n\tconst startsWith = [];\n\tlet rangeRegex = null;\n\n\tfor (const cidr of cidrs) {\n\t\t// match cidr\n\t\tif (cidr.includes('/')) {\n\t\t\t// Split the range by the slash\n\t\t\tconst parts = cidr.split('/');\n\n\t\t\t// Work out how many IPs the /slash-part matches.\n\t\t\t// We run 2^(32-slash)\n\t\t\tconst numIps = 2 ** (32 - Number(parts[1]));\n\n\t\t\tconst ipStart = ipLib.toLong(parts[0]);\n\t\t\tconst ipEnd = (ipStart + numIps) - 1;\n\t\t\tranges.push([ipStart, ipEnd]);\n\t\t}\n\n\t\t// match range\n\t\tif (cidr.includes('-')) {\n\t\t\tconst parts = cidr.split('-');\n\t\t\tconst ipStart = ipLib.toLong(parts[0].trim());\n\t\t\tconst ipEnd = ipLib.toLong(parts[1].trim());\n\t\t\tranges.push([ipStart, ipEnd]);\n\t\t}\n\n\t\t// match a single ip or ips like (123.156)\n\t\tstartsWith.push(cidr);\n\t}\n\n\t// if we only need to check for simple ranges, do it with regex\n\tif (startsWith.length) {\n\t\trangeRegex = ipRangeCheckRegex(startsWith);\n\t}\n\n\treturn [ranges, rangeRegex];\n}\n\nfunction contains(ip, ranges, rangeRegex) {\n\tif (ranges.length) {\n\t\tconst ipLong = ipLib.toLong(ip);\n\t\tfor (const range of ranges) {\n\t\t\tif (ipLong >= range[0] && ipLong <= range[1]) return true;\n\t\t}\n\t}\n\n\tif (rangeRegex) {\n\t\treturn rangeRegex.test(ip + '.');\n\t}\n\n\treturn false;\n}\n\nclass IPRange {\n\tconstructor(cidrs) {\n\t\tconst [range, rangeRegex] = compile(cidrs || []);\n\t\tthis.range = range;\n\t\tthis.rangeRegex = rangeRegex;\n\t}\n\n\thas(ip) {\n\t\treturn contains(ip, this.range, this.rangeRegex);\n\t}\n\n\tstatic has(ip, cidrs) {\n\t\tif (!cidrs || !cidrs.length) return false;\n\t\tconst [ranges, rangeRegex] = compile(cidrs);\n\t\treturn contains(ip, ranges, rangeRegex);\n\t}\n\n\tstatic isPrivateIp(ip) {\n\t\tif (!compiledPrivateIps) {\n\t\t\tcompiledPrivateIps = new IPRange(PRIVATE_IPS);\n\t\t}\n\t\treturn compiledPrivateIps.has(ip);\n\t}\n\n\tstatic isGoogleIp(ip) {\n\t\tif (!compiledGoogleIps) {\n\t\t\tcompiledGoogleIps = new IPRange(GOOGLE_IPS);\n\t\t}\n\t\treturn compiledGoogleIps.has(ip);\n\t}\n\n\tstatic isSearchIp(ip) {\n\t\tif (!compiledSearchIps) {\n\t\t\tcompiledSearchIps = new IPRange(SEARCH_IPS);\n\t\t}\n\t\treturn this.isGoogleIp(ip) || compiledSearchIps.has(ip);\n\t}\n\n\tstatic isLocalhost(ip) {\n\t\tif (!publicIp) {\n\t\t\tpublicIp = ipLib.address('public') || '0.0.0.0';\n\t\t}\n\t\tif (!privateIp) {\n\t\t\tprivateIp = ipLib.address('private') || '127.0.0.1';\n\t\t}\n\n\t\treturn ['127.0.0.1', publicIp, privateIp].includes(ip);\n\t}\n}\n\nmodule.exports = IPRange;\n"
16
- }
17
- ]
18
- }
@@ -1,118 +0,0 @@
1
- {
2
- "sourceFile": "Request.js",
3
- "activeCommit": 0,
4
- "commits": [
5
- {
6
- "activePatchIndex": 25,
7
- "patches": [
8
- {
9
- "date": 1622037853546,
10
- "content": "Index: \n===================================================================\n--- \n+++ \n"
11
- },
12
- {
13
- "date": 1622037859422,
14
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -820,9 +820,8 @@\n \t\treturn sessionId;\n \t}\n \n \t// session id that's existing (not set in this request)\n-\t// TODO: testing & take params into account\n \texistingSessionId() {\n \t\treturn this.ctx.cookies.get(SESSIONID_COOKIE);\n \t}\n \n"
15
- },
16
- {
17
- "date": 1622037867232,
18
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1165,16 +1165,9 @@\n \t\t}\n \n \t\t// if the medium is direct then only set cookie if it doesn't already exist\n \t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n-\t\tif (!shouldSetCookie) {\n-\t\t\t// fix existing utm cookies (not urlencoded)\n-\t\t\t// TODO: remove later\n-\t\t\tconst existing = this.ctx.cookies.get(UTM_COOKIE) || '';\n-\t\t\tif (!existing.includes('|')) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t}\n+\t\tif (!shouldSetCookie) return;\n \n \t\tthis.cookie(\n \t\t\tUTM_COOKIE,\n \t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n"
19
- },
20
- {
21
- "date": 1622044243935,
22
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1176,8 +1176,50 @@\n \t\t\t},\n \t\t);\n \t}\n \n+\t/**\n+\t * sets UTM cookies from a predefined url\n+\t */\n+\tsetUTMCookieFromUrl(url) {\n+\t\tconst uri = new URL(url, 'http://localhost');\n+\n+\t\tconst params = uri.searchParams;\n+\t\tlet source = params.get('utm_source') || '';\n+\t\tlet medium = params.get('utm_medium') || '';\n+\t\tlet campaign = params.get('utm_campaign') || '';\n+\t\tlet term = params.get('utm_term') || '';\n+\t\tconst content = params.get('utm_content') || '';\n+\n+\t\tif (params.get('gclid')) {\n+\t\t\tsource = source || 'google';\n+\t\t\tmedium = medium || 'cpc';\n+\t\t\tcampaign = campaign || 'google_cpc';\n+\t\t}\n+\n+\t\tconst utmExists = Boolean(source || medium || campaign);\n+\t\tconst referer = this.parseReferer();\n+\t\tthis._refererName = referer.name;\n+\n+\t\tif (!utmExists) {\n+\t\t\tsource = referer.source;\n+\t\t\tmedium = referer.medium;\n+\t\t\tterm = referer.term;\n+\t\t}\n+\n+\t\t// if the medium is direct then only set cookie if it doesn't already exist\n+\t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n+\t\tif (!shouldSetCookie) return;\n+\n+\t\tthis.cookie(\n+\t\t\tUTM_COOKIE,\n+\t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n+\t\t\t\tmaxAge: ONE_MONTH,\n+\t\t\t\tdomain: '*',\n+\t\t\t},\n+\t\t);\n+\t}\n+\n \tsetAffidCookie() {\n \t\tconst affid = this.ctx.query[AFFID_PARAM];\n \t\tconst subaffid = this.ctx.query[SUBAFFID_PARAM];\n \t\tif (!affid) return;\n"
23
- },
24
- {
25
- "date": 1622044357897,
26
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1218,8 +1218,44 @@\n \t\t\t},\n \t\t);\n \t}\n \n+\tsetUTMCookieFromQuery(query) {\n+\t\tlet source = query.utm_source || '';\n+\t\tlet medium = query.utm_medium || '';\n+\t\tlet campaign = query.utm_campaign || '';\n+\t\tlet term = query.utm_term || '';\n+\t\tconst content = query.utm_content || '';\n+\n+\t\tif (query.gclid) {\n+\t\t\tsource = source || 'google';\n+\t\t\tmedium = medium || 'cpc';\n+\t\t\tcampaign = campaign || 'google_cpc';\n+\t\t}\n+\n+\t\tconst utmExists = Boolean(source || medium || campaign);\n+\t\tconst referer = this.parseReferer();\n+\t\tthis._refererName = referer.name;\n+\n+\t\tif (!utmExists) {\n+\t\t\tsource = referer.source;\n+\t\t\tmedium = referer.medium;\n+\t\t\tterm = referer.term;\n+\t\t}\n+\n+\t\t// if the medium is direct then only set cookie if it doesn't already exist\n+\t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n+\t\tif (!shouldSetCookie) return;\n+\n+\t\tthis.cookie(\n+\t\t\tUTM_COOKIE,\n+\t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n+\t\t\t\tmaxAge: ONE_MONTH,\n+\t\t\t\tdomain: '*',\n+\t\t\t},\n+\t\t);\n+\t}\n+\n \tsetAffidCookie() {\n \t\tconst affid = this.ctx.query[AFFID_PARAM];\n \t\tconst subaffid = this.ctx.query[SUBAFFID_PARAM];\n \t\tif (!affid) return;\n"
27
- },
28
- {
29
- "date": 1622044385879,
30
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1139,43 +1139,9 @@\n \t\t};\n \t}\n \n \tsetUTMCookie() {\n-\t\tconst ctx = this.ctx;\n-\n-\t\tlet source = ctx.query.utm_source || '';\n-\t\tlet medium = ctx.query.utm_medium || '';\n-\t\tlet campaign = ctx.query.utm_campaign || '';\n-\t\tlet term = ctx.query.utm_term || '';\n-\t\tconst content = ctx.query.utm_content || '';\n-\n-\t\tif (ctx.query.gclid) {\n-\t\t\tsource = source || 'google';\n-\t\t\tmedium = medium || 'cpc';\n-\t\t\tcampaign = campaign || 'google_cpc';\n-\t\t}\n-\n-\t\tconst utmExists = Boolean(source || medium || campaign);\n-\t\tconst referer = this.parseReferer();\n-\t\tthis._refererName = referer.name;\n-\n-\t\tif (!utmExists) {\n-\t\t\tsource = referer.source;\n-\t\t\tmedium = referer.medium;\n-\t\t\tterm = referer.term;\n-\t\t}\n-\n-\t\t// if the medium is direct then only set cookie if it doesn't already exist\n-\t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n-\t\tif (!shouldSetCookie) return;\n-\n-\t\tthis.cookie(\n-\t\t\tUTM_COOKIE,\n-\t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n-\t\t\t\tmaxAge: ONE_MONTH,\n-\t\t\t\tdomain: '*',\n-\t\t\t},\n-\t\t);\n+\t\tthis.setUTMCookieFromQuery(this.ctx.query);\n \t}\n \n \t/**\n \t * sets UTM cookies from a predefined url\n"
31
- },
32
- {
33
- "date": 1622044489994,
34
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1147,43 +1147,17 @@\n \t * sets UTM cookies from a predefined url\n \t */\n \tsetUTMCookieFromUrl(url) {\n \t\tconst uri = new URL(url, 'http://localhost');\n-\n \t\tconst params = uri.searchParams;\n-\t\tlet source = params.get('utm_source') || '';\n-\t\tlet medium = params.get('utm_medium') || '';\n-\t\tlet campaign = params.get('utm_campaign') || '';\n-\t\tlet term = params.get('utm_term') || '';\n-\t\tconst content = params.get('utm_content') || '';\n-\n-\t\tif (params.get('gclid')) {\n-\t\t\tsource = source || 'google';\n-\t\t\tmedium = medium || 'cpc';\n-\t\t\tcampaign = campaign || 'google_cpc';\n-\t\t}\n-\n-\t\tconst utmExists = Boolean(source || medium || campaign);\n-\t\tconst referer = this.parseReferer();\n-\t\tthis._refererName = referer.name;\n-\n-\t\tif (!utmExists) {\n-\t\t\tsource = referer.source;\n-\t\t\tmedium = referer.medium;\n-\t\t\tterm = referer.term;\n-\t\t}\n-\n-\t\t// if the medium is direct then only set cookie if it doesn't already exist\n-\t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n-\t\tif (!shouldSetCookie) return;\n-\n-\t\tthis.cookie(\n-\t\t\tUTM_COOKIE,\n-\t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n-\t\t\t\tmaxAge: ONE_MONTH,\n-\t\t\t\tdomain: '*',\n-\t\t\t},\n-\t\t);\n+\t\tthis.setUTMCookieFromQuery({\n+\t\t\tutm_source: params.get('utm_source'),\n+\t\t\tutm_medium: params.get('utm_medium'),\n+\t\t\tutm_campaign: params.get('utm_campaign'),\n+\t\t\tutm_term: params.get('utm_term'),\n+\t\t\tutm_content: params.get('utm_content'),\n+\t\t\tgclid: params.get('gclid'),\n+\t\t});\n \t}\n \n \tsetUTMCookieFromQuery(query) {\n \t\tlet source = query.utm_source || '';\n"
35
- },
36
- {
37
- "date": 1622044507475,
38
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1158,8 +1158,11 @@\n \t\t\tgclid: params.get('gclid'),\n \t\t});\n \t}\n \n+\t/**\n+\t * sets UTM cookies from a predefined query object\n+\t */\n \tsetUTMCookieFromQuery(query) {\n \t\tlet source = query.utm_source || '';\n \t\tlet medium = query.utm_medium || '';\n \t\tlet campaign = query.utm_campaign || '';\n"
39
- },
40
- {
41
- "date": 1622044687850,
42
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -965,12 +965,19 @@\n \t\treturn ['get', 'head', 'options'].includes(this.ctx.method.toLowerCase());\n \t}\n \n \tisAjax() {\n-\t\tif (this.ctx.headers['x-requested-with']) {\n-\t\t\treturn this.ctx.headers['x-requested-with'].toLowerCase() === 'xmlhttprequest';\n+\t\tif (this._isAjax === undefined) {\n+\t\t\tconst h = this.ctx.headers['x-requested-with'];\n+\t\t\tif (h) {\n+\t\t\t\tthis._isAjax = h.toLowerCase() === 'xmlhttprequest';\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\tthis._isAjax = false;\n+\t\t\t}\n \t\t}\n-\t\treturn false;\n+\n+\t\treturn this._isAjax;\n \t}\n \n \tisJSEnabled() {\n \t\treturn !!this.cookie('js');\n"
43
- },
44
- {
45
- "date": 1622044695588,
46
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -974,9 +974,8 @@\n \t\t\telse {\n \t\t\t\tthis._isAjax = false;\n \t\t\t}\n \t\t}\n-\n \t\treturn this._isAjax;\n \t}\n \n \tisJSEnabled() {\n"
47
- },
48
- {
49
- "date": 1622044750464,
50
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1218,10 +1218,10 @@\n \t\t);\n \t}\n \n \thandlePlatformModification() {\n-\t\t// don't change platform in mobile apps\n-\t\tif (this.isMobileApp()) return;\n+\t\t// don't change platform in mobile apps and ajax\n+\t\tif (this.isMobileApp() || this.isAjax()) return;\n \n \t\tconst platform = this.ctx.query[PLATFORM_PARAM] || this.cookie(PLATFORM_COOKIE);\n \t\tconst setPlatformCookie = this.setPlatform(platform);\n \t\tif (setPlatformCookie) {\n"
51
- },
52
- {
53
- "date": 1622044809259,
54
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -968,9 +968,9 @@\n \tisAjax() {\n \t\tif (this._isAjax === undefined) {\n \t\t\tconst h = this.ctx.headers['x-requested-with'];\n \t\t\tif (h) {\n-\t\t\t\tthis._isAjax = h.toLowerCase() === 'xmlhttprequest';\n+\t\t\t\tthis._isAjax = ['xmlhttprequest', 'fetch'].includes(h.toLowerCase());\n \t\t\t}\n \t\t\telse {\n \t\t\t\tthis._isAjax = false;\n \t\t\t}\n"
55
- },
56
- {
57
- "date": 1622044831254,
58
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1219,9 +1219,9 @@\n \t}\n \n \thandlePlatformModification() {\n \t\t// don't change platform in mobile apps and ajax\n-\t\tif (this.isMobileApp() || this.isAjax()) return;\n+\t\tif (this.isMobileApp()) return;\n \n \t\tconst platform = this.ctx.query[PLATFORM_PARAM] || this.cookie(PLATFORM_COOKIE);\n \t\tconst setPlatformCookie = this.setPlatform(platform);\n \t\tif (setPlatformCookie) {\n"
59
- },
60
- {
61
- "date": 1622044874192,
62
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -332,12 +332,14 @@\n \t\t\tconst domain = this.baseDomain();\n \t\t\tthis.ctx.set('Content-Security-Policy', `frame-ancestors https://*.${domain}`);\n \t\t}\n \n-\t\tthis.handlePlatformModification();\n-\t\tthis.setUTMCookie();\n-\t\tthis.setAffidCookie();\n-\t\tthis.handleFlashMessage();\n+\t\tif (!this.isAjax()) {\n+\t\t\tthis.handlePlatformModification();\n+\t\t\tthis.setUTMCookie();\n+\t\t\tthis.setAffidCookie();\n+\t\t\tthis.handleFlashMessage();\n+\t\t}\n \n \t\t// in case of visit out from app we need to set cookies from params\n \t\tif (this.ctx.query.installId) {\n \t\t\tawait this.setCookiesFromParams();\n"
63
- },
64
- {
65
- "date": 1622044935260,
66
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -337,13 +337,13 @@\n \t\t\tthis.handlePlatformModification();\n \t\t\tthis.setUTMCookie();\n \t\t\tthis.setAffidCookie();\n \t\t\tthis.handleFlashMessage();\n-\t\t}\n \n-\t\t// in case of visit out from app we need to set cookies from params\n-\t\tif (this.ctx.query.installId) {\n-\t\t\tawait this.setCookiesFromParams();\n+\t\t\t// in case of visit out from app we need to set cookies from params\n+\t\t\tif (this.ctx.query.installId) {\n+\t\t\t\tawait this.setCookiesFromParams();\n+\t\t\t}\n \t\t}\n \t}\n \n \tisHttp() {\n"
67
- },
68
- {
69
- "date": 1622045056034,
70
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1207,10 +1207,10 @@\n \t}\n \n \tsetAffidCookie() {\n \t\tconst affid = this.ctx.query[AFFID_PARAM];\n+\t\tif (!affid) return;\n \t\tconst subaffid = this.ctx.query[SUBAFFID_PARAM];\n-\t\tif (!affid) return;\n \n \t\tthis.cookie(\n \t\t\tAFFID_COOKIE,\n \t\t\tjoinCookieParts([affid, subaffid]), {\n@@ -1219,8 +1219,16 @@\n \t\t\t},\n \t\t);\n \t}\n \n+\tsetAffidCookieFromUrl(url) {\n+\t\tconst uri = new URL(url, 'http://localhost');\n+\t\tconst params = uri.searchParams;\n+\t\tconst affid = params.get(AFFID_PARAM);\n+\t\tif (!affid) return;\n+\t\tconst subaffid = params.get(SUBAFFID_PARAM);\n+\t}\n+\n \thandlePlatformModification() {\n \t\t// don't change platform in mobile apps and ajax\n \t\tif (this.isMobileApp()) return;\n \n"
71
- },
72
- {
73
- "date": 1622045061592,
74
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1225,8 +1225,16 @@\n \t\tconst params = uri.searchParams;\n \t\tconst affid = params.get(AFFID_PARAM);\n \t\tif (!affid) return;\n \t\tconst subaffid = params.get(SUBAFFID_PARAM);\n+\n+\t\tthis.cookie(\n+\t\t\tAFFID_COOKIE,\n+\t\t\tjoinCookieParts([affid, subaffid]), {\n+\t\t\t\tmaxAge: AFFID_COOKIE_DURATION,\n+\t\t\t\tdomain: '*',\n+\t\t\t},\n+\t\t);\n \t}\n \n \thandlePlatformModification() {\n \t\t// don't change platform in mobile apps and ajax\n"
75
- },
76
- {
77
- "date": 1622045128428,
78
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1220,9 +1220,9 @@\n \t\t);\n \t}\n \n \tsetAffidCookieFromUrl(url) {\n-\t\tconst uri = new URL(url, 'http://localhost');\n+\t\tconst uri = (url instanceof URL) ? url : new URL(url, 'http://localhost');\n \t\tconst params = uri.searchParams;\n \t\tconst affid = params.get(AFFID_PARAM);\n \t\tif (!affid) return;\n \t\tconst subaffid = params.get(SUBAFFID_PARAM);\n"
79
- },
80
- {
81
- "date": 1622045144910,
82
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1154,9 +1154,9 @@\n \t/**\n \t * sets UTM cookies from a predefined url\n \t */\n \tsetUTMCookieFromUrl(url) {\n-\t\tconst uri = new URL(url, 'http://localhost');\n+\t\tconst uri = (url instanceof URL) ? url : new URL(url, 'http://localhost');\n \t\tconst params = uri.searchParams;\n \t\tthis.setUTMCookieFromQuery({\n \t\t\tutm_source: params.get('utm_source'),\n \t\t\tutm_medium: params.get('utm_medium'),\n"
83
- },
84
- {
85
- "date": 1622045238469,
86
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1236,9 +1236,9 @@\n \t\t);\n \t}\n \n \thandlePlatformModification() {\n-\t\t// don't change platform in mobile apps and ajax\n+\t\t// don't change platform in mobile apps\n \t\tif (this.isMobileApp()) return;\n \n \t\tconst platform = this.ctx.query[PLATFORM_PARAM] || this.cookie(PLATFORM_COOKIE);\n \t\tconst setPlatformCookie = this.setPlatform(platform);\n"
87
- },
88
- {
89
- "date": 1622703215844,
90
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1127,10 +1127,10 @@\n \t\t\tif (['search', 'searches'].includes(refererSource)) {\n \t\t\t\trefererSource = host;\n \t\t\t}\n \n-\t\t\tconst query = refererUri.query(true);\n-\t\t\tconst term = query.q || query.searchfor || query.pq || 'not_available';\n+\t\t\tconst query = refererUri.searchParams;\n+\t\t\tconst term = query.get('q') || query.get('searchfor') || query.get('pq') || 'not_available';\n \n \t\t\treturn {\n \t\t\t\tname: host,\n \t\t\t\tsource: refererSource,\n"
91
- },
92
- {
93
- "date": 1623167582562,
94
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -417,9 +417,9 @@\n \t\t\t}\n \t\t}\n \n \t\tconst isHttp = this.isHttp();\n-\t\tif (options.httpOnly && !isHttp) {\n+\t\tif (!('secure' in options) && !isHttp) {\n \t\t\toptions.secure = true;\n \t\t}\n \n \t\tif (!('sameSite' in options)) {\n"
95
- },
96
- {
97
- "date": 1623168431212,
98
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -418,8 +418,11 @@\n \t\t}\n \n \t\tconst isHttp = this.isHttp();\n \t\tif (!('secure' in options) && !isHttp) {\n+\t\t\tif (this.ctx.protocol !== 'https') {\n+\t\t\t\tthis.ctx.cookies.secure = true;\n+\t\t\t}\n \t\t\toptions.secure = true;\n \t\t}\n \n \t\tif (!('sameSite' in options)) {\n"
99
- },
100
- {
101
- "date": 1623168515892,
102
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -349,8 +349,11 @@\n \tisHttp() {\n \t\tif (this._isHttp === undefined) {\n \t\t\tconst ctx = this.ctx;\n \t\t\tthis._isHttp = (ctx.headers.origin || '').startsWith('http:') || (ctx.headers.host || '').includes(':');\n+\t\t\tif (!this._isHttp && this.ctx.protocol === 'http') {\n+\t\t\t\tthis.ctx.cookies.secure = true;\n+\t\t\t}\n \t\t}\n \t\treturn this._isHttp;\n \t}\n \n"
103
- },
104
- {
105
- "date": 1623168523023,
106
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -421,11 +421,8 @@\n \t\t}\n \n \t\tconst isHttp = this.isHttp();\n \t\tif (!('secure' in options) && !isHttp) {\n-\t\t\tif (this.ctx.protocol !== 'https') {\n-\t\t\t\tthis.ctx.cookies.secure = true;\n-\t\t\t}\n \t\t\toptions.secure = true;\n \t\t}\n \n \t\tif (!('sameSite' in options)) {\n"
107
- },
108
- {
109
- "date": 1623168534456,
110
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -349,9 +349,9 @@\n \tisHttp() {\n \t\tif (this._isHttp === undefined) {\n \t\t\tconst ctx = this.ctx;\n \t\t\tthis._isHttp = (ctx.headers.origin || '').startsWith('http:') || (ctx.headers.host || '').includes(':');\n-\t\t\tif (!this._isHttp && this.ctx.protocol === 'http') {\n+\t\t\tif (!this._isHttp && this.ctx.protocol !== 'https') {\n \t\t\t\tthis.ctx.cookies.secure = true;\n \t\t\t}\n \t\t}\n \t\treturn this._isHttp;\n"
111
- }
112
- ],
113
- "date": 1622037853546,
114
- "name": "Commit-0",
115
- "content": "/* eslint-disable class-methods-use-this, max-lines */\nconst {UAParser} = require('ua-parser-js');\nconst bodyParser = require('koa-body');\n\nconst IPRange = require('./IPRange');\nconst GeoIP = require('./GeoIP');\n\nconst enableRateLimit = require('./rateLimit');\nconst enableBasicAuth = require('./basicAuth');\nconst enableStaticPaths = require('./staticPaths');\nconst enableBotBanning = require('./botBanning');\n\nconst uaParser = new UAParser();\n\nconst ONE_HOUR = 3600 * 1000;\nconst ONE_DAY = 24 * ONE_HOUR;\nconst ONE_MONTH = 30 * ONE_DAY;\nconst ONE_YEAR = 365 * ONE_DAY;\nconst TEN_YEARS = 10 * ONE_YEAR;\n\nconst PLATFORM_PARAM = 'platform';\nconst PLATFORM_COOKIE = 'platform';\nconst PLATFORM_COOKIE_DURATION = 4 * ONE_HOUR;\nconst APPINFO_PARAM = 'sm_app';\nconst APPINFO_COOKIE = 'sm_app';\nconst APPINFO_HEADER = 'sm-app';\nconst TRACKING_HEADER = 'sm-tracking';\nconst UTM_COOKIE = 'sm_utm';\nconst AFFID_PARAM = 'affid';\nconst SUBAFFID_PARAM = 'subaffid';\nconst COUNTRY_COOKIE = 'country';\nconst AFFID_COOKIE = 'sm_aff';\nconst AFFID_COOKIE_DURATION = ONE_DAY;\nconst REF_PARAM = 'ref';\nconst SESSIONID_COOKIE = 'sid';\nconst COOKIEID_COOKIE = 'id';\nconst COOKIE_PARAM_PREFIX = '_ck_';\nconst USER_TOKEN_COOKIE = 'utok';\nconst FLASH_COOKIE = 'flash';\n// these cookies are httpOnly, should not be readable from js\nconst SENSITIVE_COOKIES = [USER_TOKEN_COOKIE, COOKIEID_COOKIE, SESSIONID_COOKIE];\n\nconst APP_PLATFORMS = new Map([\n\t['android', {}],\n\t['ios', {}],\n\t['wp', {}],\n\t['tizen', {}],\n\t['jio', {}],\n]);\n\n// sometimes request parameters can be an array (like &q=s&q=b)\n// since we expect a string, this will convert array to a string\nfunction handleArray(value, defaultValue = '') {\n\tif (Array.isArray(value)) {\n\t\treturn value[value.length - 1] || value[0] || defaultValue;\n\t}\n\treturn value || defaultValue;\n}\n\nfunction sanitizeCookiePart(value, defaultValue = '') {\n\tif (!value) return defaultValue;\n\treturn handleArray(value).replace(/\\|/g, '!~!').substring(0, 255);\n}\n\nfunction joinCookieParts(parts, defaultValue = '') {\n\treturn parts.map(part => sanitizeCookiePart(part, defaultValue)).join('|');\n}\n\nfunction splitCookieParts(cookie) {\n\treturn cookie.split('|').map(part => part.replace(/!~!/g, '|'));\n}\n\n/**\n * return [subDomain, baseDomain, canHaveSubdomain]\n */\nfunction getDomainParts(domain) {\n\tconst parts = domain.split('.');\n\tif (parts.length <= 1) {\n\t\t// domain is localhost\n\t\treturn ['', domain, false];\n\t}\n\tif (/^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$/.test(domain)) {\n\t\t// domain is an ip\n\t\treturn ['', domain, false];\n\t}\n\n\tif (parts.length <= 2) {\n\t\t// a simple domain like example.com\n\t\treturn ['', domain, true];\n\t}\n\n\tconst secondTld = parts[parts.length - 2];\n\tif (/co|nic|gov/i.test(secondTld)) {\n\t\t// a domain with second level tld, like example.co.uk\n\t\tif (parts.length <= 3) {\n\t\t\treturn ['', domain, true];\n\t\t}\n\t}\n\n\treturn [parts[0], parts.slice(1).join('.'), true];\n}\n\nfunction randomId(length) {\n\tlet id = '';\n\twhile (id.length < length) {\n\t\tid += Math.random().toString(36).substring(2);\n\t}\n\treturn id.substring(0, length);\n}\n\nfunction getIntegerKey(key) {\n\tif (typeof key === 'number') return key;\n\tif (typeof key === 'string') {\n\t\tlet value = 0;\n\t\tfor (let i = 0; i < key.length; ++i) {\n\t\t\tvalue = ((value << 5) - value) + key.charCodeAt(i); // value * 31 + c\n\t\t\tvalue |= 0; // to 32bit integer\n\t\t}\n\t\treturn value;\n\t}\n\treturn key;\n}\n\nfunction addQuery(url, query = {}) {\n\tconst uri = new URL(url, 'http://localhost');\n\tif (typeof query === 'string') {\n\t\tquery = new URLSearchParams(query);\n\t\tfor (const [key, val] of query) {\n\t\t\turi.searchParams.set(key, val);\n\t\t}\n\t}\n\telse {\n\t\tfor (const [key, val] of Object.entries(query)) {\n\t\t\turi.searchParams.set(key, val);\n\t\t}\n\t}\n\treturn `${uri.pathname}${uri.search}`;\n}\n\nconst isProduction = (process.env.NODE_ENV === 'production');\n\nclass Request {\n\t/**\n\t * @param {import('koa').Context} ctx\n\t */\n\tconstructor(ctx, options = {}) {\n\t\tthis.ctx = ctx;\n\t\tthis.options = options;\n\t}\n\n\n\t/** @type {Routes.installer} */\n\tstatic install(app, options = {}) {\n\t\tenableStaticPaths(app, options.staticPaths);\n\t\tapp.use(bodyParser({\n\t\t\tmultipart: true,\n\t\t}));\n\t\tif (options.middleware) {\n\t\t\tapp.use(this.middleware(options));\n\t\t}\n\t\tenableBasicAuth(app, options.basicAuth);\n\t\tenableBotBanning(app, options.banBots);\n\t\tenableRateLimit(app, options.rateLimit);\n\t}\n\n\t/** @returns {Routes.middleware} */\n\tstatic middleware(options) {\n\t\treturn async (ctx, next) => {\n\t\t\tctx.$req = await this.from(ctx, options);\n\t\t\tawait next();\n\t\t};\n\t}\n\n\t/**\n\t * @param {import('koa').Context} ctx\n\t */\n\tstatic async from(ctx, options) {\n\t\tconst req = new this(ctx, options);\n\t\tawait req.init();\n\t\treturn req;\n\t}\n\n\tappPlatforms() {\n\t\treturn APP_PLATFORMS;\n\t}\n\n\t/**\n\t * get or set request header value\n\t * @param {string} name name of the header\n\t * @param {string} value value to set, if not given header value is return\n\t * @returns {string}\n\t * get: header value, or empty string if header not found\n\t * set: empty string\n\t * @example\n\t * // get header value\n\t * ctx.$req.header('user-agent')\n\t * // set header value\n\t * ctx.$req.header('ETag', '1234')\n\t */\n\theader(name, value) {\n\t\t// get header value\n\t\tif (value === undefined) {\n\t\t\treturn this.ctx.headers[name.toLowerCase()] || '';\n\t\t}\n\n\t\t// set header value\n\t\tthis.ctx.set(name, value);\n\t\treturn '';\n\t}\n\n\t/**\n\t * Get value from either from request body or query params\n\t * @param {string} key\n\t * @param {string} defaultValue\n\t * @return {any}\n\t */\n\tparam(key, defaultValue = '') {\n\t\tconst ctx = this.ctx;\n\t\tlet paramValue;\n\n\t\tif (ctx.request.body &&\n\t\t\tctx.request.body.fields &&\n\t\t\t(key in ctx.request.body.fields)\n\t\t) {\n\t\t\tparamValue = ctx.request.body.fields[key];\n\t\t}\n\t\tif (ctx.request.body &&\n\t\t\t(key in ctx.request.body)\n\t\t) {\n\t\t\tparamValue = ctx.request.body[key];\n\t\t}\n\t\telse {\n\t\t\tparamValue = ctx.query[key];\n\t\t}\n\n\t\tparamValue = paramValue || defaultValue;\n\n\t\tif (typeof paramValue === 'string') return paramValue.trim();\n\t\tif (Array.isArray(paramValue)) return paramValue.map(v => v.trim());\n\t\treturn paramValue;\n\t}\n\n\tparamInt(key, defaultValue = 0) {\n\t\tconst value = this.param(key, null);\n\t\tif (value === null) return defaultValue;\n\n\t\tconst intVal = Number(value);\n\t\tif (Number.isNaN(intVal)) {\n\t\t\treturn defaultValue;\n\t\t}\n\n\t\treturn intVal;\n\t}\n\n\tparamBool(key, defaultValue = false) {\n\t\tconst value = this.param(key, null);\n\t\tif (value === null) return defaultValue;\n\n\t\tif (\n\t\t\tvalue === 0 ||\n\t\t\tvalue === '0' ||\n\t\t\tvalue === 'false' ||\n\t\t\tvalue === 'no' ||\n\t\t\tvalue === 'off'\n\t\t) return false;\n\n\t\treturn true;\n\t}\n\n\tparamId(key, defaultValue = '') {\n\t\tconst value = this.param(key, null);\n\t\tif (value === null) return defaultValue;\n\t\tif (/^[A-Za-z0-9]{1,20}$/.test(value)) return value;\n\t\treturn defaultValue;\n\t}\n\n\t/**\n\t * get a string parameter and escape it for xss attacks\n\t * @param {string} key\n\t * @param {string} defaultValue\n\t * @returns {string} value of the param\n\t */\n\tparamXSS(key, defaultValue = '') {\n\t\tconst param = this.param(key, null);\n\t\tif (param === null) return defaultValue;\n\n\t\treturn param.replace(/([<>\"'])/g, (match, g1) => {\n\t\t\tswitch (g1) {\n\t\t\t\tcase '<': return '&lt;';\n\t\t\t\tcase '>': return '&gt;';\n\t\t\t\tcase '\"': return '&quot;';\n\t\t\t\tcase '\\'': return '&#39;';\n\t\t\t\tdefault: return g1;\n\t\t\t}\n\t\t});\n\t}\n\n\tfile(key) {\n\t\treturn this.files(key)[0] || null;\n\t}\n\n\tfiles(key) {\n\t\tif (this.ctx.request.files && this.ctx.request.files[key]) {\n\t\t\tconst result = this.ctx.request.files[key];\n\t\t\tif (!Array.isArray(result)) return [result];\n\t\t\treturn result.filter(Boolean);\n\t\t}\n\n\t\treturn [];\n\t}\n\n\tbearerToken() {\n\t\tconst authorization = this.ctx.headers.authorization;\n\t\tif (!authorization) return '';\n\n\t\tconst parts = authorization.split(' ');\n\t\tif (parts.length !== 2) return '';\n\t\tif (parts[0] !== 'Bearer') return '';\n\n\t\treturn parts[1];\n\t}\n\n\tapiToken() {\n\t\treturn this.header('x-api-token');\n\t}\n\n\t// initialize a request\n\t// set important cookies and all\n\tasync init() {\n\t\t// don't allow other domains to embed our iframe\n\t\tif (isProduction) {\n\t\t\tconst domain = this.baseDomain();\n\t\t\tthis.ctx.set('Content-Security-Policy', `frame-ancestors https://*.${domain}`);\n\t\t}\n\n\t\tthis.handlePlatformModification();\n\t\tthis.setUTMCookie();\n\t\tthis.setAffidCookie();\n\t\tthis.handleFlashMessage();\n\n\t\t// in case of visit out from app we need to set cookies from params\n\t\tif (this.ctx.query.installId) {\n\t\t\tawait this.setCookiesFromParams();\n\t\t}\n\t}\n\n\tisHttp() {\n\t\tif (this._isHttp === undefined) {\n\t\t\tconst ctx = this.ctx;\n\t\t\tthis._isHttp = (ctx.headers.origin || '').startsWith('http:') || (ctx.headers.host || '').includes(':');\n\t\t}\n\t\treturn this._isHttp;\n\t}\n\n\t/**\n\t * @typedef {Object} CustomCookieOpts\n\t * @property {boolean} [onlyCache]\n\t * @property {boolean} [onlyCacheIfExists]\n\t * @property {number} [days]\n\t * @property {number} [years]\n\t */\n\n\t/**\n\t * @template {string | number | boolean | undefined} V\n\t * @param {string} name\n\t * @param {V} value\n\t * @param {CustomCookieOpts & import('cookies').SetOption} [options]\n\t * @returns {V extends undefined ? string : null}\n\t */\n\tcookie(name, value, options = {}) {\n\t\tconst cookies = this._cookies || (this._cookies = {});\n\n\t\tif (value === undefined) {\n\t\t\tif (name in cookies) {\n\t\t\t\treturn cookies[name];\n\t\t\t}\n\n\t\t\tconst existing = this.ctx.cookies.get(name) || '';\n\t\t\treturn decodeURIComponent(existing);\n\t\t}\n\n\t\tcookies[name] = value;\n\n\t\t// only set the cookie in cache\n\t\t// don't set it in real\n\t\tif (options.onlyCache) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// set the cookie only if does not exist\n\t\t// but always set it in cache\n\t\tif (options.onlyCacheIfExists) {\n\t\t\tif (this.ctx.cookies.get(name)) return null;\n\t\t}\n\n\t\t// clone options\n\t\toptions = Object.assign({}, options);\n\n\t\tif (options.domain === '*') {\n\t\t\toptions.domain = this.baseDomain();\n\t\t}\n\t\tif (!('path' in options)) {\n\t\t\toptions.path = '/';\n\t\t}\n\t\tif ('days' in options) {\n\t\t\toptions.maxAge = options.days * ONE_DAY;\n\t\t}\n\t\tif ('years' in options) {\n\t\t\toptions.maxAge = options.years * ONE_YEAR;\n\t\t}\n\n\t\tif (!('httpOnly' in options)) {\n\t\t\tif (!SENSITIVE_COOKIES.includes(name)) {\n\t\t\t\toptions.httpOnly = false;\n\t\t\t}\n\t\t}\n\n\t\tconst isHttp = this.isHttp();\n\t\tif (options.httpOnly && !isHttp) {\n\t\t\toptions.secure = true;\n\t\t}\n\n\t\tif (!('sameSite' in options)) {\n\t\t\t// sameSite = none means intentionally send the cookie in 3rd party contexts\n\t\t\toptions.sameSite = isHttp ? false : 'none';\n\t\t}\n\n\t\tif (value) {\n\t\t\tvalue = encodeURIComponent(value);\n\t\t}\n\n\t\tthis.ctx.cookies.set(name, value, options);\n\t\treturn null;\n\t}\n\n\t/**\n\t * Used for sending tracking info from other servers/sites\n\t * @template {string | undefined} T\n\t * @param {T} key\n\t * @returns {T extends string ? string : Object.<string, string>}\n\t */\n\ttrackingHeader(key) {\n\t\tif (!this._trackingHeader) {\n\t\t\tconst header = this.header(TRACKING_HEADER);\n\t\t\tif (!header) {\n\t\t\t\tthis._trackingHeader = {};\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttry {\n\t\t\t\t\tthis._trackingHeader = JSON.parse(header) || {};\n\t\t\t\t}\n\t\t\t\tcatch (e) {\n\t\t\t\t\tthis._trackingHeader = {};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn key ? this._trackingHeader[key] : this._trackingHeader;\n\t}\n\n\tuserAgent() {\n\t\treturn this.trackingHeader('user-agent') || this.header('user-agent');\n\t}\n\n\treferer() {\n\t\treturn this.trackingHeader('referer') || this.header('referer');\n\t}\n\n\trefererName() {\n\t\treturn this._refererName;\n\t}\n\n\tparseUserAgent() {\n\t\tif (!this._ua) {\n\t\t\tthis._ua = uaParser.setUA(this.userAgent()).getResult() || {};\n\t\t}\n\t\treturn this._ua;\n\t}\n\n\tbrowser() {\n\t\tconst ua = this.parseUserAgent();\n\t\tconst deviceType = (ua && ua.device && ua.device.type) || '';\n\t\tconst browserName = (ua && ua.browser && ua.browser.name) || '';\n\n\t\tif (deviceType === 'mobile') {\n\t\t\tif (browserName === 'Chrome') return 'Chrome Mobile';\n\t\t\tif (browserName === 'Firefox') return 'Firefox Mobile';\n\t\t}\n\n\t\treturn browserName;\n\t}\n\n\tbrowserVersion() {\n\t\tconst ua = this.parseUserAgent();\n\t\tlet browerVersion = (ua.browser && ua.browser.version) || '';\n\t\tif (browerVersion) browerVersion = ' ' + browerVersion;\n\t\treturn this.browser() + browerVersion;\n\t}\n\n\tbrowserVersionRaw() {\n\t\tconst ua = this.parseUserAgent();\n\t\tconst version = (ua.browser && ua.browser.version) || '';\n\t\treturn String(version);\n\t}\n\n\tos() {\n\t\tconst ua = this.parseUserAgent();\n\t\treturn (ua.os && ua.os.name) || '';\n\t}\n\n\tosVersion() {\n\t\tconst ua = this.parseUserAgent();\n\t\tlet osVersion = (ua.os && ua.os.version) || '';\n\t\tif (osVersion) osVersion = ' ' + osVersion;\n\t\treturn this.os() + osVersion;\n\t}\n\n\tsmUserAgent() {\n\t\treturn this.header('sm-user-agent');\n\t}\n\n\tappUserAgent() {\n\t\treturn this.smUserAgent() || this.userAgent();\n\t}\n\n\tgetAppInfoFromUserAgent() {\n\t\treturn null;\n\t}\n\n\t_getAppInfoFromString(infoStr, separator = '#') {\n\t\tif (!infoStr) return null;\n\t\t// eslint-disable-next-line prefer-const\n\t\tlet [platform, appVersion, installId] = infoStr.split(separator);\n\t\tplatform = platform.toLowerCase();\n\n\t\tconst appInfo = {\n\t\t\tplatform,\n\t\t\tappVersion,\n\t\t\tinstallId,\n\t\t};\n\n\t\tif (this.appPlatforms().has(platform)) {\n\t\t\tappInfo.isMobileApp = true;\n\t\t}\n\n\t\treturn appInfo;\n\t}\n\n\tgetAppInfoFromParam() {\n\t\tconst appInfoParam = this.ctx.query[APPINFO_PARAM];\n\t\tif (appInfoParam) {\n\t\t\tthis.cookie(APPINFO_COOKIE, appInfoParam, {\n\t\t\t\tpath: '/',\n\t\t\t\tmaxAge: TEN_YEARS,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\n\t\t\treturn this._getAppInfoFromString(appInfoParam, ':');\n\t\t}\n\n\t\tconst appInfoCookie = this.cookie(APPINFO_COOKIE);\n\t\tif (appInfoCookie) {\n\t\t\treturn this._getAppInfoFromString(appInfoParam, ':');\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tgetAppInfoFromHeader() {\n\t\treturn this._getAppInfoFromString(this.ctx.headers[APPINFO_HEADER]);\n\t}\n\n\tgetAppInfo() {\n\t\tconst appInfoHeader = this.getAppInfoFromHeader();\n\t\tif (appInfoHeader) return appInfoHeader;\n\n\t\tconst appInfoParam = this.getAppInfoFromParam();\n\t\tif (appInfoParam) return appInfoParam;\n\n\t\tconst appInfoUserAgent = this.getAppInfoFromUserAgent();\n\t\tif (appInfoUserAgent) return appInfoUserAgent;\n\n\t\treturn {};\n\t}\n\n\t/*\n\t * get the app related info using a header / query parameter / user agent\n\t * this means that you can have the url as sm_app=android\n\t * and it'll automatically identify it as an android app\n\t */\n\tappInfo(param = null) {\n\t\tif (!this._appInfo) {\n\t\t\tthis._appInfo = this.getAppInfo() || {};\n\t\t}\n\t\treturn param ? this._appInfo[param] : this._appInfo;\n\t}\n\n\tinstallId() {\n\t\treturn this.appInfo('installId') || this.ctx.query.installId || '';\n\t}\n\n\tappVersion() {\n\t\treturn this.appInfo('appVersion') || this.ctx.query.appVersion || '';\n\t}\n\n\tisAndroidApp() {\n\t\treturn this.appInfo('platform') === 'android';\n\t}\n\n\tisIOSApp() {\n\t\treturn this.appInfo('platform') === 'ios';\n\t}\n\n\tisWPApp() {\n\t\treturn this.appInfo('platform') === 'wp';\n\t}\n\n\tisTizenApp() {\n\t\treturn this.appInfo('platform') === 'tizen';\n\t}\n\n\tisJIOApp() {\n\t\treturn this.appInfo('platform') === 'jio';\n\t}\n\n\tisMobileApp() {\n\t\t// for setPlatform cases\n\t\tconst platform = this._platform;\n\t\tif (platform) {\n\t\t\tif (platform === 'mobile_app') return true;\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !!this.appInfo('isMobileApp');\n\t}\n\n\tisMobileWeb() {\n\t\t// for setPlatform cases\n\t\tconst platform = this._platform;\n\t\tif (platform) {\n\t\t\tif (platform === 'mobile_web') return true;\n\t\t\treturn false;\n\t\t}\n\n\t\tif (!this._isMobileWeb) {\n\t\t\tconst ua = this.parseUserAgent();\n\t\t\tthis._isMobileWeb = (ua && ua.device && ua.device.type === 'mobile') || false;\n\t\t}\n\t\treturn this._isMobileWeb;\n\t}\n\n\tisMobile() {\n\t\treturn this.isMobileApp() || this.isMobileWeb();\n\t}\n\n\tplatform() {\n\t\tif (!this._platform) {\n\t\t\tif (this.isMobileApp()) {\n\t\t\t\tthis._platform = 'mobile_app';\n\t\t\t}\n\t\t\telse if (this.isMobileWeb()) {\n\t\t\t\tthis._platform = 'mobile_web';\n\t\t\t}\n\t\t\telse if (this.isAPI()) {\n\t\t\t\tthis._platform = 'api';\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis._platform = 'desktop';\n\t\t\t}\n\t\t}\n\n\t\treturn this._platform;\n\t}\n\n\tsetPlatform(platform) {\n\t\tif (!platform) return false;\n\t\tplatform = handleArray(platform);\n\t\tconst appPlatform = platform.replace('_app', '');\n\n\t\tif (this.appPlatforms().has(appPlatform)) {\n\t\t\tthis._platform = 'mobile_app';\n\t\t\tthis._subPlatform = `${appPlatform}_app`;\n\t\t\treturn false;\n\t\t}\n\n\t\tswitch (platform) {\n\t\t\tcase 'mobile':\n\t\t\tcase 'mobile_web':\n\t\t\t\tthis._platform = 'mobile_web';\n\t\t\t\treturn true;\n\t\t\tcase 'www':\n\t\t\tcase 'desktop':\n\t\t\t\tthis._platform = 'desktop';\n\t\t\t\treturn true;\n\t\t\tcase 'mobile_app':\n\t\t\t\tthis._platform = 'mobile_app';\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\tsubPlatform() {\n\t\tif (!this._subPlatform) {\n\t\t\tconst appPlatform = this.appInfo('platform');\n\t\t\tif (appPlatform && this.isMobileApp()) {\n\t\t\t\tthis._subPlatform = `${appPlatform}_app`;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis._subPlatform = this.platform();\n\t\t\t}\n\t\t}\n\t\treturn this._subPlatform;\n\t}\n\n\tisDesktop() {\n\t\t// for setPlatform cases\n\t\tconst platform = this._platform;\n\t\tif (platform) {\n\t\t\tif (platform === 'desktop') return true;\n\t\t\treturn false;\n\t\t}\n\n\t\treturn this.platform() === 'desktop';\n\t}\n\n\t/**\n\t * @typedef {Object} UTMObject\n\t * @property {string} source\n\t * @property {string} medium\n\t * @property {string} campaign\n\t * @property {string} term\n\t * @property {string} content\n\t * @property {string} sourceMedium\n\t */\n\n\t/**\n\t * @template {string | null} P\n\t * @param {P} [param]\n\t * @returns {P extends null ? UTMObject : P extends string ? string : UTMObject}\n\t */\n\tutm(param = null) {\n\t\tif (!this._utm) {\n\t\t\tconst utmCookie = this.cookie(UTM_COOKIE);\n\t\t\tif (!utmCookie) {\n\t\t\t\tthis._utm = {};\n\t\t\t}\n\t\t\telse {\n\t\t\t\tconst [source, medium, campaign, term, content] = splitCookieParts(utmCookie);\n\t\t\t\tthis._utm = {\n\t\t\t\t\tsource,\n\t\t\t\t\tmedium,\n\t\t\t\t\tcampaign,\n\t\t\t\t\tterm,\n\t\t\t\t\tcontent,\n\t\t\t\t\tsourceMedium: `${source}/${medium}`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn param ? this._utm[param] : this._utm;\n\t}\n\n\t/**\n\t * @returns {string}\n\t */\n\tref() {\n\t\treturn this.param(REF_PARAM).replace(/[^a-zA-Z0-9_.:~-]+/g, '-');\n\t}\n\n\t_affidSubaffid() {\n\t\tconst affidCookie = this.cookie(AFFID_COOKIE);\n\t\tif (!affidCookie) return ['', ''];\n\n\t\tconst [affid, subaffid] = affidCookie.split('|');\n\t\treturn [affid || '', subaffid || ''];\n\t}\n\n\taffid() {\n\t\treturn this._affidSubaffid()[0];\n\t}\n\n\tsubaffid() {\n\t\treturn this._affidSubaffid()[1];\n\t}\n\n\tsubAffid() {\n\t\treturn this.subaffid();\n\t}\n\n\tcookieId() {\n\t\tlet cookieId = this.cookie(COOKIEID_COOKIE);\n\t\tif (!cookieId) {\n\t\t\tconst version = '1';\n\t\t\tcookieId = version + randomId(15);\n\t\t\tthis.cookie(COOKIEID_COOKIE, cookieId, {\n\t\t\t\tmaxAge: TEN_YEARS,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\treturn cookieId;\n\t}\n\n\t// cookie id that's existing (not set in this request)\n\texistingCookieId() {\n\t\treturn this.ctx.cookies.get(COOKIEID_COOKIE);\n\t}\n\n\tsessionId() {\n\t\tlet sessionId = this.cookie(SESSIONID_COOKIE);\n\t\tif (!sessionId) {\n\t\t\tconst version = '1';\n\t\t\tsessionId = version + randomId(15);\n\t\t\tthis.cookie(SESSIONID_COOKIE, sessionId, {\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\treturn sessionId;\n\t}\n\n\t// session id that's existing (not set in this request)\n\t// TODO: testing & take params into account\n\texistingSessionId() {\n\t\treturn this.ctx.cookies.get(SESSIONID_COOKIE);\n\t}\n\n\tisOriginRequest() {\n\t\tif (!isProduction) return true;\n\n\t\tconst baseDomain = this.options.baseDomain;\n\t\tif (!baseDomain) return true;\n\n\t\tconst origin = this.ctx.headers.origin;\n\n\t\t// this is needed because firefox currently does not\n\t\t// send origin with form submit requests (sends with xhr)\n\t\t// so this might cause csrf on firefox\n\t\tif (!origin) return true;\n\n\t\tconst matches = origin.match(/^((http|https):\\/\\/)?([^/:]+)[/]?/i);\n\t\tif (!matches) return false;\n\t\tif (('.' + matches[3]).endsWith('.' + baseDomain)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * @returns {string}\n\t */\n\tip() {\n\t\tif (!this._ip) {\n\t\t\tif (this.trackingHeader('ip')) {\n\t\t\t\tthis._ip = this.trackingHeader('ip');\n\t\t\t}\n\t\t\t// nginx proxy sets x-real-ip header as real ip address\n\t\t\telse if (this.ctx.headers['x-real-ip']) {\n\t\t\t\tthis._ip = this.ctx.headers['x-real-ip'];\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// ip is of format ::ffff:127.1.0.1, strip ::ffff: from it\n\t\t\t\tthis._ip = this.ctx.ip.replace(/^.*:/, '');\n\t\t\t}\n\t\t}\n\n\t\treturn this._ip;\n\t}\n\n\t/**\n\t * @returns {string}\n\t */\n\trealIP() {\n\t\tif (this._realIP) return this._realIP;\n\n\t\tconst forwardedFor = this.header('x-forwarded-for');\n\t\tif (!forwardedFor) {\n\t\t\tthis._realIP = this.ip();\n\t\t\treturn this._realIP;\n\t\t}\n\n\t\tconst forwardedList = forwardedFor.split(',');\n\t\tfor (let ipAddress of forwardedList) {\n\t\t\tipAddress = ipAddress.trim();\n\n\t\t\t// validate ipv4\n\t\t\tconst ipv4Regex = /(::ffff:)?(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})/;\n\t\t\tconst matches = ipAddress.match(ipv4Regex);\n\t\t\tif (!matches) continue;\n\t\t\tipAddress = matches[2];\n\n\t\t\t// validate not private\n\t\t\tif (IPRange.isPrivateIp(ipAddress)) continue;\n\t\t\tthis._realIP = ipAddress;\n\t\t\treturn this._realIP;\n\t\t}\n\n\t\tthis._realIP = this.ip();\n\t\treturn this._realIP;\n\t}\n\n\t/**\n\t * Is being proxied through local proxy\n\t */\n\tisProxied() {\n\t\treturn this.header('x-real-ip') && this.ctx.ip.endsWith('127.0.0.1');\n\t}\n\n\t/**\n\t * Is request being proxied through an external proxy (like chrome data-saver)\n\t */\n\tisVia() {\n\t\treturn this.header('via') && this.realIP() === this.ip();\n\t}\n\n\tparseUrl() {\n\t\tif (!this._uri) {\n\t\t\tthis._uri = new URL(this.ctx.href);\n\t\t}\n\t\treturn this._uri;\n\t}\n\n\tbaseDomain() {\n\t\tif (!this._domainParts) {\n\t\t\tthis._domainParts = getDomainParts(this.domain());\n\t\t}\n\t\treturn this._domainParts[1];\n\t}\n\n\tdomain() {\n\t\treturn this.ctx.hostname;\n\t}\n\n\tsubDomain() {\n\t\tif (!this._domainParts) {\n\t\t\tthis._domainParts = getDomainParts(this.domain());\n\t\t}\n\t\treturn this._domainParts[0];\n\t}\n\n\tport() {\n\t\tconst port = this.ctx.host.split(':')[1];\n\t\treturn port ? Number(port) : 80;\n\t}\n\n\tisLocalhost() {\n\t\treturn IPRange.isLocalhost(this.ip());\n\t}\n\n\tisGoogleIP() {\n\t\treturn IPRange.isGoogleIp(this.ip());\n\t}\n\n\tisSearchIP() {\n\t\treturn IPRange.isSearchIp(this.ip());\n\t}\n\n\tisGet() {\n\t\treturn this.ctx.method.toLowerCase() === 'get';\n\t}\n\n\tisPost() {\n\t\treturn this.ctx.method.toLowerCase() === 'post';\n\t}\n\n\tisGetLike() {\n\t\treturn ['get', 'head', 'options'].includes(this.ctx.method.toLowerCase());\n\t}\n\n\tisAjax() {\n\t\tif (this.ctx.headers['x-requested-with']) {\n\t\t\treturn this.ctx.headers['x-requested-with'].toLowerCase() === 'xmlhttprequest';\n\t\t}\n\t\treturn false;\n\t}\n\n\tisJSEnabled() {\n\t\treturn !!this.cookie('js');\n\t}\n\n\tnextUrl() {\n\t\tconst ctx = this.ctx;\n\t\tif (ctx.query.next) return ctx.query.next;\n\n\t\tconst paramNext = this.param('next');\n\t\tif (paramNext) return paramNext;\n\n\t\tif (!['/login', '/logout', '/signup', '/user/oauth'].includes(ctx.path)) {\n\t\t\treturn ctx.url;\n\t\t}\n\n\t\treturn '/';\n\t}\n\n\tlogoutUrl(redirectUrl = null) {\n\t\tconst nextUrl = redirectUrl || this.nextUrl();\n\t\treturn '/logout?next=' + encodeURIComponent(nextUrl);\n\t}\n\n\tloginUrl() {\n\t\treturn '/login?next=' + encodeURIComponent(this.nextUrl());\n\t}\n\n\tsignupUrl() {\n\t\treturn '/signup?next=' + encodeURIComponent(this.nextUrl());\n\t}\n\n\tmobileUrl() {\n\t\treturn addQuery(this.ctx.url, {[PLATFORM_PARAM]: 'mobile'});\n\t}\n\n\tdesktopUrl() {\n\t\treturn addQuery(this.ctx.url, {[PLATFORM_PARAM]: 'desktop'});\n\t}\n\n\tredirect(url, qs = true) {\n\t\tif (qs === true) {\n\t\t\turl = addQuery(url, this.ctx.querystring);\n\t\t}\n\t\telse if (typeof qs === 'object' || typeof qs === 'string') {\n\t\t\turl = addQuery(url, qs);\n\t\t}\n\n\t\treturn this.ctx.redirect(url);\n\t}\n\n\tredirectPermanent(url, qs = true) {\n\t\tif (qs === true) {\n\t\t\turl = addQuery(url, this.ctx.querystring);\n\t\t}\n\t\telse if (typeof qs === 'object' || typeof qs === 'string') {\n\t\t\turl = addQuery(url, qs);\n\t\t}\n\n\t\tthis.ctx.status = 301;\n\t\treturn this.ctx.redirect(url);\n\t}\n\n\t// this is when user visits an out link from our app\n\t// we send all the cookies as params\n\t// so we need to set the cookies on browser if they don't exist\n\tasync setCookiesFromParams() {\n\t\tconst query = this.ctx.query;\n\n\t\tconst utmCookie = query[COOKIE_PARAM_PREFIX + UTM_COOKIE];\n\t\tif (utmCookie) {\n\t\t\tthis.cookie(UTM_COOKIE, utmCookie, {\n\t\t\t\tmaxAge: ONE_MONTH,\n\t\t\t\tonlyCacheIfExists: true,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\tconst idCookie = query[COOKIE_PARAM_PREFIX + COOKIEID_COOKIE];\n\t\tif (idCookie) {\n\t\t\tthis.cookie(COOKIEID_COOKIE, idCookie, {\n\t\t\t\tmaxAge: TEN_YEARS,\n\t\t\t\tonlyCacheIfExists: true,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\tconst sessionIdCookie = query[COOKIE_PARAM_PREFIX + SESSIONID_COOKIE];\n\t\tif (sessionIdCookie) {\n\t\t\tthis.cookie(SESSIONID_COOKIE, sessionIdCookie, {\n\t\t\t\tonlyCacheIfExists: true,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\tconst affidCookie = query[COOKIE_PARAM_PREFIX + AFFID_COOKIE];\n\t\tif (affidCookie) {\n\t\t\tthis.cookie(AFFID_COOKIE, affidCookie, {\n\t\t\t\tmaxAge: ONE_DAY,\n\t\t\t\tonlyCacheIfExists: true,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\n\t\tconst countryCookie = query[COOKIE_PARAM_PREFIX + COUNTRY_COOKIE];\n\t\tif (countryCookie) {\n\t\t\tthis.cookie(COUNTRY_COOKIE, countryCookie, {\n\t\t\t\tmaxAge: TEN_YEARS,\n\t\t\t});\n\t\t}\n\t}\n\n\tparseReferer() {\n\t\tconst referer = this.referer();\n\t\tif (!referer) {\n\t\t\treturn {\n\t\t\t\tname: '',\n\t\t\t\tsource: 'direct',\n\t\t\t\tmedium: 'direct',\n\t\t\t\tterm: '',\n\t\t\t};\n\t\t}\n\n\t\tconst refererUri = new URL(referer);\n\t\tlet host = refererUri.hostname;\n\t\tconst baseDomain = this.baseDomain();\n\n\t\tif (host.endsWith(baseDomain)) {\n\t\t\treturn {\n\t\t\t\tname: host,\n\t\t\t\tsource: host,\n\t\t\t\tmedium: 'direct',\n\t\t\t\tterm: '',\n\t\t\t};\n\t\t}\n\n\t\thost = host.replace(/^(?:www|m|shop|mobile|lm|l)\\./, '')\n\t\t\t.replace('search.yahoo', 'yahoo');\n\n\t\t// extract search engine names\n\t\tconst searchRegex = /\\.(images\\.google|google|yahoo|bing|ask|duckduckgo|yandex|baidu|babylon|avg|wow|reliancenetconnect|webcrawler|inspsearch|speedbit|searches|search)\\./;\n\t\tconst matches = `.${host}`.match(searchRegex);\n\n\t\tif (matches) {\n\t\t\tlet refererSource = matches[1];\n\t\t\tif (['search', 'searches'].includes(refererSource)) {\n\t\t\t\trefererSource = host;\n\t\t\t}\n\n\t\t\tconst query = refererUri.query(true);\n\t\t\tconst term = query.q || query.searchfor || query.pq || 'not_available';\n\n\t\t\treturn {\n\t\t\t\tname: host,\n\t\t\t\tsource: refererSource,\n\t\t\t\tmedium: 'organic',\n\t\t\t\tterm: handleArray(term).trim(),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tname: host,\n\t\t\tsource: host,\n\t\t\tmedium: 'referral',\n\t\t\tterm: '',\n\t\t};\n\t}\n\n\tsetUTMCookie() {\n\t\tconst ctx = this.ctx;\n\n\t\tlet source = ctx.query.utm_source || '';\n\t\tlet medium = ctx.query.utm_medium || '';\n\t\tlet campaign = ctx.query.utm_campaign || '';\n\t\tlet term = ctx.query.utm_term || '';\n\t\tconst content = ctx.query.utm_content || '';\n\n\t\tif (ctx.query.gclid) {\n\t\t\tsource = source || 'google';\n\t\t\tmedium = medium || 'cpc';\n\t\t\tcampaign = campaign || 'google_cpc';\n\t\t}\n\n\t\tconst utmExists = Boolean(source || medium || campaign);\n\t\tconst referer = this.parseReferer();\n\t\tthis._refererName = referer.name;\n\n\t\tif (!utmExists) {\n\t\t\tsource = referer.source;\n\t\t\tmedium = referer.medium;\n\t\t\tterm = referer.term;\n\t\t}\n\n\t\t// if the medium is direct then only set cookie if it doesn't already exist\n\t\tconst shouldSetCookie = Boolean(utmExists || medium !== 'direct' || !this.cookie(UTM_COOKIE));\n\t\tif (!shouldSetCookie) {\n\t\t\t// fix existing utm cookies (not urlencoded)\n\t\t\t// TODO: remove later\n\t\t\tconst existing = this.ctx.cookies.get(UTM_COOKIE) || '';\n\t\t\tif (!existing.includes('|')) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.cookie(\n\t\t\tUTM_COOKIE,\n\t\t\tjoinCookieParts([source, medium, campaign, term, content], 'none'), {\n\t\t\t\tmaxAge: ONE_MONTH,\n\t\t\t\tdomain: '*',\n\t\t\t},\n\t\t);\n\t}\n\n\tsetAffidCookie() {\n\t\tconst affid = this.ctx.query[AFFID_PARAM];\n\t\tconst subaffid = this.ctx.query[SUBAFFID_PARAM];\n\t\tif (!affid) return;\n\n\t\tthis.cookie(\n\t\t\tAFFID_COOKIE,\n\t\t\tjoinCookieParts([affid, subaffid]), {\n\t\t\t\tmaxAge: AFFID_COOKIE_DURATION,\n\t\t\t\tdomain: '*',\n\t\t\t},\n\t\t);\n\t}\n\n\thandlePlatformModification() {\n\t\t// don't change platform in mobile apps\n\t\tif (this.isMobileApp()) return;\n\n\t\tconst platform = this.ctx.query[PLATFORM_PARAM] || this.cookie(PLATFORM_COOKIE);\n\t\tconst setPlatformCookie = this.setPlatform(platform);\n\t\tif (setPlatformCookie) {\n\t\t\tthis.cookie(PLATFORM_COOKIE, platform, {\n\t\t\t\tmaxAge: PLATFORM_COOKIE_DURATION,\n\t\t\t\tdomain: '*',\n\t\t\t});\n\t\t}\n\t}\n\n\tipLocation() {\n\t\tif (this._ipLoc === undefined) {\n\t\t\tconst loc = GeoIP.getSync(this.ip()) || {};\n\t\t\tconst country = loc.country;\n\t\t\tconst city = loc.city;\n\t\t\tconst state = loc.subdivisions && loc.subdivisions[0];\n\t\t\tthis._ipLoc = {\n\t\t\t\tcountry: {\n\t\t\t\t\tname: (country && country.names.en) || '',\n\t\t\t\t\tisoCode: (country && country.iso_code) || '',\n\t\t\t\t},\n\t\t\t\tcity: {\n\t\t\t\t\tname: (city && city.names.en) || '',\n\t\t\t\t\tisoCode: (city && city.iso_code) || '',\n\t\t\t\t},\n\t\t\t\tstate: {\n\t\t\t\t\tname: (state && state.names.en) || '',\n\t\t\t\t\tisoCode: (state && state.iso_code) || '',\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn this._ipLoc;\n\t}\n\n\tipCountry() {\n\t\treturn this.ipLocation().country.name;\n\t}\n\n\tipCity() {\n\t\treturn this.ipLocation().city.name;\n\t}\n\n\tipState() {\n\t\treturn this.ipLocation().state.name;\n\t}\n\n\tgetABNumber(min = 0, max = false) {\n\t\tconst cookieId = this.cookieId() || 'nocookie';\n\t\tconst num = getIntegerKey(cookieId.substr(-4));\n\t\tif (max === false) return num;\n\t\treturn (num % ((max + 1) - min)) + min;\n\t}\n\n\tgetBot() {\n\t\tconst bots = [\n\t\t\t['googlebot', 'googlebot'],\n\t\t\t['googlepreview', 'google web preview'],\n\t\t\t['googlemobile', 'google wireless'],\n\t\t\t['googleadsbot', 'Mediapartners-Google'],\n\n\t\t\t['bingbot', 'bingbot', 'msnbot'],\n\n\t\t\t['yahoobot', 'yahoo'],\n\t\t\t['facebookbot', 'facebook'],\n\t\t\t['alexabot', 'ia_archiver'],\n\n\t\t\t['baidubot', 'baidu'],\n\t\t\t['twitterbot', 'twitter'],\n\n\t\t\t['bot', 'bot', 'spider', 'crawl', 'dig', 'search', 'http', 'url'],\n\t\t];\n\n\t\tconst userAgent = this.userAgent().toLowerCase();\n\t\tif (!userAgent) {\n\t\t\treturn 'emptybot';\n\t\t}\n\n\t\tfor (const bot of bots) {\n\t\t\tconst botName = bot[0];\n\t\t\tfor (let i = 1; i < bot.length; i++) {\n\t\t\t\tif (userAgent.includes(bot[i].toLowerCase())) {\n\t\t\t\t\treturn botName;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tisBot() {\n\t\treturn Boolean(this.getBot());\n\t}\n\n\tsupportsWebp() {\n\t\tconst accept = this.ctx.headers.accept || '';\n\t\treturn accept.includes('image/webp');\n\t}\n\n\tbestImageFormat(defaultFormat = 'jpg') {\n\t\tif (this.supportsWebp()) return 'webp';\n\t\treturn defaultFormat;\n\t}\n\n\thandleFlashMessage() {\n\t\tif (this.isAjax()) return;\n\n\t\tthis._flash = '';\n\t\tconst cookieVal = this.cookie(FLASH_COOKIE);\n\t\tif (!cookieVal) return;\n\n\t\ttry {\n\t\t\t// cookie format is `format:actualMessage`\n\t\t\tconst formatIndex = cookieVal.indexOf(':');\n\t\t\tconst format = cookieVal.substring(0, formatIndex);\n\t\t\tif (format !== 'json') {\n\t\t\t\tthrow new Error(`Unknown flash message format ${format}`);\n\t\t\t}\n\n\t\t\tthis._flash = JSON.parse(cookieVal.substring(formatIndex + 1));\n\t\t}\n\t\tcatch (e) {\n\t\t\tthis._flash = '';\n\t\t\tconsole.error('Error parsing flash message', e);\n\t\t}\n\n\t\tthis.cookie(FLASH_COOKIE, null);\n\t}\n\n\t/**\n\t * get or set a flash message (can be an object too)\n\t * @example\n\t * $req.flash('info', 'hello'); // set a flash message\n\t * $req.flash('info'); // get a flash message\n\t *\n\t * @param {string} key key of the flash message\n\t * @param {*} message message to set (skip to get the message)\n\t * @returns {*} flash message\n\t */\n\tflash(key, message) {\n\t\tif (message === undefined) {\n\t\t\treturn (this._flash && this._flash[key]) || '';\n\t\t}\n\n\t\t// ignore empty flash message\n\t\tif (!message) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet flashes = this._responseFlash;\n\t\tif (!flashes) {\n\t\t\tflashes = {};\n\t\t\tthis._responseFlash = flashes;\n\t\t}\n\n\t\tflashes[key] = message;\n\n\t\tconst maxAge = 3 * 60 * 1000; // valid till 3 minutes\n\t\tconst cookieValue = `json:${JSON.stringify(flashes)}`;\n\t\tthis.cookie(\n\t\t\tFLASH_COOKIE,\n\t\t\tcookieValue,\n\t\t\t{maxAge, httpOnly: false},\n\t\t);\n\t\treturn null;\n\t}\n\n\tstatic async getDummyCtx({\n\t\tparams = {},\n\t\tquery = {},\n\t\tstate = {},\n\t\tmethod = 'GET',\n\t\tpath = '/',\n\t\turl,\n\t\treq = false,\n\t} = {}) {\n\t\tconst context = {\n\t\t\tquery,\n\t\t\tparams,\n\t\t\tstate,\n\t\t\turl: url || path,\n\t\t\tpath,\n\t\t\tthrow: (...args) => {\n\t\t\t\tthrow new Error(args.join(' '));\n\t\t\t},\n\t\t\tmethod,\n\t\t\tsession: {},\n\t\t\theaders: {},\n\t\t\tcookies: new Map(),\n\t\t\thost: this.options.baseDomain || 'localhost',\n\t\t\thostname: this.options.baseDomain || 'localhost',\n\t\t\tredirect: (...args) => { console.warn('Redirect to', args) },\n\t\t\tset(key, val) {\n\t\t\t\tif (typeof key === 'string') {\n\t\t\t\t\tthis.headers[key] = val;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tObject.assign(this.headers, key);\n\t\t\t},\n\t\t};\n\t\tif (req) context.$req = await this.from(context);\n\t\treturn context;\n\t}\n}\n\nmodule.exports = Request;\n"
116
- }
117
- ]
118
- }
@@ -1,22 +0,0 @@
1
- {
2
- "sourceFile": "postinstall.js",
3
- "activeCommit": 0,
4
- "commits": [
5
- {
6
- "activePatchIndex": 1,
7
- "patches": [
8
- {
9
- "date": 1622571930512,
10
- "content": "Index: \n===================================================================\n--- \n+++ \n"
11
- },
12
- {
13
- "date": 1622571942798,
14
- "content": "Index: \n===================================================================\n--- \n+++ \n@@ -7,5 +7,6 @@\n main().then(() => {\n \tprocess.exit();\n }).catch((err) => {\n \tconsole.error('KoaRequest postinstall error', err);\n+\tprocess.exit(1);\n });\n"
15
- }
16
- ],
17
- "date": 1622571930512,
18
- "name": "Commit-0",
19
- "content": "const GeoIP = require('./GeoIP');\n\nasync function main() {\n\tawait GeoIP.init();\n}\n\nmain().then(() => {\n\tprocess.exit();\n}).catch((err) => {\n\tconsole.error('KoaRequest postinstall error', err);\n});\n"
20
- }
21
- ]
22
- }