bridge-ruby 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|