pcp-client 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.
@@ -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: