-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
external_url_service.ts
100 lines (81 loc) · 3.07 KB
/
external_url_service.ts
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
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IExternalUrlPolicy } from 'src/core/server/types';
import { CoreService } from 'src/core/types';
import { IExternalUrl } from './types';
import { InjectedMetadataSetup } from '../injected_metadata';
import { Sha256 } from '../utils';
interface SetupDeps {
location: Pick<Location, 'origin'>;
injectedMetadata: InjectedMetadataSetup;
}
function* getHostHashes(actualHost: string) {
yield new Sha256().update(actualHost, 'utf8').digest('hex');
let host = actualHost.substr(actualHost.indexOf('.') + 1);
while (host) {
yield new Sha256().update(host, 'utf8').digest('hex');
if (host.indexOf('.') === -1) {
break;
}
host = host.substr(host.indexOf('.') + 1);
}
}
const isHostMatch = (actualHost: string, ruleHostHash: string) => {
// If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one
const hostToHash =
!actualHost.includes('[') && !actualHost.endsWith('.') ? `${actualHost}.` : actualHost;
for (const hash of getHostHashes(hostToHash)) {
if (hash === ruleHostHash) {
return true;
}
}
return false;
};
const isProtocolMatch = (actualProtocol: string, ruleProtocol: string) => {
return normalizeProtocol(actualProtocol) === normalizeProtocol(ruleProtocol);
};
function normalizeProtocol(protocol: string) {
return protocol.endsWith(':') ? protocol.slice(0, -1).toLowerCase() : protocol.toLowerCase();
}
const createExternalUrlValidation = (
rules: IExternalUrlPolicy[],
location: Pick<Location, 'origin'>,
serverBasePath: string
) => {
const base = new URL(location.origin + serverBasePath);
return function validateExternalUrl(next: string) {
const url = new URL(next, base);
const isInternalURL =
url.origin === base.origin &&
(!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`));
if (isInternalURL) {
return url;
}
let allowed: null | boolean = null;
rules.forEach((rule) => {
const hostMatch = rule.host ? isHostMatch(url.hostname || '', rule.host) : true;
const protocolMatch = rule.protocol ? isProtocolMatch(url.protocol, rule.protocol) : true;
const isRuleMatch = hostMatch && protocolMatch;
if (isRuleMatch && allowed !== false) {
allowed = rule.allow;
}
});
return allowed === true ? url : null;
};
};
export class ExternalUrlService implements CoreService<IExternalUrl> {
setup({ injectedMetadata, location }: SetupDeps): IExternalUrl {
const serverBasePath = injectedMetadata.getServerBasePath();
const { policy } = injectedMetadata.getExternalUrlConfig();
return {
validateUrl: createExternalUrlValidation(policy, location, serverBasePath),
};
}
start() {}
stop() {}
}