opscode-pushy-client 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|