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.
- checksums.yaml +7 -0
- data/lib/pcp.rb +3 -0
- data/lib/pcp/client.rb +162 -0
- data/lib/pcp/message.rb +165 -0
- data/lib/pcp/protocol.rb +45 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -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
|
data/lib/pcp.rb
ADDED
data/lib/pcp/client.rb
ADDED
|
@@ -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
|
data/lib/pcp/message.rb
ADDED
|
@@ -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
|
data/lib/pcp/protocol.rb
ADDED
|
@@ -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:
|