@eggjs/jsonp 3.0.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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/commonjs/app/extend/application.d.ts +10 -0
- package/dist/commonjs/app/extend/application.js +115 -0
- package/dist/commonjs/app/extend/context.d.ts +15 -0
- package/dist/commonjs/app/extend/context.js +35 -0
- package/dist/commonjs/config/config.default.d.ts +5 -0
- package/dist/commonjs/config/config.default.js +11 -0
- package/dist/commonjs/error/JSONPForbiddenReferrerError.d.ts +5 -0
- package/dist/commonjs/error/JSONPForbiddenReferrerError.js +16 -0
- package/dist/commonjs/index.d.ts +1 -0
- package/dist/commonjs/index.js +4 -0
- package/dist/commonjs/lib/private_key.d.ts +1 -0
- package/dist/commonjs/lib/private_key.js +5 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/types.d.ts +50 -0
- package/dist/commonjs/types.js +3 -0
- package/dist/esm/app/extend/application.d.ts +10 -0
- package/dist/esm/app/extend/application.js +112 -0
- package/dist/esm/app/extend/context.d.ts +15 -0
- package/dist/esm/app/extend/context.js +32 -0
- package/dist/esm/config/config.default.d.ts +5 -0
- package/dist/esm/config/config.default.js +9 -0
- package/dist/esm/error/JSONPForbiddenReferrerError.d.ts +5 -0
- package/dist/esm/error/JSONPForbiddenReferrerError.js +12 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/lib/private_key.d.ts +1 -0
- package/dist/esm/lib/private_key.js +2 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/types.d.ts +50 -0
- package/dist/esm/types.js +2 -0
- package/dist/package.json +4 -0
- package/package.json +94 -0
- package/src/app/extend/application.ts +131 -0
- package/src/app/extend/context.ts +34 -0
- package/src/config/config.default.ts +10 -0
- package/src/error/JSONPForbiddenReferrerError.ts +12 -0
- package/src/index.ts +1 -0
- package/src/lib/private_key.ts +1 -0
- package/src/types.ts +55 -0
- package/src/typings/index.d.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Alibaba Group Holding Limited and other contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @eggjs/jsonp
|
|
2
|
+
|
|
3
|
+
[![NPM version][npm-image]][npm-url]
|
|
4
|
+
[](https://github.com/eggjs/jsonp/actions/workflows/nodejs.yml)
|
|
5
|
+
[![Test coverage][codecov-image]][codecov-url]
|
|
6
|
+
[![Known Vulnerabilities][snyk-image]][snyk-url]
|
|
7
|
+
[![npm download][download-image]][download-url]
|
|
8
|
+
[](https://nodejs.org/en/download/)
|
|
9
|
+
[](https://makeapullrequest.com)
|
|
10
|
+
|
|
11
|
+
[npm-image]: https://img.shields.io/npm/v/@eggjs/jsonp.svg?style=flat-square
|
|
12
|
+
[npm-url]: https://npmjs.org/package/@eggjs/jsonp
|
|
13
|
+
[codecov-image]: https://img.shields.io/codecov/c/github/eggjs/jsonp.svg?style=flat-square
|
|
14
|
+
[codecov-url]: https://codecov.io/github/eggjs/jsonp?branch=master
|
|
15
|
+
[snyk-image]: https://snyk.io/test/npm/@eggjs/jsonp/badge.svg?style=flat-square
|
|
16
|
+
[snyk-url]: https://snyk.io/test/npm/@eggjs/jsonp
|
|
17
|
+
[download-image]: https://img.shields.io/npm/dm/@eggjs/jsonp.svg?style=flat-square
|
|
18
|
+
[download-url]: https://npmjs.org/package/@eggjs/jsonp
|
|
19
|
+
|
|
20
|
+
An egg plugin for jsonp support.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- egg >= 4.x
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm i @eggjs/jsonp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// {app_root}/config/plugin.ts
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
jsonp: {
|
|
39
|
+
enable: true,
|
|
40
|
+
package: '@eggjs/jsonp',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
- {String|Array} callback - jsonp callback method key, default to `[ '_callback', 'callback' ]`
|
|
48
|
+
- {Number} limit - callback method name's max length, default to `50`
|
|
49
|
+
- {Boolean} csrf - enable csrf check or not. default to false
|
|
50
|
+
- {String|RegExp|Array} whiteList - referrer white list
|
|
51
|
+
|
|
52
|
+
if whiteList's type is `RegExp`, referrer must match `whiteList`, pay attention to the first `^` and last `/`.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export default {
|
|
56
|
+
jsonp: {
|
|
57
|
+
whiteList: /^https?:\/\/test.com\//,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// matchs referrer:
|
|
62
|
+
// https://test.com/hello
|
|
63
|
+
// http://test.com/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
if whiteList's type is `String` and starts with `.`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
export default {
|
|
70
|
+
jsonp: {
|
|
71
|
+
whiteList: '.test.com',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// matchs domain test.com:
|
|
76
|
+
// https://test.com/hello
|
|
77
|
+
// http://test.com/
|
|
78
|
+
|
|
79
|
+
// matchs subdomain
|
|
80
|
+
// https://sub.test.com/hello
|
|
81
|
+
// http://sub.sub.test.com/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
if whiteList's type is `String` and not starts with `.`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export default {
|
|
88
|
+
jsonp: {
|
|
89
|
+
whiteList: 'sub.test.com',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// only matchs domain sub.test.com:
|
|
94
|
+
// https://sub.test.com/hello
|
|
95
|
+
// http://sub.test.com/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
whiteList also can be an array:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export default {
|
|
102
|
+
jsonp: {
|
|
103
|
+
whiteList: [ '.foo.com', '.bar.com' ],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
see [config/config.default.ts](https://github.com/eggjs/jsonp/blob/master/src/config/config.default.ts) for more detail.
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
- ctx.acceptJSONP - detect if response should be jsonp, readonly
|
|
113
|
+
|
|
114
|
+
## Example
|
|
115
|
+
|
|
116
|
+
In `app/router.ts`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Create once and use in any router you want to support jsonp.
|
|
120
|
+
const jsonp = app.jsonp();
|
|
121
|
+
|
|
122
|
+
app.get('/default', jsonp, 'jsonp.index');
|
|
123
|
+
app.get('/another', jsonp, 'jsonp.another');
|
|
124
|
+
|
|
125
|
+
// Customize by create another jsonp middleware with specific configurations.
|
|
126
|
+
app.get('/customize', app.jsonp({ callback: 'fn' }), 'jsonp.customize');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Questions & Suggestions
|
|
130
|
+
|
|
131
|
+
Please open an issue [here](https://github.com/eggjs/egg/issues).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE)
|
|
136
|
+
|
|
137
|
+
## Contributors
|
|
138
|
+
|
|
139
|
+
[](https://github.com/eggjs/jsonp/graphs/contributors)
|
|
140
|
+
|
|
141
|
+
Made with [contributors-img](https://contrib.rocks).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EggCore, type MiddlewareFunc } from '@eggjs/core';
|
|
2
|
+
import { JSONPConfig } from '../../types.js';
|
|
3
|
+
export default class JSONPApplication extends EggCore {
|
|
4
|
+
/**
|
|
5
|
+
* return a middleware to enable jsonp response.
|
|
6
|
+
* will do some security check inside.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
jsonp(initOptions?: Partial<JSONPConfig>): MiddlewareFunc;
|
|
10
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_util_1 = require("node:util");
|
|
4
|
+
const node_url_1 = require("node:url");
|
|
5
|
+
const core_1 = require("@eggjs/core");
|
|
6
|
+
const private_key_js_1 = require("../../lib/private_key.js");
|
|
7
|
+
const JSONPForbiddenReferrerError_js_1 = require("../../error/JSONPForbiddenReferrerError.js");
|
|
8
|
+
const debug = (0, node_util_1.debuglog)('@egg/jsonp/app/extend/application');
|
|
9
|
+
class JSONPApplication extends core_1.EggCore {
|
|
10
|
+
/**
|
|
11
|
+
* return a middleware to enable jsonp response.
|
|
12
|
+
* will do some security check inside.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
jsonp(initOptions = {}) {
|
|
16
|
+
const options = {
|
|
17
|
+
...this.config.jsonp,
|
|
18
|
+
...initOptions,
|
|
19
|
+
};
|
|
20
|
+
if (!Array.isArray(options.callback)) {
|
|
21
|
+
options.callback = [options.callback];
|
|
22
|
+
}
|
|
23
|
+
const csrfEnable = this.plugins.security && this.plugins.security.enable // security enable
|
|
24
|
+
&& this.config.security.csrf && this.config.security.csrf.enable !== false // csrf enable
|
|
25
|
+
&& options.csrf; // jsonp csrf enabled
|
|
26
|
+
const validateReferrer = options.whiteList && createValidateReferer(options.whiteList);
|
|
27
|
+
if (!csrfEnable && !validateReferrer) {
|
|
28
|
+
this.coreLogger.warn('[@eggjs/jsonp] SECURITY WARNING!! csrf check and referrer check are both closed!');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* jsonp request security check, pass if
|
|
32
|
+
*
|
|
33
|
+
* 1. hit referrer white list
|
|
34
|
+
* 2. or pass csrf check
|
|
35
|
+
* 3. both check are disabled
|
|
36
|
+
*
|
|
37
|
+
* @param {Context} ctx request context
|
|
38
|
+
*/
|
|
39
|
+
function securityAssert(ctx) {
|
|
40
|
+
// all disabled. don't need check
|
|
41
|
+
if (!csrfEnable && !validateReferrer)
|
|
42
|
+
return;
|
|
43
|
+
// pass referrer check
|
|
44
|
+
const referrer = ctx.get('referrer');
|
|
45
|
+
if (validateReferrer && validateReferrer(referrer))
|
|
46
|
+
return;
|
|
47
|
+
if (csrfEnable && validateCsrf(ctx))
|
|
48
|
+
return;
|
|
49
|
+
throw new JSONPForbiddenReferrerError_js_1.JSONPForbiddenReferrerError('jsonp request security validate failed', referrer, 403);
|
|
50
|
+
}
|
|
51
|
+
return async function jsonp(ctx, next) {
|
|
52
|
+
const jsonpFunction = getJsonpFunction(ctx.query, options.callback);
|
|
53
|
+
ctx[private_key_js_1.JSONP_CONFIG] = {
|
|
54
|
+
jsonpFunction,
|
|
55
|
+
options,
|
|
56
|
+
};
|
|
57
|
+
// before handle request, must do some security checks
|
|
58
|
+
securityAssert(ctx);
|
|
59
|
+
await next();
|
|
60
|
+
// generate jsonp body
|
|
61
|
+
ctx.createJsonpBody(ctx.body);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.default = JSONPApplication;
|
|
66
|
+
function createValidateReferer(whiteList) {
|
|
67
|
+
if (!Array.isArray(whiteList)) {
|
|
68
|
+
whiteList = [whiteList];
|
|
69
|
+
}
|
|
70
|
+
return (referrer) => {
|
|
71
|
+
let parsed;
|
|
72
|
+
for (const rule of whiteList) {
|
|
73
|
+
if (rule instanceof RegExp) {
|
|
74
|
+
if (rule.test(referrer)) {
|
|
75
|
+
// regexp(/^https?:\/\/github.com\//): test the referrer with rule
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
parsed = parsed ?? (0, node_url_1.parse)(referrer);
|
|
81
|
+
const hostname = parsed.hostname || '';
|
|
82
|
+
// check if referrer's hostname match the string rule
|
|
83
|
+
if (rule[0] === '.' &&
|
|
84
|
+
(hostname.endsWith(rule) || hostname === rule.slice(1))) {
|
|
85
|
+
// string start with `.`(.github.com): referrer's hostname must ends with rule
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
else if (hostname === rule) {
|
|
89
|
+
// string not start with `.`(github.com): referrer's hostname must strict equal to rule
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// no rule matched
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function validateCsrf(ctx) {
|
|
98
|
+
try {
|
|
99
|
+
// TODO(fengmk2): remove this when @eggjs/security support ctx.assertCsrf type define
|
|
100
|
+
ctx.assertCsrf();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
debug('validate csrf failed: %s', err);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function getJsonpFunction(query, callbacks) {
|
|
109
|
+
for (const callback of callbacks) {
|
|
110
|
+
if (query[callback]) {
|
|
111
|
+
return query[callback];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwbGljYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvYXBwL2V4dGVuZC9hcHBsaWNhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUFxQztBQUNyQyx1Q0FBc0U7QUFFdEUsc0NBQTJEO0FBQzNELDZEQUF3RDtBQUV4RCwrRkFBeUY7QUFHekYsTUFBTSxLQUFLLEdBQUcsSUFBQSxvQkFBUSxFQUFDLG1DQUFtQyxDQUFDLENBQUM7QUFFNUQsTUFBcUIsZ0JBQWlCLFNBQVEsY0FBTztJQUNuRDs7OztPQUlHO0lBQ0gsS0FBSyxDQUFDLGNBQW9DLEVBQUU7UUFDMUMsTUFBTSxPQUFPLEdBQUc7WUFDZCxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSztZQUNwQixHQUFHLFdBQVc7U0FDeUIsQ0FBQztRQUMxQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztZQUNyQyxPQUFPLENBQUMsUUFBUSxHQUFHLENBQUUsT0FBTyxDQUFDLFFBQVEsQ0FBRSxDQUFDO1FBQzFDLENBQUM7UUFFRCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsa0JBQWtCO2VBQ3RGLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsTUFBTSxLQUFLLEtBQUssQ0FBQyxjQUFjO2VBQ3RGLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxxQkFBcUI7UUFFeEMsTUFBTSxnQkFBZ0IsR0FBRyxPQUFPLENBQUMsU0FBUyxJQUFJLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUV2RixJQUFJLENBQUMsVUFBVSxJQUFJLENBQUMsZ0JBQWdC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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from '@eggjs/core';
|
|
2
|
+
export default class JSONPContext extends Context {
|
|
3
|
+
/**
|
|
4
|
+
* detect if response should be jsonp
|
|
5
|
+
*/
|
|
6
|
+
get acceptJSONP(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* JSONP wrap body function
|
|
9
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
10
|
+
* If not necessary, please don't use this method in your application code.
|
|
11
|
+
* @param {Object} body response body
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
createJsonpBody(body: any): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsonp_body_1 = require("jsonp-body");
|
|
4
|
+
const core_1 = require("@eggjs/core");
|
|
5
|
+
const private_key_js_1 = require("../../lib/private_key.js");
|
|
6
|
+
class JSONPContext extends core_1.Context {
|
|
7
|
+
/**
|
|
8
|
+
* detect if response should be jsonp
|
|
9
|
+
*/
|
|
10
|
+
get acceptJSONP() {
|
|
11
|
+
const jsonpConfig = Reflect.get(this, private_key_js_1.JSONP_CONFIG);
|
|
12
|
+
return !!(jsonpConfig?.jsonpFunction);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* JSONP wrap body function
|
|
16
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
17
|
+
* If not necessary, please don't use this method in your application code.
|
|
18
|
+
* @param {Object} body response body
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
createJsonpBody(body) {
|
|
22
|
+
const jsonpConfig = Reflect.get(this, private_key_js_1.JSONP_CONFIG);
|
|
23
|
+
if (!jsonpConfig?.jsonpFunction) {
|
|
24
|
+
this.body = body;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.set('x-content-type-options', 'nosniff');
|
|
28
|
+
this.type = 'js';
|
|
29
|
+
body = body === undefined ? null : body;
|
|
30
|
+
// protect from jsonp xss
|
|
31
|
+
this.body = (0, jsonp_body_1.jsonp)(body, jsonpConfig.jsonpFunction, jsonpConfig.options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.default = JSONPContext;
|
|
35
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9hcHAvZXh0ZW5kL2NvbnRleHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQ0FBZ0Q7QUFDaEQsc0NBQXNDO0FBQ3RDLDZEQUF3RDtBQUV4RCxNQUFxQixZQUFhLFNBQVEsY0FBTztJQUMvQzs7T0FFRztJQUNILElBQUksV0FBVztRQUNiLE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLDZCQUFZLENBQVEsQ0FBQztRQUMzRCxPQUFPLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxhQUFhLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBRUQ7Ozs7OztPQU1HO0lBQ0gsZUFBZSxDQUFDLElBQVM7UUFDdkIsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsNkJBQVksQ0FBUSxDQUFDO1FBQzNELElBQUksQ0FBQyxXQUFXLEVBQUUsYUFBYSxFQUFFLENBQUM7WUFDaEMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7WUFDakIsT0FBTztRQUNULENBQUM7UUFFRCxJQUFJLENBQUMsR0FBRyxDQUFDLHdCQUF3QixFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBQzlDLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1FBQ2pCLElBQUksR0FBRyxJQUFJLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUN4Qyx5QkFBeUI7UUFDekIsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFBLGtCQUFTLEVBQUMsSUFBSSxFQUFFLFdBQVcsQ0FBQyxhQUFhLEVBQUUsV0FBVyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzlFLENBQUM7Q0FDRjtBQTdCRCwrQkE2QkMifQ==
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = {
|
|
4
|
+
jsonp: {
|
|
5
|
+
limit: 50,
|
|
6
|
+
callback: ['_callback', 'callback'],
|
|
7
|
+
csrf: false,
|
|
8
|
+
whiteList: undefined,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmRlZmF1bHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29uZmlnL2NvbmZpZy5kZWZhdWx0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBRUEsa0JBQWU7SUFDYixLQUFLLEVBQUU7UUFDTCxLQUFLLEVBQUUsRUFBRTtRQUNULFFBQVEsRUFBRSxDQUFFLFdBQVcsRUFBRSxVQUFVLENBQUU7UUFDckMsSUFBSSxFQUFFLEtBQUs7UUFDWCxTQUFTLEVBQUUsU0FBUztLQUNOO0NBQ2pCLENBQUMifQ==
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JSONPForbiddenReferrerError = void 0;
|
|
4
|
+
class JSONPForbiddenReferrerError extends Error {
|
|
5
|
+
referrer;
|
|
6
|
+
status;
|
|
7
|
+
constructor(message, referrer, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
this.referrer = referrer;
|
|
11
|
+
this.status = status;
|
|
12
|
+
Error.captureStackTrace(this, this.constructor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.JSONPForbiddenReferrerError = JSONPForbiddenReferrerError;
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiSlNPTlBGb3JiaWRkZW5SZWZlcnJlckVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2Vycm9yL0pTT05QRm9yYmlkZGVuUmVmZXJyZXJFcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxNQUFhLDJCQUE0QixTQUFRLEtBQUs7SUFDcEQsUUFBUSxDQUFTO0lBQ2pCLE1BQU0sQ0FBUztJQUVmLFlBQVksT0FBZSxFQUFFLFFBQWdCLEVBQUUsTUFBYztRQUMzRCxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDZixJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDO1FBQ2xDLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO1FBQ3pCLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDO1FBQ3JCLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBQ2xELENBQUM7Q0FDRjtBQVhELGtFQVdDIn0=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './types.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("./types.js");
|
|
4
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxzQkFBb0IifQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const JSONP_CONFIG: unique symbol;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JSONP_CONFIG = void 0;
|
|
4
|
+
exports.JSONP_CONFIG = Symbol('jsonp#config');
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJpdmF0ZV9rZXkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL3ByaXZhdGVfa2V5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFhLFFBQUEsWUFBWSxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MiddlewareFunc } from '@eggjs/core';
|
|
2
|
+
/**
|
|
3
|
+
* jsonp options
|
|
4
|
+
* @member Config#jsonp
|
|
5
|
+
*/
|
|
6
|
+
export interface JSONPConfig {
|
|
7
|
+
/**
|
|
8
|
+
* jsonp callback methods key, default to `['_callback', 'callback' ]`
|
|
9
|
+
*/
|
|
10
|
+
callback: string[] | string;
|
|
11
|
+
/**
|
|
12
|
+
* callback method name's max length, default to `50`
|
|
13
|
+
*/
|
|
14
|
+
limit: number;
|
|
15
|
+
/**
|
|
16
|
+
* enable csrf check or not, default to `false`
|
|
17
|
+
*/
|
|
18
|
+
csrf: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* referrer white list, default to `undefined`
|
|
21
|
+
*/
|
|
22
|
+
whiteList?: string | RegExp | (string | RegExp)[];
|
|
23
|
+
}
|
|
24
|
+
declare module '@eggjs/core' {
|
|
25
|
+
interface EggAppConfig {
|
|
26
|
+
jsonp: JSONPConfig;
|
|
27
|
+
}
|
|
28
|
+
interface Context {
|
|
29
|
+
/**
|
|
30
|
+
* detect if response should be jsonp
|
|
31
|
+
*/
|
|
32
|
+
acceptJSONP: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* JSONP wrap body function
|
|
35
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
36
|
+
* If not necessary, please don't use this method in your application code.
|
|
37
|
+
* @param {Object} body response body
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
createJsonpBody(body: any): void;
|
|
41
|
+
}
|
|
42
|
+
interface EggCore {
|
|
43
|
+
/**
|
|
44
|
+
* return a middleware to enable jsonp response.
|
|
45
|
+
* will do some security check inside.
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
jsonp(initOptions?: Partial<JSONPConfig>): MiddlewareFunc;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EggCore, type MiddlewareFunc } from '@eggjs/core';
|
|
2
|
+
import { JSONPConfig } from '../../types.js';
|
|
3
|
+
export default class JSONPApplication extends EggCore {
|
|
4
|
+
/**
|
|
5
|
+
* return a middleware to enable jsonp response.
|
|
6
|
+
* will do some security check inside.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
jsonp(initOptions?: Partial<JSONPConfig>): MiddlewareFunc;
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { debuglog } from 'node:util';
|
|
2
|
+
import { parse as urlParse } from 'node:url';
|
|
3
|
+
import { EggCore } from '@eggjs/core';
|
|
4
|
+
import { JSONP_CONFIG } from '../../lib/private_key.js';
|
|
5
|
+
import { JSONPForbiddenReferrerError } from '../../error/JSONPForbiddenReferrerError.js';
|
|
6
|
+
const debug = debuglog('@egg/jsonp/app/extend/application');
|
|
7
|
+
export default class JSONPApplication extends EggCore {
|
|
8
|
+
/**
|
|
9
|
+
* return a middleware to enable jsonp response.
|
|
10
|
+
* will do some security check inside.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
jsonp(initOptions = {}) {
|
|
14
|
+
const options = {
|
|
15
|
+
...this.config.jsonp,
|
|
16
|
+
...initOptions,
|
|
17
|
+
};
|
|
18
|
+
if (!Array.isArray(options.callback)) {
|
|
19
|
+
options.callback = [options.callback];
|
|
20
|
+
}
|
|
21
|
+
const csrfEnable = this.plugins.security && this.plugins.security.enable // security enable
|
|
22
|
+
&& this.config.security.csrf && this.config.security.csrf.enable !== false // csrf enable
|
|
23
|
+
&& options.csrf; // jsonp csrf enabled
|
|
24
|
+
const validateReferrer = options.whiteList && createValidateReferer(options.whiteList);
|
|
25
|
+
if (!csrfEnable && !validateReferrer) {
|
|
26
|
+
this.coreLogger.warn('[@eggjs/jsonp] SECURITY WARNING!! csrf check and referrer check are both closed!');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* jsonp request security check, pass if
|
|
30
|
+
*
|
|
31
|
+
* 1. hit referrer white list
|
|
32
|
+
* 2. or pass csrf check
|
|
33
|
+
* 3. both check are disabled
|
|
34
|
+
*
|
|
35
|
+
* @param {Context} ctx request context
|
|
36
|
+
*/
|
|
37
|
+
function securityAssert(ctx) {
|
|
38
|
+
// all disabled. don't need check
|
|
39
|
+
if (!csrfEnable && !validateReferrer)
|
|
40
|
+
return;
|
|
41
|
+
// pass referrer check
|
|
42
|
+
const referrer = ctx.get('referrer');
|
|
43
|
+
if (validateReferrer && validateReferrer(referrer))
|
|
44
|
+
return;
|
|
45
|
+
if (csrfEnable && validateCsrf(ctx))
|
|
46
|
+
return;
|
|
47
|
+
throw new JSONPForbiddenReferrerError('jsonp request security validate failed', referrer, 403);
|
|
48
|
+
}
|
|
49
|
+
return async function jsonp(ctx, next) {
|
|
50
|
+
const jsonpFunction = getJsonpFunction(ctx.query, options.callback);
|
|
51
|
+
ctx[JSONP_CONFIG] = {
|
|
52
|
+
jsonpFunction,
|
|
53
|
+
options,
|
|
54
|
+
};
|
|
55
|
+
// before handle request, must do some security checks
|
|
56
|
+
securityAssert(ctx);
|
|
57
|
+
await next();
|
|
58
|
+
// generate jsonp body
|
|
59
|
+
ctx.createJsonpBody(ctx.body);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createValidateReferer(whiteList) {
|
|
64
|
+
if (!Array.isArray(whiteList)) {
|
|
65
|
+
whiteList = [whiteList];
|
|
66
|
+
}
|
|
67
|
+
return (referrer) => {
|
|
68
|
+
let parsed;
|
|
69
|
+
for (const rule of whiteList) {
|
|
70
|
+
if (rule instanceof RegExp) {
|
|
71
|
+
if (rule.test(referrer)) {
|
|
72
|
+
// regexp(/^https?:\/\/github.com\//): test the referrer with rule
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
parsed = parsed ?? urlParse(referrer);
|
|
78
|
+
const hostname = parsed.hostname || '';
|
|
79
|
+
// check if referrer's hostname match the string rule
|
|
80
|
+
if (rule[0] === '.' &&
|
|
81
|
+
(hostname.endsWith(rule) || hostname === rule.slice(1))) {
|
|
82
|
+
// string start with `.`(.github.com): referrer's hostname must ends with rule
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
else if (hostname === rule) {
|
|
86
|
+
// string not start with `.`(github.com): referrer's hostname must strict equal to rule
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// no rule matched
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function validateCsrf(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
// TODO(fengmk2): remove this when @eggjs/security support ctx.assertCsrf type define
|
|
97
|
+
ctx.assertCsrf();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
debug('validate csrf failed: %s', err);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function getJsonpFunction(query, callbacks) {
|
|
106
|
+
for (const callback of callbacks) {
|
|
107
|
+
if (query[callback]) {
|
|
108
|
+
return query[callback];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwbGljYXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvYXBwL2V4dGVuZC9hcHBsaWNhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ3JDLE9BQU8sRUFBRSxLQUFLLElBQUksUUFBUSxFQUEyQixNQUFNLFVBQVUsQ0FBQztBQUV0RSxPQUFPLEVBQUUsT0FBTyxFQUF1QixNQUFNLGFBQWEsQ0FBQztBQUMzRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFeEQsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sNENBQTRDLENBQUM7QUFHekYsTUFBTSxLQUFLLEdBQUcsUUFBUSxDQUFDLG1DQUFtQyxDQUFDLENBQUM7QUFFNUQsTUFBTSxDQUFDLE9BQU8sT0FBTyxnQkFBaUIsU0FBUSxPQUFPO0lBQ25EOzs7O09BSUc7SUFDSCxLQUFLLENBQUMsY0FBb0MsRUFBRTtRQUMxQyxNQUFNLE9BQU8sR0FBRztZQUNkLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLO1lBQ3BCLEdBQUcsV0FBVztTQUN5QixDQUFDO1FBQzFDLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1lBQ3JDLE9BQU8sQ0FBQyxRQUFRLEdBQUcsQ0FBRSxPQUFPLENBQUMsUUFBUSxDQUFFLENBQUM7UUFDMUMsQ0FBQztRQUVELE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxrQkFBa0I7ZUFDdEYsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxNQUFNLEtBQUssS0FBSyxD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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from '@eggjs/core';
|
|
2
|
+
export default class JSONPContext extends Context {
|
|
3
|
+
/**
|
|
4
|
+
* detect if response should be jsonp
|
|
5
|
+
*/
|
|
6
|
+
get acceptJSONP(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* JSONP wrap body function
|
|
9
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
10
|
+
* If not necessary, please don't use this method in your application code.
|
|
11
|
+
* @param {Object} body response body
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
createJsonpBody(body: any): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsonp as jsonpBody } from 'jsonp-body';
|
|
2
|
+
import { Context } from '@eggjs/core';
|
|
3
|
+
import { JSONP_CONFIG } from '../../lib/private_key.js';
|
|
4
|
+
export default class JSONPContext extends Context {
|
|
5
|
+
/**
|
|
6
|
+
* detect if response should be jsonp
|
|
7
|
+
*/
|
|
8
|
+
get acceptJSONP() {
|
|
9
|
+
const jsonpConfig = Reflect.get(this, JSONP_CONFIG);
|
|
10
|
+
return !!(jsonpConfig?.jsonpFunction);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* JSONP wrap body function
|
|
14
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
15
|
+
* If not necessary, please don't use this method in your application code.
|
|
16
|
+
* @param {Object} body response body
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
createJsonpBody(body) {
|
|
20
|
+
const jsonpConfig = Reflect.get(this, JSONP_CONFIG);
|
|
21
|
+
if (!jsonpConfig?.jsonpFunction) {
|
|
22
|
+
this.body = body;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.set('x-content-type-options', 'nosniff');
|
|
26
|
+
this.type = 'js';
|
|
27
|
+
body = body === undefined ? null : body;
|
|
28
|
+
// protect from jsonp xss
|
|
29
|
+
this.body = jsonpBody(body, jsonpConfig.jsonpFunction, jsonpConfig.options);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9hcHAvZXh0ZW5kL2NvbnRleHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssSUFBSSxTQUFTLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFDaEQsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFeEQsTUFBTSxDQUFDLE9BQU8sT0FBTyxZQUFhLFNBQVEsT0FBTztJQUMvQzs7T0FFRztJQUNILElBQUksV0FBVztRQUNiLE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLFlBQVksQ0FBUSxDQUFDO1FBQzNELE9BQU8sQ0FBQyxDQUFDLENBQUMsV0FBVyxFQUFFLGFBQWEsQ0FBQyxDQUFDO0lBQ3hDLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSCxlQUFlLENBQUMsSUFBUztRQUN2QixNQUFNLFdBQVcsR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxZQUFZLENBQVEsQ0FBQztRQUMzRCxJQUFJLENBQUMsV0FBVyxFQUFFLGFBQWEsRUFBRSxDQUFDO1lBQ2hDLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1lBQ2pCLE9BQU87UUFDVCxDQUFDO1FBRUQsSUFBSSxDQUFDLEdBQUcsQ0FBQyx3QkFBd0IsRUFBRSxTQUFTLENBQUMsQ0FBQztRQUM5QyxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLEdBQUcsSUFBSSxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDeEMseUJBQXlCO1FBQ3pCLElBQUksQ0FBQyxJQUFJLEdBQUcsU0FBUyxDQUFDLElBQUksRUFBRSxXQUFXLENBQUMsYUFBYSxFQUFFLFdBQVcsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUM5RSxDQUFDO0NBQ0YifQ==
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
jsonp: {
|
|
3
|
+
limit: 50,
|
|
4
|
+
callback: ['_callback', 'callback'],
|
|
5
|
+
csrf: false,
|
|
6
|
+
whiteList: undefined,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmRlZmF1bHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29uZmlnL2NvbmZpZy5kZWZhdWx0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLGVBQWU7SUFDYixLQUFLLEVBQUU7UUFDTCxLQUFLLEVBQUUsRUFBRTtRQUNULFFBQVEsRUFBRSxDQUFFLFdBQVcsRUFBRSxVQUFVLENBQUU7UUFDckMsSUFBSSxFQUFFLEtBQUs7UUFDWCxTQUFTLEVBQUUsU0FBUztLQUNOO0NBQ2pCLENBQUMifQ==
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class JSONPForbiddenReferrerError extends Error {
|
|
2
|
+
referrer;
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, referrer, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = this.constructor.name;
|
|
7
|
+
this.referrer = referrer;
|
|
8
|
+
this.status = status;
|
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiSlNPTlBGb3JiaWRkZW5SZWZlcnJlckVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2Vycm9yL0pTT05QRm9yYmlkZGVuUmVmZXJyZXJFcnJvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLE9BQU8sMkJBQTRCLFNBQVEsS0FBSztJQUNwRCxRQUFRLENBQVM7SUFDakIsTUFBTSxDQUFTO0lBRWYsWUFBWSxPQUFlLEVBQUUsUUFBZ0IsRUFBRSxNQUFjO1FBQzNELEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNmLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUM7UUFDbEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUM7UUFDekIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsS0FBSyxDQUFDLGlCQUFpQixDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUM7SUFDbEQsQ0FBQztDQUNGIn0=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const JSONP_CONFIG: unique symbol;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const JSONP_CONFIG = Symbol('jsonp#config');
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJpdmF0ZV9rZXkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbGliL3ByaXZhdGVfa2V5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE1BQU0sQ0FBQyxNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUMifQ==
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MiddlewareFunc } from '@eggjs/core';
|
|
2
|
+
/**
|
|
3
|
+
* jsonp options
|
|
4
|
+
* @member Config#jsonp
|
|
5
|
+
*/
|
|
6
|
+
export interface JSONPConfig {
|
|
7
|
+
/**
|
|
8
|
+
* jsonp callback methods key, default to `['_callback', 'callback' ]`
|
|
9
|
+
*/
|
|
10
|
+
callback: string[] | string;
|
|
11
|
+
/**
|
|
12
|
+
* callback method name's max length, default to `50`
|
|
13
|
+
*/
|
|
14
|
+
limit: number;
|
|
15
|
+
/**
|
|
16
|
+
* enable csrf check or not, default to `false`
|
|
17
|
+
*/
|
|
18
|
+
csrf: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* referrer white list, default to `undefined`
|
|
21
|
+
*/
|
|
22
|
+
whiteList?: string | RegExp | (string | RegExp)[];
|
|
23
|
+
}
|
|
24
|
+
declare module '@eggjs/core' {
|
|
25
|
+
interface EggAppConfig {
|
|
26
|
+
jsonp: JSONPConfig;
|
|
27
|
+
}
|
|
28
|
+
interface Context {
|
|
29
|
+
/**
|
|
30
|
+
* detect if response should be jsonp
|
|
31
|
+
*/
|
|
32
|
+
acceptJSONP: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* JSONP wrap body function
|
|
35
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
36
|
+
* If not necessary, please don't use this method in your application code.
|
|
37
|
+
* @param {Object} body response body
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
createJsonpBody(body: any): void;
|
|
41
|
+
}
|
|
42
|
+
interface EggCore {
|
|
43
|
+
/**
|
|
44
|
+
* return a middleware to enable jsonp response.
|
|
45
|
+
* will do some security check inside.
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
jsonp(initOptions?: Partial<JSONPConfig>): MiddlewareFunc;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eggjs/jsonp",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "jsonp support for egg",
|
|
8
|
+
"eggPlugin": {
|
|
9
|
+
"name": "jsonp",
|
|
10
|
+
"optionalDependencies": [
|
|
11
|
+
"security"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
"import": "./dist/esm",
|
|
15
|
+
"require": "./dist/commonjs",
|
|
16
|
+
"typescript": "./src"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"egg",
|
|
21
|
+
"egg-plugin",
|
|
22
|
+
"jsonp",
|
|
23
|
+
"security"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/eggjs/jsonp.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/eggjs/egg/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/eggjs/jsonp#readme",
|
|
33
|
+
"author": "dead-horse",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">= 18.19.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@eggjs/core": "^6.2.13",
|
|
40
|
+
"jsonp-body": "^2.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@arethetypeswrong/cli": "^0.17.1",
|
|
44
|
+
"@eggjs/bin": "7",
|
|
45
|
+
"@eggjs/mock": "6",
|
|
46
|
+
"@eggjs/tsconfig": "1",
|
|
47
|
+
"@types/mocha": "10",
|
|
48
|
+
"@types/node": "22",
|
|
49
|
+
"egg": "4",
|
|
50
|
+
"eslint": "8",
|
|
51
|
+
"eslint-config-egg": "14",
|
|
52
|
+
"rimraf": "6",
|
|
53
|
+
"tshy": "3",
|
|
54
|
+
"tshy-after": "1",
|
|
55
|
+
"typescript": "5"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"lint": "eslint --cache src test --ext .ts",
|
|
59
|
+
"pretest": "npm run clean && npm run lint -- --fix",
|
|
60
|
+
"test": "egg-bin test",
|
|
61
|
+
"preci": "npm run clean && npm run lint",
|
|
62
|
+
"ci": "egg-bin cov",
|
|
63
|
+
"postci": "npm run prepublishOnly && npm run clean",
|
|
64
|
+
"clean": "rimraf dist",
|
|
65
|
+
"prepublishOnly": "tshy && tshy-after && attw --pack"
|
|
66
|
+
},
|
|
67
|
+
"type": "module",
|
|
68
|
+
"tshy": {
|
|
69
|
+
"exports": {
|
|
70
|
+
".": "./src/index.ts",
|
|
71
|
+
"./package.json": "./package.json"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"exports": {
|
|
75
|
+
".": {
|
|
76
|
+
"import": {
|
|
77
|
+
"types": "./dist/esm/index.d.ts",
|
|
78
|
+
"default": "./dist/esm/index.js"
|
|
79
|
+
},
|
|
80
|
+
"require": {
|
|
81
|
+
"types": "./dist/commonjs/index.d.ts",
|
|
82
|
+
"default": "./dist/commonjs/index.js"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"./package.json": "./package.json"
|
|
86
|
+
},
|
|
87
|
+
"files": [
|
|
88
|
+
"dist",
|
|
89
|
+
"src"
|
|
90
|
+
],
|
|
91
|
+
"types": "./dist/commonjs/index.d.ts",
|
|
92
|
+
"main": "./dist/commonjs/index.js",
|
|
93
|
+
"module": "./dist/esm/index.js"
|
|
94
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { debuglog } from 'node:util';
|
|
2
|
+
import { parse as urlParse, type UrlWithStringQuery } from 'node:url';
|
|
3
|
+
import type { ParsedUrlQuery } from 'node:querystring';
|
|
4
|
+
import { EggCore, type MiddlewareFunc } from '@eggjs/core';
|
|
5
|
+
import { JSONP_CONFIG } from '../../lib/private_key.js';
|
|
6
|
+
import { JSONPConfig } from '../../types.js';
|
|
7
|
+
import { JSONPForbiddenReferrerError } from '../../error/JSONPForbiddenReferrerError.js';
|
|
8
|
+
import JSONPContext from './context.js';
|
|
9
|
+
|
|
10
|
+
const debug = debuglog('@egg/jsonp/app/extend/application');
|
|
11
|
+
|
|
12
|
+
export default class JSONPApplication extends EggCore {
|
|
13
|
+
/**
|
|
14
|
+
* return a middleware to enable jsonp response.
|
|
15
|
+
* will do some security check inside.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
jsonp(initOptions: Partial<JSONPConfig> = {}): MiddlewareFunc {
|
|
19
|
+
const options = {
|
|
20
|
+
...this.config.jsonp,
|
|
21
|
+
...initOptions,
|
|
22
|
+
} as JSONPConfig & { callback: string[] };
|
|
23
|
+
if (!Array.isArray(options.callback)) {
|
|
24
|
+
options.callback = [ options.callback ];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const csrfEnable = this.plugins.security && this.plugins.security.enable // security enable
|
|
28
|
+
&& this.config.security.csrf && this.config.security.csrf.enable !== false // csrf enable
|
|
29
|
+
&& options.csrf; // jsonp csrf enabled
|
|
30
|
+
|
|
31
|
+
const validateReferrer = options.whiteList && createValidateReferer(options.whiteList);
|
|
32
|
+
|
|
33
|
+
if (!csrfEnable && !validateReferrer) {
|
|
34
|
+
this.coreLogger.warn('[@eggjs/jsonp] SECURITY WARNING!! csrf check and referrer check are both closed!');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* jsonp request security check, pass if
|
|
38
|
+
*
|
|
39
|
+
* 1. hit referrer white list
|
|
40
|
+
* 2. or pass csrf check
|
|
41
|
+
* 3. both check are disabled
|
|
42
|
+
*
|
|
43
|
+
* @param {Context} ctx request context
|
|
44
|
+
*/
|
|
45
|
+
function securityAssert(ctx: JSONPContext) {
|
|
46
|
+
// all disabled. don't need check
|
|
47
|
+
if (!csrfEnable && !validateReferrer) return;
|
|
48
|
+
|
|
49
|
+
// pass referrer check
|
|
50
|
+
const referrer = ctx.get<string>('referrer');
|
|
51
|
+
if (validateReferrer && validateReferrer(referrer)) return;
|
|
52
|
+
if (csrfEnable && validateCsrf(ctx)) return;
|
|
53
|
+
|
|
54
|
+
throw new JSONPForbiddenReferrerError(
|
|
55
|
+
'jsonp request security validate failed',
|
|
56
|
+
referrer,
|
|
57
|
+
403);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return async function jsonp(ctx: JSONPContext, next) {
|
|
61
|
+
const jsonpFunction = getJsonpFunction(ctx.query, options.callback);
|
|
62
|
+
|
|
63
|
+
ctx[JSONP_CONFIG] = {
|
|
64
|
+
jsonpFunction,
|
|
65
|
+
options,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// before handle request, must do some security checks
|
|
69
|
+
securityAssert(ctx);
|
|
70
|
+
|
|
71
|
+
await next();
|
|
72
|
+
|
|
73
|
+
// generate jsonp body
|
|
74
|
+
ctx.createJsonpBody(ctx.body);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createValidateReferer(whiteList: Required<JSONPConfig>['whiteList']) {
|
|
80
|
+
if (!Array.isArray(whiteList)) {
|
|
81
|
+
whiteList = [ whiteList ];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (referrer: string) => {
|
|
85
|
+
let parsed: UrlWithStringQuery | undefined;
|
|
86
|
+
for (const rule of whiteList) {
|
|
87
|
+
if (rule instanceof RegExp) {
|
|
88
|
+
if (rule.test(referrer)) {
|
|
89
|
+
// regexp(/^https?:\/\/github.com\//): test the referrer with rule
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parsed = parsed ?? urlParse(referrer);
|
|
96
|
+
const hostname = parsed.hostname || '';
|
|
97
|
+
|
|
98
|
+
// check if referrer's hostname match the string rule
|
|
99
|
+
if (rule[0] === '.' &&
|
|
100
|
+
(hostname.endsWith(rule) || hostname === rule.slice(1))) {
|
|
101
|
+
// string start with `.`(.github.com): referrer's hostname must ends with rule
|
|
102
|
+
return true;
|
|
103
|
+
} else if (hostname === rule) {
|
|
104
|
+
// string not start with `.`(github.com): referrer's hostname must strict equal to rule
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// no rule matched
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateCsrf(ctx: any) {
|
|
115
|
+
try {
|
|
116
|
+
// TODO(fengmk2): remove this when @eggjs/security support ctx.assertCsrf type define
|
|
117
|
+
ctx.assertCsrf();
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
debug('validate csrf failed: %s', err);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getJsonpFunction(query: ParsedUrlQuery, callbacks: string[]) {
|
|
126
|
+
for (const callback of callbacks) {
|
|
127
|
+
if (query[callback]) {
|
|
128
|
+
return query[callback] as string;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsonp as jsonpBody } from 'jsonp-body';
|
|
2
|
+
import { Context } from '@eggjs/core';
|
|
3
|
+
import { JSONP_CONFIG } from '../../lib/private_key.js';
|
|
4
|
+
|
|
5
|
+
export default class JSONPContext extends Context {
|
|
6
|
+
/**
|
|
7
|
+
* detect if response should be jsonp
|
|
8
|
+
*/
|
|
9
|
+
get acceptJSONP() {
|
|
10
|
+
const jsonpConfig = Reflect.get(this, JSONP_CONFIG) as any;
|
|
11
|
+
return !!(jsonpConfig?.jsonpFunction);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* JSONP wrap body function
|
|
16
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
17
|
+
* If not necessary, please don't use this method in your application code.
|
|
18
|
+
* @param {Object} body response body
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
createJsonpBody(body: any) {
|
|
22
|
+
const jsonpConfig = Reflect.get(this, JSONP_CONFIG) as any;
|
|
23
|
+
if (!jsonpConfig?.jsonpFunction) {
|
|
24
|
+
this.body = body;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.set('x-content-type-options', 'nosniff');
|
|
29
|
+
this.type = 'js';
|
|
30
|
+
body = body === undefined ? null : body;
|
|
31
|
+
// protect from jsonp xss
|
|
32
|
+
this.body = jsonpBody(body, jsonpConfig.jsonpFunction, jsonpConfig.options);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class JSONPForbiddenReferrerError extends Error {
|
|
2
|
+
referrer: string;
|
|
3
|
+
status: number;
|
|
4
|
+
|
|
5
|
+
constructor(message: string, referrer: string, status: number) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
this.referrer = referrer;
|
|
9
|
+
this.status = status;
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const JSONP_CONFIG = Symbol('jsonp#config');
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MiddlewareFunc } from '@eggjs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jsonp options
|
|
5
|
+
* @member Config#jsonp
|
|
6
|
+
*/
|
|
7
|
+
export interface JSONPConfig {
|
|
8
|
+
/**
|
|
9
|
+
* jsonp callback methods key, default to `['_callback', 'callback' ]`
|
|
10
|
+
*/
|
|
11
|
+
callback: string[] | string;
|
|
12
|
+
/**
|
|
13
|
+
* callback method name's max length, default to `50`
|
|
14
|
+
*/
|
|
15
|
+
limit: number;
|
|
16
|
+
/**
|
|
17
|
+
* enable csrf check or not, default to `false`
|
|
18
|
+
*/
|
|
19
|
+
csrf: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* referrer white list, default to `undefined`
|
|
22
|
+
*/
|
|
23
|
+
whiteList?: string | RegExp | (string | RegExp)[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module '@eggjs/core' {
|
|
27
|
+
// add EggAppConfig overrides types
|
|
28
|
+
interface EggAppConfig {
|
|
29
|
+
jsonp: JSONPConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Context {
|
|
33
|
+
/**
|
|
34
|
+
* detect if response should be jsonp
|
|
35
|
+
*/
|
|
36
|
+
acceptJSONP: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* JSONP wrap body function
|
|
39
|
+
* Set jsonp response wrap function, other plugin can use it.
|
|
40
|
+
* If not necessary, please don't use this method in your application code.
|
|
41
|
+
* @param {Object} body response body
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
createJsonpBody(body: any): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface EggCore {
|
|
48
|
+
/**
|
|
49
|
+
* return a middleware to enable jsonp response.
|
|
50
|
+
* will do some security check inside.
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
jsonp(initOptions?: Partial<JSONPConfig>): MiddlewareFunc;
|
|
54
|
+
}
|
|
55
|
+
}
|