11import type {
22 OEmbedUrlContentResult ,
3+ MessageUrl ,
34 OEmbedUrlWithMetadata ,
4- IMessage ,
5- MessageAttachment ,
65 OEmbedMeta ,
7- MessageUrl ,
6+ IMessage ,
7+ OEmbedUrlContent ,
88} from '@rocket.chat/core-typings' ;
99import { isOEmbedUrlWithMetadata } from '@rocket.chat/core-typings' ;
1010import { Logger } from '@rocket.chat/logger' ;
11- import { Messages , OEmbedCache } from '@rocket.chat/models' ;
11+ import { OEmbedCache , Messages } from '@rocket.chat/models' ;
1212import { serverFetch as fetch } from '@rocket.chat/server-fetch' ;
13- import { camelCase } from 'change-case' ;
1413import he from 'he' ;
1514import iconv from 'iconv-lite' ;
1615import ipRangeCheck from 'ip-range-check' ;
1716import jschardet from 'jschardet' ;
17+ import { camelCase } from 'lodash' ;
1818
19- import { isURL } from '../../../lib/utils/isURL ' ;
20- import { callbacks } from '../../../server/lib/callbacks ' ;
21- import { settings } from '../../settings/server ' ;
22- import { Info } from '../../utils/rocketchat.info ' ;
19+ import { settings } from '../../../../app/settings/server ' ;
20+ import { Info } from '../../../../app/utils/rocketchat.info ' ;
21+ import { isURL } from '../../../../lib/utils/isURL ' ;
22+ import { afterParseUrlContent , beforeGetUrlContent } from '../lib/oembed/providers ' ;
2323
2424const MAX_EXTERNAL_URL_PREVIEWS = 5 ;
2525const log = new Logger ( 'OEmbed' ) ;
@@ -65,7 +65,7 @@ const toUtf8 = function (contentType: string, body: Buffer): string {
6565 return iconv . decode ( body , getCharset ( contentType , body ) ) ;
6666} ;
6767
68- const getUrlContent = async ( urlObj : URL , redirectCount = 5 ) : Promise < OEmbedUrlContentResult > => {
68+ const getUrlContent = async ( urlObj : URL , redirectCount = 5 ) : Promise < OEmbedUrlContent > => {
6969 const portsProtocol = new Map < string , string > (
7070 Object . entries ( {
7171 80 : 'http:' ,
@@ -74,9 +74,48 @@ const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise<OEmbedUrlC
7474 } ) ,
7575 ) ;
7676
77- const ignoredHosts = settings . get < string > ( 'API_EmbedIgnoredHosts' ) . replace ( / \s / g, '' ) . split ( ',' ) || [ ] ;
78- if ( urlObj . hostname && ( ignoredHosts . includes ( urlObj . hostname ) || ipRangeCheck ( urlObj . hostname , ignoredHosts ) ) ) {
79- throw new Error ( 'invalid host' ) ;
77+ const ignoredHosts =
78+ settings
79+ . get < string > ( 'API_EmbedIgnoredHosts' )
80+ . replace ( / \s / g, '' )
81+ . split ( ',' )
82+ . filter ( Boolean )
83+ . map ( ( host ) => host . toLowerCase ( ) ) || [ ] ;
84+
85+ const isIgnoredHost = ( hostname : string | undefined ) : boolean => {
86+ hostname = hostname ?. toLowerCase ( ) ;
87+ if ( ! hostname || ! ignoredHosts . length ) {
88+ return false ;
89+ }
90+
91+ const exactHosts = ignoredHosts . filter ( ( h ) => ! h . includes ( '*' ) ) ;
92+ if ( exactHosts . includes ( hostname ) || ipRangeCheck ( hostname , exactHosts ) ) {
93+ return true ;
94+ }
95+
96+ return ignoredHosts
97+ . filter ( ( h ) => h . includes ( '*' ) )
98+ . some ( ( pattern ) => {
99+ const validationRegex = / ^ (?: \* \. ) ? (?: \* | [ a - z 0 - 9 - ] + ) (?: \. (?: \* | [ a - z 0 - 9 - ] + ) ) * $ / i;
100+ if ( ! validationRegex . test ( pattern ) || pattern === '*' ) {
101+ return false ;
102+ }
103+
104+ const escaped = pattern . replace ( / [ - / \\ ^ $ + ? . ( ) | [ \] { } ] / g, '\\$&' ) ;
105+ const source = `^${ escaped . replace ( / \* / g, '[^.]*' ) } $` ;
106+
107+ try {
108+ const regex = new RegExp ( source , 'i' ) ;
109+ return regex . test ( hostname ) ;
110+ } catch {
111+ // fail safe on invalid patterns
112+ return false ;
113+ }
114+ } ) ;
115+ } ;
116+
117+ if ( isIgnoredHost ( urlObj . hostname ) ) {
118+ throw new Error ( 'host is ignored' ) ;
80119 }
81120
82121 const safePorts = settings . get < string > ( 'API_EmbedSafePorts' ) . replace ( / \s / g, '' ) . split ( ',' ) || [ ] ;
@@ -91,14 +130,13 @@ const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise<OEmbedUrlC
91130 throw new Error ( 'invalid/unsafe port' ) ;
92131 }
93132
94- const data = await callbacks . run ( 'oembed:beforeGetUrlContent' , {
95- urlObj,
96- } ) ;
133+ const data = beforeGetUrlContent ( { urlObj } ) ;
97134
98135 const url = data . urlObj . toString ( ) ;
99136 const sizeLimit = 250000 ;
100137
101138 log . debug ( { msg : 'Fetching URL for OEmbed' , url, redirectCount } ) ;
139+ const start = Date . now ( ) ;
102140 const response = await fetch (
103141 url ,
104142 {
@@ -109,10 +147,12 @@ const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise<OEmbedUrlC
109147 'Accept-Language' : settings . get ( 'Language' ) || 'en' ,
110148 ...data . headerOverrides ,
111149 } ,
150+ timeout : settings . get < number > ( 'API_EmbedTimeout' ) * 1000 ,
112151 size : sizeLimit , // max size of the response body, this was not working as expected so I'm also manually verifying that on the iterator
113152 } ,
114153 settings . get ( 'Allow_Invalid_SelfSigned_Certs' ) ,
115154 ) ;
155+ const end = Date . now ( ) ;
116156
117157 let totalSize = 0 ;
118158 const chunks = [ ] ;
@@ -126,13 +166,14 @@ const getUrlContent = async (urlObj: URL, redirectCount = 5): Promise<OEmbedUrlC
126166 }
127167 }
128168
129- log . debug ( { msg : 'Obtained response from server' , length : totalSize } ) ;
169+ log . debug ( { msg : 'Obtained response from server' , length : totalSize , timeSpent : ` ${ end - start } ms` } ) ;
130170 const buffer = Buffer . concat ( chunks ) ;
131171
132172 return {
133173 headers : Object . fromEntries ( response . headers ) ,
134174 body : toUtf8 ( response . headers . get ( 'content-type' ) || 'text/plain' , buffer ) ,
135175 statusCode : response . status ,
176+ urlObj,
136177 } ;
137178} ;
138179
@@ -183,7 +224,7 @@ const getUrlMeta = async function (
183224 }
184225
185226 log . debug ( { msg : 'Fetching URL content' , url : urlObj . toString ( ) } ) ;
186- let content : OEmbedUrlContentResult | undefined ;
227+ let content : OEmbedUrlContent | undefined ;
187228 try {
188229 content = await getUrlContent ( urlObj , 5 ) ;
189230 } catch ( err ) {
@@ -195,7 +236,7 @@ const getUrlMeta = async function (
195236 }
196237
197238 log . debug ( { msg : 'Parsing metadata for URL' , url } ) ;
198- const metas : { [ k : string ] : string } = { } ;
239+ const metas : OEmbedMeta = { } as any ;
199240
200241 if ( content ?. body ) {
201242 const escapeMeta = ( name : string , value : string ) : string => {
@@ -233,7 +274,7 @@ const getUrlMeta = async function (
233274 if ( content && content . statusCode !== 200 ) {
234275 return ;
235276 }
236- return callbacks . run ( 'oembed:afterParseContent' , {
277+ return afterParseUrlContent ( {
237278 url,
238279 meta : metas ,
239280 headers,
@@ -289,6 +330,10 @@ const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined =>
289330const rocketUrlParser = async function ( message : IMessage ) : Promise < IMessage > {
290331 log . debug ( { msg : 'Parsing message URLs' } ) ;
291332
333+ if ( ! settings . get ( 'API_Embed' ) ) {
334+ return message ;
335+ }
336+
292337 if ( ! Array . isArray ( message . urls ) ) {
293338 return message ;
294339 }
@@ -303,8 +348,6 @@ const rocketUrlParser = async function (message: IMessage): Promise<IMessage> {
303348 return message ;
304349 }
305350
306- const attachments : MessageAttachment [ ] = [ ] ;
307-
308351 let changed = false ;
309352 for await ( const item of message . urls ) {
310353 if ( item . ignoreParse === true ) {
@@ -318,34 +361,17 @@ const rocketUrlParser = async function (message: IMessage): Promise<IMessage> {
318361 changed = changed || foundMeta ;
319362 }
320363
321- if ( attachments . length ) {
322- await Messages . setMessageAttachments ( message . _id , attachments ) ;
323- }
324-
325364 if ( changed === true ) {
326365 await Messages . setUrlsById ( message . _id , message . urls ) ;
327366 }
328367
329368 return message ;
330369} ;
331370
332- const OEmbed : {
333- getUrlMeta : ( url : string , withFragment ?: boolean ) => Promise < OEmbedUrlWithMetadata | undefined | OEmbedUrlContentResult > ;
334- getUrlMetaWithCache : ( url : string , withFragment ?: boolean ) => Promise < OEmbedUrlWithMetadata | OEmbedUrlContentResult | undefined > ;
371+ export const OEmbed : {
335372 rocketUrlParser : ( message : IMessage ) => Promise < IMessage > ;
336373 parseUrl : ( url : string ) => Promise < { urlPreview : MessageUrl ; foundMeta : boolean } > ;
337374} = {
338375 rocketUrlParser,
339- getUrlMetaWithCache,
340- getUrlMeta,
341376 parseUrl,
342377} ;
343-
344- settings . watch ( 'API_Embed' , ( value ) => {
345- if ( value ) {
346- return callbacks . add ( 'afterSaveMessage' , ( message ) => OEmbed . rocketUrlParser ( message ) , callbacks . priority . LOW , 'API_Embed' ) ;
347- }
348- return callbacks . remove ( 'afterSaveMessage' , 'API_Embed' ) ;
349- } ) ;
350-
351- export { OEmbed } ;
0 commit comments