mixlib-shellout 2.4.4-universal-mingw32 → 3.0.4-universal-mingw32

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: b4709592fd85b38ce2040327ad6be689c349b58b563eb0327ee3038112e0c4f6
4
- data.tar.gz: 5c7520acca04f64210fdbd2b660a216ca2d2b432607dcdedc0985bca9ab69b7e
3
+ metadata.gz: 68567052a253b4c6bba0f3ff4c884402a41dd3175a00a3a4cef1d9fab8ca753a
4
+ data.tar.gz: ca9b2511628cb6655397abf24ea39a9d49a7c72f5550708e1daa2fe553328e91
5
5
  SHA512:
6
- metadata.gz: b98c77f78e9ffe650bb8e108a62db5ac4867b1b6770e0837c362394e789ecda94059a15b258c2a400a456d0581703145c75cc0937d036541a63e05a69d9d092f
7
- data.tar.gz: f0872fa824d2caad075e197fcf9f54963745e5b2f38e9c58341ccc03097358a0e3337760ba0a9b7756b43944559d09cbc31f42fb95ac526dc84c22190c4321bd
6
+ metadata.gz: 215b9612e24919b285530b202cb3e62ab27436f82fb5cb95efc0bccfc4538794800c57c88a66cfab978479c3718d5b9d5fa40de43833fd05c61a8a769831c571
7
+ data.tar.gz: 40458ab0286b067c4811be58fa4ad4c93568f25c5ca036137023690588c0b65e84f2f8dd034e0b0dc1f7070113305a6a262242207cab0eb8f9c53b38ffa125fe
@@ -1,5 +1,5 @@
1
1
  module Mixlib
2
2
  class ShellOut
3
- VERSION = "2.4.4".freeze
3
+ VERSION = "3.0.4".freeze
4
4
  end
5
5
  end
@@ -2,7 +2,7 @@
2
2
  # Author:: Daniel DeLeo (<dan@chef.io>)
3
3
  # Author:: John Keiser (<jkeiser@chef.io>)
4
4
  # Author:: Ho-Sheng Hsiao (<hosh@chef.io>)
5
- # Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
5
+ # Copyright:: Copyright (c) 2011-2019, Chef Software Inc.
6
6
  # License:: Apache License, Version 2.0
7
7
  #
8
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -66,7 +66,7 @@ module Mixlib
66
66
  #
67
67
  # Set cwd, environment, appname, etc.
68
68
  #
69
- app_name, command_line = command_to_run(command)
69
+ app_name, command_line = command_to_run(combine_args(*command))
70
70
  create_process_args = {
71
71
  app_name: app_name,
72
72
  command_line: command_line,
@@ -88,7 +88,7 @@ module Mixlib
88
88
  #
89
89
  # Start the process
90
90
  #
91
- process = Process.create(create_process_args)
91
+ process, profile, token = Process.create(create_process_args)
92
92
  logger.debug(format_process(process, app_name, command_line, timeout)) if logger
93
93
  begin
94
94
  # Start pushing data into input
@@ -143,6 +143,8 @@ module Mixlib
143
143
  ensure
144
144
  CloseHandle(process.thread_handle) if process.thread_handle
145
145
  CloseHandle(process.process_handle) if process.process_handle
146
+ Process.unload_user_profile(token, profile) if profile
147
+ CloseHandle(token) if token
146
148
  end
147
149
 
148
150
  ensure
@@ -196,6 +198,46 @@ module Mixlib
196
198
  true
197
199
  end
198
200
 
201
+ # Use to support array passing semantics on windows
202
+ #
203
+ # 1. strings with whitespace or quotes in them need quotes around them.
204
+ # 2. interior quotes need to get backslash escaped (parser needs to know when it really ends).
205
+ # 3. random backlsashes in paths themselves remain untouched.
206
+ # 4. if the argument must be quoted by #1 and terminates in a sequence of backslashes then all the backlashes must themselves
207
+ # be backslash excaped (double the backslashes).
208
+ # 5. if an interior quote that must be escaped by #2 has a sequence of backslashes before it then all the backslashes must
209
+ # themselves be backslash excaped along with the backslash ecape of the interior quote (double plus one backslashes).
210
+ #
211
+ # And to restate. We are constructing a string which will be parsed by the windows parser into arguments, and we want those
212
+ # arguments to match the *args array we are passed here. So call the windows parser operation A then we need to apply A^-1 to
213
+ # our args to construct the string so that applying A gives windows back our *args.
214
+ #
215
+ # And when the windows parser sees a series of backslashes followed by a double quote, it has to determine if that double quote
216
+ # is terminating or not, and how many backslashes to insert in the args. So what it does is divide it by two (rounding down) to
217
+ # get the number of backslashes to insert. Then if it is even the double quotes terminate the argument. If it is even the
218
+ # double quotes are interior double quotes (the extra backslash quotes the double quote).
219
+ #
220
+ # We construct the inverse operation so interior double quotes preceeded by N backslashes get 2N+1 backslashes in front of the quote,
221
+ # while trailing N backslashes get 2N backslashes in front of the quote that terminates the argument.
222
+ #
223
+ # see: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
224
+ #
225
+ # @api private
226
+ # @param args [Array<String>] array of command arguments
227
+ # @return String
228
+ def combine_args(*args)
229
+ return args[0] if args.length == 1
230
+ args.map do |arg|
231
+ if arg =~ /[ \t\n\v"]/
232
+ arg = arg.gsub(/(\\*)"/, '\1\1\"') # interior quotes with N preceeding backslashes need 2N+1 backslashes
233
+ arg = arg.sub(/(\\+)$/, '\1\1') # trailing N backslashes need to become 2N backslashes
234
+ "\"#{arg}\""
235
+ else
236
+ arg
237
+ end
238
+ end.join(" ")
239
+ end
240
+
199
241
  def command_to_run(command)
200
242
  return run_under_cmd(command) if should_run_under_cmd?(command)
201
243
 
@@ -36,10 +36,48 @@ module Process::Constants
36
36
 
37
37
  ERROR_PRIVILEGE_NOT_HELD = 1314
38
38
  ERROR_LOGON_TYPE_NOT_GRANTED = 0x569
39
+
40
+ # Only documented in Userenv.h ???
41
+ # - ZERO (type Local) is assumed, no docs found
42
+ WIN32_PROFILETYPE_LOCAL = 0x00
43
+ WIN32_PROFILETYPE_PT_TEMPORARY = 0x01
44
+ WIN32_PROFILETYPE_PT_ROAMING = 0x02
45
+ WIN32_PROFILETYPE_PT_MANDATORY = 0x04
46
+ WIN32_PROFILETYPE_PT_ROAMING_PREEXISTING = 0x08
47
+
48
+ end
49
+
50
+ # Structs required for data handling
51
+ module Process::Structs
52
+
53
+ class PROFILEINFO < FFI::Struct
54
+ layout(
55
+ :dwSize, :dword,
56
+ :dwFlags, :dword,
57
+ :lpUserName, :pointer,
58
+ :lpProfilePath, :pointer,
59
+ :lpDefaultPath, :pointer,
60
+ :lpServerName, :pointer,
61
+ :lpPolicyPath, :pointer,
62
+ :hProfile, :handle
63
+ )
64
+ end
65
+
39
66
  end
40
67
 
41
68
  # Define the functions needed to check with Service windows station
42
69
  module Process::Functions
70
+ ffi_lib :userenv
71
+
72
+ attach_pfunc :GetProfileType,
73
+ [:pointer], :bool
74
+
75
+ attach_pfunc :LoadUserProfileW,
76
+ [:handle, :pointer], :bool
77
+
78
+ attach_pfunc :UnloadUserProfile,
79
+ [:handle, :handle], :bool
80
+
43
81
  ffi_lib :advapi32
44
82
 
45
83
  attach_pfunc :LogonUserW,
@@ -64,9 +102,12 @@ end
64
102
  # as of 2015-10-15 from commit cc066e5df25048f9806a610f54bf5f7f253e86f7
65
103
  module Process
66
104
 
105
+ class UnsupportedFeature < StandardError; end
106
+
67
107
  # Explicitly reopen singleton class so that class/constant declarations from
68
108
  # extensions are visible in Modules.nesting.
69
109
  class << self
110
+
70
111
  def create(args)
71
112
  unless args.kind_of?(Hash)
72
113
  raise TypeError, "hash keyword arguments expected"
@@ -85,9 +126,9 @@ module Process
85
126
 
86
127
  # Set default values
87
128
  hash = {
88
- "app_name" => nil,
129
+ "app_name" => nil,
89
130
  "creation_flags" => 0,
90
- "close_handles" => true,
131
+ "close_handles" => true,
91
132
  }
92
133
 
93
134
  # Validate the keys, and convert symbols and case to lowercase strings.
@@ -238,6 +279,7 @@ module Process
238
279
  inherit = hash["inherit"] ? 1 : 0
239
280
 
240
281
  if hash["with_logon"]
282
+
241
283
  logon, passwd, domain = format_creds_from_hash(hash)
242
284
 
243
285
  hash["creation_flags"] |= CREATE_UNICODE_ENVIRONMENT
@@ -255,51 +297,42 @@ module Process
255
297
  # can simulate running a command 'elevated' by running it under a separate
256
298
  # logon as a batch process.
257
299
  if hash["elevated"] || winsta_name =~ /^Service-0x0-.*$/i
258
- logon_type = if hash["elevated"]
259
- LOGON32_LOGON_BATCH
260
- else
261
- LOGON32_LOGON_INTERACTIVE
262
- end
263
300
 
264
- token = logon_user(logon, domain, passwd, logon_type)
301
+ logon_type = hash["elevated"] ? LOGON32_LOGON_BATCH : LOGON32_LOGON_INTERACTIVE
302
+ token = logon_user(logon, domain, passwd, logon_type)
303
+ logon_ptr = FFI::MemoryPointer.from_string(logon)
304
+ profile = PROFILEINFO.new.tap do |dat|
305
+ dat[:dwSize] = dat.size
306
+ dat[:dwFlags] = 1
307
+ dat[:lpUserName] = logon_ptr
308
+ end
309
+
310
+ if logon_has_roaming_profile?
311
+ msg = %w{
312
+ Mixlib does not currently support executing commands as users
313
+ configured with Roaming Profiles. [%s]
314
+ }.join(" ") % logon.encode("UTF-8").unpack("A*")
315
+ raise UnsupportedFeature.new(msg)
316
+ end
317
+
318
+ load_user_profile(token, profile.pointer)
319
+
320
+ create_process_as_user(token, app, cmd, process_security,
321
+ thread_security, inherit, hash["creation_flags"], env,
322
+ cwd, startinfo, procinfo)
265
323
 
266
- create_process_as_user(token, app, cmd, process_security, thread_security, inherit, hash["creation_flags"], env, cwd, startinfo, procinfo)
267
324
  else
268
- bool = CreateProcessWithLogonW(
269
- logon, # User
270
- domain, # Domain
271
- passwd, # Password
272
- LOGON_WITH_PROFILE, # Logon flags
273
- app, # App name
274
- cmd, # Command line
275
- hash["creation_flags"], # Creation flags
276
- env, # Environment
277
- cwd, # Working directory
278
- startinfo, # Startup Info
279
- procinfo # Process Info
280
- )
281
325
 
282
- unless bool
283
- raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
284
- end
326
+ create_process_with_logon(logon, domain, passwd, LOGON_WITH_PROFILE,
327
+ app, cmd, hash["creation_flags"], env, cwd, startinfo, procinfo)
328
+
285
329
  end
330
+
286
331
  else
287
- bool = CreateProcessW(
288
- app, # App name
289
- cmd, # Command line
290
- process_security, # Process attributes
291
- thread_security, # Thread attributes
292
- inherit, # Inherit handles?
293
- hash["creation_flags"], # Creation flags
294
- env, # Environment
295
- cwd, # Working directory
296
- startinfo, # Startup Info
297
- procinfo # Process Info
298
- )
299
-
300
- unless bool
301
- raise SystemCallError.new("CreateProcessW", FFI.errno)
302
- end
332
+
333
+ create_process(app, cmd, process_security, thread_security, inherit,
334
+ hash["creation_flags"], env, cwd, startinfo, procinfo)
335
+
303
336
  end
304
337
 
305
338
  # Automatically close the process and thread handles in the
@@ -314,12 +347,120 @@ module Process
314
347
  procinfo[:hThread] = 0
315
348
  end
316
349
 
317
- ProcessInfo.new(
350
+ process = ProcessInfo.new(
318
351
  procinfo[:hProcess],
319
352
  procinfo[:hThread],
320
353
  procinfo[:dwProcessId],
321
354
  procinfo[:dwThreadId]
322
355
  )
356
+
357
+ [ process, profile, token ]
358
+ end
359
+
360
+ # See Process::Constants::WIN32_PROFILETYPE
361
+ def logon_has_roaming_profile?
362
+ get_profile_type >= 2
363
+ end
364
+
365
+ def get_profile_type
366
+ ptr = FFI::MemoryPointer.new(:uint)
367
+ unless GetProfileType(ptr)
368
+ raise SystemCallError.new("GetProfileType", FFI.errno)
369
+ end
370
+ ptr.read_uint
371
+ end
372
+
373
+ def load_user_profile(token, profile_ptr)
374
+ unless LoadUserProfileW(token, profile_ptr)
375
+ raise SystemCallError.new("LoadUserProfileW", FFI.errno)
376
+ end
377
+ true
378
+ end
379
+
380
+ def unload_user_profile(token, profile)
381
+ if profile[:hProfile] == 0
382
+ warn "\n\nWARNING: Profile not loaded\n"
383
+ else
384
+ unless UnloadUserProfile(token, profile[:hProfile])
385
+ raise SystemCallError.new("UnloadUserProfile", FFI.errno)
386
+ end
387
+ end
388
+ true
389
+ end
390
+
391
+ def create_process_as_user(token, app, cmd, process_security,
392
+ thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
393
+
394
+ bool = CreateProcessAsUserW(
395
+ token, # User token handle
396
+ app, # App name
397
+ cmd, # Command line
398
+ process_security, # Process attributes
399
+ thread_security, # Thread attributes
400
+ inherit, # Inherit handles
401
+ creation_flags, # Creation Flags
402
+ env, # Environment
403
+ cwd, # Working directory
404
+ startinfo, # Startup Info
405
+ procinfo # Process Info
406
+ )
407
+
408
+ unless bool
409
+ msg = case FFI.errno
410
+ when ERROR_PRIVILEGE_NOT_HELD
411
+ [
412
+ %{CreateProcessAsUserW (User '%s' must hold the 'Replace a process},
413
+ %{level token' and 'Adjust Memory Quotas for a process' permissions.},
414
+ %{Logoff the user after adding this right to make it effective.)},
415
+ ].join(" ") % ::ENV["USERNAME"]
416
+ else
417
+ "CreateProcessAsUserW failed."
418
+ end
419
+ raise SystemCallError.new(msg, FFI.errno)
420
+ end
421
+ end
422
+
423
+ def create_process_with_logon(logon, domain, passwd, logon_flags, app, cmd,
424
+ creation_flags, env, cwd, startinfo, procinfo)
425
+
426
+ bool = CreateProcessWithLogonW(
427
+ logon, # User
428
+ domain, # Domain
429
+ passwd, # Password
430
+ logon_flags, # Logon flags
431
+ app, # App name
432
+ cmd, # Command line
433
+ creation_flags, # Creation flags
434
+ env, # Environment
435
+ cwd, # Working directory
436
+ startinfo, # Startup Info
437
+ procinfo # Process Info
438
+ )
439
+
440
+ unless bool
441
+ raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
442
+ end
443
+ end
444
+
445
+ def create_process(app, cmd, process_security, thread_security, inherit,
446
+ creation_flags, env, cwd, startinfo, procinfo)
447
+
448
+ bool = CreateProcessW(
449
+ app, # App name
450
+ cmd, # Command line
451
+ process_security, # Process attributes
452
+ thread_security, # Thread attributes
453
+ inherit, # Inherit handles?
454
+ creation_flags, # Creation flags
455
+ env, # Environment
456
+ cwd, # Working directory
457
+ startinfo, # Startup Info
458
+ procinfo # Process Info
459
+ )
460
+
461
+ unless bool
462
+ raise SystemCallError.new("CreateProcessW", FFI.errno)
463
+ end
323
464
  end
324
465
 
325
466
  def logon_user(user, domain, passwd, type, provider = LOGON32_PROVIDER_DEFAULT)
@@ -346,32 +487,6 @@ module Process
346
487
  token.read_ulong
347
488
  end
348
489
 
349
- def create_process_as_user(token, app, cmd, process_security, thread_security, inherit, creation_flags, env, cwd, startinfo, procinfo)
350
- bool = CreateProcessAsUserW(
351
- token, # User token handle
352
- app, # App name
353
- cmd, # Command line
354
- process_security, # Process attributes
355
- thread_security, # Thread attributes
356
- inherit, # Inherit handles
357
- creation_flags, # Creation Flags
358
- env, # Environment
359
- cwd, # Working directory
360
- startinfo, # Startup Info
361
- procinfo # Process Info
362
- )
363
-
364
- unless bool
365
- if FFI.errno == ERROR_PRIVILEGE_NOT_HELD
366
- raise SystemCallError.new("CreateProcessAsUserW (User '#{::ENV['USERNAME']}' must hold the 'Replace a process level token' and 'Adjust Memory Quotas for a process' permissions. Logoff the user after adding this right to make it effective.)", FFI.errno)
367
- else
368
- raise SystemCallError.new("CreateProcessAsUserW failed.", FFI.errno)
369
- end
370
- end
371
- ensure
372
- CloseHandle(token)
373
- end
374
-
375
490
  def get_windows_station_name
376
491
  winsta_name = FFI::MemoryPointer.new(:char, 256)
377
492
  return_size = FFI::MemoryPointer.new(:ulong)
@@ -406,5 +521,6 @@ module Process
406
521
 
407
522
  [ logon, passwd, domain ]
408
523
  end
524
+
409
525
  end
410
526
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixlib-shellout
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.4
4
+ version: 3.0.4
5
5
  platform: universal-mingw32
6
6
  authors:
7
7
  - Chef Software Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-12 00:00:00.000000000 Z
11
+ date: 2019-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: win32-process
@@ -69,8 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  - !ruby/object:Gem::Version
70
70
  version: '0'
71
71
  requirements: []
72
- rubyforge_project:
73
- rubygems_version: 2.7.6
72
+ rubygems_version: 3.0.3
74
73
  signing_key:
75
74
  specification_version: 4
76
75
  summary: Run external commands on Unix or Windows