mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[proxyConfig] expanded and tested the proxy config
This commit is contained in:
parent
694c2c00b9
commit
f0aaff5817
9 changed files with 515 additions and 60 deletions
22
index.js
22
index.js
|
@ -42,9 +42,15 @@ module.exports = function (kibana) {
|
|||
ssl: Joi.object({
|
||||
verify: Joi.boolean(),
|
||||
}).default(),
|
||||
hosts: Joi.array().items(
|
||||
proxyConfig: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
host: Joi.array().items(Joi.string()).single().default(['.*']),
|
||||
match: Joi.object().keys({
|
||||
protocol: Joi.string().default('*'),
|
||||
host: Joi.string().default('*'),
|
||||
port: Joi.string().default('*'),
|
||||
path: Joi.string().default('*')
|
||||
}),
|
||||
|
||||
timeout: Joi.number(),
|
||||
ssl: Joi.object().keys({
|
||||
verify: Joi.boolean(),
|
||||
|
@ -55,7 +61,13 @@ module.exports = function (kibana) {
|
|||
})
|
||||
).default([
|
||||
{
|
||||
host: '.*',
|
||||
match: {
|
||||
protocol: '*',
|
||||
hostname: '*',
|
||||
port: '*',
|
||||
path: '*'
|
||||
},
|
||||
|
||||
timeout: 180000,
|
||||
ssl: {
|
||||
verify: true
|
||||
|
@ -72,7 +84,7 @@ module.exports = function (kibana) {
|
|||
throw new Error('sense.ssl.version is no longer supported.');
|
||||
}
|
||||
|
||||
const hostBasedProxyConfig = new HostBasedProxyConfigCollection(options.hosts);
|
||||
const hostBasedProxyConfig = new HostBasedProxyConfigCollection(options.proxyConfig);
|
||||
|
||||
// http://hapijs.com/api/8.8.1#route-configuration
|
||||
server.route({
|
||||
|
@ -119,7 +131,7 @@ module.exports = function (kibana) {
|
|||
}
|
||||
},
|
||||
|
||||
...hostBasedProxyConfig.forUri(uri)
|
||||
...hostBasedProxyConfig.configForUri(uri)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"eslint": "^1.7.3",
|
||||
"eslint-config-airbnb": "^0.1.0",
|
||||
"eslint-plugin-react": "^3.6.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-aws": "^0.6.1",
|
||||
"grunt-cli": "^0.1.13",
|
||||
|
@ -31,12 +32,15 @@
|
|||
"grunt-replace": "^0.11.0",
|
||||
"grunt-run": "^0.5.2",
|
||||
"gruntify-eslint": "^1.2.0",
|
||||
"jit-grunt": "^0.9.1"
|
||||
"jit-grunt": "^0.9.1",
|
||||
"mocha": "^2.4.5",
|
||||
"sinon": "^1.17.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"boom": "2.8.0",
|
||||
"joi": "6.6.1",
|
||||
"lodash": "3.10.1"
|
||||
"lodash": "3.10.1",
|
||||
"minimatch": "^3.0.0"
|
||||
},
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
170
server/__tests__/proxy_config.js
Normal file
170
server/__tests__/proxy_config.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/* eslint-env mocha */
|
||||
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import fs from 'fs';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
import { parse as parseUrl } from 'url';
|
||||
|
||||
import { ProxyConfig } from '../proxy_config'
|
||||
|
||||
const matchGoogle = {
|
||||
protocol: 'https',
|
||||
host: 'google.com',
|
||||
path: '/search'
|
||||
}
|
||||
const parsedGoogle = parseUrl('https://google.com/search');
|
||||
const parsedLocalEs = parseUrl('https://localhost:5601/search');
|
||||
|
||||
describe('ProxyConfig', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(fs, 'readFileSync', function (path) {
|
||||
return { path }
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.readFileSync.restore();
|
||||
});
|
||||
|
||||
describe('#getForParsedUri', function () {
|
||||
context('parsed url does not match', function () {
|
||||
it('returns {}', function () {
|
||||
const config = new ProxyConfig({
|
||||
match: matchGoogle,
|
||||
timeout: 100
|
||||
});
|
||||
|
||||
expect(config.getForParsedUri(parsedLocalEs)).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
context('parsed url does match', function () {
|
||||
it('assigns timeout value', function () {
|
||||
const football = {};
|
||||
const config = new ProxyConfig({
|
||||
match: matchGoogle,
|
||||
timeout: football
|
||||
});
|
||||
|
||||
expect(config.getForParsedUri(parsedGoogle).timeout).to.be(football);
|
||||
});
|
||||
|
||||
it('assigns ssl.verify to rejectUnauthorized', function () {
|
||||
const football = {};
|
||||
const config = new ProxyConfig({
|
||||
match: matchGoogle,
|
||||
ssl: {
|
||||
verify: football
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.getForParsedUri(parsedGoogle).rejectUnauthorized).to.be(football);
|
||||
});
|
||||
|
||||
context('uri us http', function () {
|
||||
context('ca is set', function () {
|
||||
it('creates but does not output the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
ca: 'path/to/ca'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
|
||||
});
|
||||
});
|
||||
context('cert is set', function () {
|
||||
it('creates but does not output the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
cert: 'path/to/cert'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
|
||||
});
|
||||
});
|
||||
context('key is set', function () {
|
||||
it('creates but does not output the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
key: 'path/to/key'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
|
||||
});
|
||||
});
|
||||
context('cert + key are set', function () {
|
||||
it('creates but does not output the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
cert: 'path/to/cert',
|
||||
key: 'path/to/key'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'http:' }).agent).to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('uri us https', function () {
|
||||
context('ca is set', function () {
|
||||
it('creates and outputs the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
ca: 'path/to/ca'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
|
||||
});
|
||||
});
|
||||
context('cert is set', function () {
|
||||
it('creates and outputs the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
cert: 'path/to/cert'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
|
||||
});
|
||||
});
|
||||
context('key is set', function () {
|
||||
it('creates and outputs the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
key: 'path/to/key'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
|
||||
});
|
||||
});
|
||||
context('cert + key are set', function () {
|
||||
it('creates and outputs the agent', function () {
|
||||
const config = new ProxyConfig({
|
||||
ssl: {
|
||||
cert: 'path/to/cert',
|
||||
key: 'path/to/key'
|
||||
}
|
||||
});
|
||||
|
||||
expect(config.sslAgent).to.be.an(HttpsAgent);
|
||||
expect(config.getForParsedUri({ protocol: 'https:' }).agent).to.be(config.sslAgent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
156
server/__tests__/proxy_config_collection.js
Normal file
156
server/__tests__/proxy_config_collection.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
/* eslint-env mocha */
|
||||
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import fs from 'fs';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
|
||||
import { ProxyConfigCollection } from '../proxy_config_collection'
|
||||
|
||||
describe('ProxyConfigCollection', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(fs, 'readFileSync', () => new Buffer(0));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.readFileSync.restore();
|
||||
});
|
||||
|
||||
const proxyConfigs = [
|
||||
{
|
||||
match: {
|
||||
protocol: 'https',
|
||||
host: 'localhost',
|
||||
port: 5601,
|
||||
path: '/.kibana'
|
||||
},
|
||||
|
||||
timeout: 1,
|
||||
},
|
||||
|
||||
{
|
||||
match: {
|
||||
protocol: 'https',
|
||||
host: 'localhost',
|
||||
port: 5601
|
||||
},
|
||||
|
||||
timeout: 2,
|
||||
},
|
||||
|
||||
{
|
||||
match: {
|
||||
host: 'localhost',
|
||||
port: 5601
|
||||
},
|
||||
|
||||
timeout: 3,
|
||||
},
|
||||
|
||||
{
|
||||
match: {
|
||||
host: 'localhost'
|
||||
},
|
||||
|
||||
timeout: 4,
|
||||
},
|
||||
|
||||
{
|
||||
match: {},
|
||||
|
||||
timeout: 5
|
||||
}
|
||||
]
|
||||
|
||||
function getTimeout(uri) {
|
||||
const collection = new ProxyConfigCollection(proxyConfigs);
|
||||
return collection.configForUri(uri).timeout;
|
||||
}
|
||||
|
||||
context('http://localhost:5601', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('http://localhost:5601')).to.be(3)
|
||||
});
|
||||
});
|
||||
|
||||
context('https://localhost:5601/.kibana', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('https://localhost:5601/.kibana')).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
context('http://localhost:5602', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('http://localhost:5602')).to.be(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('https://localhost:5602', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('https://localhost:5602')).to.be(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('http://localhost:5603', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('http://localhost:5603')).to.be(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('https://localhost:5603', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('https://localhost:5603')).to.be(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('https://localhost:5601/index', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('https://localhost:5601/index')).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
context('http://localhost:5601/index', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('http://localhost:5601/index')).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
context('https://localhost:5601/index/type', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('https://localhost:5601/index/type')).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
context('http://notlocalhost', function () {
|
||||
it('defaults to the first matching timeout', function () {
|
||||
expect(getTimeout('http://notlocalhost')).to.be(5);
|
||||
});
|
||||
});
|
||||
|
||||
context('collection with ssl config and root level verify:false', function () {
|
||||
function makeCollection() {
|
||||
return new ProxyConfigCollection([
|
||||
{
|
||||
match: { host: '*.internal.org' },
|
||||
ssl: { ca: 'path/to/ca' }
|
||||
},
|
||||
{
|
||||
match: { host: '*' },
|
||||
ssl: { verify: false }
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
it('verifies for config that produces ssl agent', function () {
|
||||
const conf = makeCollection().configForUri('https://es.internal.org/_search');
|
||||
expect(conf).to.have.property('rejectUnauthorized', true);
|
||||
expect(conf.agent).to.be.an(HttpsAgent);
|
||||
});
|
||||
|
||||
it('disabled verification for * config', function () {
|
||||
const conf = makeCollection().configForUri('https://extenal.org/_search');
|
||||
expect(conf).to.have.property('rejectUnauthorized', false);
|
||||
expect(conf.agent).to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
55
server/__tests__/wildcard_matcher.js
Normal file
55
server/__tests__/wildcard_matcher.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* eslint-env mocha */
|
||||
import expect from 'expect.js'
|
||||
|
||||
import { WildcardMatcher } from '../wildcard_matcher'
|
||||
|
||||
function should(candidate, ...constructorArgs) {
|
||||
if (!new WildcardMatcher(...constructorArgs).match(candidate)) {
|
||||
throw new Error(`Expected pattern ${[...constructorArgs]} to match ${candidate}`);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldNot(candidate, ...constructorArgs) {
|
||||
if (new WildcardMatcher(...constructorArgs).match(candidate)) {
|
||||
throw new Error(`Expected pattern ${[...constructorArgs]} to not match ${candidate}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('WildcardMatcher', function () {
|
||||
context('pattern = *', function () {
|
||||
it('matches http', () => should('http', '*'));
|
||||
it('matches https', () => should('https', '*'));
|
||||
it('matches nothing', () => should('', '*'));
|
||||
it('does not match /', () => shouldNot('/', '*'));
|
||||
it('matches localhost', () => should('localhost', '*'));
|
||||
it('matches a path', () => should('/index/type/_search', '*'));
|
||||
|
||||
context('defaultValue = /', function () {
|
||||
it('matches /', () => should('/', '*', '/'));
|
||||
});
|
||||
});
|
||||
|
||||
context('pattern = http', function () {
|
||||
it('matches http', () => should('http', 'http'));
|
||||
it('does not match https', () => shouldNot('https', 'http'));
|
||||
it('does not match nothing', () => shouldNot('', 'http'));
|
||||
it('does not match localhost', () => shouldNot('localhost', 'http'));
|
||||
it('does not match a path', () => shouldNot('/index/type/_search', 'http'));
|
||||
});
|
||||
|
||||
context('pattern = 560{1..9}', function () {
|
||||
it('does not match http', () => shouldNot('http', '560{1..9}'));
|
||||
it('does not matches 5600', () => shouldNot('5600', '560{1..9}'));
|
||||
it('matches 5601', () => should('5601', '560{1..9}'));
|
||||
it('matches 5602', () => should('5602', '560{1..9}'));
|
||||
it('matches 5603', () => should('5603', '560{1..9}'));
|
||||
it('matches 5604', () => should('5604', '560{1..9}'));
|
||||
it('matches 5605', () => should('5605', '560{1..9}'));
|
||||
it('matches 5606', () => should('5606', '560{1..9}'));
|
||||
it('matches 5607', () => should('5607', '560{1..9}'));
|
||||
it('matches 5608', () => should('5608', '560{1..9}'));
|
||||
it('matches 5609', () => should('5609', '560{1..9}'));
|
||||
it('does not matches 5610', () => shouldNot('5610', '560{1..9}'));
|
||||
});
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
import { defaultsDeep, memoize, values } from 'lodash'
|
||||
import { Agent as HttpAgent } from 'http'
|
||||
import { Agent as HttpsAgent } from 'https'
|
||||
import { readFileSync } from 'lodash'
|
||||
import { parse as parseUrl } from 'url'
|
||||
|
||||
const makeHttpsAgent = memoize(
|
||||
opts => new HttpsAgent(opts),
|
||||
opts => JSON.stringify(opts)
|
||||
)
|
||||
|
||||
export class HostBasedProxyConfig {
|
||||
constructor(config) {
|
||||
config = Object.assign({}, config);
|
||||
|
||||
this.host = config.host;
|
||||
this.re = new RegExp(this.host);
|
||||
|
||||
const ssl = config.ssl || {};
|
||||
this.verifySsl = ssl.verify;
|
||||
|
||||
const sslAgentOpts = {
|
||||
ca: ssl.ca && readFileSync(ssl.ca),
|
||||
cert: ssl.cert && readFileSync(ssl.cert),
|
||||
key: ssl.key && readFileSync(ssl.key),
|
||||
};
|
||||
|
||||
if (values(sslAgentOpts).filter(Boolean).length) {
|
||||
this.sslAgent = new HttpsAgent(sslAgentOpts);
|
||||
}
|
||||
}
|
||||
|
||||
getSettings({ protocol, host }) {
|
||||
if (!this.re.test(`${protocol}//${host}`)) return {};
|
||||
return {
|
||||
rejectUnauthorized: this.verifySsl,
|
||||
agent: protocol === 'https:' ? this.sslAgent : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class HostBasedProxyConfigCollection {
|
||||
constructor(hosts = []) {
|
||||
this.hosts = hosts.map(settings => new HostBasedProxyConfig(settings))
|
||||
}
|
||||
|
||||
forUri(uri) {
|
||||
const parsedUri = parseUrl(uri);
|
||||
const settings = this.hosts.map(host => host.getSettings(parsedUri));
|
||||
return defaultsDeep({}, ...settings);
|
||||
}
|
||||
}
|
70
server/proxy_config.js
Normal file
70
server/proxy_config.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { memoize, values } from 'lodash'
|
||||
import { format as formatUrl } from 'url'
|
||||
import { Agent as HttpsAgent } from 'https'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import { WildcardMatcher } from './wildcard_matcher'
|
||||
|
||||
const makeHttpsAgent = memoize(
|
||||
opts => new HttpsAgent(opts),
|
||||
opts => JSON.stringify(opts)
|
||||
)
|
||||
|
||||
export class ProxyConfig {
|
||||
constructor(config) {
|
||||
config = Object.assign({}, config);
|
||||
|
||||
// -----
|
||||
// read "match" info
|
||||
// -----
|
||||
const rawMatches = Object.assign({}, config.match);
|
||||
this.id = formatUrl({
|
||||
protocol: rawMatches.protocol,
|
||||
hostname: rawMatches.host,
|
||||
port: rawMatches.port,
|
||||
pathname: rawMatches.path
|
||||
}) || '*';
|
||||
|
||||
this.matchers = {
|
||||
protocol: new WildcardMatcher(rawMatches.protocol),
|
||||
host: new WildcardMatcher(rawMatches.host),
|
||||
port: new WildcardMatcher(rawMatches.port),
|
||||
path: new WildcardMatcher(rawMatches.path, '/'),
|
||||
};
|
||||
|
||||
// -----
|
||||
// read config vars
|
||||
// -----
|
||||
this.timeout = config.timeout;
|
||||
this.sslAgent = this._makeSslAgent(config);
|
||||
}
|
||||
|
||||
_makeSslAgent(config) {
|
||||
const ssl = config.ssl || {};
|
||||
this.verifySsl = ssl.verify;
|
||||
|
||||
const sslAgentOpts = {
|
||||
ca: ssl.ca && readFileSync(ssl.ca),
|
||||
cert: ssl.cert && readFileSync(ssl.cert),
|
||||
key: ssl.key && readFileSync(ssl.key),
|
||||
};
|
||||
|
||||
if (values(sslAgentOpts).filter(Boolean).length) {
|
||||
return new HttpsAgent(sslAgentOpts);
|
||||
}
|
||||
}
|
||||
|
||||
getForParsedUri({ protocol, hostname, port, pathname }) {
|
||||
let match = this.matchers.protocol.match(protocol.slice(0, -1));
|
||||
match = match && this.matchers.host.match(hostname);
|
||||
match = match && this.matchers.port.match(port);
|
||||
match = match && this.matchers.path.match(pathname);
|
||||
|
||||
if (!match) return {};
|
||||
return {
|
||||
timeout: this.timeout,
|
||||
rejectUnauthorized: this.sslAgent ? true : this.verifySsl,
|
||||
agent: protocol === 'https:' ? this.sslAgent : undefined
|
||||
};
|
||||
}
|
||||
}
|
17
server/proxy_config_collection.js
Normal file
17
server/proxy_config_collection.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { defaultsDeep } from 'lodash'
|
||||
|
||||
import { ProxyConfig } from './proxy_config'
|
||||
import { parse as parseUrl } from 'url'
|
||||
|
||||
|
||||
export class ProxyConfigCollection {
|
||||
constructor(configs = []) {
|
||||
this.configs = configs.map(settings => new ProxyConfig(settings))
|
||||
}
|
||||
|
||||
configForUri(uri) {
|
||||
const parsedUri = parseUrl(uri);
|
||||
const settings = this.configs.map(config => config.getForParsedUri(parsedUri));
|
||||
return defaultsDeep({}, ...settings);
|
||||
}
|
||||
}
|
24
server/wildcard_matcher.js
Normal file
24
server/wildcard_matcher.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Minimatch } from 'minimatch'
|
||||
|
||||
export class WildcardMatcher {
|
||||
constructor(wildcardPattern, emptyVal) {
|
||||
this.emptyVal = emptyVal;
|
||||
this.pattern = String(wildcardPattern || '*');
|
||||
this.matcher = new Minimatch(this.pattern, {
|
||||
noglobstar: true,
|
||||
dot: true,
|
||||
nocase: true,
|
||||
matchBase: true,
|
||||
nocomment: true
|
||||
})
|
||||
}
|
||||
|
||||
match(candidate) {
|
||||
const empty = !candidate || candidate === this.emptyVal;
|
||||
if (empty && this.pattern === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.matcher.match(candidate || '')
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue