[7.17] Adds new proxy tests and manual proxy tester (#138071) (#138920)

* Adds new proxy tests and manual proxy tester (#138071)

The new proxy tests added can test a variety of different proxy
and target server configurations, however many of those tests
are broken with our current proxy agents.  Hopefully to be fixed
by replacing with hpagent instead.  In the meantime, we wanted to
get the basic test framework in as well.

In addition to tests, the stand-alone forward proxy has been
enhanced to use a better proxy server, `proxy`.  The existing
proxy server `http-proxy` does not support HTTPS out of the box,
and so any HTTPS testing with it is going to be a little sketchy.

Using the stand-alone forward proxy, I was able to post to Slack
through http/https proxies with and without auth, with
proxyRequestUnauthorized set to false.  Which shows the existing
proxy agents do work in _some_ environments.

(cherry picked from commit 9631649e72)

# Conflicts:
#	package.json
#	x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
#	yarn.lock

* fix backport changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Patrick Mueller 2022-08-16 14:44:38 -04:00 committed by GitHub
parent 4136e41afe
commit 231a29eb50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1398 additions and 83 deletions

View file

@ -148,6 +148,22 @@ Specifies preconfigured connector IDs and configs. Default: {}.
`xpack.actions.proxyUrl` {ess-icon}::
Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used.
+
Proxies may be used to proxy http or https requests through a proxy using the http or https protocol. Kibana only uses proxies in "CONNECT" mode (sometimes referred to as "tunneling" TCP mode, compared to HTTP mode). That is, Kibana will always make requests through a proxy using the HTTP `CONNECT` method.
+
If your proxy is using the https protocol (vs the http protocol), the setting `xpack.actions.ssl.proxyVerificationMode: none` will likely be needed, unless your proxy's certificates are signed using a publicly available certificate authority.
+
There is currently no support for using basic authentication with a proxy (authentication for the proxy itself, not the URL being requested through the proxy).
+
To help diagnose problems using a proxy, you can use the `curl` command with options to use your proxy, and log debug information, with the following command, replacing the proxy and target URLs as appropriate. This will force the request to be made to the
proxy in tunneling mode, and display some of the interaction between the client and the proxy.
+
[source,sh]
--
curl --verbose --proxytunnel --proxy http://localhost:8080 http://example.com
--
+
`xpack.actions.proxyBypassHosts` {ess-icon}::
Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time.

View file

@ -792,6 +792,7 @@
"postcss-prefix-selector": "^1.7.2",
"prettier": "^2.4.0",
"pretty-ms": "5.0.0",
"proxy": "^1.0.2",
"q": "^1.5.1",
"react-test-renderer": "^16.12.0",
"read-pkg": "^5.2.0",

View file

@ -0,0 +1,12 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
module.exports = {
preset: '@kbn/test/jest_integration_node',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/actions'],
};

View file

@ -0,0 +1,596 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires
const proxySetup = require('proxy');
import { readFileSync as fsReadFileSync } from 'fs';
import { resolve as pathResolve, join as pathJoin } from 'path';
import http from 'http';
import https from 'https';
import axios from 'axios';
import { duration as momentDuration } from 'moment';
import { schema } from '@kbn/config-schema';
import getPort from 'get-port';
import { request } from '../builtin_action_types/lib/axios_utils';
import { ByteSizeValue } from '@kbn/config-schema';
import { Logger } from 'src/core/server';
import { loggingSystemMock } from 'src/core/server/mocks';
import { createReadySignal } from '../../../event_log/server';
import { ActionsConfig } from '../config';
import { ActionsConfigurationUtilities, getActionsConfigurationUtilities } from '../actions_config';
import { resolveCustomHosts } from '../lib/custom_host_settings';
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const CERT_DIR = '../../../../../../packages/kbn-dev-utils/certs';
const KIBANA_CRT_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'kibana.crt'));
const KIBANA_KEY_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'kibana.key'));
const CA_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'ca.crt'));
const KIBANA_KEY = fsReadFileSync(KIBANA_KEY_FILE, 'utf8');
const KIBANA_CRT = fsReadFileSync(KIBANA_CRT_FILE, 'utf8');
const CA = fsReadFileSync(CA_FILE, 'utf8');
const Auth = 'elastic:changeme';
const AuthB64 = Buffer.from(Auth).toString('base64');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AxiosDefaultsAadapter = require('axios/lib/adapters/http');
describe('axios connections', () => {
let testServer: http.Server | https.Server | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let savedAxiosDefaultsAdapter: any;
beforeEach(() => {
// needed to prevent the dreaded Error: Cross origin http://localhost forbidden
// see: https://github.com/axios/axios/issues/1754#issuecomment-572778305
savedAxiosDefaultsAdapter = axios.defaults.adapter;
axios.defaults.adapter = AxiosDefaultsAadapter;
});
afterEach(() => {
axios.defaults.adapter = savedAxiosDefaultsAdapter;
});
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
testServer?.close();
testServer = null;
});
describe('http', () => {
test('it works', async () => {
const { url, server } = await createServer({ useHttps: false });
testServer = server;
const configurationUtilities = getACUfromConfig();
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
});
describe('https', () => {
test('it fails with self-signed cert and no overrides', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig();
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it works with verificationMode "none" config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
ssl: {
verificationMode: 'none',
},
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
test('it works with verificationMode "none" for custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
test('it works with ca in custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
test('it fails with incorrect ca in custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: KIBANA_CRT } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it works with incorrect ca in custom host config but verificationMode "none"', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [
{
url,
ssl: {
certificateAuthoritiesData: CA,
verificationMode: 'none',
},
},
],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
test('it works with incorrect ca in custom host config but verificationMode config "full"', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
ssl: {
verificationMode: 'none',
},
customHostSettings: [
{
url,
ssl: {
certificateAuthoritiesData: CA,
},
},
],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
});
test('it fails with no matching custom host settings', async () => {
const { url, server } = await createServer({ useHttps: true });
const otherUrl = 'https://example.com';
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url: otherUrl, ssl: { verificationMode: 'none' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it fails cleanly with a garbage CA 1', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: 'garbage' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it fails cleanly with a garbage CA 2', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const ca = '-----BEGIN CERTIFICATE-----\ngarbage\n-----END CERTIFICATE-----\n';
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: ca } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
});
// targetHttps, proxyHttps, and proxyAuth should all range over [false, true], but
// currently the true versions are not passing
describe(`proxy`, () => {
for (const targetHttps of [false]) {
for (const targetAuth of [false, true]) {
for (const proxyHttps of [false]) {
for (const proxyAuth of [false]) {
const targetLabel = testLabel('target', targetHttps, targetAuth);
const proxyLabel = testLabel('proxy', proxyHttps, proxyAuth);
const testName = `${targetLabel} :: ${proxyLabel}`;
const opts = { targetHttps, targetAuth, proxyHttps, proxyAuth };
test(`basic; ${testName}`, async () => await basicProxyTest(opts));
if (targetAuth) {
test(`wrong target password; ${testName}`, async () =>
await wrongTargetPasswordProxyTest(opts));
test(`missing target password; ${testName}`, async () =>
await missingTargetPasswordProxyTest(opts));
}
if (proxyAuth) {
test(`wrong proxy password; ${testName}`, async () =>
await wrongProxyPasswordProxyTest(opts));
test(`missing proxy password; ${testName}`, async () =>
await missingProxyPasswordProxyTest(opts));
}
if (targetHttps) {
test(`missing CA; ${testName}`, async () =>
await missingCaProxyTest(opts));
test(`rejectUnauthorized target; ${testName}`, async () =>
await rejectUnauthorizedTargetProxyTest(opts));
test(`custom CA target; ${testName}`, async () =>
await customCAProxyTest(opts));
test(`verModeNone target; ${testName}`, async () =>
await verModeNoneTargetProxyTest(opts));
}
}
}
}
}
});
});
async function basicProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
});
}
async function wrongTargetPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const wrongUrl = manglePassword(target.url);
const res = await request({ ...axiosDefaults, url: wrongUrl, configurationUtilities: acu });
expect(res.status).toBe(403);
});
}
async function missingTargetPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const anonUrl = removePassword(target.url);
const res = await request({ ...axiosDefaults, url: anonUrl, configurationUtilities: acu });
expect(res.status).toBe(401);
});
}
async function wrongProxyPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const wrongUrl = manglePassword(proxy.url);
const acu = getACUfromConfig({
proxyUrl: wrongUrl,
ssl: { verificationMode: 'none' },
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.message).toMatch('407');
}
});
}
async function missingProxyPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const anonUrl = removePassword(proxy.url);
const acu = getACUfromConfig({
proxyUrl: anonUrl,
ssl: { verificationMode: 'none' },
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.message).toMatch('407');
}
});
}
async function missingCaProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.code).toEqual('UNABLE_TO_VERIFY_LEAF_SIGNATURE');
}
});
}
async function rejectUnauthorizedTargetProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
rejectUnauthorized: false,
customHostSettings: [{ url: target.url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
});
}
async function customCAProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
});
}
async function verModeNoneTargetProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
customHostSettings: [{ url: target.url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
});
}
interface RunTestOptions {
targetHttps: boolean;
targetAuth: boolean;
proxyHttps: boolean;
proxyAuth: boolean;
}
type AxiosParams = Parameters<typeof request>[0];
type Test = (
target: CreateServerResult,
proxy: CreateProxyResult,
axiosDefaults: AxiosParams
) => Promise<void>;
async function runWithSetup(opts: RunTestOptions, fn: Test) {
const target = await createServer({
useHttps: opts.targetHttps,
requireAuth: opts.targetAuth,
});
const proxy = await createProxy({
useHttps: opts.proxyHttps,
requireAuth: opts.proxyAuth,
});
const axiosDefaults = {
axios,
logger,
validateStatus,
url: target.url,
configurationUtilities: getACUfromConfig({
proxyUrl: proxy.url,
}),
};
try {
await fn(target, proxy, axiosDefaults);
} catch (err) {
expect(err).toBeUndefined();
}
target.server.close();
proxy.server.close();
}
function testLabel(type: string, tls: boolean, auth: boolean) {
return `${type} https ${tls ? 'X' : '-'} auth ${auth ? 'X' : '-'}`;
}
function validateStatus(status: number) {
return true;
}
function manglePassword(url: string) {
const parsed = new URL(url);
parsed.password = `nope-${parsed.password}-nope`;
return parsed.toString();
}
function removePassword(url: string) {
const parsed = new URL(url);
parsed.username = '';
parsed.password = '';
return parsed.toString();
}
const TlsOptions = {
cert: KIBANA_CRT,
key: KIBANA_KEY,
};
interface CreateServerOptions {
useHttps: boolean;
requireAuth?: boolean;
}
interface CreateServerResult {
url: string;
server: http.Server | https.Server;
}
async function createServer(options: CreateServerOptions): Promise<CreateServerResult> {
const { useHttps, requireAuth = false } = options;
const port = await getPort();
const url = `http${useHttps ? 's' : ''}://${requireAuth ? `${Auth}@` : ''}localhost:${port}`;
function requestHandler(req: http.IncomingMessage, res: http.ServerResponse) {
if (requireAuth) {
const auth = req.headers.authorization;
if (auth == null) {
res.setHeader('WWW-Authenticate', 'Basic');
res.writeHead(401);
res.end('authorization required');
return;
}
if (auth !== `Basic ${AuthB64}`) {
res.writeHead(403);
res.end('not authorized');
return;
}
}
res.writeHead(200);
res.end('http: just testing that a connection could be made');
}
let server: http.Server | https.Server;
if (!useHttps) {
server = http.createServer(requestHandler);
} else {
server = https.createServer(TlsOptions, requestHandler);
}
server.unref();
const readySignal = createReadySignal<CreateServerResult>();
server.listen(port, 'localhost', () => {
readySignal.signal({ url, server });
});
return readySignal.wait();
}
interface CreateProxyOptions {
useHttps: boolean;
requireAuth?: boolean;
}
interface CreateProxyResult {
url: string;
server: http.Server | https.Server;
}
type AuthenticateCallback = (err: null | Error, authenticated: boolean) => void;
interface IAuthenticate {
authenticate(req: http.IncomingMessage, callback: AuthenticateCallback): void;
}
async function createProxy(options: CreateProxyOptions): Promise<CreateProxyResult> {
const { useHttps, requireAuth = false } = options;
const port = await getPort();
const url = getUrl(useHttps, requireAuth, port);
let proxyServer: http.Server | https.Server;
if (!useHttps) {
proxyServer = http.createServer();
} else {
proxyServer = https.createServer(TlsOptions);
}
proxyServer.unref();
proxySetup(proxyServer);
if (requireAuth) {
(proxyServer as unknown as IAuthenticate).authenticate = (req, callback) => {
const auth = req.headers['proxy-authorization'];
callback(null, auth === `Basic ${AuthB64}`);
};
}
const readySignal = createReadySignal<CreateProxyResult>();
proxyServer.listen(port, 'localhost', () => {
readySignal.signal({ server: proxyServer, url });
});
return readySignal.wait();
}
function getUrl(useHttps: boolean, requiresAuth: boolean, port: number) {
return `http${useHttps ? 's' : ''}://${requiresAuth ? `${Auth}@` : ''}localhost:${port}`;
}
const BaseActionsConfig: ActionsConfig = {
enabled: true,
allowedHosts: ['*'],
enabledActionTypes: ['*'],
preconfiguredAlertHistoryEsIndex: false,
preconfigured: {},
proxyUrl: undefined,
proxyHeaders: undefined,
proxyRejectUnauthorizedCertificates: true,
ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
rejectUnauthorized: true,
maxResponseContentLength: ByteSizeValue.parse('1mb'),
responseTimeout: momentDuration(1000 * 30),
customHostSettings: undefined,
cleanupFailedExecutionsTask: {
enabled: true,
cleanupInterval: schema.duration().validate('5m'),
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
};
function getACUfromConfig(config: Partial<ActionsConfig> = {}): ActionsConfigurationUtilities {
const resolvedConfig = resolveCustomHosts(logger, { ...BaseActionsConfig, ...config });
return getActionsConfigurationUtilities(resolvedConfig);
}

View file

@ -0,0 +1,608 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires
const proxySetup = require('proxy');
import { readFileSync as fsReadFileSync } from 'fs';
import { resolve as pathResolve, join as pathJoin } from 'path';
import http from 'http';
import https from 'https';
import axios from 'axios';
import { duration as momentDuration } from 'moment';
import { schema } from '@kbn/config-schema';
import getPort from 'get-port';
import { request } from '../builtin_action_types/lib/axios_utils';
import { ByteSizeValue } from '@kbn/config-schema';
import { Logger } from 'src/core/server';
import { loggingSystemMock } from 'src/core/server/mocks';
import { createReadySignal } from '../../../event_log/server';
import { ActionsConfig } from '../config';
import { ActionsConfigurationUtilities, getActionsConfigurationUtilities } from '../actions_config';
import { resolveCustomHosts } from '../lib/custom_host_settings';
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const CERT_DIR = '../../../../../../packages/kbn-dev-utils/certs';
const KIBANA_CRT_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'kibana.crt'));
const KIBANA_KEY_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'kibana.key'));
const CA_FILE = pathResolve(__filename, pathJoin(CERT_DIR, 'ca.crt'));
const KIBANA_KEY = fsReadFileSync(KIBANA_KEY_FILE, 'utf8');
const KIBANA_CRT = fsReadFileSync(KIBANA_CRT_FILE, 'utf8');
const CA = fsReadFileSync(CA_FILE, 'utf8');
const Auth = 'elastic:changeme';
const AuthB64 = Buffer.from(Auth).toString('base64');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AxiosDefaultsAadapter = require('axios/lib/adapters/http');
const ServerResponse = 'A unique response returned by the server!';
describe('axios connections', () => {
let testServer: http.Server | https.Server | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let savedAxiosDefaultsAdapter: any;
beforeEach(() => {
// needed to prevent the dreaded Error: Cross origin http://localhost forbidden
// see: https://github.com/axios/axios/issues/1754#issuecomment-572778305
savedAxiosDefaultsAdapter = axios.defaults.adapter;
axios.defaults.adapter = AxiosDefaultsAadapter;
});
afterEach(() => {
axios.defaults.adapter = savedAxiosDefaultsAdapter;
});
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
testServer?.close();
testServer = null;
});
describe('http', () => {
test('it works', async () => {
const { url, server } = await createServer({ useHttps: false });
testServer = server;
const configurationUtilities = getACUfromConfig();
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
});
describe('https', () => {
test('it fails with self-signed cert and no overrides', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig();
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it works with verificationMode "none" config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
ssl: {
verificationMode: 'none',
},
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
test('it works with verificationMode "none" for custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
test('it works with ca in custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
test('it fails with incorrect ca in custom host config', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: KIBANA_CRT } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it works with incorrect ca in custom host config but verificationMode "none"', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [
{
url,
ssl: {
certificateAuthoritiesData: CA,
verificationMode: 'none',
},
},
],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
test('it works with incorrect ca in custom host config but verificationMode config "full"', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
ssl: {
verificationMode: 'none',
},
customHostSettings: [
{
url,
ssl: {
certificateAuthoritiesData: CA,
},
},
],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
test('it fails with no matching custom host settings', async () => {
const { url, server } = await createServer({ useHttps: true });
const otherUrl = 'https://example.com';
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url: otherUrl, ssl: { verificationMode: 'none' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it fails cleanly with a garbage CA 1', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: 'garbage' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
test('it fails cleanly with a garbage CA 2', async () => {
const { url, server } = await createServer({ useHttps: true });
testServer = server;
const ca = '-----BEGIN CERTIFICATE-----\ngarbage\n-----END CERTIFICATE-----\n';
const configurationUtilities = getACUfromConfig({
customHostSettings: [{ url, ssl: { certificateAuthoritiesData: ca } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
});
});
// targetHttps, proxyHttps, and proxyAuth should all range over [false, true], but
// currently the true versions are not passing
describe(`proxy`, () => {
for (const targetHttps of [false]) {
for (const targetAuth of [false, true]) {
for (const proxyHttps of [false]) {
for (const proxyAuth of [false]) {
const targetLabel = testLabel('target', targetHttps, targetAuth);
const proxyLabel = testLabel('proxy', proxyHttps, proxyAuth);
const testName = `${targetLabel} :: ${proxyLabel}`;
const opts = { targetHttps, targetAuth, proxyHttps, proxyAuth };
test(`basic; ${testName}`, async () => await basicProxyTest(opts));
if (targetAuth) {
test(`wrong target password; ${testName}`, async () =>
await wrongTargetPasswordProxyTest(opts));
test(`missing target password; ${testName}`, async () =>
await missingTargetPasswordProxyTest(opts));
}
if (proxyAuth) {
test(`wrong proxy password; ${testName}`, async () =>
await wrongProxyPasswordProxyTest(opts));
test(`missing proxy password; ${testName}`, async () =>
await missingProxyPasswordProxyTest(opts));
}
if (targetHttps) {
test(`missing CA; ${testName}`, async () =>
await missingCaProxyTest(opts));
test(`rejectUnauthorized target; ${testName}`, async () =>
await rejectUnauthorizedTargetProxyTest(opts));
test(`custom CA target; ${testName}`, async () =>
await customCAProxyTest(opts));
test(`verModeNone target; ${testName}`, async () =>
await verModeNoneTargetProxyTest(opts));
}
}
}
}
}
});
});
async function basicProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
}
async function wrongTargetPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const wrongUrl = manglePassword(target.url);
const res = await request({ ...axiosDefaults, url: wrongUrl, configurationUtilities: acu });
expect(res.status).toBe(403);
});
}
async function missingTargetPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
ssl: { verificationMode: 'none' },
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const anonUrl = removePassword(target.url);
const res = await request({ ...axiosDefaults, url: anonUrl, configurationUtilities: acu });
expect(res.status).toBe(401);
});
}
async function wrongProxyPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const wrongUrl = manglePassword(proxy.url);
const acu = getACUfromConfig({
proxyUrl: wrongUrl,
ssl: { verificationMode: 'none' },
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.message).toMatch('407');
}
});
}
async function missingProxyPasswordProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const anonUrl = removePassword(proxy.url);
const acu = getACUfromConfig({
proxyUrl: anonUrl,
ssl: { verificationMode: 'none' },
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.message).toMatch('407');
}
});
}
async function missingCaProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
});
try {
await request({ ...axiosDefaults, configurationUtilities: acu });
expect('request should have thrown error').toBeUndefined();
} catch (err) {
expect(err.code).toEqual('UNABLE_TO_VERIFY_LEAF_SIGNATURE');
}
});
}
async function rejectUnauthorizedTargetProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
rejectUnauthorized: false,
customHostSettings: [{ url: target.url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
}
async function customCAProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
customHostSettings: [{ url: target.url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
}
async function verModeNoneTargetProxyTest(opts: RunTestOptions) {
await runWithSetup(opts, async (target, proxy, axiosDefaults) => {
const acu = getACUfromConfig({
proxyUrl: proxy.url,
customHostSettings: [{ url: target.url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ ...axiosDefaults, configurationUtilities: acu });
expect(res.status).toBe(200);
expect(res.data).toBe(ServerResponse);
});
}
interface RunTestOptions {
targetHttps: boolean;
targetAuth: boolean;
proxyHttps: boolean;
proxyAuth: boolean;
}
type AxiosParams = Parameters<typeof request>[0];
type Test = (
target: CreateServerResult,
proxy: CreateProxyResult,
axiosDefaults: AxiosParams
) => Promise<void>;
async function runWithSetup(opts: RunTestOptions, fn: Test) {
const target = await createServer({
useHttps: opts.targetHttps,
requireAuth: opts.targetAuth,
});
const proxy = await createProxy({
useHttps: opts.proxyHttps,
requireAuth: opts.proxyAuth,
});
const axiosDefaults = {
axios,
logger,
validateStatus,
url: target.url,
configurationUtilities: getACUfromConfig({
proxyUrl: proxy.url,
}),
};
try {
await fn(target, proxy, axiosDefaults);
} catch (err) {
expect(err).toBeUndefined();
}
target.server.close();
proxy.server.close();
}
function testLabel(type: string, tls: boolean, auth: boolean) {
return `${type} https ${tls ? 'X' : '-'} auth ${auth ? 'X' : '-'}`;
}
function validateStatus(status: number) {
return true;
}
function manglePassword(url: string) {
const parsed = new URL(url);
parsed.password = `nope-${parsed.password}-nope`;
return parsed.toString();
}
function removePassword(url: string) {
const parsed = new URL(url);
parsed.username = '';
parsed.password = '';
return parsed.toString();
}
const TlsOptions = {
cert: KIBANA_CRT,
key: KIBANA_KEY,
};
interface CreateServerOptions {
useHttps: boolean;
requireAuth?: boolean;
}
interface CreateServerResult {
url: string;
server: http.Server | https.Server;
}
async function createServer(options: CreateServerOptions): Promise<CreateServerResult> {
const { useHttps, requireAuth = false } = options;
const port = await getPort();
const url = `http${useHttps ? 's' : ''}://${requireAuth ? `${Auth}@` : ''}localhost:${port}`;
function requestHandler(req: http.IncomingMessage, res: http.ServerResponse) {
if (requireAuth) {
const auth = req.headers.authorization;
if (auth == null) {
res.setHeader('WWW-Authenticate', 'Basic');
res.writeHead(401);
res.end('authorization required');
return;
}
if (auth !== `Basic ${AuthB64}`) {
res.writeHead(403);
res.end('not authorized');
return;
}
}
res.writeHead(200);
res.end(ServerResponse);
}
let server: http.Server | https.Server;
if (!useHttps) {
server = http.createServer(requestHandler);
} else {
server = https.createServer(TlsOptions, requestHandler);
}
server.unref();
const readySignal = createReadySignal<CreateServerResult>();
server.listen(port, 'localhost', () => {
readySignal.signal({ url, server });
});
return readySignal.wait();
}
interface CreateProxyOptions {
useHttps: boolean;
requireAuth?: boolean;
}
interface CreateProxyResult {
url: string;
server: http.Server | https.Server;
}
type AuthenticateCallback = (err: null | Error, authenticated: boolean) => void;
interface IAuthenticate {
authenticate(req: http.IncomingMessage, callback: AuthenticateCallback): void;
}
async function createProxy(options: CreateProxyOptions): Promise<CreateProxyResult> {
const { useHttps, requireAuth = false } = options;
const port = await getPort();
const url = getUrl(useHttps, requireAuth, port);
let proxyServer: http.Server | https.Server;
if (!useHttps) {
proxyServer = http.createServer();
} else {
proxyServer = https.createServer(TlsOptions);
}
proxyServer.unref();
proxySetup(proxyServer);
if (requireAuth) {
(proxyServer as unknown as IAuthenticate).authenticate = (req, callback) => {
const auth = req.headers['proxy-authorization'];
callback(null, auth === `Basic ${AuthB64}`);
};
}
const readySignal = createReadySignal<CreateProxyResult>();
proxyServer.listen(port, 'localhost', () => {
readySignal.signal({ server: proxyServer, url });
});
return readySignal.wait();
}
function getUrl(useHttps: boolean, requiresAuth: boolean, port: number) {
return `http${useHttps ? 's' : ''}://${requiresAuth ? `${Auth}@` : ''}localhost:${port}`;
}
const BaseActionsConfig: ActionsConfig = {
enabled: true,
allowedHosts: ['*'],
enabledActionTypes: ['*'],
preconfiguredAlertHistoryEsIndex: false,
preconfigured: {},
proxyUrl: undefined,
proxyHeaders: undefined,
proxyRejectUnauthorizedCertificates: true,
ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
rejectUnauthorized: true,
maxResponseContentLength: ByteSizeValue.parse('1mb'),
responseTimeout: momentDuration(1000 * 30),
customHostSettings: undefined,
cleanupFailedExecutionsTask: {
enabled: true,
cleanupInterval: schema.duration().validate('5m'),
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
};
function getACUfromConfig(config: Partial<ActionsConfig> = {}): ActionsConfigurationUtilities {
const resolvedConfig = resolveCustomHosts(logger, { ...BaseActionsConfig, ...config });
return getActionsConfigurationUtilities(resolvedConfig);
}

View file

@ -6,108 +6,151 @@
*/
/*
This module implements two forward http proxies, http on 8080 and https on 8443,
which can be used with the config xpack.actions.proxyUrl to emulate customers
using forward proxies with Kibana actions. You can use either the http or https
versions, both can forward proxy http and https traffic:
xpack.actions.proxyUrl: http://localhost:8080
OR
xpack.actions.proxyUrl: https://localhost:8443
Starts http and https proxies to use to test actions within Kibana or with curl.
When using the https-based version, you may need to set the following option
as well:
Assumes you have elasticsearch running on https://elastic:changeme@localhost:9200,
otherwise expect 500 responses from those requests. All other requests should
work as expected.
xpack.actions.rejectUnauthorized: false
# start 4 proxies:
If the server you are connecting to via the proxy is https and has self-signed
certificates, you'll also need to set
node x-pack/plugins/actions/server/manual_tests/forward_proxy.js http-8080-open http-8081-auth https-8443-open https-8444-auth
# issue some requests through the proxies
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8080 http://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8080 https://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8443 http://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8443 https://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8080 https://elastic:changeme@localhost:9200; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8443 https://elastic:changeme@localhost:9200; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8081 --proxy-user elastic:changeme http://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8081 --proxy-user elastic:changeme https://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8444 --proxy-user elastic:changeme http://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8444 --proxy-user elastic:changeme https://www.example.com; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x http://127.0.0.1:8081 --proxy-user elastic:changeme https://elastic:changeme@localhost:9200; \
curl -k --no-alpn -o /dev/null --proxy-insecure -x https://127.0.0.1:8444 --proxy-user elastic:changeme https://elastic:changeme@localhost:9200; \
echo done - you should run all the lines above as one command
xpack.actions.proxyRejectUnauthorizedCertificates: false
*/
const HTTP_PORT = 8080;
const HTTPS_PORT = 8443;
// starts http and https proxies to use to test actions within Kibana
const fs = require('fs');
const net = require('net');
const url = require('url');
const path = require('path');
const http = require('http');
const https = require('https');
const httpProxy = require('http-proxy');
const proxySetup = require('proxy');
const httpsOptions = {
key: fs.readFileSync('packages/kbn-dev-utils/certs/kibana.key', 'utf8'),
cert: fs.readFileSync('packages/kbn-dev-utils/certs/kibana.crt', 'utf8'),
const PROGRAM = path.basename(__filename).replace(/.js$/, '');
const CertDir = path.resolve(__dirname, '../../../../../packages/kbn-dev-utils/certs');
const Auth = 'elastic:changeme';
const AuthB64 = Buffer.from(Auth).toString('base64');
const HttpsOptions = {
key: fs.readFileSync(path.join(CertDir, 'kibana.key'), 'utf8'),
cert: fs.readFileSync(path.join(CertDir, 'kibana.crt'), 'utf8'),
};
const proxy = httpProxy.createServer();
createServer('http', HTTP_PORT);
createServer('https', HTTPS_PORT);
function createServer(protocol, port) {
let httpServer;
if (protocol === 'http') {
httpServer = http.createServer();
} else {
httpServer = https.createServer(httpsOptions);
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
help();
process.exit(1);
}
httpServer.on('request', httpRequest);
httpServer.on('connect', httpsRequest);
httpServer.listen(port);
log(`proxy server started: ${protocol}:/localhost:${port}`);
// handle http requests
function httpRequest(req, res) {
log(`${protocol} server: request for: ${req.url}`);
const parsedUrl = url.parse(req.url);
if (parsedUrl.hostname == null) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('this is a proxy server');
return;
}
const target = parsedUrl.protocol + '//' + parsedUrl.hostname;
proxy.web(req, res, { target: target, secure: false });
}
// handle https requests
// see: https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_event_connect
function httpsRequest(req, socket, head) {
log(`${protocol} proxy server: request for target: https://${req.url}`);
const serverUrl = url.parse('https://' + req.url);
const serverSocket = net.connect(serverUrl.port, serverUrl.hostname, () => {
socket.write('HTTP/1.1 200 Connection Established\r\nProxy-agent: Node-Proxy\r\n\r\n');
serverSocket.write(head);
serverSocket.pipe(socket);
socket.pipe(serverSocket);
});
socket.on('error', (err) => {
log(`error on socket to proxy: ${err}`);
socket.destroy();
serverSocket.destroy();
});
serverSocket.on('error', (err) => {
log(`error on socket to target: ${err}`);
socket.destroy();
serverSocket.destroy();
});
const specs = args.map(argToSpec);
for (const spec of specs) {
const { protocol, port, auth } = spec;
createServer(protocol, port, auth);
}
}
/** @type { (protocol: string, port: number, auth: boolean) => Promise<http.Server | httpServer> } */
async function createServer(protocol, port, auth) {
let proxyServer;
if (protocol === 'http') {
proxyServer = http.createServer();
} else {
proxyServer = https.createServer(HttpsOptions);
}
proxySetup(proxyServer);
let authLabel = '';
if (auth) {
authLabel = `${Auth}@`;
proxyServer.authenticate = (req, callback) => {
const auth = req.headers['proxy-authorization'];
callback(null, auth === `Basic ${AuthB64}`);
};
}
const serverLabel = `${protocol}://${authLabel}localhost:${port}`;
proxyServer.listen(port, 'localhost', () => {
console.log(`proxy server started on ${serverLabel}`);
});
}
/* convert 'proto-port-auth' into object with shape shown below */
/** @type { (arg: string) => void | { protocol: string, port: number, auth: boolean } } */
function argToSpec(arg) {
const parts = arg.split('-');
if (parts.length < 2) {
return logError(`invalid spec: ${arg}`);
}
const [protocol, portString, authString] = parts;
if (!protocol) return logError(`empty protocol in '${arg}'`);
if (protocol !== 'http' && protocol !== 'https')
return logError(`invalid protocol in '${arg}': '${protocol}'`);
if (!portString) return logError(`empty port in '${arg}'`);
const port = Number.parseInt(portString, 10);
if (isNaN(port)) return logError(`invalid port in '${arg}': ${portString}`);
let auth;
if (!authString) {
auth = false;
} else {
if (authString !== 'auth' && authString !== 'open')
return logError(`invalid auth in '${arg}': '${authString}'`);
auth = authString === 'auth';
}
return { protocol, port, auth };
}
/** @type { (message: string) => void } */
function log(message) {
console.log(`${new Date().toISOString()} - ${message}`);
}
/*
Test with:
/** @type { (message: string) => void } */
function logError(message) {
log(message);
process.exit(1);
}
curl -v -k --proxy-insecure -x http://127.0.0.1:8080 http://www.google.com
curl -v -k --proxy-insecure -x http://127.0.0.1:8080 https://www.google.com
curl -v -k --proxy-insecure -x https://127.0.0.1:8443 http://www.google.com
curl -v -k --proxy-insecure -x https://127.0.0.1:8443 https://www.google.com
*/
main();
function help() {
console.log(`${PROGRAM} - create http proxies to test connectors with`);
console.log(`usage:`);
console.log(` ${PROGRAM} spec spec spec ...`);
console.log(``);
console.log(`options:`);
console.log(` - none yet`);
console.log(``);
console.log(`parameters:`);
console.log(` spec: spec is a 3-part token, separated by '-' chars`);
console.log(` [proto]-[port]-[auth]`);
console.log(` proto - 'http' or 'https'`);
console.log(` port - port to open the proxy on`);
console.log(` auth - 'auth' or 'open' (auth expects user/pass elastic:change)`);
console.log(``);
console.log(`example:`);
console.log(` ${PROGRAM} {options} http-8080-open https-8443-open`);
console.log(` `);
}

View file

@ -7564,6 +7564,16 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
args@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761"
integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==
dependencies:
camelcase "5.0.0"
chalk "2.4.2"
leven "2.1.0"
mri "1.1.4"
argsplit@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/argsplit/-/argsplit-1.0.5.tgz#9319a6ef63411716cfeb216c45ec1d13b35c5e99"
@ -8474,6 +8484,11 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
basic-auth-parser@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz#ce9e71a77f23c1279eecd2659b2a46244c156e41"
integrity sha512-Y7OBvWn+JnW45JWHLY6ybYub2k9cXCMrtCyO1Hds2s6eqClqWhPnOQpgXUPjAiMHj+A8TEPIQQ1dYENnJoBOHQ==
basic-auth@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
@ -9274,6 +9289,11 @@ camelcase-keys@^6.2.2:
map-obj "^4.0.0"
quick-lru "^4.0.1"
camelcase@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
camelcase@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@ -18388,6 +18408,11 @@ leaflet@1.5.1:
request "^2.88.2"
source-map "^0.5.3"
leven@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@ -19956,6 +19981,11 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mri@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a"
integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==
ms-chromium-edge-driver@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.4.3.tgz#808723efaf24da086ebc2a2feb0975162164d2ff"
@ -22732,6 +22762,15 @@ proxy-from-env@1.1.0, proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
proxy@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/proxy/-/proxy-1.0.2.tgz#e0cfbe11c0a7a8b238fd2d7134de4e2867578e7f"
integrity sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==
dependencies:
args "5.0.1"
basic-auth-parser "0.0.2"
debug "^4.1.1"
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"