Skip to content

Commit 296b203

Browse files
authored
fix: proxy SSL CONNECT issues with openwhisk library (#222)
* fix: add support for use_proxy_from_env_var needle option in openwhisk.js by runtime patching pin openwhisk lib version * add tests
1 parent bd93547 commit 296b203

11 files changed

+1374
-5
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
"folder-hash": "^4.0.4",
2121
"fs-extra": "^11.3.0",
2222
"globby": "^11.0.1",
23+
"http-proxy-agent": "^7.0.2",
24+
"https-proxy-agent": "^7.0.6",
2325
"js-yaml": "^4.1.0",
2426
"lodash.clonedeep": "^4.5.0",
25-
"openwhisk": "^3.21.8",
27+
"openwhisk": "3.21.8",
2628
"openwhisk-fqn": "0.0.2",
2729
"proxy-from-env": "^1.1.0",
2830
"sha1": "^1.1.1",

src/PatchedHttpsProxyAgent.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software distributed under
7+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
OF ANY KIND, either express or implied. See the License for the specific language
9+
governing permissions and limitations under the License.
10+
*/
11+
12+
const { HttpsProxyAgent } = require('https-proxy-agent')
13+
14+
/**
15+
* HttpsProxyAgent needs a patch for TLS connections.
16+
* It doesn't pass in the original options during a SSL connect.
17+
*
18+
* See https://github.com/TooTallNate/proxy-agents/issues/89
19+
* @private
20+
*/
21+
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
22+
constructor (proxyUrl, opts) {
23+
super(proxyUrl, opts)
24+
this.savedOpts = opts
25+
}
26+
27+
async connect (req, opts) {
28+
return super.connect(req, {
29+
...this.savedOpts,
30+
keepAliveInitialDelay: 1000,
31+
keepAlive: true,
32+
...opts
33+
})
34+
}
35+
}
36+
37+
module.exports = PatchedHttpsProxyAgent

src/RuntimeAPI.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const deepCopy = require('lodash.clonedeep')
1717
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:RuntimeAPI', { provider: 'debug', level: process.env.LOG_LEVEL })
1818
const LogForwarding = require('./LogForwarding')
1919
const LogForwardingLocalDestinationsProvider = require('./LogForwardingLocalDestinationsProvider')
20+
const { patchOWForTunnelingIssue } = require('./openwhisk-patch')
21+
const { getProxyAgent } = require('./utils')
2022

2123
require('./types.jsdoc') // for VS Code autocomplete
2224
/* global OpenwhiskOptions, OpenwhiskClient */ // for linter
@@ -35,8 +37,14 @@ class RuntimeAPI {
3537
*/
3638
async init (options) {
3739
aioLogger.debug(`init options: ${JSON.stringify(options, null, 2)}`)
40+
3841
const clonedOptions = deepCopy(options)
3942

43+
clonedOptions.use_proxy_from_env_var = false // default, unless env var is set
44+
if (process.env.NEEDLE_USE_PROXY_FROM_ENV_VAR === 'true') { // legacy support
45+
clonedOptions.use_proxy_from_env_var = true
46+
}
47+
4048
const initErrors = []
4149
if (!clonedOptions || !clonedOptions.api_key) {
4250
initErrors.push('api_key')
@@ -53,7 +61,13 @@ class RuntimeAPI {
5361
const proxyUrl = getProxyForUrl(clonedOptions.apihost)
5462
if (proxyUrl) {
5563
aioLogger.debug(`using proxy url: ${proxyUrl}`)
56-
clonedOptions.proxy = proxyUrl
64+
if (clonedOptions.use_proxy_from_env_var !== false) {
65+
clonedOptions.proxy = proxyUrl
66+
clonedOptions.agent = null
67+
} else {
68+
clonedOptions.proxy = null
69+
clonedOptions.agent = getProxyAgent(clonedOptions.apihost, proxyUrl)
70+
}
5771
} else {
5872
aioLogger.debug('proxy settings not found')
5973
}
@@ -67,7 +81,7 @@ class RuntimeAPI {
6781
const shouldIgnoreCerts = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0'
6882
clonedOptions.ignore_certs = clonedOptions.ignore_certs || shouldIgnoreCerts
6983

70-
this.ow = ow(clonedOptions)
84+
this.ow = patchOWForTunnelingIssue(ow(clonedOptions), clonedOptions.use_proxy_from_env_var)
7185
const self = this
7286

7387
return {

src/openwhisk-patch.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-disable no-param-reassign */
2+
/* eslint-disable camelcase */
3+
/*
4+
Copyright 2025 Adobe. All rights reserved.
5+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License. You may obtain a copy
7+
of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software distributed under
10+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11+
OF ANY KIND, either express or implied. See the License for the specific language
12+
governing permissions and limitations under the License.
13+
*/
14+
15+
/**
16+
* This patches the Openwhisk client to handle a tunneling issue with openwhisk > v3.0.0
17+
* See https://github.com/tomas/needle/issues/406
18+
*
19+
* Once openwhisk.js supports the use_proxy_from_env_var option (for needle), we can remove this patch.
20+
*
21+
* @param {object} ow the Openwhisk object to patch
22+
* @param {boolean} use_proxy_from_env_var the needle option to add
23+
* @returns {object} the patched openwhisk object
24+
*/
25+
function patchOWForTunnelingIssue (ow, use_proxy_from_env_var) {
26+
// we must set proxy to null here if agent is set, since it was already
27+
// internally initialzed in Openwhisk with the proxy url from env vars
28+
const agentIsSet = ow.actions.client.options.agent !== null
29+
if (agentIsSet && use_proxy_from_env_var === false) {
30+
ow.actions.client.options.proxy = undefined
31+
}
32+
33+
// The issue is patching openwhisk.js to use use_proxy_from_env_var (a needle option) - the contribution process might take too long.
34+
// monkey-patch client.params: patch one, all the rest should be patched (shared client)
35+
// we wrap the original params to add the use_proxy_from_env_var boolean
36+
const originalParams = ow.actions.client.params.bind(ow.actions.client)
37+
ow.actions.client.params = function (...args) {
38+
return originalParams(...args).then(params => {
39+
params.use_proxy_from_env_var = use_proxy_from_env_var
40+
return params
41+
}).catch(err => {
42+
console.error('Error patching openwhisk client params: ', err)
43+
throw err
44+
})
45+
}
46+
47+
return ow
48+
}
49+
50+
module.exports = {
51+
patchOWForTunnelingIssue
52+
}

src/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const path = require('path')
2121
const archiver = require('archiver')
2222
// this is a static list that comes from here: https://developer.adobe.com/runtime/docs/guides/reference/runtimes/
2323
const SupportedRuntimes = ['sequence', 'blackbox', 'nodejs:10', 'nodejs:12', 'nodejs:14', 'nodejs:16', 'nodejs:18', 'nodejs:20', 'nodejs:22']
24+
const { HttpProxyAgent } = require('http-proxy-agent')
25+
const PatchedHttpsProxyAgent = require('./PatchedHttpsProxyAgent.js')
2426

2527
// must cover 'deploy-service[-region][.env].app-builder[.int|.corp].adp.adobe.io/runtime
2628
const SUPPORTED_ADOBE_ANNOTATION_ENDPOINT_REGEXES = [
@@ -2113,7 +2115,24 @@ async function getSupportedServerRuntimes (apihost) {
21132115
return json.runtimes.nodejs.map(item => item.kind)
21142116
}
21152117

2118+
/**
2119+
* Get the proxy agent for the given endpoint
2120+
*
2121+
* @param {string} endpoint - The endpoint to get the proxy agent for
2122+
* @param {string} proxyUrl - The proxy URL to use
2123+
* @param {object} proxyOptions - The proxy options to use
2124+
* @returns {PatchedHttpsProxyAgent | HttpProxyAgent} - The proxy agent
2125+
*/
2126+
function getProxyAgent (endpoint, proxyUrl, proxyOptions = {}) {
2127+
if (endpoint.startsWith('https')) {
2128+
return new PatchedHttpsProxyAgent(proxyUrl, proxyOptions)
2129+
} else {
2130+
return new HttpProxyAgent(proxyUrl, proxyOptions)
2131+
}
2132+
}
2133+
21162134
module.exports = {
2135+
getProxyAgent,
21172136
getSupportedServerRuntimes,
21182137
checkOpenWhiskCredentials,
21192138
getActionEntryFile,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2025 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
const { HttpsProxyAgent } = require('https-proxy-agent')
14+
const PatchedHttpsProxyAgent = require('../src/PatchedHttpsProxyAgent')
15+
16+
jest.mock('https-proxy-agent')
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks()
20+
})
21+
22+
describe('constructor', () => {
23+
test('should call parent constructor with proxyUrl and opts', () => {
24+
const proxyUrl = 'https://proxy.example.com:8080'
25+
const req = { url: 'https://example.com' }
26+
const constructorOpts = { hostname: 'example.com', port: 443 }
27+
const connectOpts = { some: 'value' }
28+
29+
const patchedAgent = new PatchedHttpsProxyAgent(proxyUrl, constructorOpts)
30+
31+
patchedAgent.connect(req, connectOpts)
32+
expect(patchedAgent.savedOpts).toBe(constructorOpts)
33+
34+
expect(HttpsProxyAgent.prototype.connect).toHaveBeenCalledWith(req, {
35+
...constructorOpts,
36+
keepAliveInitialDelay: 1000,
37+
keepAlive: true,
38+
...connectOpts
39+
})
40+
})
41+
})

0 commit comments

Comments
 (0)