Skip to content

Commit 1e443da

Browse files
wip
1 parent bcd5071 commit 1e443da

File tree

2 files changed

+299
-1
lines changed

2 files changed

+299
-1
lines changed

packages/rocketchat-oembed/package.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Package.onUse(function(api) {
4747
api.addFiles('client/oembedSandstormGrain.html', 'client');
4848
api.addFiles('client/oembedSandstormGrain.coffee', 'client');
4949

50-
api.addFiles('server/server.coffee', 'server');
50+
api.addFiles('server/server.js', 'server');
5151
api.addFiles('server/providers.coffee', 'server');
5252
api.addFiles('server/jumpToMessage.js', 'server');
5353
api.addFiles('server/models/OEmbedCache.coffee', 'server');
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/*globals HTTPInternals, changeCase */
2+
const indexOf = [].indexOf || function(item) { for (let i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) { return i; } } return -1; };
3+
4+
const URL = Npm.require('url');
5+
6+
const querystring = Npm.require('querystring');
7+
8+
const request = HTTPInternals.NpmModules.request.module;
9+
10+
const iconv = Npm.require('iconv-lite');
11+
12+
const ipRangeCheck = Npm.require('ip-range-check');
13+
14+
const he = Npm.require('he');
15+
16+
const jschardet = Npm.require('jschardet');
17+
18+
const OEmbed = {};
19+
20+
const getCharset = function(contentType, body) {
21+
let detectedCharset;
22+
let httpHeaderCharset;
23+
let htmlMetaCharset;
24+
let result;
25+
26+
contentType = contentType || '';
27+
28+
const binary = body.toString('binary');
29+
const detected = jschardet.detect(binary);
30+
if (detected.confidence > 0.8) {
31+
detectedCharset = detected.encoding.toLowerCase();
32+
}
33+
const m1 = contentType.match(/charset=([\w\-]+)/i);
34+
if (m1) {
35+
httpHeaderCharset = m1[1].toLowerCase();
36+
}
37+
const m2 = binary.match(/<meta\b[^>]*charset=["']?([\w\-]+)/i);
38+
if (m2) {
39+
htmlMetaCharset = m2[1].toLowerCase();
40+
}
41+
if (detectedCharset) {
42+
if (detectedCharset === httpHeaderCharset) {
43+
result = httpHeaderCharset;
44+
} else if (detectedCharset === htmlMetaCharset) {
45+
result = htmlMetaCharset;
46+
}
47+
}
48+
if (!result) {
49+
result = httpHeaderCharset || htmlMetaCharset || detectedCharset;
50+
}
51+
return result || 'utf-8';
52+
};
53+
54+
const toUtf8 = function(contentType, body) {
55+
return iconv.decode(body, getCharset(contentType, body));
56+
};
57+
58+
const getUrlContent = function(urlObj, redirectCount, callback) {
59+
let ref;
60+
let ref1;
61+
if (redirectCount == null) {
62+
redirectCount = 5;
63+
}
64+
if (_.isString(urlObj)) {
65+
urlObj = URL.parse(urlObj);
66+
}
67+
68+
const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']);
69+
const ignoredHosts = RocketChat.settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || [];
70+
if ((ref = parsedUrl.hostname, indexOf.call(ignoredHosts, ref) >= 0) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) {
71+
return callback();
72+
}
73+
const safePorts = RocketChat.settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || [];
74+
if (parsedUrl.port && safePorts.length > 0 && (ref1 = parsedUrl.port, indexOf.call(safePorts, ref1) < 0)) {
75+
return callback();
76+
}
77+
const data = RocketChat.callbacks.run('oembed:beforeGetUrlContent', {
78+
urlObj,
79+
parsedUrl
80+
});
81+
if (data.attachments != null) {
82+
return callback(null, data);
83+
}
84+
const url = URL.format(data.urlObj);
85+
const opts = {
86+
url,
87+
strictSSL: !RocketChat.settings.get('Allow_Invalid_SelfSigned_Certs'),
88+
gzip: true,
89+
maxRedirects: redirectCount,
90+
headers: {
91+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'
92+
}
93+
};
94+
let headers = null;
95+
let statusCode = null;
96+
let error = null;
97+
const chunks = [];
98+
let chunksTotalLength = 0;
99+
const stream = request(opts);
100+
stream.on('response', function(response) {
101+
statusCode = response.statusCode;
102+
headers = response.headers;
103+
if (response.statusCode !== 200) {
104+
return stream.abort();
105+
}
106+
});
107+
stream.on('data', function(chunk) {
108+
chunks.push(chunk);
109+
chunksTotalLength += chunk.length;
110+
if (chunksTotalLength > 250000) {
111+
return stream.abort();
112+
}
113+
});
114+
stream.on('end', Meteor.bindEnvironment(function() {
115+
if (error != null) {
116+
return callback(null, {
117+
error,
118+
parsedUrl
119+
});
120+
}
121+
const buffer = Buffer.concat(chunks);
122+
return callback(null, {
123+
headers,
124+
body: toUtf8(headers['content-type'], buffer),
125+
parsedUrl,
126+
statusCode
127+
});
128+
}));
129+
return stream.on('error', function(err) {
130+
return error = err;
131+
});
132+
};
133+
134+
OEmbed.getUrlMeta = function(url, withFragment) {
135+
const getUrlContentSync = Meteor.wrapAsync(getUrlContent);
136+
const urlObj = URL.parse(url);
137+
if (withFragment != null) {
138+
const queryStringObj = querystring.parse(urlObj.query);
139+
queryStringObj._escaped_fragment_ = '';
140+
urlObj.query = querystring.stringify(queryStringObj);
141+
let path = urlObj.pathname;
142+
if (urlObj.query != null) {
143+
path += `?${ urlObj.query }`;
144+
}
145+
urlObj.path = path;
146+
}
147+
const content = getUrlContentSync(urlObj, 5);
148+
if (!content) {
149+
return;
150+
}
151+
if (content.attachments != null) {
152+
return content;
153+
}
154+
let metas = undefined;
155+
if (content && content.body) {
156+
metas = {};
157+
content.body.replace(/<title[^>]*>([^<]*)<\/title>/gmi, function(meta, title) {
158+
return metas.pageTitle != null ? metas.pageTitle : metas.pageTitle = he.unescape(title);
159+
});
160+
content.body.replace(/<meta[^>]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gmi, function(meta, name, value) {
161+
let name1;
162+
return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
163+
});
164+
content.body.replace(/<meta[^>]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gmi, function(meta, name, value) {
165+
let name1;
166+
return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
167+
});
168+
content.body.replace(/<meta[^>]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, function(meta, value, name) {
169+
let name1;
170+
return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
171+
});
172+
content.body.replace(/<meta[^>]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, function(meta, value, name) {
173+
let name1;
174+
return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value);
175+
});
176+
if (metas.fragment === '!' && (withFragment == null)) {
177+
return OEmbed.getUrlMeta(url, true);
178+
}
179+
}
180+
let headers = undefined;
181+
182+
183+
if (content && content.headers) {
184+
headers = {};
185+
const ref = content.headers;
186+
for (const header in ref) {
187+
const value = ref[header];
188+
headers[changeCase.camelCase(header)] = value;
189+
}
190+
}
191+
if (content && content.statusCode !== 200) {
192+
return;
193+
}
194+
const data = RocketChat.callbacks.run('oembed:afterParseContent', {
195+
meta: metas,
196+
headers,
197+
parsedUrl: content.parsedUrl,
198+
content
199+
});
200+
return data;
201+
};
202+
203+
OEmbed.getUrlMetaWithCache = function(url, withFragment) {
204+
const cache = RocketChat.models.OEmbedCache.findOneById(url);
205+
if (cache != null) {
206+
return cache.data;
207+
}
208+
const data = OEmbed.getUrlMeta(url, withFragment);
209+
if (data != null) {
210+
try {
211+
RocketChat.models.OEmbedCache.createWithIdAndData(url, data);
212+
} catch (_error) {
213+
console.error('OEmbed duplicated record', url);
214+
}
215+
return data;
216+
}
217+
};
218+
219+
const getRelevantHeaders = function(headersObj) {
220+
const headers = {};
221+
for (const key in headersObj) {
222+
const value = headersObj[key];
223+
let ref;
224+
if (((ref = key.toLowerCase()) === 'contenttype' || ref === 'contentlength') && (value != null ? value.trim() : void 0) !== '') {
225+
headers[key] = value;
226+
}
227+
}
228+
if (Object.keys(headers).length > 0) {
229+
return headers;
230+
}
231+
};
232+
233+
const getRelevantMetaTags = function(metaObj) {
234+
const tags = {};
235+
for (const key in metaObj) {
236+
const value = metaObj[key];
237+
if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && (value != null ? value.trim() : void 0) !== '') {
238+
tags[key] = value;
239+
}
240+
}
241+
if (Object.keys(tags).length > 0) {
242+
return tags;
243+
}
244+
};
245+
246+
OEmbed.rocketUrlParser = function(message) {
247+
if (Array.isArray(message.urls)) {
248+
let attachments = [];
249+
let changed = false;
250+
message.urls.forEach(function(item) {
251+
if (item.ignoreParse === true) {
252+
return;
253+
}
254+
if (item.url.startsWith('grain://')) {
255+
changed = true;
256+
item.meta = {
257+
sandstorm: {
258+
grain: item.sandstormViewInfo
259+
}
260+
};
261+
return;
262+
}
263+
if (!/^https?:\/\//i.test(item.url)) {
264+
return;
265+
}
266+
const data = OEmbed.getUrlMetaWithCache(item.url);
267+
if (data != null) {
268+
if (data.attachments) {
269+
return attachments = _.union(attachments, data.attachments);
270+
} else {
271+
if (data.meta != null) {
272+
item.meta = getRelevantMetaTags(data.meta);
273+
}
274+
if (data.headers != null) {
275+
item.headers = getRelevantHeaders(data.headers);
276+
}
277+
item.parsedUrl = data.parsedUrl;
278+
return changed = true;
279+
}
280+
}
281+
});
282+
if (attachments.length) {
283+
RocketChat.models.Messages.setMessageAttachments(message._id, attachments);
284+
}
285+
if (changed === true) {
286+
RocketChat.models.Messages.setUrlsById(message._id, message.urls);
287+
}
288+
}
289+
return message;
290+
};
291+
292+
RocketChat.settings.get('API_Embed', function(key, value) {
293+
if (value) {
294+
return RocketChat.callbacks.add('afterSaveMessage', OEmbed.rocketUrlParser, RocketChat.callbacks.priority.LOW, 'API_Embed');
295+
} else {
296+
return RocketChat.callbacks.remove('afterSaveMessage', 'API_Embed');
297+
}
298+
});

0 commit comments

Comments
 (0)