train-winrm 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9ef423ad3bbdda0bb8bef459e8a124e0cc40d53b33d7a288fcf76e4a62c134c
4
- data.tar.gz: b0c1dd4c13379e8e3c2ff3ebb6d18e3e8cdd43ac2f0a3bbe4bbe4abc8477cc5c
3
+ metadata.gz: 5b04c6c445681047b4681b59960762a6a4493340596dad7285f5f2f4ea1639e5
4
+ data.tar.gz: 77cddd410ec5e3053c727674c7338432479bc0b315ffc97ddaace31711435c06
5
5
  SHA512:
6
- metadata.gz: c77123919b5639f935f9c4a7944ba423e63fecb73fc4028fa29ae879ecb80d921406076a96f0ccb7b4ec15aeed95aed622ef9a15dc6f0b214533eddd0151eb34
7
- data.tar.gz: 79693ec60c636a3b705b463a623484b1b27b5cc0c8df19fb8991f02a9a5d86c65fa8b7a5fb9361368e4d3518360c4f20075f29b817de928c93c0816d63a351d7
6
+ metadata.gz: c1edf24433ce19b6e0a778a224b9a882e70e31a07f108d5a9c5d6224ef2b6f4d736502844fbd384d00a88dbc8a97c2dec90885f84804e230e25b5ffff311d4fc
7
+ data.tar.gz: 0c2127482ba6cd7747871b53c41c3b72b2ed20abe29968e6675bf1e9ff53967b3ae2745a745d2d4349183062b5caaeccd0d3bb16a1fa70b157d56b9e408181d0
@@ -31,6 +31,8 @@
31
31
  # * marshalling to / from JSON
32
32
  # You don't have to worry about most of this.
33
33
 
34
+ # frozen_string_literal: true
35
+
34
36
  require "train"
35
37
  require "train/plugins"
36
38
  # This module may need to directly require WinRM to reference its exception classes
@@ -50,6 +52,9 @@ module TrainPlugins
50
52
  @max_wait_until_ready = @options.delete(:max_wait_until_ready)
51
53
  @operation_timeout = @options.delete(:operation_timeout)
52
54
  @shell_type = @options.delete(:winrm_shell_type)
55
+
56
+ # SOCKS proxy patch for HTTPClient
57
+ apply_socks_proxy_patch if @options[:socks_proxy]
53
58
  end
54
59
 
55
60
  # (see Base::Connection#close)
@@ -104,7 +109,7 @@ module TrainPlugins
104
109
 
105
110
  private
106
111
 
107
- PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
112
+ PING_COMMAND = "Write-Host '[WinRM] Established\n'"
108
113
 
109
114
  def file_via_connection(path)
110
115
  Train::File::Remote::Windows.new(self, path)
@@ -114,7 +119,7 @@ module TrainPlugins
114
119
  return if command.nil?
115
120
 
116
121
  logger.debug("[WinRM] #{self} (#{command})")
117
- out = ""
122
+ out = +""
118
123
  response = nil
119
124
  timeout = opts[:timeout]&.to_i
120
125
 
@@ -234,6 +239,16 @@ module TrainPlugins
234
239
  end
235
240
  end
236
241
 
242
+ def apply_socks_proxy_patch
243
+ require_relative "socks_proxy_patch"
244
+
245
+ ::SocksProxyPatch.apply(
246
+ socks_proxy: @options[:socks_proxy],
247
+ socks_user: @options[:socks_user],
248
+ socks_password: @options[:socks_password]
249
+ )
250
+ end
251
+
237
252
  # String representation of object, reporting its connection details and
238
253
  # configuration.
239
254
  #
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Patches the `HTTPClient` and `TCPSocket` classes to use a SOCKS5H proxy.
4
+ # This class modifies the behavior of `HTTPClient` to route HTTP requests
5
+ # through a SOCKS proxy by overriding the socket creation method.
6
+ # It also configures `TCPSocket` to use the specified SOCKS proxy server,
7
+ # and validates that the provided proxy is correct, resolvable, and accessible.
8
+
9
+ require "socksify"
10
+ require "httpclient"
11
+ require "socket" unless defined?(Socket)
12
+
13
+ class SocksProxyPatch
14
+ # Applies the SOCKS proxy settings to `HTTPClient` and `TCPSocket`.
15
+ #
16
+ # @param socks_proxy [String] The SOCKS proxy address in the format `host:port`.
17
+ # @param socks_user [String, nil] Optional SOCKS proxy username.
18
+ # @param socks_password [String, nil] Optional SOCKS proxy password.
19
+ # @example
20
+ # SocksProxyPatch.apply(socks_proxy: "127.0.0.1:1080", socks_user: "user", socks_password: "pass")
21
+ def self.apply(socks_proxy:, socks_user: nil, socks_password: nil)
22
+ new(socks_proxy: socks_proxy, socks_user: socks_user, socks_password: socks_password).apply
23
+ end
24
+
25
+ def initialize(socks_proxy:, socks_user:, socks_password:)
26
+ @socks_proxy = socks_proxy
27
+ @socks_user = socks_user
28
+ @socks_password = socks_password
29
+ end
30
+
31
+ def apply
32
+ @proxy_host, @proxy_port = parse_and_validate_proxy(@socks_proxy)
33
+ configure_socks
34
+ patch_http_client
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :proxy_host, :proxy_port, :socks_user, :socks_password
40
+
41
+ # Parses the proxy string and validates its format, DNS resolution, and port.
42
+ def parse_and_validate_proxy(socks_proxy)
43
+ proxy_host, proxy_port = socks_proxy.split(":")
44
+
45
+ unless proxy_host && proxy_port
46
+ raise Train::ClientError, "Invalid SOCKS proxy format: '#{socks_proxy}'. Expected format is 'host:port'."
47
+ end
48
+
49
+ port = validate_port(proxy_port)
50
+ validate_dns_resolution(proxy_host)
51
+
52
+ [proxy_host, port]
53
+ end
54
+
55
+ # Ensures port is a valid integer in range.
56
+ def validate_port(port_str)
57
+ port = Integer(port_str)
58
+ unless port.between?(1, 65_535)
59
+ raise Train::ClientError, "SOCKS proxy port '#{port}' is out of valid range (1-65535)."
60
+ end
61
+
62
+ port
63
+ rescue ArgumentError
64
+ raise Train::ClientError, "Invalid SOCKS proxy port '#{port_str}'. Port must be an integer."
65
+ end
66
+
67
+ # Checks if the hostname is resolvable.
68
+ def validate_dns_resolution(host)
69
+ Socket.getaddrinfo(host, nil)
70
+ rescue SocketError => e
71
+ raise Train::ClientError, "DNS resolution failed for SOCKS proxy host '#{host}': #{e.message}"
72
+ end
73
+
74
+ # Applies the validated proxy settings to TCPSocket.
75
+ def configure_socks
76
+ TCPSocket.socks_username = socks_user if socks_user
77
+ TCPSocket.socks_password = socks_password if socks_password
78
+ TCPSocket.socks_server = proxy_host
79
+ TCPSocket.socks_port = proxy_port
80
+ end
81
+
82
+ # Patches HTTPClient to route all connections through the SOCKS proxy.
83
+ def patch_http_client
84
+ HTTPClient::Session.class_eval do
85
+ unless method_defined?(:original_create_socket)
86
+ alias_method :original_create_socket, :create_socket
87
+
88
+ # Override the `create_socket` method to use `TCPSocket` with SOCKS proxy.
89
+ def create_socket(host, port, *args)
90
+ socket = ::TCPSocket.new(host, port)
91
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [5, 0].pack("l_2"))
92
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, [5, 0].pack("l_2"))
93
+ socket
94
+ rescue HTTPClient::ConnectTimeoutError
95
+ raise Train::ClientError, "SOCKS proxy connection to #{host}:#{port} timed out (HTTPClient::ConnectTimeoutError)"
96
+ rescue Errno::ETIMEDOUT
97
+ raise Train::ClientError, "Connection to #{host}:#{port} timed out through SOCKS proxy."
98
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
99
+ raise Train::ClientError, "Network unreachable when connecting to #{host}:#{port} via SOCKS proxy."
100
+ rescue Errno::ECONNREFUSED
101
+ raise Train::ClientError, "Connection refused by SOCKS proxy when connecting to #{host}:#{port}."
102
+ rescue SocketError => e
103
+ raise Train::ClientError, "Socket error while connecting to #{host}:#{port} via SOCKS proxy: #{e.message}"
104
+ rescue StandardError => e
105
+ raise Train::ClientError, "Unexpected error during SOCKS proxy connection: #{e.class} - #{e.message}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -79,6 +79,11 @@ module TrainPlugins
79
79
  option :client_key , default: nil
80
80
  option :client_key_pass , default: nil
81
81
 
82
+ # new socks proxy options
83
+ option :socks_proxy, default: nil
84
+ option :socks_user, default: nil
85
+ option :socks_password, default: nil
86
+
82
87
  def initialize(opts)
83
88
  super(opts)
84
89
  load_needed_dependencies!
@@ -102,24 +107,40 @@ module TrainPlugins
102
107
  def validate_options(opts)
103
108
  super(opts)
104
109
 
105
- # set scheme and port based on ssl activation
106
- scheme = opts[:ssl] ? "https" : "http"
107
- port = opts[:port]
108
- port = (opts[:ssl] ? 5986 : 5985) if port.nil?
109
- winrm_transport = opts[:winrm_transport].to_sym
110
+ # Normalize to symbols
111
+ opts[:winrm_transport] = opts[:winrm_transport].to_s.downcase.to_sym if opts[:winrm_transport]
112
+ opts[:winrm_shell_type] = opts[:winrm_shell_type].to_s.downcase.to_sym if opts[:winrm_shell_type]
113
+
114
+ winrm_transport = opts[:winrm_transport]
115
+ winrm_shell_type = opts[:winrm_shell_type]
116
+
110
117
  unless SUPPORTED_WINRM_TRANSPORTS.include?(winrm_transport)
111
118
  raise Train::ClientError, "Unsupported transport type: #{winrm_transport.inspect}"
112
119
  end
113
120
 
114
- winrm_shell_type = opts[:winrm_shell_type].to_sym
115
121
  unless SUPPORTED_WINRM_SHELL_TYPES.include?(winrm_shell_type)
116
122
  raise Train::ClientError, "Unsupported winrm shell type: #{winrm_shell_type.inspect}"
117
123
  end
118
124
 
119
- # remove leading '/'
125
+ # Set scheme, port, endpoint
126
+ scheme = opts[:ssl] ? "https" : "http"
127
+ port = opts[:port] || (opts[:ssl] ? 5986 : 5985)
120
128
  path = (opts[:path] || "").sub(%r{^/+}, "")
121
129
 
122
130
  opts[:endpoint] = "#{scheme}://#{opts[:host]}:#{port}/#{path}"
131
+
132
+ # Auto-detect realm when not provided
133
+ if winrm_transport == :kerberos && opts[:kerberos_realm].nil?
134
+ begin
135
+ krb_realm = File.read("/etc/krb5.conf")[/default_realm\s*=\s*(\S+)/i, 1]
136
+ if krb_realm
137
+ opts[:kerberos_realm] = krb_realm
138
+ logger.debug("Kerberos realm auto-detected: #{krb_realm}")
139
+ end
140
+ rescue => e
141
+ logger.warn("Could not auto-detect Kerberos realm: #{e.message}")
142
+ end
143
+ end
123
144
  end
124
145
 
125
146
  WINRM_FS_SPEC_VERSION = ">= 1.3.7".freeze
@@ -134,7 +155,7 @@ module TrainPlugins
134
155
  def connection_options(opts)
135
156
  {
136
157
  logger: logger,
137
- transport: opts[:winrm_transport].to_sym,
158
+ transport: opts[:winrm_transport],
138
159
  disable_sspi: opts[:winrm_disable_sspi],
139
160
  basic_auth_only: opts[:winrm_basic_auth_only],
140
161
  hostname: opts[:host],
@@ -154,6 +175,9 @@ module TrainPlugins
154
175
  client_cert: opts[:client_cert],
155
176
  client_key: opts[:client_key],
156
177
  key_pass: opts[:client_key_pass],
178
+ socks_proxy: opts[:socks_proxy],
179
+ socks_user: opts[:socks_user],
180
+ socks_password: opts[:socks_password],
157
181
  }
158
182
  end
159
183
 
@@ -5,6 +5,6 @@
5
5
 
6
6
  module TrainPlugins
7
7
  module WinRM
8
- VERSION = "0.3.1".freeze
8
+ VERSION = "0.4.0".freeze
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: train-winrm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef InSpec Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-06 00:00:00.000000000 Z
11
+ date: 2025-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chef-winrm
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.4'
19
+ version: 2.4.4
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.4'
26
+ version: 2.4.4
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: chef-winrm-elevated
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.4.0
47
+ version: 1.4.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.4.0
54
+ version: 1.4.1
55
55
  - !ruby/object:Gem::Dependency
56
- name: syslog
56
+ name: socksify
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.1'
61
+ version: '1.8'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.1'
68
+ version: '1.8'
69
69
  description: Allows applictaions using Train to speak to Windows using Remote Management;
70
70
  handles authentication, cacheing, and SDK dependency management.
71
71
  email:
@@ -77,6 +77,7 @@ files:
77
77
  - LICENSE
78
78
  - lib/train-winrm.rb
79
79
  - lib/train-winrm/connection.rb
80
+ - lib/train-winrm/socks_proxy_patch.rb
80
81
  - lib/train-winrm/transport.rb
81
82
  - lib/train-winrm/version.rb
82
83
  homepage: https://github.com/inspec/train-winrm