pantry 0.0.0 → 0.1.0

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