bridge-ruby 0.2.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.
- data/LICENSE +19 -0
- data/README.md +31 -0
- data/Rakefile +24 -0
- data/bridge-ruby.gemspec +24 -0
- data/doc/Bridge.html +276 -0
- data/doc/Bridge/Bridge.html +1874 -0
- data/doc/Bridge/Bridge/SystemService.html +396 -0
- data/doc/Bridge/Client.html +271 -0
- data/doc/Bridge/Connection.html +1180 -0
- data/doc/Bridge/Connection/SockBuffer.html +322 -0
- data/doc/Bridge/Reference.html +605 -0
- data/doc/Bridge/Serializer.html +405 -0
- data/doc/Bridge/Serializer/Callback.html +498 -0
- data/doc/Bridge/Tcp.html +657 -0
- data/doc/Bridge/Util.html +643 -0
- data/doc/Bridge/Util/CallbackReference.html +557 -0
- data/doc/OpenSSL/X509/Certificate.html +275 -0
- data/doc/SSLCertificateVerification.html +446 -0
- data/doc/_index.html +239 -0
- data/doc/class_list.html +53 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +328 -0
- data/doc/file.README.html +106 -0
- data/doc/file_list.html +55 -0
- data/doc/frames.html +28 -0
- data/doc/index.html +106 -0
- data/doc/js/app.js +214 -0
- data/doc/js/full_list.js +173 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +772 -0
- data/doc/top-level-namespace.html +112 -0
- data/examples/channels/client-writeable.rb +24 -0
- data/examples/channels/client.rb +23 -0
- data/examples/channels/server.rb +24 -0
- data/examples/chat/chatclient.rb +21 -0
- data/examples/chat/chatserver.rb +24 -0
- data/examples/client-context/client.rb +21 -0
- data/examples/client-context/server.rb +25 -0
- data/examples/secure/example.rb +8 -0
- data/examples/simple/channels.rb +47 -0
- data/examples/simple/services.rb +41 -0
- data/include/ssl/cacert.pem +3331 -0
- data/lib/bridge-ruby.rb +441 -0
- data/lib/client.rb +14 -0
- data/lib/connection.rb +162 -0
- data/lib/reference.rb +49 -0
- data/lib/serializer.rb +104 -0
- data/lib/ssl_utils.rb +68 -0
- data/lib/tcp.rb +73 -0
- data/lib/util.rb +101 -0
- data/lib/version.rb +3 -0
- data/rakelib/package.rake +4 -0
- data/rakelib/test.rake +8 -0
- data/test/regression/reconnect.rb +48 -0
- data/test/regression/rpc.rb +39 -0
- data/test/regression/test.rb +58 -0
- data/test/unit/bridge_dummy.rb +26 -0
- data/test/unit/connection_dummy.rb +21 -0
- data/test/unit/reference_dummy.rb +11 -0
- data/test/unit/tcp_dummy.rb +12 -0
- data/test/unit/test.rb +20 -0
- data/test/unit/test_reference.rb +30 -0
- data/test/unit/test_serializer.rb +109 -0
- data/test/unit/test_tcp.rb +51 -0
- data/test/unit/test_util.rb +59 -0
- metadata +162 -0
data/lib/connection.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'util.rb'
|
3
|
+
require 'tcp.rb'
|
4
|
+
require 'serializer.rb'
|
5
|
+
|
6
|
+
module Bridge
|
7
|
+
class Connection #:nodoc: all
|
8
|
+
|
9
|
+
attr_accessor :connected, :client_id, :sock, :options
|
10
|
+
|
11
|
+
def initialize bridge
|
12
|
+
# Set associated bridge object
|
13
|
+
@bridge = bridge
|
14
|
+
|
15
|
+
@options = bridge.options
|
16
|
+
|
17
|
+
# Preconnect buffer
|
18
|
+
@sock_buffer = SockBuffer.new
|
19
|
+
@sock = @sock_buffer
|
20
|
+
|
21
|
+
# Connection configuration
|
22
|
+
@interval = 0.4
|
23
|
+
end
|
24
|
+
|
25
|
+
# Contact redirector for host and ports
|
26
|
+
def redirector
|
27
|
+
# Support for redirector.
|
28
|
+
uri = URI(@options[:redirector])
|
29
|
+
conn = EventMachine::Protocols::HttpClient2.connect(:host => uri.host, :port => uri.port, :ssl => @options[:secure])
|
30
|
+
req = conn.get({:uri => "/redirect/#{@options[:api_key]}"})
|
31
|
+
req.callback do |obj|
|
32
|
+
begin
|
33
|
+
obj = JSON::parse obj.content
|
34
|
+
rescue Exception => e
|
35
|
+
Util.error "Unable to parse redirector response #{obj.content}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
if obj.has_key?('data') and obj['data'].has_key?('bridge_host') and obj['data'].has_key?('bridge_port')
|
39
|
+
obj = obj['data']
|
40
|
+
@options[:host] = obj['bridge_host']
|
41
|
+
@options[:port] = obj['bridge_port']
|
42
|
+
establish_connection
|
43
|
+
else
|
44
|
+
Util.error "Could not find host and port in JSON body"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def reconnect
|
50
|
+
Util.info "Attempting to reconnect"
|
51
|
+
if @interval < 32768
|
52
|
+
EventMachine::Timer.new(@interval) do
|
53
|
+
establish_connection
|
54
|
+
# Grow timeout for next reconnect attempt
|
55
|
+
@interval *= 2
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def establish_connection
|
61
|
+
Util.info "Starting TCP connection #{@options[:host]}, #{@options[:port]}"
|
62
|
+
EventMachine::connect(@options[:host], @options[:port], Tcp, self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def onmessage data, sock
|
66
|
+
# Parse for client id and secret
|
67
|
+
m = /^(\w+)\|(\w+)$/.match data[:data]
|
68
|
+
if not m
|
69
|
+
# Handle message normally if not a correct CONNECT response
|
70
|
+
process_message data
|
71
|
+
else
|
72
|
+
Util.info "client_id received, #{m[1]}"
|
73
|
+
@client_id = m[1]
|
74
|
+
@secret = m[2]
|
75
|
+
# Reset reconnect interval
|
76
|
+
@interval = 0.4
|
77
|
+
# Send preconnect queued messages
|
78
|
+
@sock_buffer.process_queue sock, @client_id
|
79
|
+
# Set connection socket to connected socket
|
80
|
+
@sock = sock
|
81
|
+
Util.info('Handshake complete')
|
82
|
+
# Trigger ready callback
|
83
|
+
if not @bridge.is_ready
|
84
|
+
@bridge.is_ready = true
|
85
|
+
@bridge.emit 'ready'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def onopen sock
|
91
|
+
Util.info 'Beginning handshake'
|
92
|
+
msg = Util.stringify(:command => :CONNECT, :data => {:session => [@client_id || nil, @secret || nil], :api_key => @options[:api_key]})
|
93
|
+
sock.send msg
|
94
|
+
end
|
95
|
+
|
96
|
+
def onclose
|
97
|
+
Util.warn 'Connection closed'
|
98
|
+
# Restore preconnect buffer as socket connection
|
99
|
+
@sock = @sock_buffer
|
100
|
+
if @options[:reconnect]
|
101
|
+
reconnect
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def process_message message
|
106
|
+
begin
|
107
|
+
Util.info "Received #{message[:data]}"
|
108
|
+
message = Util.parse(message[:data])
|
109
|
+
rescue Exception => e
|
110
|
+
Util.error "Message parsing failed"
|
111
|
+
end
|
112
|
+
# Convert serialized ref objects to callable references
|
113
|
+
Serializer.unserialize(@bridge, message['args'])
|
114
|
+
# Extract RPC destination address
|
115
|
+
destination = message['destination']
|
116
|
+
if !destination
|
117
|
+
Util.warn "No destination in message #{message}"
|
118
|
+
return
|
119
|
+
end
|
120
|
+
if message['source']
|
121
|
+
@bridge.context = Client.new(@bridge, message['source'])
|
122
|
+
end
|
123
|
+
@bridge.execute message['destination']['ref'], message['args']
|
124
|
+
end
|
125
|
+
|
126
|
+
def send_command command, data
|
127
|
+
data.delete :callback if data.key? :callback and data[:callback].nil?
|
128
|
+
msg = Util.stringify :command => command, :data => data
|
129
|
+
Util.info "Sending #{msg}"
|
130
|
+
@sock.send msg
|
131
|
+
end
|
132
|
+
|
133
|
+
def start
|
134
|
+
if !@options.has_key? :host or !@options.has_key? :port
|
135
|
+
redirector
|
136
|
+
else
|
137
|
+
# Host and port are specified
|
138
|
+
establish_connection
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class SockBuffer
|
143
|
+
def initialize
|
144
|
+
# Buffer for preconnect messages
|
145
|
+
@buffer = []
|
146
|
+
end
|
147
|
+
|
148
|
+
def send msg
|
149
|
+
@buffer << msg
|
150
|
+
end
|
151
|
+
|
152
|
+
def process_queue sock, client_id
|
153
|
+
@buffer.each do |msg|
|
154
|
+
# Replace null client ids with actual client_id after handshake
|
155
|
+
sock.send( msg.gsub '"client",null', '"client","'+ client_id + '"' )
|
156
|
+
end
|
157
|
+
@buffer = []
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
data/lib/reference.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Bridge
|
2
|
+
# Instances of this class represent references to remote services.
|
3
|
+
class Reference #:nodoc: all
|
4
|
+
|
5
|
+
attr_accessor :operations, :address
|
6
|
+
|
7
|
+
def initialize bridge, address, operations = nil
|
8
|
+
@address = address
|
9
|
+
# Store operations supported by this reference if any
|
10
|
+
operations = [] if operations.nil?
|
11
|
+
@operations = operations.map do |val|
|
12
|
+
val.to_s
|
13
|
+
end
|
14
|
+
@bridge = bridge
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_dict op = nil
|
18
|
+
# Serialize the reference
|
19
|
+
result = {}
|
20
|
+
address = @address
|
21
|
+
# Add a method name to address if given
|
22
|
+
if op
|
23
|
+
address = address.slice(0..-1)
|
24
|
+
address << op
|
25
|
+
end
|
26
|
+
result[:ref] = address
|
27
|
+
# Append operations only if address refers to a handler
|
28
|
+
if address.length < 4
|
29
|
+
result[:operations] = @operations
|
30
|
+
end
|
31
|
+
return result
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing atom, *args, &blk
|
35
|
+
# If a block is given, add to arguments list
|
36
|
+
args << blk if blk
|
37
|
+
Util.info "Calling #{@address}.#{atom}"
|
38
|
+
# Serialize destination
|
39
|
+
destination = self.to_dict atom.to_s
|
40
|
+
# Send RPC
|
41
|
+
@bridge.send args, destination
|
42
|
+
end
|
43
|
+
|
44
|
+
def respond_to? atom
|
45
|
+
@operations.include?(atom.to_s) || atom == :to_dict || Class.respond_to?(atom)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
data/lib/serializer.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'util.rb'
|
3
|
+
|
4
|
+
module Bridge
|
5
|
+
module Serializer #:nodoc: all
|
6
|
+
|
7
|
+
def self.serialize bridge, obj
|
8
|
+
# Serialize immediately if obj responds to to_dict
|
9
|
+
if obj.respond_to? :to_dict
|
10
|
+
result = obj.to_dict
|
11
|
+
# Enumerate hash and serialize each member
|
12
|
+
elsif obj.is_a? Hash
|
13
|
+
result = {}
|
14
|
+
obj.each do |k, v|
|
15
|
+
result[k] = serialize bridge, v
|
16
|
+
end
|
17
|
+
# Enumerate array and serialize each member
|
18
|
+
elsif obj.is_a? Array
|
19
|
+
result = obj.map do |v|
|
20
|
+
serialize bridge, v
|
21
|
+
end
|
22
|
+
# Store as callback if callable
|
23
|
+
elsif obj.respond_to?(:call)
|
24
|
+
result = bridge.store_object(Callback.new(obj), ['callback']).to_dict
|
25
|
+
# Return obj itself is JSON serializable
|
26
|
+
elsif JSON::Ext::Generator::GeneratorMethods.constants.include? obj.class.name.to_sym
|
27
|
+
result = obj
|
28
|
+
# Otherwise store as service. Obj is a class instance or module
|
29
|
+
else
|
30
|
+
result = bridge.store_object(obj, Util.find_ops(obj)).to_dict
|
31
|
+
end
|
32
|
+
return result
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.unserialize bridge, obj
|
36
|
+
if obj.is_a? Hash
|
37
|
+
obj.each do |k, v|
|
38
|
+
unserialize_helper bridge, obj, k, v
|
39
|
+
end
|
40
|
+
elsif obj.is_a? Array
|
41
|
+
obj.each_with_index do |v, k|
|
42
|
+
unserialize_helper bridge, obj, k, v
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.unserialize_helper bridge, obj, k, v
|
48
|
+
if v.is_a? Hash
|
49
|
+
# If object has ref key, convert to reference
|
50
|
+
if v.has_key? 'ref'
|
51
|
+
if v['ref'][1] == bridge.connection.client_id and v['ref'][0] == 'client' and
|
52
|
+
obj[k] = bridge.store[v['ref'][2]]
|
53
|
+
if obj[k].is_a? Callback
|
54
|
+
obj[k] = obj[k].method :callback
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# Create reference
|
58
|
+
ref = Reference.new(bridge, v['ref'], v['operations'])
|
59
|
+
if v.has_key? 'operations' and v['operations'].length == 1 and v['operations'][0] == 'callback'
|
60
|
+
# Callback wrapper
|
61
|
+
obj[k] = Util.ref_callback ref
|
62
|
+
else
|
63
|
+
obj[k] = ref
|
64
|
+
end
|
65
|
+
end
|
66
|
+
return
|
67
|
+
end
|
68
|
+
end
|
69
|
+
unserialize bridge, v
|
70
|
+
end
|
71
|
+
|
72
|
+
class Callback
|
73
|
+
def initialize fun
|
74
|
+
@fun = fun
|
75
|
+
end
|
76
|
+
|
77
|
+
def callback *args
|
78
|
+
@fun.call *args
|
79
|
+
end
|
80
|
+
|
81
|
+
def call *args
|
82
|
+
@fun.call(*args)
|
83
|
+
end
|
84
|
+
|
85
|
+
def method atom
|
86
|
+
if atom.to_s == 'callback'
|
87
|
+
@fun
|
88
|
+
else
|
89
|
+
Class.method atom
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def methods bool
|
94
|
+
[:callback]
|
95
|
+
end
|
96
|
+
|
97
|
+
def respond_to? atom
|
98
|
+
atom.to_s == 'callback' || Class.respond_to?(atom)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
data/lib/ssl_utils.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class OpenSSL::X509::Certificate
|
4
|
+
def ==(other)
|
5
|
+
other.respond_to?(:to_pem) && to_pem == other.to_pem
|
6
|
+
end
|
7
|
+
|
8
|
+
# A serial *must* be unique for each certificate. Self-signed certificates,
|
9
|
+
# and thus root CA certificates, have the same `issuer' as `subject'.
|
10
|
+
def top_level?
|
11
|
+
serial == serial && issuer.to_s == subject.to_s
|
12
|
+
end
|
13
|
+
alias_method :root?, :top_level?
|
14
|
+
alias_method :self_signed?, :top_level?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Verifies that the peer certificate is a valid chained certificate. That is,
|
18
|
+
# it's signed by a root CA or a CA signed by a root CA.
|
19
|
+
#
|
20
|
+
# This module will also perform hostname verification against the server’s
|
21
|
+
# certificate, but _only_ if an instance variable called +@hostname+ exists.
|
22
|
+
module SSLCertificateVerification
|
23
|
+
class << self
|
24
|
+
# In PEM format.
|
25
|
+
#
|
26
|
+
# Eg: http://curl.haxx.se/docs/caextract.html
|
27
|
+
attr_accessor :ca_cert_file
|
28
|
+
end
|
29
|
+
|
30
|
+
def ca_store
|
31
|
+
unless @ca_store
|
32
|
+
if file = SSLCertificateVerification.ca_cert_file
|
33
|
+
@ca_store = OpenSSL::X509::Store.new
|
34
|
+
@ca_store.add_file(file)
|
35
|
+
else
|
36
|
+
fail "you must specify a file with root CA certificates as `SSLCertificateVerification.ca_cert_file'"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@ca_store
|
40
|
+
end
|
41
|
+
|
42
|
+
# It's important that we try to not add a certificate to the store that's
|
43
|
+
# already in the store, because OpenSSL::X509::Store will raise an exception.
|
44
|
+
def ssl_verify_peer(cert_string)
|
45
|
+
cert = OpenSSL::X509::Certificate.new(cert_string)
|
46
|
+
# Some servers send the same certificate multiple times. I'm not even joking... (gmail.com)
|
47
|
+
return true if cert == @last_seen_cert
|
48
|
+
@last_seen_cert = cert
|
49
|
+
|
50
|
+
if ca_store.verify(@last_seen_cert)
|
51
|
+
# A server may send the root certifiacte, which we already have and thus
|
52
|
+
# should not be added to the store again.
|
53
|
+
ca_store.add_cert(@last_seen_cert) unless @last_seen_cert.root?
|
54
|
+
true
|
55
|
+
else
|
56
|
+
fail "unable to verify the server certificate of `#{@hostname}'"
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def ssl_handshake_completed
|
62
|
+
if @hostname
|
63
|
+
unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, @hostname)
|
64
|
+
fail "the hostname `HOSTNAME' does not match the server certificate"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/tcp.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'openssl'
|
3
|
+
require 'ssl_utils.rb'
|
4
|
+
|
5
|
+
module Bridge
|
6
|
+
class Tcp < EventMachine::Connection #:nodoc: all
|
7
|
+
|
8
|
+
include SSLCertificateVerification
|
9
|
+
|
10
|
+
def initialize connection
|
11
|
+
@buffer = ''
|
12
|
+
@len = 0
|
13
|
+
@pos = 0
|
14
|
+
@callback = nil
|
15
|
+
@connection = connection
|
16
|
+
SSLCertificateVerification.ca_cert_file = File.expand_path('../../include/ssl/cacert.pem', __FILE__)
|
17
|
+
start
|
18
|
+
end
|
19
|
+
|
20
|
+
def post_init
|
21
|
+
start_tls(:verify_peer => true) if @connection.options[:secure]
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection_completed
|
25
|
+
# connection now ready. call the callback
|
26
|
+
@connection.onopen self
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_data data
|
30
|
+
left = @len - @pos
|
31
|
+
if data.length >= left
|
32
|
+
@buffer << data.slice(0, left)
|
33
|
+
@callback.call @buffer
|
34
|
+
receive_data data.slice(left..-1) unless data.nil?
|
35
|
+
else
|
36
|
+
@buffer << data
|
37
|
+
@pos = @pos + data.length
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def read len, &cb
|
42
|
+
@buffer = ''
|
43
|
+
@len = len
|
44
|
+
@pos = 0
|
45
|
+
@callback = cb
|
46
|
+
end
|
47
|
+
|
48
|
+
def start
|
49
|
+
# Read header bytes
|
50
|
+
read 4 do |data|
|
51
|
+
# Read body bytes
|
52
|
+
read data.unpack('N')[0] do |data|
|
53
|
+
# Call message handler
|
54
|
+
@connection.onmessage({:data => data}, self)
|
55
|
+
# Await next message
|
56
|
+
start
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def send arg
|
62
|
+
# Prepend length header to message
|
63
|
+
send_data([arg.length].pack("N") + arg)
|
64
|
+
end
|
65
|
+
|
66
|
+
def unbind
|
67
|
+
@connection.onclose
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|