opscode-pushy-client 2.3.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 +7 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +242 -0
- data/LICENSE +201 -0
- data/README.md +43 -0
- data/RELEASE_PROCESS.md +105 -0
- data/Rakefile +42 -0
- data/bin/print_execution_environment +18 -0
- data/bin/push-apply +47 -0
- data/bin/pushy-client +8 -0
- data/bin/pushy-service-manager +19 -0
- data/jenkins/jenkins_run_tests.sh +9 -0
- data/keys/client_private.pem +27 -0
- data/keys/server_public.pem +9 -0
- data/lib/pushy_client.rb +268 -0
- data/lib/pushy_client/cli.rb +168 -0
- data/lib/pushy_client/heartbeater.rb +153 -0
- data/lib/pushy_client/job_runner.rb +316 -0
- data/lib/pushy_client/periodic_reconfigurer.rb +62 -0
- data/lib/pushy_client/protocol_handler.rb +508 -0
- data/lib/pushy_client/version.rb +23 -0
- data/lib/pushy_client/whitelist.rb +66 -0
- data/lib/pushy_client/win32.rb +27 -0
- data/lib/pushy_client/windows_service.rb +253 -0
- data/omnibus/Berksfile +12 -0
- data/omnibus/Gemfile +15 -0
- data/omnibus/Gemfile.lock +232 -0
- data/omnibus/LICENSE +201 -0
- data/omnibus/README.md +141 -0
- data/omnibus/acceptance/Berksfile +6 -0
- data/omnibus/acceptance/Berksfile.lock +35 -0
- data/omnibus/acceptance/Makefile +13 -0
- data/omnibus/acceptance/README.md +29 -0
- data/omnibus/acceptance/metadata.rb +12 -0
- data/omnibus/acceptance/recipes/chef-server-user-org.rb +31 -0
- data/omnibus/config/projects/push-jobs-client.rb +83 -0
- data/omnibus/config/software/opscode-pushy-client.rb +78 -0
- data/omnibus/files/mapfiles/solaris +18 -0
- data/omnibus/files/openssl-customization/windows/ssl_env_hack.rb +34 -0
- data/omnibus/omnibus.rb +54 -0
- data/omnibus/package-scripts/push-jobs-client/postinst +55 -0
- data/omnibus/package-scripts/push-jobs-client/postrm +39 -0
- data/omnibus/resources/push-jobs-client/dmg/background.png +0 -0
- data/omnibus/resources/push-jobs-client/dmg/icon.png +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/LICENSE.rtf +197 -0
- data/omnibus/resources/push-jobs-client/msi/assets/banner_background.bmp +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/dialog_background.bmp +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc_16x16.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/assets/oc_32x32.ico +0 -0
- data/omnibus/resources/push-jobs-client/msi/localization-en-us.wxl.erb +26 -0
- data/omnibus/resources/push-jobs-client/msi/parameters.wxi.erb +9 -0
- data/omnibus/resources/push-jobs-client/msi/source.wxs.erb +138 -0
- data/omnibus/resources/push-jobs-client/pkg/background.png +0 -0
- data/omnibus/resources/push-jobs-client/pkg/license.html.erb +202 -0
- data/omnibus/resources/push-jobs-client/pkg/welcome.html.erb +5 -0
- data/opscode-pushy-client.gemspec +28 -0
- data/pkg/opscode-pushy-client-2.3.0.gem +0 -0
- data/spec/pushy_client/protocol_handler_spec.rb +48 -0
- data/spec/pushy_client/whitelist_spec.rb +70 -0
- data/spec/spec_helper.rb +12 -0
- metadata +235 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# This file is provided to you under the Apache License,
|
4
|
+
# Version 2.0 (the "License"); you may not use this file
|
5
|
+
# except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing,
|
11
|
+
# software distributed under the License is distributed on an
|
12
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
13
|
+
# KIND, either express or implied. See the License for the
|
14
|
+
# specific language governing permissions and limitations
|
15
|
+
# under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
class PushyClient
|
19
|
+
class PeriodicReconfigurer
|
20
|
+
SPLAY = 0.10
|
21
|
+
def initialize(client)
|
22
|
+
@client = client
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :client
|
26
|
+
attr_reader :lifetime
|
27
|
+
|
28
|
+
def node_name
|
29
|
+
client.node_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def start
|
33
|
+
@lifetime = client.config['lifetime']
|
34
|
+
prng = Random.new
|
35
|
+
@reconfigure_thread = Thread.new do
|
36
|
+
Chef::Log.info "[#{node_name}] Starting reconfigure thread. Will reconfigure / reload keys after #{@lifetime} seconds, less up to splay #{SPLAY}."
|
37
|
+
while true
|
38
|
+
begin
|
39
|
+
@delay = @lifetime * (1 - prng.rand(SPLAY))
|
40
|
+
sleep(@delay)
|
41
|
+
Chef::Log.info "[#{node_name}] Config is now #{@delay} seconds old. Reconfiguring / reloading keys ..."
|
42
|
+
client.trigger_reconfigure
|
43
|
+
rescue
|
44
|
+
client.log_exception("Error in reconfigure thread", $!)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
Chef::Log.info "[#{node_name}] Stopping reconfigure thread ..."
|
52
|
+
@reconfigure_thread.kill
|
53
|
+
@reconfigure_thread.join
|
54
|
+
@reconfigure_thread = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def reconfigure
|
58
|
+
stop
|
59
|
+
start
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,508 @@
|
|
1
|
+
# @copyright Copyright 2014 Chef Software, Inc. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# This file is provided to you under the Apache License,
|
4
|
+
# Version 2.0 (the "License"); you may not use this file
|
5
|
+
# except in compliance with the License. You may obtain
|
6
|
+
# a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing,
|
11
|
+
# software distributed under the License is distributed on an
|
12
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
13
|
+
# KIND, either express or implied. See the License for the
|
14
|
+
# specific language governing permissions and limitations
|
15
|
+
# under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'ffi-rzmq'
|
19
|
+
require 'ffi-rzmq-core'
|
20
|
+
require 'json'
|
21
|
+
require 'time'
|
22
|
+
require 'resolv'
|
23
|
+
require 'openssl'
|
24
|
+
require 'mixlib/authentication/digester'
|
25
|
+
require_relative "version"
|
26
|
+
|
27
|
+
class PushyClient
|
28
|
+
|
29
|
+
# Wrap the context with a lock.
|
30
|
+
# ZMQ gets grouchy if multiple threads access the context. Wrap it with a lock
|
31
|
+
# to block this. This mostly applies to the test system, where we run multiple clients in the same
|
32
|
+
# process
|
33
|
+
class ZmqContext
|
34
|
+
ZMQ_CONTEXT = ZMQ::Context.new(1)
|
35
|
+
@@lock = Mutex.new
|
36
|
+
|
37
|
+
def self.socket(arg)
|
38
|
+
@@lock.synchronize do
|
39
|
+
ZMQ_CONTEXT.socket(arg)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class ProtocolHandler
|
45
|
+
##
|
46
|
+
## Allow send and receive times to be independently stubbed in testing.
|
47
|
+
##
|
48
|
+
class TimeSendWrapper
|
49
|
+
def self.now
|
50
|
+
Time.now()
|
51
|
+
end
|
52
|
+
end
|
53
|
+
class TimeRecvWrapper
|
54
|
+
def self.now
|
55
|
+
Time.now()
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# The maximum size, in bytes, allowed for a message body. This is not
|
60
|
+
# configurable because it is configurable (though not documented) on the
|
61
|
+
# server, and we don't want to make users have to sync the two values.
|
62
|
+
# The max on the server is actually 65536, but we leave a little room since
|
63
|
+
# the server is measuring the signed message and we're just counting
|
64
|
+
# the size of the stderr and stdout.
|
65
|
+
MAX_BODY_SIZE = 63000
|
66
|
+
|
67
|
+
def initialize(client)
|
68
|
+
@client = client
|
69
|
+
# We synchronize on this when we change the socket (so if you want a
|
70
|
+
# valid socket to send or receive on, synchronize on this)
|
71
|
+
@socket_lock = Mutex.new
|
72
|
+
# This holds the same purpose, but receive blocks for a while so it gets
|
73
|
+
# its own lock to avoid blocking sends. reconfigure will take both locks.
|
74
|
+
@receive_socket_lock = Mutex.new
|
75
|
+
|
76
|
+
# When the server goes down, close and reopen sockets.
|
77
|
+
client.on_server_availability_change do |available|
|
78
|
+
if !available
|
79
|
+
Thread.new do
|
80
|
+
begin
|
81
|
+
Chef::Log.info "[#{node_name}] Closing and reopening sockets since server is down ..."
|
82
|
+
reconfigure
|
83
|
+
Chef::Log.info "[#{node_name}] Done closing and reopening sockets."
|
84
|
+
rescue
|
85
|
+
client.log_exception("Error reconfiguring sockets when server went down", $!)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_reader :client
|
93
|
+
attr_reader :server_heartbeat_address
|
94
|
+
attr_reader :command_address
|
95
|
+
attr_reader :server_public_key
|
96
|
+
attr_reader :session_key
|
97
|
+
attr_reader :session_method
|
98
|
+
attr_reader :client_private_key
|
99
|
+
|
100
|
+
def node_name
|
101
|
+
client.node_name
|
102
|
+
end
|
103
|
+
|
104
|
+
def start
|
105
|
+
server_address = URI(client.config['push_jobs']['heartbeat']['out_addr']).host
|
106
|
+
check_server_address(node_name, server_address)
|
107
|
+
|
108
|
+
@server_heartbeat_address = client.config['push_jobs']['heartbeat']['out_addr']
|
109
|
+
@command_address = client.config['push_jobs']['heartbeat']['command_addr']
|
110
|
+
@server_public_key = OpenSSL::PKey::RSA.new(client.config['public_key'])
|
111
|
+
@client_private_key = ProtocolHandler::load_key(client.client_key)
|
112
|
+
@max_message_skew = client.config['max_message_skew']
|
113
|
+
|
114
|
+
if client.using_curve
|
115
|
+
server_curve_pub_key = client.config['curve_public_key']
|
116
|
+
|
117
|
+
# decode and extract session key
|
118
|
+
begin
|
119
|
+
@session_method = client.config['encoded_session_key']['method']
|
120
|
+
enc_session_key = Base64::decode64(client.config['encoded_session_key']['key'])
|
121
|
+
@session_key = @client_private_key.private_decrypt(enc_session_key)
|
122
|
+
rescue =>_
|
123
|
+
Chef::Log.error "[#{node_name}] No session key found in config"
|
124
|
+
exit(-1)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# decode and extract session key
|
128
|
+
begin
|
129
|
+
@session_method = client.config['encoded_session_key']['method']
|
130
|
+
enc_session_key = Base64::decode64(client.config['encoded_session_key']['key'])
|
131
|
+
@session_key = @client_private_key.private_decrypt(enc_session_key)
|
132
|
+
rescue =>e
|
133
|
+
Chef::Log.error "[#{node_name}] No session key found in config"
|
134
|
+
exit(-1)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
Chef::Log.info "[#{node_name}] Starting ZMQ version #{LibZMQ.version}"
|
139
|
+
|
140
|
+
# Server heartbeat socket
|
141
|
+
Chef::Log.info "[#{node_name}] Listening for server heartbeat at #{@server_heartbeat_address}"
|
142
|
+
@server_heartbeat_socket = PushyClient::ZmqContext.socket(ZMQ::SUB)
|
143
|
+
@server_heartbeat_socket.connect(@server_heartbeat_address)
|
144
|
+
@server_heartbeat_socket.setsockopt(ZMQ::SUBSCRIBE, "")
|
145
|
+
@server_heartbeat_seq_no = -1
|
146
|
+
|
147
|
+
# Command socket
|
148
|
+
Chef::Log.info "[#{node_name}] Connecting to command channel at #{@command_address}"
|
149
|
+
# TODO
|
150
|
+
# This needs to be set up to be able to handle bidirectional messages; right now this is Tx only
|
151
|
+
# Probably need to set it up with a handler, like the subscriber socket above.
|
152
|
+
@command_socket = PushyClient::ZmqContext.socket(ZMQ::DEALER)
|
153
|
+
@command_socket.setsockopt(ZMQ::LINGER, 0)
|
154
|
+
# Note setting this to '1' causes the client to crash on send, but perhaps that
|
155
|
+
# beats storming the server when the server restarts
|
156
|
+
@command_socket.setsockopt(ZMQ::RCVHWM, 0)
|
157
|
+
# Buffering more than a few heartbeats can cause trauma on the server after restart
|
158
|
+
@command_socket.setsockopt(ZMQ::SNDHWM, 3)
|
159
|
+
|
160
|
+
if client.using_curve
|
161
|
+
@command_socket.setsockopt(ZMQ::CURVE_SERVERKEY, server_curve_pub_key)
|
162
|
+
@command_socket.setsockopt(ZMQ::CURVE_PUBLICKEY, client.client_curve_pub_key)
|
163
|
+
@command_socket.setsockopt(ZMQ::CURVE_SECRETKEY, client.client_curve_sec_key)
|
164
|
+
end
|
165
|
+
|
166
|
+
@command_socket.connect(@command_address)
|
167
|
+
@command_socket_server_seq_no = -1
|
168
|
+
|
169
|
+
@command_socket_outgoing_seq = 0
|
170
|
+
|
171
|
+
@receive_thread = start_receive_thread
|
172
|
+
end
|
173
|
+
|
174
|
+
def stop
|
175
|
+
@socket_lock.synchronize do
|
176
|
+
@receive_socket_lock.synchronize do
|
177
|
+
internal_stop
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def reconfigure
|
183
|
+
@socket_lock.synchronize do
|
184
|
+
@receive_socket_lock.synchronize do
|
185
|
+
internal_stop
|
186
|
+
start # Start picks up new configuration
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def send_command(message_type, job_id, params)
|
192
|
+
Chef::Log.debug("[#{node_name}] Sending command #{message_type} for job #{job_id}")
|
193
|
+
# Nil params will be stripped by JSON.generate()
|
194
|
+
message = {
|
195
|
+
:node => node_name,
|
196
|
+
:client => client.hostname,
|
197
|
+
:protocol_version => PushyClient::PROTOCOL_VERSION,
|
198
|
+
:org => client.org_name,
|
199
|
+
:type => message_type,
|
200
|
+
:sequence => -1,
|
201
|
+
:timestamp => TimeSendWrapper.now.httpdate,
|
202
|
+
:incarnation_id => client.incarnation_id,
|
203
|
+
:job_id => job_id
|
204
|
+
}.merge(validate_params(params))
|
205
|
+
|
206
|
+
send_signed_json_command(:hmac_sha256, message)
|
207
|
+
end
|
208
|
+
|
209
|
+
def send_heartbeat(sequence)
|
210
|
+
Chef::Log.debug("[#{node_name}] Sending heartbeat (sequence ##{sequence})")
|
211
|
+
job_state = client.job_state
|
212
|
+
message = {
|
213
|
+
:node => node_name,
|
214
|
+
:client => client.hostname,
|
215
|
+
:protocol_version => PushyClient::PROTOCOL_VERSION,
|
216
|
+
:org => client.org_name,
|
217
|
+
:type => :heartbeat,
|
218
|
+
:sequence => -1,
|
219
|
+
:timestamp => TimeSendWrapper.now.httpdate,
|
220
|
+
:incarnation_id => client.incarnation_id,
|
221
|
+
:job_state => job_state[:state],
|
222
|
+
:job_id => job_state[:job_id]
|
223
|
+
}
|
224
|
+
|
225
|
+
send_signed_json_command(:hmac_sha256, message)
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
def internal_stop
|
231
|
+
Chef::Log.info "[#{node_name}] Stopping command / server heartbeat receive thread and destroying sockets ..."
|
232
|
+
@command_socket.close
|
233
|
+
@command_socket = nil
|
234
|
+
@server_heartbeat_socket.close
|
235
|
+
@server_heartbeat_socket = nil
|
236
|
+
@receive_thread.kill
|
237
|
+
@receive_thread.join
|
238
|
+
@receive_thread = nil
|
239
|
+
end
|
240
|
+
|
241
|
+
def start_receive_thread
|
242
|
+
Thread.new do
|
243
|
+
Chef::Log.info "[#{node_name}] Starting command / server heartbeat receive thread ..."
|
244
|
+
received_command = false
|
245
|
+
seconds_since_connection = 0
|
246
|
+
poller = ZMQ::Poller.new
|
247
|
+
poller.register_readable(@command_socket)
|
248
|
+
poller.register_readable(@server_heartbeat_socket)
|
249
|
+
while true
|
250
|
+
begin
|
251
|
+
messages = []
|
252
|
+
@receive_socket_lock.synchronize do
|
253
|
+
# Time out after 1 second to relinquish the lock and give
|
254
|
+
# reconfigure a chance.
|
255
|
+
poller.poll(1000)
|
256
|
+
ready_sockets = poller.readables
|
257
|
+
# Grab messages from the socket, but don't process them yet (we
|
258
|
+
# want to relinquish the socket_lock as soon as we can)
|
259
|
+
if ready_sockets
|
260
|
+
ready_sockets.each do |socket|
|
261
|
+
header = ''
|
262
|
+
socket.recv_string(header)
|
263
|
+
if socket.more_parts?
|
264
|
+
message = ''
|
265
|
+
socket.recv_string(message)
|
266
|
+
if !socket.more_parts?
|
267
|
+
Chef::Log.debug("[#{node_name}] Received ZMQ message (#{header}, #{message.length}")
|
268
|
+
messages << [header, message]
|
269
|
+
if socket == @command_socket
|
270
|
+
received_command = true
|
271
|
+
end
|
272
|
+
else
|
273
|
+
# Eat up the useless packets
|
274
|
+
begin
|
275
|
+
s = ''
|
276
|
+
socket.recv(s)
|
277
|
+
end while socket.more_parts?
|
278
|
+
Chef::Log.error "[#{node_name}] Received ZMQ message with more than two packets! Should only have header and data packets."
|
279
|
+
end
|
280
|
+
else
|
281
|
+
Chef::Log.error "[#{node_name}] Received ZMQ message with only one packet! Need both header and data packets."
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Need to do this to ensure reconfigure thread gets a chance to
|
288
|
+
# wake up and grab the lock.
|
289
|
+
sleep(0.005)
|
290
|
+
|
291
|
+
messages.each do |message|
|
292
|
+
if ProtocolHandler::valid?(message[0], message[1], @server_public_key, @session_key)
|
293
|
+
handle_message(message[1])
|
294
|
+
else
|
295
|
+
Chef::Log.error "[#{node_name}] Received invalid message: header=#{message[0]}, message=#{message[1]}}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
if !received_command && !@client.legacy_mode
|
300
|
+
seconds_since_connection += 1
|
301
|
+
if (seconds_since_connection > 3 )
|
302
|
+
Chef::Log.error "[#{node_name}] No messages being received on command port in #{seconds_since_connection}s. Possible encryption problem?"
|
303
|
+
client.trigger_reconfigure
|
304
|
+
break
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
rescue
|
309
|
+
client.log_exception "Error in command / server heartbeat receive thread", $!
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def handle_message(message)
|
316
|
+
begin
|
317
|
+
json = JSON.parse(message, :create_additions => false)
|
318
|
+
|
319
|
+
# Verify timestamp
|
320
|
+
if !json.has_key?('timestamp')
|
321
|
+
Chef::Log.error "[#{node_name}] Received invalid message: missing timestamp"
|
322
|
+
return
|
323
|
+
end
|
324
|
+
begin
|
325
|
+
ts = Time.parse(json['timestamp'])
|
326
|
+
delta = ts - TimeRecvWrapper.now
|
327
|
+
if delta > @max_message_skew
|
328
|
+
Chef::Log.error "[#{node_name}] Received message with timestamp too far from current time (Msg: #{json['timestamp']}, delta #{delta}, max allowed #{@max_message_skew} )"
|
329
|
+
return
|
330
|
+
end
|
331
|
+
rescue
|
332
|
+
Chef::Log.error "[#{node_name}] Received message unparseable timestamp (Msg: #{json['timestamp']})"
|
333
|
+
return
|
334
|
+
end
|
335
|
+
|
336
|
+
case json['type']
|
337
|
+
when "heartbeat"
|
338
|
+
incarnation_id = json['incarnation_id']
|
339
|
+
if !incarnation_id
|
340
|
+
Chef::Log.error "[#{node_name}] Missing incarnation_id in heartbeat message: #{message}"
|
341
|
+
end
|
342
|
+
sequence = json['sequence']
|
343
|
+
if !sequence
|
344
|
+
Chef::Log.error "[#{node_name}] Missing sequence in heartbeat message: #{message}"
|
345
|
+
end
|
346
|
+
client.heartbeat_received(incarnation_id, sequence)
|
347
|
+
|
348
|
+
when "commit"
|
349
|
+
job_id = json['job_id']
|
350
|
+
if job_id
|
351
|
+
command = json['command']
|
352
|
+
if command
|
353
|
+
opts = {}
|
354
|
+
opts.merge!('user' => json['user']) if json['user']
|
355
|
+
opts.merge!('dir' => json['dir']) if json['dir']
|
356
|
+
opts.merge!('env' => json['env']) if json['env']
|
357
|
+
opts.merge!('capture' => json['capture']) if json['capture']
|
358
|
+
opts.merge!('file' => json['file']) if json['file']
|
359
|
+
client.commit(job_id, command, opts)
|
360
|
+
else
|
361
|
+
Chef::Log.error "[#{node_name}] Missing command in commit message: #{message}"
|
362
|
+
client.send_command(:nack_commit, command)
|
363
|
+
end
|
364
|
+
else
|
365
|
+
Chef::Log.error "[#{node_name}] Missing job_id in commit message: #{message}"
|
366
|
+
client.send_command(:nack_commit, job_id)
|
367
|
+
end
|
368
|
+
|
369
|
+
when "run"
|
370
|
+
job_id = json['job_id']
|
371
|
+
if job_id
|
372
|
+
client.run(job_id)
|
373
|
+
else
|
374
|
+
Chef::Log.error "[#{node_name}] Missing job_id in commit message: #{message}"
|
375
|
+
client.send_command(:nack_run, job_id)
|
376
|
+
end
|
377
|
+
|
378
|
+
when "abort"
|
379
|
+
client.abort
|
380
|
+
|
381
|
+
when "ack"
|
382
|
+
# Do nothing. If this _didn't_ come through, it might mean there was
|
383
|
+
# an encryption problem in the command port.
|
384
|
+
nil
|
385
|
+
|
386
|
+
else
|
387
|
+
Chef::Log.error "[#{node_name}] Missing type in ZMQ message: #{message}"
|
388
|
+
end
|
389
|
+
rescue JSON::ParserError
|
390
|
+
Chef::Log.error "[#{node_name}] Invalid JSON in ZMQ message: #{message}"
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def check_server_address(name, server_address)
|
395
|
+
if server_address =~ Resolv::IPv4::Regex || server_address =~ Resolv::IPv6::Regex
|
396
|
+
return true
|
397
|
+
else
|
398
|
+
begin
|
399
|
+
addrs = Resolv::DNS.new.getaddresses(server_address) + Resolv::Hosts.new.getaddresses(server_address)
|
400
|
+
Chef::Log.info "[#{node_name}] Resolved #{server_address} to '#{addrs.first}' and #{addrs.length-1} others"
|
401
|
+
return true
|
402
|
+
rescue =>_
|
403
|
+
Chef::Log.error "[#{node_name}] Could not resolve #{server_address}"
|
404
|
+
return false
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Take the params and check to see if the stdout and stderr keys exceed the
|
411
|
+
# MAX_BODY_SIZE. If they do, log a warning and return the params without the
|
412
|
+
# stdout and stderr keys. If they do not, return the params hash unmodified.
|
413
|
+
def validate_params(params = {})
|
414
|
+
stdout_bytes = params[:stdout].to_s.bytesize
|
415
|
+
stderr_bytes = params[:stderr].to_s.bytesize
|
416
|
+
|
417
|
+
if (stdout_bytes + stderr_bytes) > MAX_BODY_SIZE
|
418
|
+
Chef::Log.warn("Command output too long. Will not be sent to server.")
|
419
|
+
params.delete_if { |k| [:stdout, :stderr].include? k }
|
420
|
+
else
|
421
|
+
params
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# Message authentication (on receive)
|
426
|
+
def self.valid?(header, message, server_public_key, session_key)
|
427
|
+
headers = header.split(';')
|
428
|
+
header_map = headers.inject({}) do |a,e|
|
429
|
+
k,v = e.split(':')
|
430
|
+
a[k] = v
|
431
|
+
a
|
432
|
+
end
|
433
|
+
|
434
|
+
auth_sig = header_map["Signature"]
|
435
|
+
if !auth_sig
|
436
|
+
return false
|
437
|
+
end
|
438
|
+
|
439
|
+
binary_sig = Base64.decode64(auth_sig)
|
440
|
+
|
441
|
+
auth_method = header_map["SigningMethod"]
|
442
|
+
case auth_method
|
443
|
+
when "rsa2048_sha1"
|
444
|
+
rsa_valid?(message, binary_sig, server_public_key)
|
445
|
+
when "hmac_sha256"
|
446
|
+
hmac_valid?(message, binary_sig, session_key)
|
447
|
+
else
|
448
|
+
false
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def self.rsa_valid?(message, sig, server_public_key)
|
453
|
+
decrypted_checksum = server_public_key.public_decrypt(sig)
|
454
|
+
hashed_message = Mixlib::Authentication::Digester.hash_string(message)
|
455
|
+
decrypted_checksum == hashed_message
|
456
|
+
end
|
457
|
+
|
458
|
+
def self.hmac_valid?(message, sig, session_key)
|
459
|
+
message_sig = OpenSSL::HMAC.digest('sha256', session_key, message)
|
460
|
+
# Defeat timing attacks; attacking this requires breaking SHA.
|
461
|
+
sha = OpenSSL::Digest::SHA512.new
|
462
|
+
sha.digest(sig) == sha.digest(message_sig)
|
463
|
+
end
|
464
|
+
|
465
|
+
# Message signing and sending (on send)
|
466
|
+
def send_signed_json_command(method, json)
|
467
|
+
@socket_lock.synchronize do
|
468
|
+
@command_socket_outgoing_seq += 1
|
469
|
+
json[:sequence] = @command_socket_outgoing_seq
|
470
|
+
message = JSON.generate(json)
|
471
|
+
if @command_socket
|
472
|
+
ProtocolHandler::send_signed_message(@command_socket, method, @client_private_key, @session_key, message)
|
473
|
+
else
|
474
|
+
Chef::Log.warn("[#{node_name}] Dropping packet because client was stopped: #{message}")
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
def self.send_signed_message(socket, method, client_private_key, session_key, message)
|
480
|
+
auth = case method
|
481
|
+
when :rsa2048_sha1
|
482
|
+
make_header_rsa(message, client_private_key)
|
483
|
+
when :hmac_sha256
|
484
|
+
make_header_hmac(message, session_key)
|
485
|
+
end
|
486
|
+
socket.send_string(auth, ZMQ::SNDMORE)
|
487
|
+
socket.send_string(message)
|
488
|
+
end
|
489
|
+
|
490
|
+
def self.load_key(key_path)
|
491
|
+
raw_key = IO.read(key_path).strip
|
492
|
+
OpenSSL::PKey::RSA.new(raw_key)
|
493
|
+
end
|
494
|
+
|
495
|
+
def self.make_header_rsa(json, client_private_key)
|
496
|
+
checksum = Mixlib::Authentication::Digester.hash_string(json)
|
497
|
+
b64_sig = Base64.encode64(client_private_key.private_encrypt(checksum)).chomp
|
498
|
+
"Version:2.0;SigningMethod:rsa2048_sha1;Signature:#{b64_sig}"
|
499
|
+
end
|
500
|
+
|
501
|
+
def self.make_header_hmac(json, session_key)
|
502
|
+
sig = OpenSSL::HMAC.digest('sha256', session_key, json)
|
503
|
+
b64_sig = Base64.encode64(sig).chomp
|
504
|
+
"Version:2.0;SigningMethod:hmac_sha256;Signature:#{b64_sig}"
|
505
|
+
end
|
506
|
+
|
507
|
+
end
|
508
|
+
end
|