-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Connectors] implement support for proxy-authorization #258443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
48c553f
3199152
38f46f6
00c13ed
307e3c0
9efc7ba
943fc92
fb3e499
2e6691b
64d2234
5503818
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -220,7 +220,7 @@ $$$action-config-email-domain-allowlist$$$ | |||||
|
|
||||||
| 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). | ||||||
| You can supply proxy credentials in the URL (`http://user:password@proxy-host:8080`) or use [`xpack.actions.proxyUser`](#action-config-proxy-user) and [`xpack.actions.proxyPassword`](#action-config-proxy-password). If the URL already includes a username and password, those take precedence over the separate settings. | ||||||
|
|
||||||
| Data type: `string` | ||||||
|
|
||||||
|
|
@@ -230,6 +230,20 @@ $$$action-config-email-domain-allowlist$$$ | |||||
| curl --verbose --proxytunnel --proxy http://localhost:8080 <EXAMPLE_URL> | ||||||
| ``` | ||||||
|
|
||||||
| $$$action-config-proxy-user$$$ | ||||||
|
|
||||||
| `xpack.actions.proxyUser`  | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| : Username for HTTP basic authentication with the proxy when [`xpack.actions.proxyUrl`](#action-settings) is set. Use with `xpack.actions.proxyPassword`. Ignored if the proxy URL already includes credentials. Store the password in the [Kibana keystore](docs-content://deploy-manage/security/secure-settings.md) when possible. | ||||||
|
|
||||||
| Data type: `string` | ||||||
|
|
||||||
| $$$action-config-proxy-password$$$ | ||||||
|
|
||||||
| `xpack.actions.proxyPassword`  | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| : Password for HTTP basic authentication with the proxy when [`xpack.actions.proxyUrl`](#action-settings) is set. Use with `xpack.actions.proxyUser`. | ||||||
|
|
||||||
| Data type: `string` | ||||||
|
|
||||||
| `xpack.actions.proxyBypassHosts`  | ||||||
| : Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,17 +133,39 @@ export function getCustomAgents(opts: GetCustomAgentsOpts): GetCustomAgentsRespo | |
| proxySettings.proxySSLSettings.verificationMode, | ||
| sslOverrides | ||
| ); | ||
|
|
||
| const hasUrlAuth = Boolean(proxyUrl.username && proxyUrl.password); | ||
| const hasConfigAuth = Boolean( | ||
| proxySettings.proxyUser && | ||
| proxySettings.proxyPassword && | ||
| proxySettings.proxyUser !== '' && | ||
| proxySettings.proxyPassword !== '' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| ); | ||
| let proxyAuth: string | undefined; | ||
| if (hasUrlAuth) { | ||
| proxyAuth = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was surprised to learn that the username/password from URL are encoded! $ node -p 'new URL("https://elas<tic:changeme@example.com")'
URL {
...
username: 'elas%3Ctic',
...
} |
||
| } else if (hasConfigAuth) { | ||
| proxyAuth = `${proxySettings.proxyUser}:${proxySettings.proxyPassword}`; | ||
| } | ||
|
|
||
| let httpProxyAgentUrl = proxySettings.proxyUrl; | ||
| if (!hasUrlAuth && hasConfigAuth) { | ||
| const withAuth = new URL(proxySettings.proxyUrl); | ||
| withAuth.username = proxySettings.proxyUser as string; | ||
| withAuth.password = proxySettings.proxyPassword as string; | ||
| httpProxyAgentUrl = withAuth.toString(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This adds the auth info to the URL. Which the http agents we use check for explicitly (I don't remember them doing that when we first started using it), and send the appropriate auth headers for. You'll also see we set the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, should we align them with this PR? |
||
| } | ||
|
|
||
| // At this point, we are going to use a proxy, so we need new agents. | ||
| // We will though, copy over the calculated ssl options from above, into | ||
| // the https agent. | ||
| const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl) as unknown as HttpAgent; | ||
| const httpAgent = new HttpProxyAgent(httpProxyAgentUrl) as unknown as HttpAgent; | ||
| const httpsAgent = new HttpsProxyAgent({ | ||
| host: proxyUrl.hostname, | ||
| port: Number(proxyUrl.port), | ||
| port: Number(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80), | ||
| protocol: proxyUrl.protocol, | ||
| headers: proxySettings.proxyHeaders, | ||
| ...(proxyUrl.username && | ||
| proxyUrl.password && { auth: `${proxyUrl.username}:${proxyUrl.password}` }), | ||
| ...(proxyAuth && { auth: proxyAuth }), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be a breaking change for the existing users if they have special characters in their creds? |
||
| // do not fail on invalid certs if value is false | ||
| ...proxyNodeSSLOptions, | ||
| }) as unknown as HttpsAgent; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| export const ProxyAuthUser = 'ftr_proxy_user'; | ||
| export const ProxyAuthPassword = 'ftr_proxy_pass'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,43 +5,111 @@ | |
| * 2.0. | ||
| */ | ||
|
|
||
| import http from 'http'; | ||
| import httpProxy from 'http-proxy'; | ||
|
|
||
| function getProxyBasicAuthFromServerArgs( | ||
| kbnTestServerConfig: string[] | ||
| ): { user: string; password: string } | undefined { | ||
| const userLine = kbnTestServerConfig.find((val: string) => | ||
| val.startsWith('--xpack.actions.proxyUser=') | ||
| ); | ||
| const passLine = kbnTestServerConfig.find((val: string) => | ||
| val.startsWith('--xpack.actions.proxyPassword=') | ||
| ); | ||
| if (!userLine || !passLine) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const result = { | ||
| user: userLine.replace('--xpack.actions.proxyUser=', ''), | ||
| password: passLine.replace('--xpack.actions.proxyPassword=', ''), | ||
| }; | ||
| return result; | ||
| } | ||
|
|
||
| type ProxyReqHandler = ( | ||
| proxyReq?: http.ClientRequest, | ||
| req?: http.IncomingMessage, | ||
| res?: http.ServerResponse | ||
| ) => void; | ||
|
|
||
| type ProxyResHandler = ( | ||
| proxyRes?: http.IncomingMessage, | ||
| req?: http.IncomingMessage, | ||
| res?: http.ServerResponse | ||
| ) => void; | ||
|
|
||
| export const getHttpProxyServer = async ( | ||
| targetUrl: string, | ||
| kbnTestServerConfig: any, | ||
| onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void | ||
| kbnTestServerConfig: string[], | ||
| onProxyResHandler: ProxyResHandler, | ||
| onProxyReqHandler?: ProxyReqHandler | ||
| ): Promise<httpProxy> => { | ||
| const proxyServer = httpProxy.createProxyServer({ | ||
| target: targetUrl, | ||
| secure: false, | ||
| selfHandleResponse: false, | ||
| }); | ||
|
|
||
| proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { | ||
| proxyServer.on('proxyRes', (proxyRes, req, res) => { | ||
| onProxyResHandler(proxyRes, req, res); | ||
| }); | ||
|
|
||
| // http-proxy doesn't propagate client disconnects to the target server. | ||
| // Tear down the proxied request when the client disconnects early (e.g. when the request is aborted). | ||
| proxyServer.on('proxyReq', (proxyReq, req, res) => { | ||
| res.on('close', () => { | ||
| if (!res.writableFinished) { | ||
| proxyReq.destroy(); | ||
| } | ||
| }); | ||
| onProxyReqHandler?.(proxyReq, req, res); | ||
| }); | ||
|
|
||
| const proxyPort = getProxyPort(kbnTestServerConfig); | ||
| const basicAuth = getProxyBasicAuthFromServerArgs(kbnTestServerConfig); | ||
|
|
||
| if (basicAuth) { | ||
| const expectedAuth = `Basic ${Buffer.from( | ||
| `${basicAuth.user}:${basicAuth.password}`, | ||
| 'utf8' | ||
| ).toString('base64')}`; | ||
| const server = http.createServer((req, res) => { | ||
| const proxyAuthHeader = req.headers['proxy-authorization']; | ||
|
|
||
| if (proxyAuthHeader !== expectedAuth) { | ||
| if (proxyAuthHeader == null) { | ||
| // eslint-disable-next-line no-console | ||
| console.log('Proxy-Authorization header is missing'); | ||
| } else { | ||
| const encodedCreds = proxyAuthHeader.replace('Basic ', ''); | ||
| const decodedAuth = Buffer.from(encodedCreds, 'base64').toString('utf8'); | ||
| // eslint-disable-next-line no-console | ||
| console.log(`Proxy-Authorization header is using credentials ${decodedAuth}`); | ||
| } | ||
|
|
||
| res.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="proxy"' }); | ||
| res.end(); | ||
| return; | ||
| } | ||
| proxyServer.web(req, res); | ||
| }); | ||
| server.listen(proxyPort); | ||
| return server as unknown as httpProxy; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would changing the return type to |
||
| } | ||
|
|
||
| proxyServer.listen(proxyPort); | ||
|
|
||
| return proxyServer; | ||
| }; | ||
|
|
||
| export const getProxyPort = (kbnTestServerConfig: any): number => { | ||
| export const getProxyPort = (kbnTestServerConfig: string[]): number => { | ||
| const proxyUrl = kbnTestServerConfig | ||
| .find((val: string) => val.startsWith('--xpack.actions.proxyUrl=')) | ||
| .replace('--xpack.actions.proxyUrl=', ''); | ||
| ?.replace('--xpack.actions.proxyUrl=', ''); | ||
|
|
||
| if (!proxyUrl) { | ||
| throw new Error('Expected --xpack.actions.proxyUrl= in kbn test server args'); | ||
| } | ||
|
|
||
| const urlObject = new URL(proxyUrl); | ||
| return Number(urlObject.port); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.