From 80fe1c1c5246763f2064c4c8e9d4ed094ef33405 Mon Sep 17 00:00:00 2001 From: Alejo Bozas <105816486+ibozas@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:26:23 +0000 Subject: [PATCH 1/2] Add NO_PROXY support for automatic proxy bypass based on target host - Add 'No Proxy List' config parameter to all three providers (NodeExecutor, FileCopier, WinRMCheck) - Centralize proxy logic in configure_proxy() function in common.py - Delegate proxy routing to requests library via HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars - Add INFO-level log messages showing connection path (DIRECTLY vs via PROXY) - Maintain backward compatibility: proxy-only config preserves legacy explicit proxy behavior - Supports exact IPs, CIDR notation, domain suffixes, hostnames, and wildcard patterns --- contents/common.py | 27 +++++++++++++++++++++++++++ contents/winrm-check.py | 12 ++++++++++++ contents/winrm-exec.py | 8 ++++++-- contents/winrm-filecopier.py | 7 +++++-- plugin.yaml | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/contents/common.py b/contents/common.py index ad98af3..46d3e2e 100644 --- a/contents/common.py +++ b/contents/common.py @@ -1,4 +1,5 @@ import re +import os def check_is_file(destination): @@ -64,3 +65,29 @@ def conditionalReplace( aMatch ) : result = '"' + result + '"' return result+' ' + + +def configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log): + from requests.utils import should_bypass_proxies + + if winrmproxy: + if winrmnoproxy: + # Delegate to requests via env vars so NO_PROXY matching works + os.environ['HTTP_PROXY'] = winrmproxy + os.environ['HTTPS_PROXY'] = winrmproxy + os.environ['NO_PROXY'] = winrmnoproxy + log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy)) + + if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): + log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) + else: + log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) + else: + # Legacy: explicit proxy for all connections + arguments["proxy"] = winrmproxy + log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) + else: + if winrmnoproxy: + log.warning("noproxy is set but no proxy configured; noproxy ignored") + log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint) + return arguments diff --git a/contents/winrm-check.py b/contents/winrm-check.py index 70a97a3..ba53b19 100644 --- a/contents/winrm-check.py +++ b/contents/winrm-check.py @@ -9,6 +9,7 @@ import colored_formatter from colored_formatter import ColoredFormatter import kerberosauth +import common #checking and importing dependencies @@ -105,6 +106,8 @@ parser.add_argument('--debug', help='debug',default="False") parser.add_argument('--certpath', help='certpath') parser.add_argument('--krb5config', help='krb5config',default="/etc/krb5.conf") +parser.add_argument('--proxy', help='proxy', default=None) +parser.add_argument('--noproxy', help='noproxy patterns', default=None) args = parser.parse_args() @@ -161,6 +164,13 @@ if args.certpath: certpath = args.certpath +winrmproxy = None +winrmnoproxy = None +if args.proxy and args.proxy not in ("None", ""): + winrmproxy = args.proxy +if args.noproxy and args.noproxy not in ("None", ""): + winrmnoproxy = args.noproxy + if not hostname: print("hostname is required") sys.exit(1) @@ -243,6 +253,8 @@ arguments["credssp_disable_tlsv1_2"] = diabletls12 +common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log) + if authentication == "kerberos": k5bConfig = kerberosauth.KerberosAuth(krb5config=krb5config, log=log, kinit_command=kinit,username=username, password=password) k5bConfig.get_ticket() diff --git a/contents/winrm-exec.py b/contents/winrm-exec.py index a70de9f..7eb3fe6 100755 --- a/contents/winrm-exec.py +++ b/contents/winrm-exec.py @@ -139,6 +139,7 @@ def httpclient_log(*args): retryconnectiondelay = 0 username = None winrmproxy = None +winrmnoproxy = None if "RD_CONFIG_AUTHTYPE" in os.environ: authentication = os.getenv("RD_CONFIG_AUTHTYPE") @@ -189,6 +190,10 @@ def httpclient_log(*args): winrmproxy = os.getenv("RD_CONFIG_WINRMPROXY") log.debug("winrmproxy: " + str(winrmproxy)) +if "RD_CONFIG_WINRMNOPROXY" in os.environ: + winrmnoproxy = os.getenv("RD_CONFIG_WINRMNOPROXY") + log.debug("winrmnoproxy: " + str(winrmnoproxy)) + if "RD_CONFIG_ENABLEDHTTPDEBUG" in os.environ: if os.getenv("RD_CONFIG_ENABLEDHTTPDEBUG") == "true": enabledHttpDebug = True @@ -311,8 +316,7 @@ def httpclient_log(*args): if(readtimeout): arguments["read_timeout_sec"] = readtimeout -if(winrmproxy): - arguments["proxy"] = winrmproxy +common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log) if(operationtimeout): arguments["operation_timeout_sec"] = operationtimeout diff --git a/contents/winrm-filecopier.py b/contents/winrm-filecopier.py index 784fe73..50c3802 100644 --- a/contents/winrm-filecopier.py +++ b/contents/winrm-filecopier.py @@ -268,6 +268,7 @@ def winrm_upload(self, certpath = None username = None winrmproxy = None +winrmnoproxy = None if os.environ.get('RD_CONFIG_OVERRIDE') == 'true': override = True @@ -299,6 +300,9 @@ def winrm_upload(self, if "RD_CONFIG_WINRMPROXY" in os.environ: winrmproxy = os.getenv("RD_CONFIG_WINRMPROXY") +if "RD_CONFIG_WINRMNOPROXY" in os.environ: + winrmnoproxy = os.getenv("RD_CONFIG_WINRMNOPROXY") + if "RD_OPTION_USERNAME" in os.environ and os.getenv("RD_OPTION_USERNAME"): #take user from job username = os.getenv("RD_OPTION_USERNAME").strip('\'') @@ -367,8 +371,7 @@ def winrm_upload(self, arguments["credssp_disable_tlsv1_2"] = diabletls12 -if(winrmproxy): - arguments["proxy"] = winrmproxy +common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log) if(readtimeout): arguments["read_timeout_sec"] = readtimeout diff --git a/plugin.yaml b/plugin.yaml index d2f5334..f8292bb 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -110,6 +110,15 @@ providers: renderingOptions: groupName: Connection instance-scope-node-attribute: "winrm-proxy" + - name: winrmnoproxy + title: No Proxy List + description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`" + type: String + required: false + scope: Instance + renderingOptions: + groupName: Connection + instance-scope-node-attribute: "winrm-noproxy" - name: operationtimeout title: operation timeout description: "maximum allowed time in seconds for any single wsman HTTP operation (default 20). Note that operation timeouts while receiving output (the only wsman operation that should take any significant time, and where these timeouts are expected) will be silently retried indefinitely. It can be overwriting at node level using `winrm-operationtimeout`" @@ -380,6 +389,15 @@ providers: renderingOptions: groupName: Connection instance-scope-node-attribute: "winrm-proxy" + - name: winrmnoproxy + title: No Proxy List + description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`" + type: String + required: false + scope: Instance + renderingOptions: + groupName: Connection + instance-scope-node-attribute: "winrm-noproxy" - name: enabledhttpdebug title: Enable HTTP logging in debug mode description: "Print extra http logging in debug mode" @@ -417,7 +435,7 @@ providers: plugin-type: script script-interpreter: ${config.interpreter} -u script-file: winrm-check.py - script-args: --username ${config.username} --hostname ${config.hostname} --password ${config.password_storage_path} --authentication ${config.authtype} --transport ${config.winrmtransport} --port ${config.winrmport} --nossl ${config.nossl} --debug ${config.debug} --certpath ${config.certpath} + script-args: --username ${config.username} --hostname ${config.hostname} --password ${config.password_storage_path} --authentication ${config.authtype} --transport ${config.winrmtransport} --port ${config.winrmport} --nossl ${config.nossl} --debug ${config.debug} --certpath ${config.certpath} --proxy ${config.winrmproxy} --noproxy ${config.winrmnoproxy} config: - name: interpreter title: Python Interpreter @@ -512,4 +530,14 @@ providers: default: "false" required: false renderingOptions: - groupName: Kerberos \ No newline at end of file + groupName: Kerberos + - name: winrmproxy + title: Proxy + description: "Specify a proxy address for communicating with Windows nodes. Example HTTP proxy strings are http://server:port and http://user:pass@server:port. An example SOCKS5 proxy string is socks5://user:pass@server:port." + type: String + required: false + - name: winrmnoproxy + title: No Proxy List + description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*)." + type: String + required: false \ No newline at end of file From 474437d2a76f7da4751d970a1b08d43a5eed5ec4 Mon Sep 17 00:00:00 2001 From: Alejo Bozas <105816486+ibozas@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:28:57 +0100 Subject: [PATCH 2/2] Address review feedback from jayas006 - Add _redact_proxy_url() helper to strip credentials from proxy URLs before logging - Wrap should_bypass_proxies import in try/except for compatibility with requests < 2.14.0 - Redact proxy URL in all debug and info log messages - Fallback to legacy explicit proxy if should_bypass_proxies is unavailable --- contents/common.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/contents/common.py b/contents/common.py index 46d3e2e..4703d6f 100644 --- a/contents/common.py +++ b/contents/common.py @@ -67,8 +67,35 @@ def conditionalReplace( aMatch ) : return result+' ' +def _redact_proxy_url(url): + """Redact userinfo from proxy URL for safe logging.""" + if not url: + return url + try: + try: + from urllib.parse import urlparse, urlunparse + except ImportError: + from urlparse import urlparse, urlunparse + parsed = urlparse(url) + if parsed.username or parsed.password: + netloc = parsed.hostname or '' + if parsed.port: + netloc += ':%s' % parsed.port + return urlunparse(parsed._replace(netloc=netloc)) + except Exception: + return '[REDACTED]' + return url + + def configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log): - from requests.utils import should_bypass_proxies + try: + from requests.utils import should_bypass_proxies + except (ImportError, AttributeError): + log.warning("requests.utils.should_bypass_proxies not available; NO_PROXY requires requests >= 2.14.0") + if winrmproxy: + arguments["proxy"] = winrmproxy + log.info("Connecting to %s via PROXY (%s)" % (endpoint, _redact_proxy_url(winrmproxy))) + return arguments if winrmproxy: if winrmnoproxy: @@ -76,16 +103,16 @@ def configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log): os.environ['HTTP_PROXY'] = winrmproxy os.environ['HTTPS_PROXY'] = winrmproxy os.environ['NO_PROXY'] = winrmnoproxy - log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy)) + log.debug("Proxy via env vars: HTTP(S)_PROXY set, NO_PROXY=%s" % winrmnoproxy) if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy): log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy)) else: - log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) + log.info("Connecting to %s via PROXY (%s)" % (endpoint, _redact_proxy_url(winrmproxy))) else: # Legacy: explicit proxy for all connections arguments["proxy"] = winrmproxy - log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy)) + log.info("Connecting to %s via PROXY (%s)" % (endpoint, _redact_proxy_url(winrmproxy))) else: if winrmnoproxy: log.warning("noproxy is set but no proxy configured; noproxy ignored")