mixlib-shellout 2.4.4 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a77caa01ee050dc884322465862b5eaa2b197b78ebe07f3ac6a4e3526924ce8
4
- data.tar.gz: d09b463bd4e2ce05d35a34df03684c4e033af823f54ab510804c648d018391d0
3
+ metadata.gz: a1d11760771e83f70693a84e1073b52deb6577247c15c171812b9a3b872654f0
4
+ data.tar.gz: 6c18e96112d21e666513555dc776259f7400142787ff21bef27b2063ea282c43
5
5
  SHA512:
6
- metadata.gz: 3f531586506e3461104c21931b216203e8e884549c5f069753bf6633882632b3a0330384d33a1c1b7fb1f628c9b4fefa0ec620ecd6929fec0630bbadc54f0c2c
7
- data.tar.gz: ae952774568dc2d26bd382ed40056bc7544353eb70538161449cb7f704811ef09462b992a8cc83b4a8ff72131f6e4d9925f93a90326ed7f3ededa045f5439ca4
6
+ metadata.gz: c746c6741617f66101f8c8a8817b84efa403dbe0f2bb2c9276ebb82377c6b89f7223e6ccac89d7a717a8e4534bb4005212b43d63754975503baf513b7d547536
7
+ data.tar.gz: e351af12c38924c2f4000146d3a309cd319045c658f864839418c9a3ab6d71b5f3bf0991c6b51bad8b7ed3afe7d40bf0d96e7e0b7e7da5df33b0d7390dd748de
@@ -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: ruby
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
  description: Run external commands on Unix or Windows
14
14
  email: info@chef.io
@@ -41,8 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
41
41
  - !ruby/object:Gem::Version
42
42
  version: '0'
43
43
  requirements: []
44
- rubyforge_project:
45
- rubygems_version: 2.7.6
44
+ rubygems_version: 3.0.3
46
45
  signing_key:
47
46
  specification_version: 4
48
47
  summary: Run external commands on Unix or Windows