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