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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +19 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +16 -0
  7. data/LICENSE +20 -0
  8. data/README.md +53 -0
  9. data/Rakefile +18 -0
  10. data/Vagrantfile +86 -0
  11. data/bin/pantry +11 -0
  12. data/bin/pantry-client +38 -0
  13. data/bin/pantry-server +33 -0
  14. data/dist/client.yml +79 -0
  15. data/dist/server.yml +56 -0
  16. data/dist/upstart/pantry-client.conf +12 -0
  17. data/dist/upstart/pantry-server.conf +12 -0
  18. data/doc/message_packet.dot +19 -0
  19. data/doc/message_packet.dot.png +0 -0
  20. data/doc/network_topology.dot +42 -0
  21. data/doc/network_topology.dot.png +0 -0
  22. data/lib/celluloid_zmq_patches.rb +16 -0
  23. data/lib/opt_parse_plus.rb +184 -0
  24. data/lib/pantry.rb +197 -0
  25. data/lib/pantry/cli.rb +154 -0
  26. data/lib/pantry/client.rb +131 -0
  27. data/lib/pantry/client_info.rb +34 -0
  28. data/lib/pantry/client_registry.rb +104 -0
  29. data/lib/pantry/command.rb +194 -0
  30. data/lib/pantry/command_handler.rb +53 -0
  31. data/lib/pantry/command_line.rb +115 -0
  32. data/lib/pantry/commands/create_client.rb +30 -0
  33. data/lib/pantry/commands/download_directory.rb +35 -0
  34. data/lib/pantry/commands/echo.rb +32 -0
  35. data/lib/pantry/commands/edit_application.rb +60 -0
  36. data/lib/pantry/commands/register_client.rb +38 -0
  37. data/lib/pantry/commands/status.rb +78 -0
  38. data/lib/pantry/commands/sync_directory.rb +50 -0
  39. data/lib/pantry/commands/update_application.rb +45 -0
  40. data/lib/pantry/commands/upload_file.rb +68 -0
  41. data/lib/pantry/communication.rb +20 -0
  42. data/lib/pantry/communication/client.rb +75 -0
  43. data/lib/pantry/communication/client_filter.rb +117 -0
  44. data/lib/pantry/communication/file_service.rb +125 -0
  45. data/lib/pantry/communication/file_service/file_progress.rb +164 -0
  46. data/lib/pantry/communication/file_service/receive_file.rb +97 -0
  47. data/lib/pantry/communication/file_service/send_file.rb +74 -0
  48. data/lib/pantry/communication/publish_socket.rb +20 -0
  49. data/lib/pantry/communication/reading_socket.rb +89 -0
  50. data/lib/pantry/communication/receive_socket.rb +23 -0
  51. data/lib/pantry/communication/security.rb +44 -0
  52. data/lib/pantry/communication/security/authentication.rb +98 -0
  53. data/lib/pantry/communication/security/curve_key_store.rb +120 -0
  54. data/lib/pantry/communication/security/curve_security.rb +70 -0
  55. data/lib/pantry/communication/security/null_security.rb +32 -0
  56. data/lib/pantry/communication/send_socket.rb +19 -0
  57. data/lib/pantry/communication/serialize_message.rb +84 -0
  58. data/lib/pantry/communication/server.rb +97 -0
  59. data/lib/pantry/communication/subscribe_socket.rb +33 -0
  60. data/lib/pantry/communication/wait_list.rb +45 -0
  61. data/lib/pantry/communication/writing_socket.rb +46 -0
  62. data/lib/pantry/config.rb +182 -0
  63. data/lib/pantry/file_editor.rb +67 -0
  64. data/lib/pantry/logger.rb +78 -0
  65. data/lib/pantry/message.rb +134 -0
  66. data/lib/pantry/multi_command.rb +36 -0
  67. data/lib/pantry/server.rb +132 -0
  68. data/lib/pantry/test/acceptance.rb +83 -0
  69. data/lib/pantry/test/support/fake_fs.rb +31 -0
  70. data/lib/pantry/test/support/matchers.rb +13 -0
  71. data/lib/pantry/test/support/minitest.rb +13 -0
  72. data/lib/pantry/test/support/mock_ui.rb +23 -0
  73. data/lib/pantry/test/unit.rb +13 -0
  74. data/lib/pantry/ui.rb +68 -0
  75. data/lib/pantry/version.rb +3 -0
  76. data/pantry.gemspec +40 -0
  77. data/test/acceptance/cli/error_handling_test.rb +7 -0
  78. data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
  79. data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
  80. data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
  81. data/test/acceptance/communication/heartbeat_test.rb +19 -0
  82. data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
  83. data/test/acceptance/communication/security_test.rb +117 -0
  84. data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
  85. data/test/acceptance/test_helper.rb +25 -0
  86. data/test/fixtures/config.yml +22 -0
  87. data/test/fixtures/empty.yml +2 -0
  88. data/test/fixtures/file_to_upload +3 -0
  89. data/test/root_dir/.gitkeep +0 -0
  90. data/test/unit/cli_test.rb +173 -0
  91. data/test/unit/client_registry_test.rb +61 -0
  92. data/test/unit/client_test.rb +128 -0
  93. data/test/unit/command_handler_test.rb +79 -0
  94. data/test/unit/command_line_test.rb +5 -0
  95. data/test/unit/command_test.rb +206 -0
  96. data/test/unit/commands/create_client_test.rb +25 -0
  97. data/test/unit/commands/download_directory_test.rb +58 -0
  98. data/test/unit/commands/echo_test.rb +22 -0
  99. data/test/unit/commands/edit_application_test.rb +84 -0
  100. data/test/unit/commands/register_client_test.rb +41 -0
  101. data/test/unit/commands/status_test.rb +81 -0
  102. data/test/unit/commands/sync_directory_test.rb +75 -0
  103. data/test/unit/commands/update_application_test.rb +35 -0
  104. data/test/unit/commands/upload_file_test.rb +51 -0
  105. data/test/unit/communication/client_filter_test.rb +262 -0
  106. data/test/unit/communication/client_test.rb +99 -0
  107. data/test/unit/communication/file_service/receive_file_test.rb +214 -0
  108. data/test/unit/communication/file_service/send_file_test.rb +110 -0
  109. data/test/unit/communication/file_service_test.rb +56 -0
  110. data/test/unit/communication/publish_socket_test.rb +19 -0
  111. data/test/unit/communication/reading_socket_test.rb +110 -0
  112. data/test/unit/communication/receive_socket_test.rb +20 -0
  113. data/test/unit/communication/security/authentication_test.rb +97 -0
  114. data/test/unit/communication/security/curve_key_store_test.rb +110 -0
  115. data/test/unit/communication/security/curve_security_test.rb +44 -0
  116. data/test/unit/communication/security/null_security_test.rb +15 -0
  117. data/test/unit/communication/security_test.rb +49 -0
  118. data/test/unit/communication/send_socket_test.rb +19 -0
  119. data/test/unit/communication/serialize_message_test.rb +128 -0
  120. data/test/unit/communication/server_test.rb +106 -0
  121. data/test/unit/communication/subscribe_socket_test.rb +46 -0
  122. data/test/unit/communication/wait_list_test.rb +60 -0
  123. data/test/unit/communication/writing_socket_test.rb +46 -0
  124. data/test/unit/config_test.rb +150 -0
  125. data/test/unit/logger_test.rb +79 -0
  126. data/test/unit/message_test.rb +179 -0
  127. data/test/unit/multi_command_test.rb +45 -0
  128. data/test/unit/opt_parse_plus_test.rb +218 -0
  129. data/test/unit/pantry_test.rb +82 -0
  130. data/test/unit/server_test.rb +166 -0
  131. data/test/unit/test_helper.rb +25 -0
  132. data/test/unit/ui_test.rb +58 -0
  133. 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