pcp-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9b54b2bb443dc46522c23c6dd86a444ac6cd12fd
4
+ data.tar.gz: 2df868e9976252c6e01d912dc0df0e35c6d6d780
5
+ SHA512:
6
+ metadata.gz: 241e5b58d9565e9a2c8feb30dabf421a9e5631b980140629d93dd84a418a0ebbd300c4ef5bdb290e0173d118bdce6e1d5753cb16a084748621be7d1dcf8878b9
7
+ data.tar.gz: 215de150101529209af1c283e542ad75093ad13f6559ac44613617557a25b59a4d3a974080334df3862e881be72ac1eae67f9f41e887e3636934b8a82e89b715
@@ -0,0 +1,3 @@
1
+ # Puppet Communication Protocol
2
+ module PCP
3
+ end
@@ -0,0 +1,162 @@
1
+ require 'eventmachine-le'
2
+ require 'faye/websocket'
3
+ require 'pcp/message'
4
+ require 'logger'
5
+
6
+ module PCP
7
+ # Manages a client connection to a pcp broker
8
+ class Client
9
+ # Read the @identity property
10
+ #
11
+ # @api public
12
+ # @return [String]
13
+ attr_accessor :identity
14
+
15
+ # Set a proc that will be used to handle messages
16
+ #
17
+ # @api public
18
+ # @return [Proc]
19
+ attr_accessor :on_message
20
+
21
+ # Construct a new disconnected client
22
+ #
23
+ # @api public
24
+ # @param params [Hash<Symbol,Object>]
25
+ # @return a new client
26
+ def initialize(params = {})
27
+ @server = params[:server] || 'wss://localhost:8142/pcp'
28
+ @ssl_key = params[:ssl_key]
29
+ @ssl_cert = params[:ssl_cert]
30
+ @logger = Logger.new(STDOUT)
31
+ @logger.level = params[:loglevel] || Logger::WARN
32
+ @connection = nil
33
+ type = params[:type] || "ruby-pcp-client-#{$$}"
34
+ @identity = make_identity(@ssl_cert, type)
35
+ @on_message = params[:on_message]
36
+ @associated = false
37
+ end
38
+
39
+ # Connect to the server
40
+ #
41
+ # @api public
42
+ # @param seconds [Numeric]
43
+ # @return [true,false,nil]
44
+ def connect(seconds = 0)
45
+ mutex = Mutex.new
46
+ associated_cv = ConditionVariable.new
47
+
48
+ @logger.debug { [:connect, @server] }
49
+ @connection = Faye::WebSocket::Client.new(@server, nil, {:tls => {:private_key_file => @ssl_key,
50
+ :cert_chain_file => @ssl_cert,
51
+ :ssl_version => :TLSv1}})
52
+
53
+ @connection.on :open do |event|
54
+ begin
55
+ @logger.info { [:open] }
56
+ send(associate_request)
57
+ rescue Exception => e
58
+ @logger.error { [:open_exception, e] }
59
+ end
60
+ end
61
+
62
+ @connection.on :message do |event|
63
+ begin
64
+ message = ::PCP::Message.new(event.data)
65
+ @logger.debug { [:message, :decoded, message] }
66
+
67
+ if message[:message_type] == 'http://puppetlabs.com/associate_response'
68
+ mutex.synchronize do
69
+ @associated = JSON.load(message.data)["success"]
70
+ associated_cv.signal
71
+ end
72
+ elsif @on_message
73
+ @on_message.call(message)
74
+ end
75
+ rescue Exception => e
76
+ @logger.error { [:message_exception, e] }
77
+ end
78
+ end
79
+
80
+ @connection.on :close do |event|
81
+ begin
82
+ @logger.info { [:close, event.code, event.reason] }
83
+ mutex.synchronize do
84
+ @associated = false
85
+ associated_cv.signal
86
+ end
87
+ rescue Exception => e
88
+ @logger.error { [:close_exception, e] }
89
+ end
90
+ end
91
+
92
+ @connection.on :error do |event|
93
+ @logger.error { [:error, event] }
94
+ @associated = false
95
+ end
96
+
97
+ begin
98
+ Timeout::timeout(seconds) do
99
+ mutex.synchronize do
100
+ associated_cv.wait(mutex)
101
+ return @associated
102
+ end
103
+ end
104
+ rescue Timeout::Error
105
+ return nil
106
+ end
107
+ end
108
+
109
+ # Is the client associated with the server
110
+ #
111
+ # @api public
112
+ # @return [true,false]
113
+ def associated?
114
+ @associated
115
+ end
116
+
117
+ # Send a message to the server
118
+ #
119
+ # @api public
120
+ # @param message [PCP::Message]
121
+ # @return unused
122
+ def send(message)
123
+ @logger.debug { [:send, message] }
124
+ message[:sender] = identity
125
+ @connection.send(message.encode)
126
+ end
127
+
128
+ private
129
+
130
+ # Get the common name from an X509 certficate in file
131
+ #
132
+ # @api private
133
+ # @param [String] file
134
+ # @return [String]
135
+ def get_common_name(file)
136
+ raw = File.read file
137
+ cert = OpenSSL::X509::Certificate.new raw
138
+ cert.subject.to_a.assoc('CN')[1]
139
+ end
140
+
141
+ # Make the PCP Uri for this client
142
+ #
143
+ # @api private
144
+ # @param cert [String]
145
+ # @param type [String]
146
+ # @return [String]
147
+ def make_identity(cert, type)
148
+ cn = get_common_name(cert)
149
+ "pcp://#{cn}/#{type}"
150
+ end
151
+
152
+ # Make an association request message for this client
153
+ #
154
+ # @api private
155
+ # @return [PCP::Message]
156
+ def associate_request
157
+ Message.new({:message_type => 'http://puppetlabs.com/associate_request',
158
+ :sender => @identity,
159
+ :targets => ['pcp:///server']}).expires(3)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,165 @@
1
+ require 'json'
2
+ require 'securerandom'
3
+ require 'time'
4
+ require 'pcp/protocol'
5
+
6
+ module PCP
7
+ # Represent a message that can be sent via PCP::Client
8
+ class Message
9
+ # Read access to the @envelope property
10
+ #
11
+ # @api public
12
+ # @return [Hash<Symbol,Object>]
13
+ attr_reader :envelope
14
+
15
+ # Construct a new message or decode one
16
+ #
17
+ # @api public
18
+ # @param envelope_or_bytes [Hash<Symbol,Object>,Array<Integer>]
19
+ # When supplied a Hash it is taken as being a collection of
20
+ # envelope fields.
21
+ # When supplied an Array it is taken as being an array of
22
+ # byte values, a message in wire format.
23
+ # @return a new object
24
+ def initialize(envelope_or_bytes = {})
25
+ @chunks = ['', '']
26
+
27
+ case envelope_or_bytes
28
+ when Array
29
+ # it's bytes
30
+ decode(envelope_or_bytes)
31
+ when Hash
32
+ # it's an envelope
33
+ default_envelope = {:id => SecureRandom.uuid}
34
+ @envelope = default_envelope.merge(envelope_or_bytes)
35
+ else
36
+ raise ArgumentError, "Unhandled type"
37
+ end
38
+ end
39
+
40
+ # Set the expiry of the message
41
+ #
42
+ # @api public
43
+ # @param seconds [Numeric]
44
+ # @return the object itself
45
+ def expires(seconds)
46
+ @envelope[:expires] = (Time.now + seconds).utc.iso8601
47
+ self
48
+ end
49
+
50
+ # Set an envelope field
51
+ #
52
+ # @api public
53
+ # @param key [Symbol]
54
+ # @param value
55
+ # @return value
56
+ def []=(key, value)
57
+ @envelope[key] = value
58
+ end
59
+
60
+ # Get an envelope field
61
+ #
62
+ # @api public
63
+ # @param key [Symbol]
64
+ # @return value associated with that key
65
+ def [](key)
66
+ @envelope[key]
67
+ end
68
+
69
+ # Get the content of the data chunk
70
+ #
71
+ # @api public
72
+ # @return current content of the data chunk
73
+ def data
74
+ @chunks[0]
75
+ end
76
+
77
+ # Sets the content for the data chunk
78
+ #
79
+ # @api public
80
+ # @param value
81
+ # @return value
82
+ def data=(value)
83
+ @chunks[0] = value
84
+ end
85
+
86
+ # Get the content of the debug chunk
87
+ #
88
+ # @api public
89
+ # @return current content of the debug chunk
90
+ def debug
91
+ @chunks[1]
92
+ end
93
+
94
+ # Sets the content for the debug chunk
95
+ #
96
+ # @api public
97
+ # @param value
98
+ # @return value
99
+ def debug=(value)
100
+ @chunks[1] = value
101
+ end
102
+
103
+ # Encodes the message as an array of byte values
104
+ #
105
+ # @api public
106
+ # @return [Array<Integer>]
107
+ def encode
108
+ chunks = []
109
+
110
+ @chunks.each_index do |i|
111
+ chunks << frame_chunk(i + 2, @chunks[i])
112
+ end
113
+
114
+ RSchema.validate!(PCP::Protocol::Envelope, envelope)
115
+
116
+ [1, frame_chunk(1, envelope.to_json), chunks].flatten
117
+ end
118
+
119
+ private
120
+
121
+ # Decodes an array of bytes into the message
122
+ #
123
+ # @api private
124
+ # @param bytes [Array<Integer>]
125
+ # @return ignore
126
+ def decode(bytes)
127
+ version = bytes.shift
128
+
129
+ unless version == 1
130
+ raise "Can only handle type 1 messages"
131
+ end
132
+
133
+ while bytes.size > 0
134
+ type = bytes.shift
135
+ size = bytes.take(4).pack('C*').unpack('N')[0]
136
+ bytes = bytes.drop(4)
137
+
138
+ body = bytes.take(size).pack('C*')
139
+ bytes = bytes.drop(size)
140
+
141
+ if type == 1
142
+ parsed = JSON.parse(body)
143
+ @envelope = {}
144
+ parsed.each do |k,v|
145
+ @envelope[k.to_sym] = v
146
+ end
147
+ RSchema.validate!(PCP::Protocol::Envelope, @envelope)
148
+ else
149
+ @chunks[type - 2] = body
150
+ end
151
+ end
152
+ end
153
+
154
+ # Frames a piece of data as a message chunk of given type
155
+ #
156
+ # @api private
157
+ # @param type [Integer]
158
+ # @param body [String]
159
+ # @return [Array<Integer>]
160
+ def frame_chunk(type, body)
161
+ size = [body.bytesize].pack('N').unpack('C*')
162
+ [type, size, body.bytes.to_a].flatten
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,45 @@
1
+ require 'rschema'
2
+
3
+ module PCP
4
+ # Definitions of RSchema schemas for protocol definitions
5
+ module Protocol
6
+ # A [String] that represents a time according to ISO8601
7
+ ISO8601 = RSchema.schema do
8
+ predicate do |t|
9
+ begin
10
+ t.is_a?(String) && Time.parse(t)
11
+ rescue ArgumentError
12
+ # Time.parse raises an ArgumentError if the time isn't parsable
13
+ false
14
+ end
15
+ end
16
+ end
17
+
18
+ # A [String] that is a message id
19
+ MessageId = RSchema.schema do
20
+ predicate do |id|
21
+ id.is_a?(String) && id.match(%r{^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$})
22
+ end
23
+ end
24
+
25
+ # A [String] that is a PCP uri
26
+ Uri = RSchema.schema do
27
+ predicate do |uri|
28
+ uri.is_a?(String) && uri.match(%r{^pcp://([^/]*)/[^/]+$})
29
+ end
30
+ end
31
+
32
+ # A [Hash] that complies to the properties of an Envelope
33
+ Envelope = RSchema.schema do
34
+ {
35
+ :id => MessageId,
36
+ optional(:'in-reply-to') => MessageId,
37
+ :sender => Uri,
38
+ :targets => [Uri],
39
+ :message_type => String,
40
+ :expires => ISO8601,
41
+ optional(:destination_report) => boolean,
42
+ }
43
+ end
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pcp-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Puppet Labs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine-le
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faye-websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.10'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rschema
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ description: See https://github.com/puppetlabs/pcp-specifications
56
+ email: puppet@puppetlabs.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/pcp.rb
62
+ - lib/pcp/client.rb
63
+ - lib/pcp/message.rb
64
+ - lib/pcp/protocol.rb
65
+ homepage: https://github.com/puppetlabs/ruby-pcp-client
66
+ licenses:
67
+ - ASL 2.0
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.2.5
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Client library for PCP
89
+ test_files: []
90
+ has_rdoc: