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 +4 -4
- data/lib/mixlib/shellout/version.rb +1 -1
- data/lib/mixlib/shellout/windows.rb +45 -3
- data/lib/mixlib/shellout/windows/core_ext.rb +184 -68
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1d11760771e83f70693a84e1073b52deb6577247c15c171812b9a3b872654f0
|
4
|
+
data.tar.gz: 6c18e96112d21e666513555dc776259f7400142787ff21bef27b2063ea282c43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c746c6741617f66101f8c8a8817b84efa403dbe0f2bb2c9276ebb82377c6b89f7223e6ccac89d7a717a8e4534bb4005212b43d63754975503baf513b7d547536
|
7
|
+
data.tar.gz: e351af12c38924c2f4000146d3a309cd319045c658f864839418c9a3ab6d71b5f3bf0991c6b51bad8b7ed3afe7d40bf0d96e7e0b7e7da5df33b0d7390dd748de
|
@@ -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-
|
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"
|
129
|
+
"app_name" => nil,
|
89
130
|
"creation_flags" => 0,
|
90
|
-
"close_handles"
|
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
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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:
|
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:
|
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
|
-
|
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
|