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 +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: 68567052a253b4c6bba0f3ff4c884402a41dd3175a00a3a4cef1d9fab8ca753a
|
4
|
+
data.tar.gz: ca9b2511628cb6655397abf24ea39a9d49a7c72f5550708e1daa2fe553328e91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 215b9612e24919b285530b202cb3e62ab27436f82fb5cb95efc0bccfc4538794800c57c88a66cfab978479c3718d5b9d5fa40de43833fd05c61a8a769831c571
|
7
|
+
data.tar.gz: 40458ab0286b067c4811be58fa4ad4c93568f25c5ca036137023690588c0b65e84f2f8dd034e0b0dc1f7070113305a6a262242207cab0eb8f9c53b38ffa125fe
|
@@ -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: universal-mingw32
|
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
|
- !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
|
-
|
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
|