Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
"folder-hash": "^4.0.4",
"fs-extra": "^11.3.0",
"globby": "^11.0.1",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^4.1.0",
"lodash.clonedeep": "^4.5.0",
"openwhisk": "^3.21.8",
"openwhisk": "3.21.8",
"openwhisk-fqn": "0.0.2",
"proxy-from-env": "^1.1.0",
"sha1": "^1.1.1",
Expand Down
37 changes: 37 additions & 0 deletions src/PatchedHttpsProxyAgent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const { HttpsProxyAgent } = require('https-proxy-agent')

/**
* HttpsProxyAgent needs a patch for TLS connections.
* It doesn't pass in the original options during a SSL connect.
*
* See https://github.com/TooTallNate/proxy-agents/issues/89
* @private
*/
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
constructor (proxyUrl, opts) {
super(proxyUrl, opts)
this.savedOpts = opts
}

async connect (req, opts) {
return super.connect(req, {
...this.savedOpts,
keepAliveInitialDelay: 1000,
keepAlive: true,
...opts
})
}
}

module.exports = PatchedHttpsProxyAgent
18 changes: 16 additions & 2 deletions src/RuntimeAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const deepCopy = require('lodash.clonedeep')
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-runtime:RuntimeAPI', { provider: 'debug', level: process.env.LOG_LEVEL })
const LogForwarding = require('./LogForwarding')
const LogForwardingLocalDestinationsProvider = require('./LogForwardingLocalDestinationsProvider')
const { patchOWForTunnelingIssue } = require('./openwhisk-patch')
const { getProxyAgent } = require('./utils')

require('./types.jsdoc') // for VS Code autocomplete
/* global OpenwhiskOptions, OpenwhiskClient */ // for linter
Expand All @@ -35,8 +37,14 @@ class RuntimeAPI {
*/
async init (options) {
aioLogger.debug(`init options: ${JSON.stringify(options, null, 2)}`)

const clonedOptions = deepCopy(options)

clonedOptions.use_proxy_from_env_var = false // default, unless env var is set
if (process.env.NEEDLE_USE_PROXY_FROM_ENV_VAR === 'true') { // legacy support
clonedOptions.use_proxy_from_env_var = true
}

const initErrors = []
if (!clonedOptions || !clonedOptions.api_key) {
initErrors.push('api_key')
Expand All @@ -53,7 +61,13 @@ class RuntimeAPI {
const proxyUrl = getProxyForUrl(clonedOptions.apihost)
if (proxyUrl) {
aioLogger.debug(`using proxy url: ${proxyUrl}`)
clonedOptions.proxy = proxyUrl
if (clonedOptions.use_proxy_from_env_var !== false) {
clonedOptions.proxy = proxyUrl
clonedOptions.agent = null
} else {
clonedOptions.proxy = null
clonedOptions.agent = getProxyAgent(clonedOptions.apihost, proxyUrl)
}
} else {
aioLogger.debug('proxy settings not found')
}
Expand All @@ -67,7 +81,7 @@ class RuntimeAPI {
const shouldIgnoreCerts = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0'
clonedOptions.ignore_certs = clonedOptions.ignore_certs || shouldIgnoreCerts

this.ow = ow(clonedOptions)
this.ow = patchOWForTunnelingIssue(ow(clonedOptions), clonedOptions.use_proxy_from_env_var)
const self = this

return {
Expand Down
52 changes: 52 additions & 0 deletions src/openwhisk-patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable no-param-reassign */
/* eslint-disable camelcase */
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

/**
* This patches the Openwhisk client to handle a tunneling issue with openwhisk > v3.0.0
* See https://github.com/tomas/needle/issues/406
*
* Once openwhisk.js supports the use_proxy_from_env_var option (for needle), we can remove this patch.
*
* @param {object} ow the Openwhisk object to patch
* @param {boolean} use_proxy_from_env_var the needle option to add
* @returns {object} the patched openwhisk object
*/
function patchOWForTunnelingIssue (ow, use_proxy_from_env_var) {
// we must set proxy to null here if agent is set, since it was already
// internally initialzed in Openwhisk with the proxy url from env vars
const agentIsSet = ow.actions.client.options.agent !== null
if (agentIsSet && use_proxy_from_env_var === false) {
ow.actions.client.options.proxy = undefined
}

// The issue is patching openwhisk.js to use use_proxy_from_env_var (a needle option) - the contribution process might take too long.
// monkey-patch client.params: patch one, all the rest should be patched (shared client)
// we wrap the original params to add the use_proxy_from_env_var boolean
const originalParams = ow.actions.client.params.bind(ow.actions.client)
ow.actions.client.params = function (...args) {
return originalParams(...args).then(params => {
params.use_proxy_from_env_var = use_proxy_from_env_var
return params
}).catch(err => {
console.error('Error patching openwhisk client params: ', err)
throw err
})
}

return ow
}

module.exports = {
patchOWForTunnelingIssue
}
19 changes: 19 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const path = require('path')
const archiver = require('archiver')
// this is a static list that comes from here: https://developer.adobe.com/runtime/docs/guides/reference/runtimes/
const SupportedRuntimes = ['sequence', 'blackbox', 'nodejs:10', 'nodejs:12', 'nodejs:14', 'nodejs:16', 'nodejs:18', 'nodejs:20', 'nodejs:22']
const { HttpProxyAgent } = require('http-proxy-agent')
const PatchedHttpsProxyAgent = require('./PatchedHttpsProxyAgent.js')

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

/**
* Get the proxy agent for the given endpoint
*
* @param {string} endpoint - The endpoint to get the proxy agent for
* @param {string} proxyUrl - The proxy URL to use
* @param {object} proxyOptions - The proxy options to use
* @returns {PatchedHttpsProxyAgent | HttpProxyAgent} - The proxy agent
*/
function getProxyAgent (endpoint, proxyUrl, proxyOptions = {}) {
if (endpoint.startsWith('https')) {
return new PatchedHttpsProxyAgent(proxyUrl, proxyOptions)
} else {
return new HttpProxyAgent(proxyUrl, proxyOptions)
}
}

module.exports = {
getProxyAgent,
getSupportedServerRuntimes,
checkOpenWhiskCredentials,
getActionEntryFile,
Expand Down
41 changes: 41 additions & 0 deletions test/PatchedHttpsProxyAgent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const { HttpsProxyAgent } = require('https-proxy-agent')
const PatchedHttpsProxyAgent = require('../src/PatchedHttpsProxyAgent')

jest.mock('https-proxy-agent')

beforeEach(() => {
jest.clearAllMocks()
})

describe('constructor', () => {
test('should call parent constructor with proxyUrl and opts', () => {
const proxyUrl = 'https://proxy.example.com:8080'
const req = { url: 'https://example.com' }
const constructorOpts = { hostname: 'example.com', port: 443 }
const connectOpts = { some: 'value' }

const patchedAgent = new PatchedHttpsProxyAgent(proxyUrl, constructorOpts)

patchedAgent.connect(req, connectOpts)
expect(patchedAgent.savedOpts).toBe(constructorOpts)

expect(HttpsProxyAgent.prototype.connect).toHaveBeenCalledWith(req, {
...constructorOpts,
keepAliveInitialDelay: 1000,
keepAlive: true,
...connectOpts
})
})
})
Loading