omf_common 6.0.0 → 6.0.2.pre.1

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 (50) hide show
  1. data/Gemfile +4 -0
  2. data/bin/file_broadcaster.rb +56 -0
  3. data/bin/file_receiver.rb +62 -0
  4. data/bin/omf_keygen +21 -0
  5. data/bin/{monitor_topic.rb → omf_monitor_topic} +21 -8
  6. data/bin/omf_send_create +118 -0
  7. data/bin/{send_request.rb → omf_send_request} +12 -7
  8. data/example/engine_alt.rb +23 -24
  9. data/example/ls_app.yaml +21 -0
  10. data/lib/omf_common.rb +73 -12
  11. data/lib/omf_common/auth.rb +15 -0
  12. data/lib/omf_common/auth/certificate.rb +174 -0
  13. data/lib/omf_common/auth/certificate_store.rb +72 -0
  14. data/lib/omf_common/auth/ssh_pub_key_convert.rb +80 -0
  15. data/lib/omf_common/comm.rb +66 -9
  16. data/lib/omf_common/comm/amqp/amqp_communicator.rb +40 -13
  17. data/lib/omf_common/comm/amqp/amqp_file_transfer.rb +259 -0
  18. data/lib/omf_common/comm/amqp/amqp_topic.rb +14 -21
  19. data/lib/omf_common/comm/local/local_communicator.rb +31 -2
  20. data/lib/omf_common/comm/local/local_topic.rb +19 -3
  21. data/lib/omf_common/comm/topic.rb +48 -34
  22. data/lib/omf_common/comm/xmpp/communicator.rb +19 -10
  23. data/lib/omf_common/comm/xmpp/topic.rb +22 -81
  24. data/lib/omf_common/default_logging.rb +11 -0
  25. data/lib/omf_common/eventloop.rb +14 -0
  26. data/lib/omf_common/eventloop/em.rb +39 -6
  27. data/lib/omf_common/eventloop/local_evl.rb +15 -0
  28. data/lib/omf_common/exec_app.rb +29 -15
  29. data/lib/omf_common/message.rb +53 -5
  30. data/lib/omf_common/message/json/json_message.rb +149 -39
  31. data/lib/omf_common/message/xml/message.rb +112 -39
  32. data/lib/omf_common/protocol/6.0.rnc +5 -1
  33. data/lib/omf_common/protocol/6.0.rng +12 -0
  34. data/lib/omf_common/version.rb +1 -1
  35. data/omf_common.gemspec +7 -2
  36. data/test/fixture/omf_test.cert.pem +15 -0
  37. data/test/fixture/omf_test.pem +15 -0
  38. data/test/fixture/omf_test.pub +1 -0
  39. data/test/fixture/omf_test.pub.pem +6 -0
  40. data/test/omf_common/auth/certificate_spec.rb +113 -0
  41. data/test/omf_common/auth/ssh_pub_key_convert_spec.rb +13 -0
  42. data/test/omf_common/comm/topic_spec.rb +175 -0
  43. data/test/omf_common/comm/xmpp/communicator_spec.rb +15 -16
  44. data/test/omf_common/comm/xmpp/topic_spec.rb +63 -10
  45. data/test/omf_common/comm_spec.rb +66 -9
  46. data/test/omf_common/message/xml/message_spec.rb +43 -13
  47. data/test/omf_common/message_spec.rb +14 -0
  48. data/test/test_helper.rb +25 -0
  49. metadata +78 -15
  50. data/bin/send_create.rb +0 -94
@@ -0,0 +1,21 @@
1
+ #
2
+ # Describes how to run simple 'ls' on a node
3
+ #
4
+ create:
5
+ type: application
6
+ properties:
7
+ binary_path: /bin/ls
8
+ state: running
9
+ membership: apps
10
+ #environment: # (Hash) the environment variables to set prior to starting this app.
11
+ parameters:
12
+ p1:
13
+ cmd: -l
14
+ value: true
15
+ type: Boolean
16
+ order: 1
17
+ p2:
18
+ cmd: /
19
+ value: true
20
+ type: Boolean
21
+ order: 2
data/lib/omf_common.rb CHANGED
@@ -6,7 +6,7 @@ require 'omf_common/measure'
6
6
  require 'omf_common/message'
7
7
  require 'omf_common/comm'
8
8
  require 'omf_common/command'
9
- require 'omf_common/key'
9
+ require 'omf_common/auth'
10
10
  require 'omf_common/core_ext/string'
11
11
  require 'omf_common/eventloop'
12
12
 
@@ -19,8 +19,9 @@ module OmfCommon
19
19
  type: 'em'
20
20
  },
21
21
  logging: {
22
- level: 'debug',
23
-
22
+ level: {
23
+ default: 'debug'
24
+ },
24
25
  appenders: {
25
26
  stdout: {
26
27
  date_pattern: '%H:%M:%S',
@@ -35,8 +36,35 @@ module OmfCommon
35
36
  type: :em
36
37
  },
37
38
  logging: {
38
- level: 'info',
39
+ level: {
40
+ default: 'info'
41
+ },
42
+ appenders: {
43
+ file: {
44
+ log_dir: '/var/log',
45
+ #log_file: 'foo.log',
46
+ date_pattern: '%F %T %z',
47
+ pattern: '[%d] %-5l %c: %m\n'
48
+ }
49
+ }
39
50
 
51
+ }
52
+ },
53
+ daemon: {
54
+ daemonize: {
55
+ dir_mode: :script,
56
+ dir: '/tmp',
57
+ backtrace: true,
58
+ log_dir: '/var/log',
59
+ log_output: true
60
+ },
61
+ eventloop: {
62
+ type: :em
63
+ },
64
+ logging: {
65
+ level: {
66
+ default: 'info'
67
+ },
40
68
  appenders: {
41
69
  file: {
42
70
  log_dir: '/var/log',
@@ -54,8 +82,9 @@ module OmfCommon
54
82
  },
55
83
  eventloop: { type: :local},
56
84
  logging: {
57
- level: 'debug',
58
-
85
+ level: {
86
+ default: 'debug'
87
+ },
59
88
  appenders: {
60
89
  stdout: {
61
90
  date_pattern: '%H:%M:%S',
@@ -65,7 +94,7 @@ module OmfCommon
65
94
  }
66
95
  }
67
96
  },
68
- test_dev: {
97
+ test_daemon: {
69
98
  daemonize: {
70
99
  dir_mode: :script,
71
100
  dir: '/tmp',
@@ -74,10 +103,12 @@ module OmfCommon
74
103
  log_output: true
75
104
  },
76
105
  eventloop: {
77
- type: :local
106
+ type: :em
78
107
  },
79
108
  logging: {
80
- level: 'debug',
109
+ level: {
110
+ default: 'debug'
111
+ },
81
112
  appenders: {
82
113
  file: {
83
114
  log_dir: '/tmp',
@@ -120,9 +151,14 @@ module OmfCommon
120
151
  unless copts = opts[:communication]
121
152
  raise "Missing :communication description"
122
153
  end
123
- eopts = opts[:eventloop]
154
+
155
+ if aopts = opts[:auth]
156
+ require 'omf_common/auth/credential_store'
157
+ OmfCommon::Auth::CredentialStore.init(aopts)
158
+ end
124
159
 
125
160
  # Initialise event loop
161
+ eopts = opts[:eventloop]
126
162
  Eventloop.init(eopts)
127
163
  # start eventloop immediately if we received a run block
128
164
  eventloop.run do
@@ -151,6 +187,10 @@ module OmfCommon
151
187
  # :same - Look in the same directory as '$0'
152
188
  # :remove_root ROOT_NAME: Remove the root node. Throw exception if not ROOT_NAME
153
189
  # :wait_for_readable SECS: Wait until the yaml file becomes readable. Check every SECS
190
+ # :erb_process flag: Run the content of the loaded file through ERB first before YAML parsing
191
+ # :erb_safe_level level: If safe_level is set to a non-nil value, ERB code will be run in a
192
+ # separate thread with $SAFE set to the provided level.
193
+ # :erb_binding binding: Optional binding given to ERB#result
154
194
  #
155
195
  def self.load_yaml(file_name, opts = {})
156
196
  if path_opt = opts[:path]
@@ -167,7 +207,14 @@ module OmfCommon
167
207
  sleep readable_check # wait until file shows up
168
208
  end
169
209
  end
170
- yh = YAML.load_file(file_name)
210
+
211
+ str = File.read(file_name)
212
+ if opts[:erb_process]
213
+ require 'erb'
214
+ str = ERB.new(str, opts[:erb_safe_level]).result(opts[:erb_binding] || binding)
215
+ end
216
+ yh = YAML.load(str)
217
+
171
218
  if opts[:symbolize_keys]
172
219
  yh = _rec_sym_keys(yh)
173
220
  end
@@ -209,11 +256,25 @@ module OmfCommon
209
256
  end
210
257
  end
211
258
  if level = opts[:level]
212
- logger.level = level.to_sym
259
+ if level.is_a? Hash
260
+ # package level settings
261
+ level.each do |name, lvl|
262
+ if name.to_s == 'default'
263
+ logger.level = lvl.to_sym
264
+ else
265
+ Logging.logger[name.to_s].level = lvl.to_sym
266
+ end
267
+ end
268
+ else
269
+ logger.level = level.to_sym
270
+ end
213
271
  end
214
272
  end
215
273
 
216
274
  def self._rec_merge(this_hash, other_hash)
275
+ # if the dominant side is not a hash we stop recursing and pick the primitive value
276
+ return other_hash unless other_hash.is_a? Hash
277
+
217
278
  r = {}
218
279
  this_hash.merge(other_hash) do |key, oldval, newval|
219
280
  r[key] = oldval.is_a?(Hash) ? _rec_merge(oldval, newval) : newval
@@ -0,0 +1,15 @@
1
+
2
+
3
+ module OmfCommon
4
+ module Auth
5
+
6
+ class AuthException < StandardError; end
7
+
8
+ def self.init(opts = {})
9
+ CertificateStore.init(opts)
10
+ end
11
+ end
12
+ end
13
+
14
+ require 'omf_common/auth/certificate_store'
15
+ require 'omf_common/auth/certificate'
@@ -0,0 +1,174 @@
1
+ require 'openssl'
2
+ require 'omf_common/auth'
3
+ require 'omf_common/auth/ssh_pub_key_convert'
4
+
5
+ module OmfCommon::Auth
6
+
7
+ class Certificate
8
+ DEF_DOMAIN_NAME = 'acme'
9
+ DEF_DURATION = 3600
10
+
11
+ BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n"
12
+ END_CERT = "\n-----END CERTIFICATE-----\n"
13
+ @@serial = 0
14
+
15
+ # @param [String] name unique name of the entity (resource name)
16
+ # @param [String] type type of the entity (resource type)
17
+ # @param [String] domain of the resource
18
+ #
19
+ def self.create(address, name, type, domain = DEF_DOMAIN_NAME, issuer = nil, not_before = Time.now, duration = 3600, key = nil)
20
+ subject = _create_name(name, type, domain)
21
+ if key.nil?
22
+ key, digest = _create_key()
23
+ else
24
+ digest = _create_digest
25
+ end
26
+
27
+ c = _create_x509_cert(address, subject, key, digest, issuer, not_before, duration)
28
+ c[:address] = address if address
29
+ self.new c
30
+ end
31
+
32
+ # @param [String] pem is the content of existing x509 cert
33
+ # @param [OpenSSL::PKey::RSA|String] key is the private key which can be attached to the instance for signing.
34
+ def self.create_from_x509(pem, key = nil)
35
+ unless pem.start_with? BEGIN_CERT
36
+ pem = "#{BEGIN_CERT}#{pem}#{END_CERT}"
37
+ end
38
+ cert = OpenSSL::X509::Certificate.new(pem)
39
+
40
+ key = OpenSSL::PKey::RSA.new(key) if key && key.is_a?(String)
41
+
42
+ if key && !cert.check_private_key(key)
43
+ raise ArgumentError, "Private key provided could not match the public key of given certificate"
44
+ end
45
+ self.new({ cert: cert, key: key })
46
+ end
47
+
48
+ # Returns an array with a new RSA key and a SHA1 digest
49
+ #
50
+ def self._create_key(size = 2048)
51
+ [OpenSSL::PKey::RSA.new(size), OpenSSL::Digest::SHA1.new]
52
+ end
53
+
54
+ def self._create_digest
55
+ OpenSSL::Digest::SHA1.new
56
+ end
57
+
58
+ # @param [String] name unique name of the entity (resource name)
59
+ # @param [String] type type of the entity (resource type)
60
+ #
61
+ def self._create_name(name, type, domain = DEF_DOMAIN_NAME)
62
+ OpenSSL::X509::Name.new [['CN', "frcp//#{domain}//frcp.#{type}.#{name}"]], {}
63
+ end
64
+
65
+ # Create a X509 certificate
66
+ #
67
+ # @param [] address
68
+ # @return {cert, key}
69
+ #
70
+ def self._create_x509_cert(address, subject, key, digest = nil,
71
+ issuer = nil, not_before = Time.now, duration = DEF_DURATION, extensions = [])
72
+ extensions << ["subjectAltName", "URI:#{address}", false] if address
73
+
74
+ cert = OpenSSL::X509::Certificate.new
75
+ cert.version = 2
76
+ # TODO change serial to non-sequential secure random numbers for production use
77
+ cert.serial = (@@serial += 1)
78
+ cert.subject = subject
79
+ cert.public_key = key.public_key
80
+ cert.not_before = not_before
81
+ cert.not_after = not_before + duration
82
+ unless extensions.empty?
83
+ issuer_cert = issuer ? issuer.to_x509 : cert
84
+ ef = OpenSSL::X509::ExtensionFactory.new
85
+ ef.subject_certificate = cert
86
+ ef.issuer_certificate = issuer_cert
87
+ extensions.each{|oid, value, critical|
88
+ cert.add_extension(ef.create_extension(oid, value, critical))
89
+ }
90
+ end
91
+ if issuer
92
+ cert.issuer = issuer.subject
93
+ cert.sign(issuer.key, issuer.digest)
94
+ else
95
+ # self signed
96
+ cert.issuer = subject
97
+ cert.sign(key, digest)
98
+ end
99
+ { cert: cert, key: key }
100
+ end
101
+
102
+ attr_reader :address, :subject, :key, :digest
103
+
104
+ def initialize(opts)
105
+ if @cert = opts[:cert]
106
+ @subject = @cert.subject
107
+ end
108
+ unless @address = opts[:address]
109
+ # try to see it it is in cert
110
+ if @cert
111
+ @cert.extensions.each do |ext|
112
+ if ext.oid == 'subjectAltName'
113
+ @address = ext.value[4 .. -1] # strip off 'URI:'
114
+ end
115
+ end
116
+ end
117
+ end
118
+ if @key = opts[:key]
119
+ @digest = opts[:digest] || OpenSSL::Digest::SHA1.new
120
+ end
121
+ unless @subject ||= opts[:subject]
122
+ name = opts[:name]
123
+ type = opts[:type]
124
+ domain = opts[:domain]
125
+ @subject = _create_name(name, type, domain)
126
+ end
127
+ @cert ||= _create_x509_cert(@address, @subject, @key, @digest)[:cert]
128
+ end
129
+
130
+ def create_for(address, name, type, domain = DEF_DOMAIN_NAME, duration = 3600, key = nil)
131
+ raise ArgumentError, "Address required" unless address
132
+ cert = self.class.create(address, name, type, domain, self, Time.now, duration, key)
133
+ CertificateStore.instance.register(cert, address)
134
+ cert
135
+ end
136
+
137
+ # Return the X509 certificate. If it hasn't been passed in, return a self-signed one
138
+ def to_x509()
139
+ @cert
140
+ end
141
+
142
+ def can_sign?
143
+ !@key.nil? && @key.private?
144
+ end
145
+
146
+ def to_pem
147
+ to_x509.to_pem
148
+ end
149
+
150
+ def to_pem_compact
151
+ to_pem.lines.to_a[1 ... -1].join.strip
152
+ end
153
+
154
+ def verify_cert
155
+ if @cert.issuer == self.subject # self signed cert
156
+ @cert.verify(@cert.public_key)
157
+ else
158
+ @cert.verify(CertificateStore.instance.cert_for(@cert.issuer).to_x509.public_key)
159
+ end
160
+ end
161
+
162
+ # Will return one of the following
163
+ #
164
+ # :HS256, :HS384, :HS512, :RS256, :RS384, :RS512, :ES256, :ES384, :ES512
165
+ #
166
+ # def key_algorithm
167
+ #
168
+ # end
169
+
170
+ def to_s
171
+ "#<#{self.class} addr=#{@address} subj=#{@subject} can-sign=#{@key != nil}>"
172
+ end
173
+ end # class
174
+ end # module
@@ -0,0 +1,72 @@
1
+ require 'openssl'
2
+
3
+ require 'omf_common/auth'
4
+
5
+ #require 'singleton'
6
+
7
+ # module OmfCommon
8
+ # class Key
9
+ # include Singleton
10
+ #
11
+ # attr_accessor :private_key
12
+ #
13
+ # def import(filename)
14
+ # self.private_key = OpenSSL::PKey.read(File.read(filename))
15
+ # end
16
+ # end
17
+ # end
18
+
19
+ module OmfCommon::Auth
20
+
21
+ class MissingPrivateKeyException < AuthException; end
22
+
23
+ class CertificateStore
24
+
25
+
26
+ @@instance = nil
27
+
28
+ def self.init(opts = {})
29
+ if @@instance
30
+ raise "CertificateStore already iniitalised"
31
+ end
32
+ @@instance = self.new(opts)
33
+ end
34
+
35
+ def self.instance
36
+ throw "CertificateStore not initialized" unless @@instance
37
+ @@instance
38
+ end
39
+
40
+ def register(certificate, address = nil)
41
+ if address ||= certificate.address
42
+ @certs[address] = certificate if address
43
+ else
44
+ warn "Register certificate without address - #{certificate}"
45
+ end
46
+ @certs[certificate.subject] = certificate
47
+ end
48
+
49
+ def register_x509(cert_pem, address = nil)
50
+ if (cert = Certificate.create_from_x509(cert_pem))
51
+ debug "REGISTERED #{cert}"
52
+ register(cert, address)
53
+ end
54
+ end
55
+
56
+ def cert_for(url)
57
+ @certs[url]
58
+ end
59
+
60
+
61
+ private
62
+ def initialize(opts)
63
+ @certs = {}
64
+ if store = opts[:store]
65
+ else
66
+ @store = {private: {}, public: {}}
67
+ end
68
+ @serial = 0
69
+ end
70
+ end # class
71
+
72
+ end # module
@@ -0,0 +1,80 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'omf_common/auth'
4
+
5
+ module OmfCommon::Auth
6
+ # This file provides a converter that accepts an SSH public key string
7
+ # and converts it to an OpenSSL::PKey::RSA object for use in verifying
8
+ # received messages. (DSA support pending).
9
+ #
10
+ class SSHPubKeyConvert
11
+ # Unpack a 4-byte unsigned integer from the +bytes+ array.
12
+ #
13
+ # Returns a pair (+u32+, +bytes+), where +u32+ is the extracted
14
+ # unsigned integer, and +bytes+ is the remainder of the original
15
+ # +bytes+ array that follows +u32+.
16
+ #
17
+ def self.unpack_u32(bytes)
18
+ return bytes.unpack("N")[0], bytes[4..-1]
19
+ end
20
+
21
+ # Unpack a string from the +bytes+ array. Exactly +len+ bytes will
22
+ # be extracted.
23
+ #
24
+ # Returns a pair (+string+, +bytes+), where +string+ is the
25
+ # extracted string (of length +len+), and +bytes+ is the remainder
26
+ # of the original +bytes+ array that follows +string+.
27
+ #
28
+ def self.unpack_string(bytes, len)
29
+ return bytes.unpack("A#{len}")[0], bytes[len..-1]
30
+ end
31
+
32
+ # Convert a string in SSH public key format to a key object
33
+ # suitable for use with OpenSSL. If the key is an RSA key then an
34
+ # OpenSSL::PKey::RSA object is returned. If the key is a DSA key
35
+ # then an OpenSSL::PKey::DSA object is returned. In either case,
36
+ # the object returned is suitable for encrypting data or verifying
37
+ # signatures, but cannot be used for decrypting or signing.
38
+ #
39
+ # The +keystring+ should be a single line, as per an SSH public key
40
+ # file as generated by +ssh-keygen+, or a line from an SSH
41
+ # +authorized_keys+ file.
42
+ #
43
+ def self.convert(keystring)
44
+ (type, b64, id) = keystring.split(' ')
45
+ decoded_key = Base64.decode64(b64)
46
+ (n, bytes) = unpack_u32(decoded_key)
47
+ (keytype, bytes) = unpack_string(bytes, n)
48
+
49
+ if keytype == "ssh-rsa"
50
+ (n, bytes) = unpack_u32(bytes)
51
+ (estr, bytes) = unpack_string(bytes, n)
52
+ (n, bytes) = unpack_u32(bytes)
53
+ (nstr, bytes) = unpack_string(bytes, n)
54
+
55
+ key = OpenSSL::PKey::RSA.new
56
+ key.n = OpenSSL::BN.new(nstr, 2)
57
+ key.e = OpenSSL::BN.new(estr, 2)
58
+ key
59
+ elsif keytype == 'ssh-dss'
60
+ (n, bytes) = unpack_u32(bytes)
61
+ (pstr, bytes) = unpack_string(bytes, n)
62
+ (n, bytes) = unpack_u32(bytes)
63
+ (qstr, bytes) = unpack_string(bytes, n)
64
+ (n, bytes) = unpack_u32(bytes)
65
+ (gstr, bytes) = unpack_string(bytes, n)
66
+ (n, bytes) = unpack_u32(bytes)
67
+ (pkstr, bytes) = unpack_string(bytes, n)
68
+
69
+ key = OpenSSL::PKey::DSA.new
70
+ key.p = OpenSSL::BN.new(pstr, 2)
71
+ key.q = OpenSSL::BN.new(qstr, 2)
72
+ key.g = OpenSSL::BN.new(gstr, 2)
73
+ key.pub_key = OpenSSL::BN.new(pkstr, 2)
74
+ key
75
+ else
76
+ raise ArgumentError, "Unknown key type '#{keytype}'"
77
+ end
78
+ end
79
+ end
80
+ end