-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
155 lines (128 loc) · 5.74 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
module.exports = function ({whiteList, blackList, interceptorTemplate, redirectUrl, encodeFunc}) {
return {
middlewares: [
async (ctx, next) => {
if (ctx.path.toLowerCase() !== '/api/comment' || ctx.method.toUpperCase() !== 'GET') {
return next();
}
const type = ctx.param('type');
if (type === 'count' || type === 'list') {
return next();
}
/**
* Black and white list judgment logic
* @param url
* @returns {boolean|*}
*/
function isDisallowedUrl(url) {
const host = (new URL(url)).host;
if (host === ctx.host) {
return true;
}
const isAllowListMode = Array.isArray(whiteList);
const isBlockListMode = Array.isArray(blackList);
// Domain Name Match Logic
const matchFunction = (e) => {
const e_str = e.replace(/\./g, '\\.').replace(/\*/g, '.*');
const regex = new RegExp(`${e_str}|([a-z0-9]+\\.)*${e_str}`);
return regex.test(host);
}
if (isAllowListMode && isBlockListMode) {
const inBlackList = blackList.find(matchFunction);
const inWhiteList = whiteList.find(matchFunction);
return !inBlackList || inWhiteList;
}
if (isAllowListMode) {
return whiteList.find(matchFunction);
}
if (isBlockListMode) {
return !blackList.find(matchFunction);
}
}
/**
* In-comment link handler function
* @param text
* @returns {*}
*/
function replaceUrl(text) {
return text.replace(/href="([^"#]+)(#[^"]+)?"/g, (originText, url, hashtag) => {
const redirectToo = redirectUrl ? redirectUrl : `${ctx.protocol}://${ctx.host}/api/redirect`;
const encodeFuncc = encodeFunc ? encodeFunc : (url) => {
return 'url=' + encodeURIComponent(url);
}
if (isDisallowedUrl(url)) {
return originText;
}
if (hashtag === undefined) {
hashtag = "";
}
return `href="${redirectToo}?${encodeFuncc(url + hashtag)}"`;
});
}
/**
* Avatar link handler function
* @param link
* @returns {string}
*/
function replaceLink(link) {
// Null link
if (!link) {
return "";
}
// Add protocol
const pattern = /^https?:\/\//;
if (!pattern.test(link)) {
link = 'https://' + link;
}
// validate URL
try {
new URL(link)
} catch (e) {
return "";
}
if (isDisallowedUrl(link)) {
return link;
}
const redirectToo = redirectUrl ? redirectUrl : `${ctx.protocol}://${ctx.host}/api/redirect`;
const encodeFuncc = encodeFunc ? encodeFunc : (url) => {
return 'url=' + encodeURIComponent(url);
}
return `${redirectToo}?${encodeFuncc(link)}`;
}
const _oldSuccess = ctx.success;
ctx.success = function (data) {
(Array.isArray(data) ? data : data.data).forEach(comment => {
comment.comment = replaceUrl(comment.comment);
comment.link = replaceLink(comment.link);
if (Array.isArray(comment.children) && comment.children.length) {
comment.children.forEach(cmt => {
cmt.comment = replaceUrl(cmt.comment);
cmt.link = replaceLink(cmt.link);
});
}
});
_oldSuccess.call(ctx, data);
};
await next();
},
async (ctx, next) => {
function outputHtml(url) {
const template = interceptorTemplate || `<!DOCTYPE html><html lang="zh-CN"><head><title>Redirect to third party website</title></head><body data-url="__URL__"><p>Redirecting to __URL__</p><script>location.href = document.body.getAttribute('data-url');</script></body></html>`;
return template.replace(/__URL__/g, () => url);
}
if (ctx.path.toLowerCase() !== '/api/redirect' || ctx.method.toUpperCase() !== 'GET') {
return next();
}
const url = ctx.param('url');
try {
// not standard url then exit to avoid xss
new URL(url);
ctx.body = outputHtml(url);
} catch (e) {
ctx.body = url;
console.log(e);
}
}
]
}
};