pantry 0.0.0 → 0.1.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 +4 -4
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +15 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/Vagrantfile +86 -0
- data/bin/pantry +11 -0
- data/bin/pantry-client +38 -0
- data/bin/pantry-server +33 -0
- data/dist/client.yml +79 -0
- data/dist/server.yml +56 -0
- data/dist/upstart/pantry-client.conf +12 -0
- data/dist/upstart/pantry-server.conf +12 -0
- data/doc/message_packet.dot +19 -0
- data/doc/message_packet.dot.png +0 -0
- data/doc/network_topology.dot +42 -0
- data/doc/network_topology.dot.png +0 -0
- data/lib/celluloid_zmq_patches.rb +16 -0
- data/lib/opt_parse_plus.rb +184 -0
- data/lib/pantry.rb +197 -0
- data/lib/pantry/cli.rb +154 -0
- data/lib/pantry/client.rb +131 -0
- data/lib/pantry/client_info.rb +34 -0
- data/lib/pantry/client_registry.rb +104 -0
- data/lib/pantry/command.rb +194 -0
- data/lib/pantry/command_handler.rb +53 -0
- data/lib/pantry/command_line.rb +115 -0
- data/lib/pantry/commands/create_client.rb +30 -0
- data/lib/pantry/commands/download_directory.rb +35 -0
- data/lib/pantry/commands/echo.rb +32 -0
- data/lib/pantry/commands/edit_application.rb +60 -0
- data/lib/pantry/commands/register_client.rb +38 -0
- data/lib/pantry/commands/status.rb +78 -0
- data/lib/pantry/commands/sync_directory.rb +50 -0
- data/lib/pantry/commands/update_application.rb +45 -0
- data/lib/pantry/commands/upload_file.rb +68 -0
- data/lib/pantry/communication.rb +20 -0
- data/lib/pantry/communication/client.rb +75 -0
- data/lib/pantry/communication/client_filter.rb +117 -0
- data/lib/pantry/communication/file_service.rb +125 -0
- data/lib/pantry/communication/file_service/file_progress.rb +164 -0
- data/lib/pantry/communication/file_service/receive_file.rb +97 -0
- data/lib/pantry/communication/file_service/send_file.rb +74 -0
- data/lib/pantry/communication/publish_socket.rb +20 -0
- data/lib/pantry/communication/reading_socket.rb +89 -0
- data/lib/pantry/communication/receive_socket.rb +23 -0
- data/lib/pantry/communication/security.rb +44 -0
- data/lib/pantry/communication/security/authentication.rb +98 -0
- data/lib/pantry/communication/security/curve_key_store.rb +120 -0
- data/lib/pantry/communication/security/curve_security.rb +70 -0
- data/lib/pantry/communication/security/null_security.rb +32 -0
- data/lib/pantry/communication/send_socket.rb +19 -0
- data/lib/pantry/communication/serialize_message.rb +84 -0
- data/lib/pantry/communication/server.rb +97 -0
- data/lib/pantry/communication/subscribe_socket.rb +33 -0
- data/lib/pantry/communication/wait_list.rb +45 -0
- data/lib/pantry/communication/writing_socket.rb +46 -0
- data/lib/pantry/config.rb +182 -0
- data/lib/pantry/file_editor.rb +67 -0
- data/lib/pantry/logger.rb +78 -0
- data/lib/pantry/message.rb +134 -0
- data/lib/pantry/multi_command.rb +36 -0
- data/lib/pantry/server.rb +132 -0
- data/lib/pantry/test/acceptance.rb +83 -0
- data/lib/pantry/test/support/fake_fs.rb +31 -0
- data/lib/pantry/test/support/matchers.rb +13 -0
- data/lib/pantry/test/support/minitest.rb +13 -0
- data/lib/pantry/test/support/mock_ui.rb +23 -0
- data/lib/pantry/test/unit.rb +13 -0
- data/lib/pantry/ui.rb +68 -0
- data/lib/pantry/version.rb +3 -0
- data/pantry.gemspec +40 -0
- data/test/acceptance/cli/error_handling_test.rb +7 -0
- data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
- data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
- data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
- data/test/acceptance/communication/heartbeat_test.rb +19 -0
- data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
- data/test/acceptance/communication/security_test.rb +117 -0
- data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/config.yml +22 -0
- data/test/fixtures/empty.yml +2 -0
- data/test/fixtures/file_to_upload +3 -0
- data/test/root_dir/.gitkeep +0 -0
- data/test/unit/cli_test.rb +173 -0
- data/test/unit/client_registry_test.rb +61 -0
- data/test/unit/client_test.rb +128 -0
- data/test/unit/command_handler_test.rb +79 -0
- data/test/unit/command_line_test.rb +5 -0
- data/test/unit/command_test.rb +206 -0
- data/test/unit/commands/create_client_test.rb +25 -0
- data/test/unit/commands/download_directory_test.rb +58 -0
- data/test/unit/commands/echo_test.rb +22 -0
- data/test/unit/commands/edit_application_test.rb +84 -0
- data/test/unit/commands/register_client_test.rb +41 -0
- data/test/unit/commands/status_test.rb +81 -0
- data/test/unit/commands/sync_directory_test.rb +75 -0
- data/test/unit/commands/update_application_test.rb +35 -0
- data/test/unit/commands/upload_file_test.rb +51 -0
- data/test/unit/communication/client_filter_test.rb +262 -0
- data/test/unit/communication/client_test.rb +99 -0
- data/test/unit/communication/file_service/receive_file_test.rb +214 -0
- data/test/unit/communication/file_service/send_file_test.rb +110 -0
- data/test/unit/communication/file_service_test.rb +56 -0
- data/test/unit/communication/publish_socket_test.rb +19 -0
- data/test/unit/communication/reading_socket_test.rb +110 -0
- data/test/unit/communication/receive_socket_test.rb +20 -0
- data/test/unit/communication/security/authentication_test.rb +97 -0
- data/test/unit/communication/security/curve_key_store_test.rb +110 -0
- data/test/unit/communication/security/curve_security_test.rb +44 -0
- data/test/unit/communication/security/null_security_test.rb +15 -0
- data/test/unit/communication/security_test.rb +49 -0
- data/test/unit/communication/send_socket_test.rb +19 -0
- data/test/unit/communication/serialize_message_test.rb +128 -0
- data/test/unit/communication/server_test.rb +106 -0
- data/test/unit/communication/subscribe_socket_test.rb +46 -0
- data/test/unit/communication/wait_list_test.rb +60 -0
- data/test/unit/communication/writing_socket_test.rb +46 -0
- data/test/unit/config_test.rb +150 -0
- data/test/unit/logger_test.rb +79 -0
- data/test/unit/message_test.rb +179 -0
- data/test/unit/multi_command_test.rb +45 -0
- data/test/unit/opt_parse_plus_test.rb +218 -0
- data/test/unit/pantry_test.rb +82 -0
- data/test/unit/server_test.rb +166 -0
- data/test/unit/test_helper.rb +25 -0
- data/test/unit/ui_test.rb +58 -0
- metadata +389 -13
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
module Security
|
|
4
|
+
|
|
5
|
+
class UnknownSecurityStrategyError < Exception; end
|
|
6
|
+
|
|
7
|
+
AVAILABLE_SECURITY = {
|
|
8
|
+
nil => Pantry::Communication::Security::NullSecurity,
|
|
9
|
+
"curve" => Pantry::Communication::Security::CurveSecurity
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Check if ZeroMQ is built properly to support Curve encryption
|
|
13
|
+
def self.curve_supported?
|
|
14
|
+
begin
|
|
15
|
+
ZMQ::Util.curve_keypair
|
|
16
|
+
true
|
|
17
|
+
rescue
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Build a Client implementation of the security strategy
|
|
23
|
+
# configured in Pantry.config.security
|
|
24
|
+
def self.new_client(config = Pantry.config)
|
|
25
|
+
handler_class(config).client
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Build a Server implementation of the security strategy
|
|
29
|
+
# configured in Pantry.config.security
|
|
30
|
+
def self.new_server(config = Pantry.config)
|
|
31
|
+
handler_class(config).server
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.handler_class(config)
|
|
35
|
+
if handler = AVAILABLE_SECURITY[config.security]
|
|
36
|
+
handler
|
|
37
|
+
else
|
|
38
|
+
raise UnknownSecurityStrategyError, "Unknown security strategy #{config.security.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
module Security
|
|
4
|
+
|
|
5
|
+
# This class implements and manages the ZAP handler.
|
|
6
|
+
# For any connecting client, this handler receives a request to
|
|
7
|
+
# authenticate the Client. If the Client is allowed in, all proceeds as
|
|
8
|
+
# normal. If a Client is not allowed in then the connection is dropped.
|
|
9
|
+
#
|
|
10
|
+
# For Pantry, this is a very strict authentication mechanism that only
|
|
11
|
+
# allows Clients in whos public keys are in the server_keys.yml keystore.
|
|
12
|
+
# It also rejects any attempts to authenticate with a mechanism other than CURVE.
|
|
13
|
+
#
|
|
14
|
+
# ZAP: ZeroMQ Authentication Protocol :: http://rfc.zeromq.org/spec:27
|
|
15
|
+
class Authentication
|
|
16
|
+
include Celluloid::ZMQ
|
|
17
|
+
finalizer :shutdown
|
|
18
|
+
|
|
19
|
+
def initialize(key_store)
|
|
20
|
+
@key_store = key_store
|
|
21
|
+
|
|
22
|
+
@socket = Celluloid::ZMQ::RepSocket.new
|
|
23
|
+
@socket.linger = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def open
|
|
27
|
+
@socket.bind("inproc://zeromq.zap.01")
|
|
28
|
+
@running = true
|
|
29
|
+
self.async.process_requests
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def shutdown
|
|
33
|
+
@socket.close
|
|
34
|
+
@running = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def process_requests
|
|
38
|
+
while @running
|
|
39
|
+
process_next_request
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_next_request
|
|
44
|
+
request = read_next_request
|
|
45
|
+
|
|
46
|
+
response_code, response_text = authenticate_request(request)
|
|
47
|
+
|
|
48
|
+
if response_code == "200"
|
|
49
|
+
Pantry.logger.debug("[AUTH] Client authentication successful")
|
|
50
|
+
else
|
|
51
|
+
Pantry.logger.debug("[AUTH] Client authentication rejected: #{response_text}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
write_response(request, response_code, response_text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_next_request
|
|
58
|
+
request = []
|
|
59
|
+
begin
|
|
60
|
+
request << @socket.read
|
|
61
|
+
end while @socket.more_parts?
|
|
62
|
+
request
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def authenticate_request(request)
|
|
66
|
+
mechanism = request[5]
|
|
67
|
+
client_key = request[6]
|
|
68
|
+
|
|
69
|
+
if mechanism != "CURVE"
|
|
70
|
+
["400", "Invalid Mechanism"]
|
|
71
|
+
else
|
|
72
|
+
authenticate_client(client_key)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def authenticate_client(client_key)
|
|
77
|
+
if @key_store.known_client?(client_key)
|
|
78
|
+
["200", "OK"]
|
|
79
|
+
else
|
|
80
|
+
["400", "Unknown Client"]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def write_response(request, response_code, response_text)
|
|
85
|
+
@socket.write([
|
|
86
|
+
request[0], # Version
|
|
87
|
+
request[1], # Sequence / Request id
|
|
88
|
+
response_code,
|
|
89
|
+
response_text,
|
|
90
|
+
"", # username
|
|
91
|
+
"" # metadata
|
|
92
|
+
])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
module Security
|
|
4
|
+
|
|
5
|
+
# CurveKeyStore manages the storage, reading, and writing of all
|
|
6
|
+
# Curve-related key-pairs.
|
|
7
|
+
#
|
|
8
|
+
# Clients keep track of the public key of the server they talk to
|
|
9
|
+
# Servers keep track of the list of public keys of Clients who are
|
|
10
|
+
# allowed to connect.
|
|
11
|
+
#
|
|
12
|
+
# All keys are stored under Pantry.root/security/curve
|
|
13
|
+
class CurveKeyStore
|
|
14
|
+
|
|
15
|
+
attr_reader :public_key, :private_key, :server_public_key
|
|
16
|
+
|
|
17
|
+
def initialize(my_key_pair_name)
|
|
18
|
+
@base_key_dir = Pantry.root.join("security", "curve")
|
|
19
|
+
@my_keys_file = @base_key_dir.join("#{my_key_pair_name}.yml")
|
|
20
|
+
@known_clients = []
|
|
21
|
+
|
|
22
|
+
ensure_directory_structure
|
|
23
|
+
check_or_generate_my_keys
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if the given client public key is known by this server or
|
|
27
|
+
# not. To facilitate the initial setup process of a new Pantry Server,
|
|
28
|
+
# this will allow and store the first client to connect to this server
|
|
29
|
+
# and will write out that client's public key as valid.
|
|
30
|
+
#
|
|
31
|
+
# Used solely by the Server
|
|
32
|
+
def known_client?(client_public_key)
|
|
33
|
+
encoded_key = z85_encode(client_public_key)
|
|
34
|
+
if @known_clients.empty?
|
|
35
|
+
store_known_client(encoded_key)
|
|
36
|
+
true
|
|
37
|
+
else
|
|
38
|
+
@known_clients.include?(encoded_key)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Generate and store a new Client pub/priv key pair
|
|
43
|
+
# Only the Public key is stored locally for authentication purposes.
|
|
44
|
+
# Returns a hash of all relevant keys for the Client to connect
|
|
45
|
+
# and Auth.
|
|
46
|
+
def create_client
|
|
47
|
+
client_public, client_private = ZMQ::Util.curve_keypair
|
|
48
|
+
store_known_client(client_public)
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
server_public_key: @public_key,
|
|
52
|
+
public_key: client_public,
|
|
53
|
+
private_key: client_private
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
# TODO Move this logic into ffi-rzmq proper
|
|
60
|
+
def z85_encode(binary_key)
|
|
61
|
+
encoded = FFI::MemoryPointer.from_string(' ' * 41)
|
|
62
|
+
LibZMQ::zmq_z85_encode(encoded, binary_key, 32)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ensure_directory_structure
|
|
66
|
+
FileUtils.mkdir_p(@base_key_dir)
|
|
67
|
+
FileUtils.chmod(0700, @base_key_dir)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_or_generate_my_keys
|
|
71
|
+
if File.exists?(@my_keys_file)
|
|
72
|
+
load_current_key_pair
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
generate_missing_keys
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def load_current_key_pair
|
|
79
|
+
keys = YAML.load_file(@my_keys_file)
|
|
80
|
+
@public_key = keys["public_key"]
|
|
81
|
+
@private_key = keys["private_key"]
|
|
82
|
+
@server_public_key = keys["server_public_key"]
|
|
83
|
+
@known_clients = keys["client_keys"] || []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def generate_missing_keys
|
|
87
|
+
if @public_key.nil? && @private_key.nil?
|
|
88
|
+
@public_key, @private_key = ZMQ::Util.curve_keypair
|
|
89
|
+
save_keys
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def store_known_client(client_public_key)
|
|
94
|
+
@known_clients << client_public_key
|
|
95
|
+
save_keys
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def save_keys
|
|
99
|
+
File.open(@my_keys_file, "w+") do |f|
|
|
100
|
+
keys = {
|
|
101
|
+
"private_key" => @private_key,
|
|
102
|
+
"public_key" => @public_key
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if @server_public_key
|
|
106
|
+
keys["server_public_key"] = @server_public_key
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if @known_clients.length > 0
|
|
110
|
+
keys["client_keys"] = @known_clients
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
f.write YAML.dump(keys)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
module Security
|
|
4
|
+
|
|
5
|
+
# ZeroMQ Curve encryption strategy.
|
|
6
|
+
# For details about how the Curve encryption works in ZeroMQ, check out
|
|
7
|
+
# the following:
|
|
8
|
+
#
|
|
9
|
+
# * http://api.zeromq.org/4-0:zmq-curve
|
|
10
|
+
# * http://curvezmq.org/
|
|
11
|
+
#
|
|
12
|
+
class CurveSecurity
|
|
13
|
+
|
|
14
|
+
def self.client
|
|
15
|
+
Client.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.server
|
|
19
|
+
Server.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Client-side handling of Curve encryption.
|
|
23
|
+
class Client
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@key_store = CurveKeyStore.new("client_keys")
|
|
27
|
+
Pantry.logger.info("Configuring Client to use Curve encryption")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure_socket(socket)
|
|
31
|
+
socket.set(::ZMQ::CURVE_SERVERKEY, @key_store.server_public_key)
|
|
32
|
+
socket.set(::ZMQ::CURVE_PUBLICKEY, @key_store.public_key)
|
|
33
|
+
socket.set(::ZMQ::CURVE_SECRETKEY, @key_store.private_key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Server
|
|
39
|
+
|
|
40
|
+
attr_reader :authentication
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@key_store = CurveKeyStore.new("server_keys")
|
|
44
|
+
@authentication = Authentication.new(@key_store)
|
|
45
|
+
@authentication.open
|
|
46
|
+
|
|
47
|
+
# We log the server's public key here to make it accessible for initial setup.
|
|
48
|
+
Pantry.logger.info("Configuring Server to use Curve encryption :: #{@key_store.public_key}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def link_to(parent)
|
|
52
|
+
parent.link(@authentication)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configure_socket(socket)
|
|
56
|
+
socket.set(::ZMQ::CURVE_SERVER, 1)
|
|
57
|
+
socket.set(::ZMQ::CURVE_SECRETKEY, @key_store.private_key)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_client
|
|
61
|
+
@key_store.create_client
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
module Security
|
|
4
|
+
|
|
5
|
+
# The no-security security strategy
|
|
6
|
+
class NullSecurity
|
|
7
|
+
|
|
8
|
+
def self.client
|
|
9
|
+
new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.server
|
|
13
|
+
new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def link_to(parent)
|
|
17
|
+
# no-op
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure_socket(socket)
|
|
21
|
+
# no-op
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_client
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
|
|
4
|
+
# The SendSocket allows one-way asynchronous communication to the server.
|
|
5
|
+
# This is implemented through the ZMQ DEALER socket, which communicates with
|
|
6
|
+
# the Server's ROUTER socket.
|
|
7
|
+
class SendSocket < WritingSocket
|
|
8
|
+
|
|
9
|
+
def build_socket
|
|
10
|
+
Celluloid::ZMQ::DealerSocket.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def open_socket(socket)
|
|
14
|
+
socket.connect("tcp://#{host}:#{port}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
|
|
4
|
+
# Handles all serialization of Pantry::Messages to and from the ZeroMQ
|
|
5
|
+
# communication stack
|
|
6
|
+
class SerializeMessage
|
|
7
|
+
|
|
8
|
+
# To prevent accidents like trying to send the raw contents of a
|
|
9
|
+
# JSON file and end up with a Ruby hash on the other side, we designate
|
|
10
|
+
# messages as being JSON using a simple one character prefix. This way
|
|
11
|
+
# we don't have to guess if it's JSON or not and will leave non encoded
|
|
12
|
+
# strings alone. Don't want to dive into anything more complicated unless
|
|
13
|
+
# it's really necessary (like msgpack).
|
|
14
|
+
IS_JSON = '⁂'
|
|
15
|
+
|
|
16
|
+
# Convert a message into an array of message parts that will
|
|
17
|
+
# be sent through ZeroMQ.
|
|
18
|
+
def self.to_zeromq(message)
|
|
19
|
+
ToZeromq.new(message).perform
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Given an array of message parts from ZeroMQ, built up a Pantry::Message
|
|
23
|
+
# containing the included information.
|
|
24
|
+
def self.from_zeromq(parts)
|
|
25
|
+
FromZeromq.new(parts).perform
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class ToZeromq
|
|
29
|
+
def initialize(message)
|
|
30
|
+
@message = message
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def perform
|
|
34
|
+
[
|
|
35
|
+
@message.to || "",
|
|
36
|
+
@message.metadata.to_json,
|
|
37
|
+
encode_message_body
|
|
38
|
+
].flatten.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
def encode_message_body
|
|
44
|
+
@message.body.map do |entry|
|
|
45
|
+
case entry
|
|
46
|
+
when Hash, Array
|
|
47
|
+
"#{IS_JSON}#{entry.to_json}"
|
|
48
|
+
else
|
|
49
|
+
entry.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class FromZeromq
|
|
56
|
+
def initialize(parts)
|
|
57
|
+
@parts = parts
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def perform
|
|
61
|
+
Pantry::Message.new.tap do |message|
|
|
62
|
+
message.metadata = JSON.parse(@parts[1], symbolize_names: true)
|
|
63
|
+
message.to = @parts[0]
|
|
64
|
+
message.body = parse_body_parts(@parts[2..-1])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
protected
|
|
69
|
+
|
|
70
|
+
def parse_body_parts(body_parts)
|
|
71
|
+
body_parts.map do |raw_part|
|
|
72
|
+
part = raw_part.force_encoding("UTF-8")
|
|
73
|
+
|
|
74
|
+
if part.start_with?(IS_JSON)
|
|
75
|
+
JSON.parse(part[1..-1], symbolize_names: true) rescue part
|
|
76
|
+
else
|
|
77
|
+
raw_part
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|